Save multiple fields from record in different collection in react-admin Form - react-admin

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>
)
}

Related

vue3 array push not woking

const myArray =[];
const nestedMenuitems = ref([
{
items:myArray
},
]);
onMounted(() => {
ApiService.getStores().then((data) => {
stores.value = data;
stores.value.map(function(value, _key) {
myArray.push({lable: value.shop});
});
});
});
i am trying show array data items:myArray this line
under the code working fine but mu custom data api to pass data its not working.what is the problem.
how to solve this?
const list= [
{
label: 'Tracker',
icon: 'pi pi-fw pi-compass'
},
{
label: 'Map',
icon: 'pi pi-fw pi-map-marker'
},
{
label: 'Manage',
icon: 'pi pi-fw pi-pencil'
}
];
const nestedMenuitems = ref([
{
items:list
},
]);
i am trying show array data items:myArray this line
under the code working fine but mu custom data api to pass data its not working.what is the problem.
how to solve this?
<Menubar :model="nestedMenuitems" optionValue="value">
</Menubar>
model="nestedMenuitems to get dropdown data
To trigger reactivity you need to replace the item inside nestedMenuItems.
What's not clear to me is why aren't you using a simpler data structure:
const state = reactive({ items: [] });
const { items } = toRefs(state);
onMounted(() => {
ApiService.getStores().then((data) => {
data.forEach((value) => {
state.items.push({ label: value.shop });
});
});
});
What do you need nestedMenuItems for? Do you have more than one nestedMenuItem?
You might want to show <Menubar />. Most likely, you don't need v-model in it, but v-bind. e.g:
<Menubar :items="items" optionValue="value" />
And another question would be: if you're storing the response in stores, why are you also storing it somewhere else (e.g: in nestedMenuItems, or items)? Why not consume it directly from stores?
For example:
const stores = ref([]);
const items = computed(() => stores.value.map(({ shop: label }) => ({ label })));
onMounted(() => {
ApiService.getStores().then((data) => {
stores.value = data;
});
});
<pre v-text="JSON.stringify({ stores, items }, null, 2)" />
It's bad practice to keep the same reactive data in two separate places, because you're always running the risk of them getting out of sync. If you're using the same source, any change will be reflected in every place where the data is consumed/displayed.

How can I load reactive data to multiselect component?

I am trying to use this library https://github.com/vueform/multiselect, but whenever I try to load fetched data, nothing shows up in there, but for e.g when I do {{ testData }} I see the value in html. Can anyone advice me how can I fix this?
Works:
<Multiselect :options="['test']" />
Doesn't work:
<Multiselect :options="testData" />
in my setup()
const testData = computed(() => {
return [
{
name: "Test"
}
]
})
return { testData }
From the above I assume it should be an array, so try this
const testData = computed(() => {
return [{ name: "Test"}]
})
return { testData }
and add the below change in template
<Multiselect :options="testData" track-by="name" label="name"/>
Modified on request:
If you wanna show the entire object then you can follow the below example
const testData = computed(() => {
return [
{ name: 'Vue.js', id: 'vue' },
{ name: 'Angular', id: 'angular' }
]
})
return { testData }
and add a method called nameWithId with the below content.
Note: I have given it based on Vue2 syntax, pls change the syntax that matches to Vue3
nameWithId({ name, id }) {
return `${name} — [${id}]`
}
then with the below changes in your template
<Multiselect v-model="selectedValue" :options="testData" :custom-label="nameWithId" placeholder="Select one" label="name" track-by="name"/>
Reference doc: https://vue-multiselect.js.org/#sub-getting-started

Reset useLazyQuery after called once

