-
Notifications
You must be signed in to change notification settings - Fork 8
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add
SaveBoundary
to allow saving multiple things (e.g., forms) with…
… a centralized save button (#1448) This introduces a SaveBoundary (context) that can contain multiple areas that get saved together using a SaveBoundarySaveButton. Any component can register itself in the SaveBoundary by providing a hasChanges boolean and a doSave method. This also adds a default implementation for FinalForm: - if FinalForm is rendered inside a SaveBoundary, it registers - else the FinalForm renders (like previously) a router Prompt Prompts are not needed for the inner components, instead SaveBoundary renders a single prompt containing all hasChanges from registered components. See products admin for an implementation example. Also changed in Demo Products: ProductsPage now contains Toolbar and Save Button for Detail Page. That way the form is now completely independent of where it is used and could also be put into an EditDialog. #### Alternative: A possible alternative would be to re-use the router Prompt component that also has a save action. But I decided for an additional implementation because: - PromptHandler stores dirty in a ref instead of state, so enabling/disabling save button is not possible (could be changed to state though) - Prompt has no clear edges, as a new SubmissionBoundary has --- see #1449 for additional usage of SubmissionBoundary --------- Co-authored-by: Johannes Obermair <48853629+johnnyomair@users.noreply.github.com>
- Loading branch information
1 parent
9d47b5b
commit 8eb1375
Showing
12 changed files
with
680 additions
and
74 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
--- | ||
"@comet/admin": minor | ||
--- | ||
|
||
Add `SaveBoundary` and `SaveBoundarySaveButton` that helps implementing multiple forms with a centralized save button | ||
|
||
Render a `Savable` Component anywhere below a `SaveBoundary`. For `FinalForm` this hasn't to be done manually. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,6 @@ export const productFormFragment = gql` | |
title | ||
slug | ||
description | ||
price | ||
type | ||
inStock | ||
image | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
import { gql } from "@apollo/client"; | ||
|
||
export const productPriceFormFragment = gql` | ||
fragment ProductPriceForm on Product { | ||
price | ||
} | ||
`; | ||
|
||
export const productPriceFormQuery = gql` | ||
query ProductPriceForm($id: ID!) { | ||
product(id: $id) { | ||
id | ||
updatedAt | ||
...ProductPriceForm | ||
} | ||
} | ||
${productPriceFormFragment} | ||
`; | ||
|
||
export const updateProductPriceFormMutation = gql` | ||
mutation ProductPriceFormUpdateProduct($id: ID!, $input: ProductUpdateInput!, $lastUpdatedAt: DateTime) { | ||
updateProduct(id: $id, input: $input, lastUpdatedAt: $lastUpdatedAt) { | ||
id | ||
updatedAt | ||
...ProductPriceForm | ||
} | ||
} | ||
${productPriceFormFragment} | ||
`; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
import { useApolloClient, useQuery } from "@apollo/client"; | ||
import { Field, FinalForm, FinalFormInput, FinalFormSubmitEvent, MainContent, useFormApiRef } from "@comet/admin"; | ||
import { EditPageLayout, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin"; | ||
import { CircularProgress } from "@mui/material"; | ||
import { FormApi } from "final-form"; | ||
import { filter } from "graphql-anywhere"; | ||
import isEqual from "lodash.isequal"; | ||
import React from "react"; | ||
import { FormattedMessage } from "react-intl"; | ||
|
||
import { productPriceFormFragment, productPriceFormQuery, updateProductPriceFormMutation } from "./ProductPriceForm.gql"; | ||
import { | ||
GQLProductPriceFormFragment, | ||
GQLProductPriceFormQuery, | ||
GQLProductPriceFormQueryVariables, | ||
GQLProductPriceFormUpdateProductMutation, | ||
GQLProductPriceFormUpdateProductMutationVariables, | ||
} from "./ProductPriceForm.gql.generated"; | ||
|
||
interface FormProps { | ||
id: string; | ||
} | ||
|
||
type FormValues = Omit<GQLProductPriceFormFragment, "price"> & { | ||
price: string; | ||
}; | ||
|
||
function ProductPriceForm({ id }: FormProps): React.ReactElement { | ||
const client = useApolloClient(); | ||
const formApiRef = useFormApiRef<FormValues>(); | ||
|
||
const { data, error, loading, refetch } = useQuery<GQLProductPriceFormQuery, GQLProductPriceFormQueryVariables>(productPriceFormQuery, { | ||
variables: { id }, | ||
}); | ||
|
||
const initialValues: Partial<FormValues> = data?.product | ||
? { | ||
...filter<GQLProductPriceFormFragment>(productPriceFormFragment, data.product), | ||
price: String(data.product.price), | ||
} | ||
: {}; | ||
|
||
const saveConflict = useFormSaveConflict({ | ||
checkConflict: async () => { | ||
const updatedAt = await queryUpdatedAt(client, "product", id); | ||
return resolveHasSaveConflict(data?.product.updatedAt, updatedAt); | ||
}, | ||
formApiRef, | ||
loadLatestVersion: async () => { | ||
await refetch(); | ||
}, | ||
}); | ||
|
||
const handleSubmit = async (formValues: FormValues, form: FormApi<FormValues>, event: FinalFormSubmitEvent) => { | ||
if (await saveConflict.checkForConflicts()) throw new Error("Conflicts detected"); | ||
const output = { | ||
...formValues, | ||
price: parseFloat(formValues.price), | ||
}; | ||
await client.mutate<GQLProductPriceFormUpdateProductMutation, GQLProductPriceFormUpdateProductMutationVariables>({ | ||
mutation: updateProductPriceFormMutation, | ||
variables: { id, input: output, lastUpdatedAt: data?.product.updatedAt }, | ||
}); | ||
}; | ||
|
||
if (error) { | ||
return <FormattedMessage id="common.error" defaultMessage="An error has occured. Please try again at later" />; | ||
} | ||
|
||
if (loading) { | ||
return <CircularProgress />; | ||
} | ||
|
||
return ( | ||
<FinalForm<FormValues> | ||
apiRef={formApiRef} | ||
onSubmit={handleSubmit} | ||
mode="edit" | ||
initialValues={initialValues} | ||
initialValuesEqual={isEqual} //required to compare block data correctly | ||
onAfterSubmit={(values, form) => { | ||
//don't go back automatically TODO remove this automatismn | ||
}} | ||
subscription={{}} | ||
> | ||
{() => ( | ||
<EditPageLayout> | ||
{saveConflict.dialogs} | ||
<MainContent> | ||
<Field | ||
fullWidth | ||
name="price" | ||
component={FinalFormInput} | ||
inputProps={{ type: "number" }} | ||
label={<FormattedMessage id="product.price" defaultMessage="Price" />} | ||
/> | ||
</MainContent> | ||
</EditPageLayout> | ||
)} | ||
</FinalForm> | ||
); | ||
} | ||
|
||
export default ProductPriceForm; |
Oops, something went wrong.