Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(dashboard): metadata component (#7117)
**What** - add new metadata component **Note** - _example of usage on customer edit form_ - we are not handling update metadata case in the internal module service so for now delete case doesn't work properly --- https://github.com/medusajs/medusa/assets/16856471/b588752d-9cf5-4d96-9cf8-760a764ab03e
- Loading branch information
Showing
11 changed files
with
278 additions
and
5 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
1 change: 1 addition & 0 deletions
1
packages/admin-next/dashboard/src/components/forms/metadata/index.ts
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 @@ | ||
export * from "./metadata" |
157 changes: 157 additions & 0 deletions
157
packages/admin-next/dashboard/src/components/forms/metadata/metadata.tsx
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,157 @@ | ||
import { useEffect } from "react" | ||
import { useTranslation } from "react-i18next" | ||
import { Alert, Button, Input, Text } from "@medusajs/ui" | ||
import { Trash } from "@medusajs/icons" | ||
import { UseFormReturn } from "react-hook-form" | ||
|
||
import { MetadataField } from "../../../lib/metadata" | ||
|
||
type MetadataProps = { | ||
form: UseFormReturn<MetadataField[]> | ||
} | ||
|
||
type FieldProps = { | ||
field: MetadataField | ||
isLast: boolean | ||
onDelete: () => void | ||
updateKey: (key: string) => void | ||
updateValue: (value: string) => void | ||
} | ||
|
||
function Field({ | ||
field, | ||
updateKey, | ||
updateValue, | ||
onDelete, | ||
isLast, | ||
}: FieldProps) { | ||
const { t } = useTranslation() | ||
|
||
/** | ||
* value on the index of deleted field will be undefined, | ||
* but we need to keep it to preserve list ordering | ||
* so React could correctly render elements when adding/deleting | ||
*/ | ||
if (field.isDeleted || field.isIgnored) { | ||
return null | ||
} | ||
|
||
return ( | ||
<tr className="group divide-x [&:not(:last-child)]:border-b"> | ||
<td> | ||
<Input | ||
className="rounded-none border-none bg-transparent !shadow-none" | ||
placeholder={t("fields.key")} | ||
defaultValue={field.key} | ||
onChange={(e) => { | ||
updateKey(e.currentTarget.value) | ||
}} | ||
/> | ||
</td> | ||
<td className="relative"> | ||
<Input | ||
className="rounded-none border-none bg-transparent pr-[40px] !shadow-none" | ||
placeholder={t("fields.value")} | ||
defaultValue={field.value} | ||
onChange={(e) => { | ||
updateValue(e.currentTarget.value) | ||
}} | ||
/> | ||
{!isLast && ( | ||
<Button | ||
size="small" | ||
variant="transparent" | ||
className="text-ui-fg-subtle invisible absolute right-0 top-0 h-[32px] w-[32px] p-0 hover:bg-transparent group-hover:visible" | ||
type="button" | ||
onClick={onDelete} | ||
> | ||
<Trash /> | ||
</Button> | ||
)} | ||
</td> | ||
</tr> | ||
) | ||
} | ||
|
||
export function Metadata({ form }: MetadataProps) { | ||
const { t } = useTranslation() | ||
|
||
const metadataWatch = form.watch("metadata") as MetadataField[] | ||
const ignoredKeys = metadataWatch.filter((k) => k.isIgnored) | ||
|
||
const addKeyPair = () => { | ||
form.setValue( | ||
`metadata.${metadataWatch.length ? metadataWatch.length : 0}`, | ||
{ key: "", value: "" } | ||
) | ||
} | ||
|
||
const onKeyChange = (index: number) => { | ||
return (key: string) => { | ||
form.setValue(`metadata.${index}.key`, key, { shouldDirty: true }) | ||
|
||
if (index === metadataWatch.length - 1) { | ||
addKeyPair() | ||
} | ||
} | ||
} | ||
|
||
const onValueChange = (index: number) => { | ||
return (value: any) => { | ||
form.setValue(`metadata.${index}.value`, value, { shouldDirty: true }) | ||
|
||
if (index === metadataWatch.length - 1) { | ||
addKeyPair() | ||
} | ||
} | ||
} | ||
|
||
const deleteKeyPair = (index: number) => { | ||
return () => { | ||
form.setValue(`metadata.${index}.isDeleted`, true, { shouldDirty: true }) | ||
} | ||
} | ||
|
||
return ( | ||
<div> | ||
<Text weight="plus" size="small"> | ||
{t("fields.metadata")} | ||
</Text> | ||
<table className="shadow-elevation-card-rest mt-2 w-full table-fixed overflow-hidden rounded"> | ||
<thead> | ||
<tr className="bg-ui-bg-field divide-x border-b"> | ||
<th> | ||
<Text className="px-2 py-1 text-left" weight="plus" size="small"> | ||
{t("fields.key")} | ||
</Text> | ||
</th> | ||
<th> | ||
<Text className="px-2 py-1 text-left" weight="plus" size="small"> | ||
{t("fields.value")} | ||
</Text> | ||
</th> | ||
</tr> | ||
</thead> | ||
<tbody> | ||
{metadataWatch.map((field, index) => { | ||
return ( | ||
<Field | ||
key={index} | ||
field={field} | ||
updateKey={onKeyChange(index)} | ||
updateValue={onValueChange(index)} | ||
onDelete={deleteKeyPair(index)} | ||
isLast={index === metadataWatch.length - 1} | ||
/> | ||
) | ||
})} | ||
</tbody> | ||
</table> | ||
{!!ignoredKeys.length && ( | ||
<Alert variant="warning" className="mt-2" dismissible> | ||
{t("metadata.warnings.ignoredKeys", { keys: ignoredKeys.join(",") })} | ||
</Alert> | ||
)} | ||
</div> | ||
) | ||
} |
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,84 @@ | ||
export type MetadataField = { | ||
key: string | ||
value: string | ||
/** | ||
* Is the field provided as initial data | ||
*/ | ||
isInitial?: boolean | ||
/** | ||
* Whether the row was deleted | ||
*/ | ||
isDeleted?: boolean | ||
/** | ||
* True for initial values that are not primitives | ||
*/ | ||
isIgnored?: boolean | ||
} | ||
|
||
const isPrimitive = (value: any): boolean => { | ||
return ( | ||
value === null || | ||
value === undefined || | ||
typeof value === "string" || | ||
typeof value === "number" || | ||
typeof value === "boolean" | ||
) | ||
} | ||
|
||
/** | ||
* Convert metadata property to an array of form filed values. | ||
*/ | ||
export const metadataToFormValues = ( | ||
metadata?: Record<string, any> | null | ||
): MetadataField[] => { | ||
const data: MetadataField[] = [] | ||
|
||
if (metadata) { | ||
Object.entries(metadata).forEach(([key, value]) => { | ||
data.push({ | ||
key, | ||
value: value as string, | ||
isInitial: true, | ||
isIgnored: !isPrimitive(value), | ||
isDeleted: false, | ||
}) | ||
}) | ||
} | ||
|
||
// DEFAULT field for adding a new metadata record | ||
// it's added here so it's registered as a default value | ||
data.push({ | ||
key: "", | ||
value: "", | ||
isInitial: false, | ||
isIgnored: false, | ||
isDeleted: false, | ||
}) | ||
|
||
return data | ||
} | ||
|
||
/** | ||
* Convert a form fields array to a metadata object | ||
*/ | ||
export const formValuesToMetadata = ( | ||
data: MetadataField[] | ||
): Record<string, unknown> => { | ||
return data.reduce((acc, { key, value, isDeleted, isIgnored, isInitial }) => { | ||
if (isIgnored) { | ||
acc[key] = value | ||
return acc | ||
} | ||
|
||
if (isDeleted && isInitial) { | ||
acc[key] = "" | ||
return acc | ||
} | ||
|
||
if (key) { | ||
acc[key] = value // TODO: since these are primitives should we parse strings to their primitive format e.g. "123" -> 123 , "true" -> true | ||
} | ||
|
||
return acc | ||
}, {} as Record<string, unknown>) | ||
} |
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
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
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
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