I'm using useLazyQuery to trigger a query on a button click. After the query is called once, the results (data, error, etc) are passed to the component render on each render. This is problematic for example when the user enters new text input to change what caused the error: the error message keeps reapearing. So I would like to "clear" the query (eg. when user types new data into TextInput) so the query results return to there inital state (everything undefined) and the error message goes away.
I can't find any clear way to do this in the Apollo docs, so how could I do that?
(I though of putting the query in the parent component so it does not update on each rerender, but I'd rather not do that)
This is how I have my component currently setup:
import { useLazyQuery } from 'react-apollo'
// ...
const [inputValue, setInputValue] = useState('')
const [getUserIdFromToken, { called, loading, data, error }] = useLazyQuery(deliveryTokenQuery, {
variables: {
id: inputValue.toUpperCase(),
},
})
useEffect(() => {
if (data && data.deliveryToken) {
onSuccess({
userId: data.deliveryToken.vytal_user_id,
token: inputValue,
})
}
}, [data, inputValue, onSuccess])
// this is called on button tap
const submitToken = async () => {
Keyboard.dismiss()
getUserIdFromToken()
}
// later in the render...
<TextInput
onChangeText={(val) => {
setInputValue(val)
if (called) {
// clean/reset query here? <----------------------
}
})
/>
Thanks #xadm for pointing out the solution: I had to give onCompleted and onError callbacks in useLazyQuery options, and pass the variables to the call function, not in useLazyQuery options. In the end the working code looks like this:
const [inputValue, setInputValue] = useState('')
const [codeError, setCodeError] = useState<string | undefined>()
const [getUserIdFromToken, { loading }] = useLazyQuery(deliveryTokenQuery, {
onCompleted: ({ deliveryToken }) => {
onSuccess({
userId: deliveryToken.vytal_user_id,
token: inputValue,
})
},
onError: (e) => {
if (e.graphQLErrors && e.graphQLErrors[0] === 'DELIVERY_TOKEN_NOT_FOUND') {
return setCodeError('DELIVERY_TOKEN_NOT_FOUND')
}
return setCodeError('UNKNOWN')
},
})
const submitToken = () => {
Keyboard.dismiss()
getUserIdFromToken({
variables: {
id: inputValue
},
})
}

Call useUpdateMany callback with data

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 )

Relay Moder - Pagination

I am already working on Pagination.
I used PaginationContainer for that. It work’s but no way what I am looking for.
I got button next which call props.relay.loadMore(2) function. So when I click on this button it will call query and add me 2 more items to list. It works like load more. But I would like instead of add these two new items to list, replace the old item with new.
I try to use this getFragmentVariables for modifying variables for reading from the store but it’s not working.
Have somebody Idea or implemented something similar before?
class QueuesBookingsList extends Component {
props: Props;
handleLoadMore = () => {
const { hasMore, isLoading, loadMore } = this.props.relay;
console.log('hasMore', hasMore());
if (!hasMore() || isLoading()) {
return;
}
this.setState({ isLoading });
loadMore(1, () => {
this.setState({ isLoading: false });
});
};
getItems = () => {
const edges = idx(this.props, _ => _.data.queuesBookings.edges) || [];
return edges.map(edge => edge && edge.node);
};
getItemUrl = ({ bid }: { bid: number }) => getDetailUrlWithId(BOOKING, bid);
render() {
return (
<div>
<button onClick={this.handleLoadMore}>TEST</button>
<GenericList
displayValue={'bid'}
items={this.getItems()}
itemUrl={this.getItemUrl}
emptyText="No matching booking found"
/>
</div>
);
}
}
export default createPaginationContainer(
QueuesBookingsList,
{
data: graphql`
fragment QueuesBookingsList_data on RootQuery {
queuesBookings(first: $count, after: $after, queueId: $queueId)
#connection(
key: "QueuesBookingsList_queuesBookings"
filters: ["queueId"]
) {
edges {
cursor
node {
id
bid
url
}
}
pageInfo {
endCursor
hasNextPage
}
}
}
`,
},
{
direction: 'forward',
query: graphql`
query QueuesBookingsListQuery(
$count: Int!
$after: String
$queueId: ID
) {
...QueuesBookingsList_data
}
`,
getConnectionFromProps(props) {
return props.data && props.data.queuesBookings;
},
getFragmentVariables(prevVars, totalCount) {
console.log({ prevVars });
return {
...prevVars,
count: totalCount,
};
},
getVariables(props, variables, fragmentVariables) {
return {
count: variables.count,
after: variables.cursor,
queueId: fragmentVariables.queueId,
};
},
},
);
As I figure out, there are two solutions, use refechConnection method for Pagination Container or use Refech Container.