Reading the docs it seems that whenever I import useUpdateMany I already have to pass the data it's going to send. My question is, is it possible to pass the data on the callback?
I want to call the updateMany in a handleSubmit function, so I will only have the data when the function is called:
export const ChangeStatus = (props) => {
const { record, version } = props;
const { t } = useTranslation('admin');
const classes = useStyles();
const refresh = useRefresh();
const notify = useNotify();
const [componentStatus, setComponentStatus] = useState(null);
const [updateMany, { loading, error }] = useUpdateMany('orders', props.selectedIds, {componentStatus });
const defaultSubscription = {
submitting: true,
pristine: true,
valid: true,
invalid: true,
};
const handleSubmit = ({ status }) => {
setComponentStatus({ status });
updateMany();
refresh();
};
return (
<Form
initialValues={record}
subscription={defaultSubscription}
key={version}
onSubmit={handleSubmit}
render={(formProps) => (
<form onSubmit={formProps.handleSubmit} className={classes.form}>
<SelectInput
label="Status"
variant="outlined"
source="status"
className={classes.selectField}
FormHelperTextProps={{ className: classes.selectHelperText }}
choices={[
{ id: 'created', name: 'Created' },
{ id: 'canceled', name: 'Canceled' },
{ id: 'active', name: 'Active' },
{ id: 'awaiting', name: 'Awaiting allocation' },
{ id: 'processing', name: 'Processing' },
{ id: 'review', name: 'Review' },
{ id: 'completed', name: 'Completed' },
]}
/>
<Button variant="contained" color="secondary" type="submit" disabled={loading}>
{t('Confirm')}
</Button>
</form>
)}
/>
);
};
Right now I'm updating a state and then calling the updateMany, but it would be much easier if I could call the updateMany passing the data:
const handleSubmit = ({ status }) =>
updateMany({status});
};
Is it possible to do it?
Thanks!
You can override the params when calling updateMany function but you have to respect the mutation params format.
In your example you can do updateMany({ payload: { data: { componentStatus } } });
Indeed useUpdateMany call useMutation under the hood and this hook allow merge with callTimeQuery
You can find some references here :
https://marmelab.com/react-admin/Actions.html#usemutation-hook
useMutation accepts a variant call where the parameters are passed to the callback instead of when calling the hook. Use this variant when some parameters are only known at call time.
https://github.com/marmelab/react-admin/blob/bdf941315be7a2a35c8da7925a2a179dbcb607a1/packages/ra-core/src/dataProvider/useMutation.ts#L299
Also there is an issue wich foresees a more logical signature at call time : https://github.com/marmelab/react-admin/issues/6047
(Update : And now this is merged for a future release: https://github.com/marmelab/react-admin/pull/6168 )
Related
I'm using react-admin to manage a MongoDB database. A simplified example of the collections in the database:
contacts = [
{ id: 8, name: "Joe", country: "UK" },
]
tasks = [
{ id: 0, description: "", dev: { contact_id: 8, name: "Joe" } },
]
The documents in tasks have to store both contact_id (contact doc reference) and name (shown in many different views, so the number of API calls can be reduced). In this case, I'd use an AutocompleteInput within a ReferenceInput to save contact_id.
<ReferenceInput source="dev.contact_id" reference="contacts">
<AutocompleteInput
source="dev.contact_id"
optionText="name"
optionValue="id"
/>
</ReferenceInput>
However, once the contact is selected in the Autocomplete, I can't find a way to save the field name, so the task document looks like in the example. So far I've tried getting the contact record and adding the name field before submitting the form, but it's not working (hooks can only be called inside the body of a function component):
export const TaskForm = ({ children }) => {
const { handleSubmit } = useForm()
const onSubmit = async (data) => {
const contact = await useGetOne('contacts', { id: data?.dev?.contact_id })
console.log(contact)
}
return (
<Form onSubmit={handleSubmit(onSubmit)}>
{children}
</Form>
)
}
Any suggestions?
Perhaps you can use the dataProvider-hook instead of the useGetOne:
export const TaskForm = ({ children }) => {
const { handleSubmit } = useForm()
const dataProvider = useDataProvider();
const onSubmit = async (data) => {
const contact = await dataProvider.getOne('contacts', { id: data?.dev?.contact_id });
console.log(contact)
}
return (
<Form onSubmit={handleSubmit(onSubmit)}>
{children}
</Form>
)
}
I've been working in a test where I need the data from Vuex. However, I'm having some problems, I need to change that data in each test in order to test the functionality of the component.
Here is my component:
<template>
<div id="cb-items-displayer" #click="textClick">
<span>(</span>
<p>{{ text }}</p>
<span>)</span>
</div>
</template>
<script lang="ts" setup>
import { capitalize } from '#/utils/capitalize'
import { ItemsDisplayer } from '#/models/ItemsDisplayer'
import { computed, PropType } from 'vue'
import { useStore } from 'vuex'
const store = useStore()
const props = defineProps({
type: {
type: String,
default: '',
},
menuType: {
type: String,
default: '',
},
items: {
type: Array as PropType<ItemsDisplayer[]>,
default: () => [],
}
})
const emit = defineEmits<{
(event: 'textClicked'): void
}>()
const text = computed(() => {
const param = props.menuType === 'radio' ? 'One' : 'Many'
console.log( "TYPEEE ", props.type, " ", param )
const itemsIds = store.getters['filters/get' + capitalize(props.type) + param]
console.log("ITEMSSS", JSON.stringify(itemsIds))
return getTextToShow(itemsIds)
})
const getTextToShow = (itemsIds: string) => {
//TODO - improve it
if (itemsIds === 'all') {
return 'all'
} else if (itemsIds.length === 0) {
return '-'
} else if (itemsIds.length === 1) {
return getName(itemsIds[0], props.items)
} else {
return itemsIds.length
}
}
const textClick = () => {
emit('textClicked')
}
const getName = (id: string, items: ItemsDisplayer[]) => {
const found: ItemsDisplayer = items.find((x) => x.id! === id) as ItemsDisplayer
console.log("GETNAME ", found.name)
return found?.name
}
</script>
And this is the test:
import { render, screen, click, waitFor } from '#tests/app-test-utils'
import ItemsDisplayer from './ItemsDisplayer.vue'
import { capitalize } from '#/utils/capitalize'
let mockStoreCommit: jest.Mock
jest.mock('vuex', () => ({
...jest.requireActual('vuex'),
useStore: () => ({
getters: {
[`filters/get${capitalize('categories')}Many`]: [],
},
commit: mockStoreCommit,
}),
}))
describe('ItemsDisplayer', () => {
beforeEach(() => {
mockStoreCommit = jest.fn()
render(
ItemsDisplayer,
{},
{
props: {
type: 'categories',
menuType: 'checkbox',
items: [
{
box_templates:"",
id:"1",
name:"Culture"
},
{
box_templates:"",
id:"2",
name:"Economy"
},
{
box_templates:"",
id:"3",
name:"Education"
}
]},
}
)
})
it('renders the component', async() => {
await screen.getByText('-')
})
it('renders the component with one item', async() => {
//DON'T WORK HERE THERE SHOULD BE A CHANGE OF DATA IN THE MOCKED STORE IN ORDER TO WORK
await screen.getByText('Culture')
})
})
My problem is that I need to change the value of [filters/get${capitalize('categories')}Many] to ["1"] in the second test.
I tried several things in order to change the mocked data but they don't work. How can I change the mocked store data in each test?
Thanks!
You can achieve this by lazy loading your vue component:
Add jest.resetModules(); in the beforeEach to reset all of the imported modules before each test so they can be re-evaluated and re-mocked:
beforeEach(() => {
jest.resetModules();
In each unit test, you will first need to import the vue component using the require syntax as follows:
const ItemsDisplayer = require('./ItemsDisplayer.vue').default;
Then add the mock directly after the import with the [`filters/get${capitalize('categories')}Many`] value being set to whatever you want it to be:
jest.mock('vuex', () => ({
...jest.requireActual('vuex'),
useStore: () => ({
getters: {
[`filters/get${capitalize('categories')}Many`]: ["1"],
},
commit: mockStoreCommit,
}),
}));
I have noticed that you do your rendering in the beforeEach. Unfortunately because you import and mock your modules during the test, the rendering will need to be done after these have taken place - hence you will need to either move that logic into your unit test or extract it into another function which can be called from within the unit test.
Each unit test should look something like this:
it('renders the component', async() => {
const ItemsDisplayer = require('./ItemsDisplayer.vue').default;
jest.mock('vuex', () => ({
...jest.requireActual('vuex'),
useStore: () => ({
getters: {
[`filters/get${capitalize('categories')}Many`]: ["1"],
},
commit: mockStoreCommit,
}),
}));
// beforeEach logic here or a call to a function that contains it
await screen.getByText('-')
})
Using StoryBook.js, when I navigate to a component, view its "Docs" and click the "Show Code" button, why do I get code that looks like this...
(args, { argTypes }) => ({
components: { Button },
props: Object.keys(argTypes),
template: '<Button v-bind="$props" />',
})
...as opposed to this...
<Button type="button" class="btn btn-primary">Label</Button>
Button.vue
<template>
<button
:type="type"
:class="'btn btn-' + (outlined ? 'outline-' : '') + variant"
:disabled="disabled">Label</button>
</template>
<script>
export default {
name: "Button",
props: {
disabled: {
type: Boolean,
default: false,
},
outlined: {
type: Boolean,
default: false,
},
type: {
type: String,
default: 'button',
},
variant: {
type: String,
default: 'primary',
validator(value) {
return ['primary', 'success', 'warning', 'danger'].includes(value)
}
}
}
}
</script>
Button.stories.js
import Button from '../components/Button'
export default {
title: 'Button',
component: Button,
parameters: {
componentSubtitle: 'Click to perform an action or submit a form.',
},
argTypes: {
disabled: {
description: 'Make a button appear to be inactive and un-clickable.',
},
outlined: {
description: 'Add a border to the button and remove the fill colour.',
},
type: {
options: ['button', 'submit'],
control: { type: 'inline-radio' },
description: 'Use "submit" when you want to submit a form. Use "button" otherwise.',
},
variant: {
options: ['primary', 'success'],
control: { type: 'select' },
description: 'Bootstrap theme colours.',
},
},
}
const Template = (args, { argTypes }) => ({
components: { Button },
props: Object.keys(argTypes),
template: '<Button v-bind="$props" />',
})
export const Filled = Template.bind({})
Filled.args = { disabled: false, outlined: false, type: 'button', variant: 'primary' }
export const Outlined = Template.bind({})
Outlined.args = { disabled: false, outlined: true, type: 'button', variant: 'primary' }
export const Disabled = Template.bind({})
Disabled.args = { disabled: true, outlined: false, type: 'button', variant: 'primary' }
I thought I followed their guides to the letter, but I just can't understand why the code output doesn't look the way I expect it to.
I simply want any of my colleagues using this to be able to copy the code from the template and paste it into their work if they want to use the component without them having to be careful what they select from the code output.
For anyone else who encounters this issue, I discovered that this is a known issue for StoryBook with Vue 3.
As mine is currently a green-field project at the time of writing this, I put a temporary workaround in place by downgrading Vue to ^2.6.
This is OK for me. I'm using the options API to build my components anyway so I'll happily upgrade to Vue ^3 when Storybook resolve the above linked issue.
One of possible options is to use current workaround that I found in the GH issue mentioned by Simon K https://github.com/storybookjs/storybook/issues/13917:
Create file withSource.js in the .storybook folder with following content:
import { addons, makeDecorator } from "#storybook/addons";
import kebabCase from "lodash.kebabcase"
import { h, onMounted } from "vue";
// this value doesn't seem to be exported by addons-docs
export const SNIPPET_RENDERED = `storybook/docs/snippet-rendered`;
function templateSourceCode (
templateSource,
args,
argTypes,
replacing = 'v-bind="args"',
) {
const componentArgs = {}
for (const [k, t] of Object.entries(argTypes)) {
const val = args[k]
if (typeof val !== 'undefined' && t.table && t.table.category === 'props' && val !== t.defaultValue) {
componentArgs[k] = val
}
}
const propToSource = (key, val) => {
const type = typeof val
switch (type) {
case "boolean":
return val ? key : ""
case "string":
return `${key}="${val}"`
default:
return `:${key}="${val}"`
}
}
return templateSource.replace(
replacing,
Object.keys(componentArgs)
.map((key) => " " + propToSource(kebabCase(key), args[key]))
.join(""),
)
}
export const withSource = makeDecorator({
name: "withSource",
wrapper: (storyFn, context) => {
const story = storyFn(context);
// this returns a new component that computes the source code when mounted
// and emits an events that is handled by addons-docs
// this approach is based on the vue (2) implementation
// see https://github.com/storybookjs/storybook/blob/next/addons/docs/src/frameworks/vue/sourceDecorator.ts
return {
components: {
Story: story,
},
setup() {
onMounted(() => {
try {
// get the story source
const src = context.originalStoryFn().template;
// generate the source code based on the current args
const code = templateSourceCode(
src,
context.args,
context.argTypes
);
const channel = addons.getChannel();
const emitFormattedTemplate = async () => {
const prettier = await import("prettier/standalone");
const prettierHtml = await import("prettier/parser-html");
// emits an event when the transformation is completed
channel.emit(
SNIPPET_RENDERED,
(context || {}).id,
prettier.format(`<template>${code}</template>`, {
parser: "vue",
plugins: [prettierHtml],
htmlWhitespaceSensitivity: "ignore",
})
);
};
setTimeout(emitFormattedTemplate, 0);
} catch (e) {
console.warn("Failed to render code", e);
}
});
return () => h(story);
},
};
},
});
And then add this decorator to preview.js:
import { withSource } from './withSource'
...
export const decorators = [
withSource
]
I'm currently working on app that has ability to add new tasks, assign them to others and choose due date.
I'd like to start sending the assignee and due date of the task to my firebase.
This is my dropdowncomponent:
class DropdownComponent extends Component {
constructor(props){
super(props);
this.state = {
assignee: '',
data: [{
value: 'apple',
}, {
value: 'lemon',
}, {
value: 'orange',
}, {
value: 'banana',
}, {
value: 'watermelon',
}],
value: ''
}
}
handleAssigneePicked = value => {
this.setState({ assignee: value })
console.log("Assignee " + value)
};
render() {
return (
<Dropdown
data={this.state.data}
value={this.state.assignee}
onChangeText={this.handleAssigneePicked}
/>
);
}
}
And this is how I render datepicker
<DateTimePicker
isVisible={this.state.isDateTimePickerVisible}
onConfirm={this.handleDatePicked}
onCancel={this.hideDateTimePicker}
/>
handleDatePicked = date => {
console.log("A date has been picked: ", date);
this.hideDateTimePicker();
this.setState({ selectedDate: moment().format('D MMM DD YYYY HH:MM')})
};
When I choose an item from dropdown, it also console logs the corresponding value, meaning the state changed, no issues there.
However, if i'm trying to send that information to firebase, using code below:
const SaveNewTask = (name, body) => {
const { currentUser } = firebase.auth();
// eslint-disable-next-line no-undef
// eslint-disable-next-line no-unused-expressions
!!name && !!body
?
firebase
.database()
.ref(`/users/${currentUser.uid}/tasks`)
.push({
name, body, assignee, selectedDate, timestamp: Date.now(), completed: false, archived: false
})
// eslint-disable-next-line no-undef
: alert('All fields are required.');
};
But I'm getting can't find variable: assignee and selectedDate, can I get some help with this? I must be missing something little.
Looks like you forgot to pass these params to action
const SaveNewTask = (name, body, assignee, selectedDate) => {
const { currentUser } = firebase.auth();
!_.isEmpty(name) && !_.isEmpty(body) && !_.isEmpty(assignee) && selectedDate != undefined
?
firebase
.database()
.ref(`/users/${currentUser.uid}/tasks`)
.push({
name, body, assignee, selectedDate, timestamp: Date.now(), completed: false, archived: false
})
: alert('All fields are required.');
};
you can use _.isEmpty() by lodash
My application to use to react-navigation the router, in accordance with the API method, the following problems:
react-navigation : why 'routes' of undefined ? https://reactnavigation.org/docs/routers
const SimpleApp = TabNavigator({})
const Simple = StackNavigator({
Home: { screen: SimpleApp },
Login: { screen: Login },
})
const defaultGetStateForAction = Simple.router.getStateForAction;
Simple.router.getStateForAction = (action, state) => {
console.log('action ===',action);
console.log('state ===',state);
console.log('......getStateForAction........');
if (true) {
const routes = [
...state.routes,
{key: 'A', routeName: 'Login', params: { name: action.name1 }},
];
return {
...state,
routes,
index: routes.length - 1,
};
}
return defaultGetStateForAction(action, state);
};
export default class App extends Component {
render() {
return (
<Simple />
)
}
}
TypeError:Cannot read property 'routes' of undefined
We are basically overriding the getStateForAction function.
This function is present in the StackRouter file. What it does is check if the initial state is undefined. If it is, it creates it.
StackRouter.js
getStateForAction(
action: NavigationAction,
state: ?NavigationState
): ?NavigationState {
// Set up the initial state if needed
if (!state) {
let route = {};
... // The code present in the file.
}
To learn more about this look into StackRouter.js.
As the state itself is undefined because initial state is not set, ...state.routes in the code is producing the error.
Currently this will work:
Simple.router.getStateForAction = (action, state) => {
console.log('action ===',action);
console.log('state ===',state);
console.log('......getStateForAction........');
// change the condition to check if state exists.
if (state) {
const routes = [
...state.routes,
{key: 'A', routeName: 'Login', params: { name: action.name1 }},
];
return {
...state,
routes,
index: routes.length - 1,
};
}
return defaultGetStateForAction(action, state);
};