react-hook-form#useFieldArray TypeScript Examples
The following examples show how to use
react-hook-form#useFieldArray.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: ConditionEntries.tsx From firebase-tools-ui with Apache License 2.0 | 6 votes |
ConditionEntries: React.FC<
React.PropsWithChildren<{ name: string }>
> = ({ name }) => {
const {
formState: { errors, touchedFields },
} = useFormContext();
const { fields, append, remove } = useFieldArray({
name,
});
useEffect(() => {
if (fields.length < 1) {
append('');
}
}, [append, fields]);
return (
<ul className={styles.conditionEntries}>
{fields.map((field, index) => {
const conditionName = `${name}[${index}]`;
return (
<li key={field.id}>
<ConditionEntry
name={conditionName}
error={touchedFields[conditionName] && errors[conditionName]}
/>
<IconButton
className={
fields.length > 1 ? styles.removeFilter : styles.hidden
}
icon="delete"
label="Remove filter"
type="button"
onClick={() => remove(index)}
/>
</li>
);
})}
<Button type="button" icon="add" onClick={() => append('')}>
Add value
</Button>
</ul>
);
}
Example #2
Source File: usePricesFieldArray.ts From admin with MIT License | 5 votes |
usePricesFieldArray = <TKeyName extends string = "id">(
currencyCodes: string[],
{ control, name, keyName }: UseFieldArrayOptions<TKeyName>,
options: UsePricesFieldArrayOptions = {
defaultAmount: 1000,
defaultCurrencyCode: "usd",
}
) => {
const { defaultAmount, defaultCurrencyCode } = options
const { fields, append, remove } = useFieldArray<PriceFormValue, TKeyName>({
control,
name,
keyName,
})
const watchedFields = useWatch({
control,
name,
defaultValue: fields,
})
const selectedCurrencies = watchedFields.map(
(field) => field?.price?.currency_code
)
const availableCurrencies = currencyCodes?.filter(
(currency) => !selectedCurrencies.includes(currency)
)
const controlledFields = fields.map((field, index) => {
return {
...field,
...watchedFields[index],
}
})
const appendPrice = () => {
let newCurrency = availableCurrencies[0]
if (!selectedCurrencies.includes(defaultCurrencyCode)) {
newCurrency = defaultCurrencyCode
}
append({
price: { currency_code: newCurrency, amount: defaultAmount },
})
}
const deletePrice = (index) => {
return () => {
remove(index)
}
}
return {
fields: controlledFields,
appendPrice,
deletePrice,
availableCurrencies,
selectedCurrencies,
} as const
}
Example #3
Source File: APISchemaForm.tsx From one-platform with MIT License | 4 votes |
APISchemaForm = forwardRef<HTMLDivElement, Props>(
({ handleSchemaValidation, isUpdate }: Props, ref): JSX.Element => {
const { control } = useFormContext<FormData>();
const { append, fields, remove } = useFieldArray({
control,
name: 'schemas',
});
const onAppendSchema = () => {
append({
appURL: '',
description: '',
docURL: '',
environments: [
{
apiBasePath: '',
headers: [{ key: '', value: '', id: undefined }],
isPublic: false,
name: '',
schemaEndpoint: '',
slug: '',
id: undefined,
},
],
});
};
const onRemoveSchema = (index: number) => {
remove(index);
};
return (
<Stack hasGutter ref={ref}>
{fields.map(({ id }, index) => (
<StackItem key={id}>
<Card>
<CardTitle>
<Split>
<SplitItem isFilled>
<Title headingLevel="h2">API Schema #{index + 1}</Title>
</SplitItem>
<SplitItem>
<Button
variant="secondary"
aria-label="Remove"
onClick={() => onRemoveSchema(index)}
className="trash-button"
>
<TrashIcon />
</Button>
</SplitItem>
</Split>
</CardTitle>
<CardBody>
<Stack hasGutter>
<StackItem>
<Controller
control={control}
name={`schemas.${index}.name`}
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormGroup
label="Schema Name"
isRequired
fieldId="api-schema-name"
validated={error ? 'error' : 'success'}
helperTextInvalid={error?.message}
>
<TextInput
isRequired
id="api-schema-name"
placeholder="Enter schema name"
{...field}
/>
</FormGroup>
)}
/>
</StackItem>
<StackItem>
<Controller
control={control}
name={`schemas.${index}.description`}
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormGroup
label="Description"
isRequired
fieldId="api-schema-name"
validated={error ? 'error' : 'success'}
helperTextInvalid={error?.message}
>
<TextArea
isRequired
id="api-source-name"
placeholder="Give a name for the API datasource"
{...field}
/>
</FormGroup>
)}
/>
</StackItem>
<StackItem>
<Controller
control={control}
name={`schemas.${index}.category`}
render={({ field: { value, onChange }, fieldState: { error } }) => (
<ApiTypeSelector
value={value as ApiCategory}
onChange={onChange}
errorMsg={error?.message}
/>
)}
/>
</StackItem>
<StackItem>
<Controller
name={`schemas.${index}.flags.isInternal`}
defaultValue={false}
render={({ field }) => (
<Checkbox
label="Is this API for internal users only?"
description="Tick this option if this particular API is designed to be used only Red Hat internally"
isChecked={field.value}
id={`api-schema-${index}-internal-flag`}
{...field}
/>
)}
/>
</StackItem>
<StackItem>
<Split hasGutter>
<SplitItem isFilled>
<Controller
control={control}
name={`schemas.${index}.appURL`}
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormGroup
label="Application URL"
isRequired
fieldId={`schemas.${index}.appUrl`}
validated={error ? 'error' : 'success'}
helperTextInvalid={error?.message}
>
<TextInput
isRequired
id={`schemas.${index}.appUrl`}
placeholder="Enter the URL of the App"
{...field}
/>
</FormGroup>
)}
/>
</SplitItem>
<SplitItem isFilled>
<Controller
control={control}
name={`schemas.${index}.docURL`}
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormGroup
label="Documentation URL"
fieldId={`schemas.${index}.docURL`}
validated={error ? 'error' : 'success'}
helperTextInvalid={error?.message}
>
<TextInput
isRequired
id={`schemas.${index}.docURL`}
placeholder="Enter the URL of API documentaiton"
{...field}
/>
</FormGroup>
)}
/>
</SplitItem>
</Split>
</StackItem>
{isUpdate && (
<StackItem>
<Controller
name={`schemas.${index}.flags.isDeprecated`}
defaultValue={false}
render={({ field }) => (
<Checkbox
label="Is this API deprecated?"
isChecked={field.value}
id={`api-schema-${index}-deprecated-flag`}
{...field}
/>
)}
/>
</StackItem>
)}
<StackItem>
<EnvironmentFormSection
schemaPos={index}
handleSchemaValidation={handleSchemaValidation}
/>
</StackItem>
{index === fields.length - 1 && (
<StackItem>
<Button icon={<PlusIcon />} iconPosition="left" onClick={onAppendSchema}>
Add Schema
</Button>
</StackItem>
)}
</Stack>
</CardBody>
</Card>
</StackItem>
))}
</Stack>
);
}
)
Example #4
Source File: EnvironmentFormSection.tsx From one-platform with MIT License | 4 votes |
EnvironmentFormSection = ({
schemaPos,
handleSchemaValidation,
}: Props): JSX.Element => {
const [envNames, setEnvNames] = useState(['prod', 'stage', 'qa', 'dev']);
const { control, watch, getValues, setError, clearErrors, setValue } = useFormContext<FormData>();
const isGraphqlAPI = watch(`schemas.${schemaPos}.category`) === ApiCategory.GRAPHQL;
const { fields, append, remove } = useFieldArray({
control,
name: `schemas.${schemaPos}.environments`,
});
const handleAddNewEnvironment = () => {
append(
{
name: '',
apiBasePath: '',
headers: [{ key: '', value: '', id: undefined }],
schemaEndpoint: '',
isPublic: false,
},
{ shouldFocus: false }
);
};
const onEnvNameClear = (onChange: (...event: any[]) => void) => {
onChange('');
};
const onEnvNameSelect = (
onChange: (...event: any[]) => void,
event: React.MouseEventHandler,
selection: string,
isPlaceholder: boolean
) => {
if (isPlaceholder) onEnvNameClear(onChange);
else {
onChange(selection);
}
};
const onEnvNameCreate = (newSelection: string) => {
if (envNames.findIndex((envName) => envName === newSelection) === -1) {
setEnvNames((envState) => [...envState, newSelection]);
}
};
const onSetIntrospectionQuery = (envIndex: number) => {
const selectedEnv = `schemas.${schemaPos}.environments.${envIndex}` as const;
const value = getValues(selectedEnv);
setValue(`schemas.${schemaPos}.environments.${envIndex}.schemaEndpoint`, value.apiBasePath);
};
const setSchemaEndpointIsInvalid = (envIndex: number) => {
setError(`schemas.${schemaPos}.environments.${envIndex}.schemaEndpoint`, {
type: 'custom',
message: `Failed to get ${isGraphqlAPI ? 'introspection url' : 'api schema'}`,
});
};
const handleSchemaVerification = async (envIndex: number, schemaURL: string) => {
if (!schemaURL) return;
const isURL = isValidURL(schemaURL);
const selectedEnv = `schemas.${schemaPos}.environments.${envIndex}` as const;
if (isURL) {
const envData = getValues(selectedEnv) as ApiEnvironmentType;
const { slug, schemaEndpoint } = envData;
const category = isGraphqlAPI ? ApiCategory.GRAPHQL : ApiCategory.REST;
const headers = (envData?.headers || []).filter(({ key, value }) => key && value) as Header[];
const data = await handleSchemaValidation({
headers,
schemaEndpoint,
envSlug: slug,
category,
});
if (!data?.file) {
setSchemaEndpointIsInvalid(envIndex);
} else {
clearErrors(`schemas.${schemaPos}.environments.${envIndex}.schemaEndpoint`);
}
} else {
setSchemaEndpointIsInvalid(envIndex);
window.OpNotification.danger({ subject: 'Invalid schema url provided' });
}
};
return (
<Stack hasGutter className="pf-u-mt-md">
<StackItem>
<p className="pf-u-font-weight-bold">Environments</p>
</StackItem>
<StackItem>
<Stack hasGutter>
{fields.map((field, index) => (
<StackItem key={field.id}>
<Card id={field.id}>
<CardBody>
<Grid hasGutter>
<GridItem span={3}>
<Controller
name={`schemas.${schemaPos}.environments.${index}.name`}
control={control}
rules={{ required: true }}
defaultValue=""
render={({ field: controllerField, fieldState: { error } }) => (
<FormGroup
fieldId={`schemas.${schemaPos}.environments.${index}.name`}
label="Name"
isRequired
validated={error ? 'error' : 'success'}
helperTextInvalid={error?.message}
>
<Select
variant={SelectVariant.typeahead}
typeAheadAriaLabel="Select a state"
onSelect={callbackify(onEnvNameSelect, controllerField.onChange)}
onClear={callbackify(onEnvNameClear, controllerField.onChange)}
selections={controllerField.value}
aria-label="env link"
placeholder="Enter environment name"
isCreatable
onCreateOption={onEnvNameCreate}
placeholderText="Enter environment name"
>
{envNames.map((env, envIndex) => (
<SelectOption key={`${env}-${envIndex + 1}`} value={env} />
))}
</Select>
</FormGroup>
)}
/>
</GridItem>
<GridItem span={8}>
<Controller
name={`schemas.${schemaPos}.environments.${index}.apiBasePath`}
control={control}
rules={{ required: true }}
defaultValue=""
render={({ field: controllerField, fieldState: { error } }) => (
<FormGroup
fieldId={`schemas.${schemaPos}.environments.${index}.apiBasePath`}
label="API Base Path"
isRequired
validated={error ? 'error' : 'success'}
helperTextInvalid={error?.message}
>
<TextInput
aria-label="env link"
placeholder="Enter base path for the api"
{...controllerField}
/>
</FormGroup>
)}
/>
</GridItem>
<GridItem
span={1}
className="pf-u-display-flex pf-u-justify-content-center pf-u-align-items-flex-end"
>
<Button
variant="secondary"
aria-label="Remove"
onClick={callbackify(remove, index)}
className={styles['trash-button']}
>
<TrashIcon />
</Button>
</GridItem>
<GridItem span={12}>
<Controller
name={`schemas.${schemaPos}.environments.${index}.schemaEndpoint`}
control={control}
rules={{ required: true }}
defaultValue=""
render={({ field: { ...controllerField }, fieldState: { error } }) => (
<EnvSchemaField
isGraphqlAPI={isGraphqlAPI}
isError={Boolean(error)}
errorMessage={error?.message}
envIndex={index}
onCopyValue={() => onSetIntrospectionQuery(index)}
onRedoValidation={() =>
handleSchemaVerification(index, controllerField.value || '')
}
{...controllerField}
/>
)}
/>
</GridItem>
<GridItem span={12}>
<EnvHeaderFormSection schemaPos={schemaPos} envPos={index} />
</GridItem>
<GridItem span={12}>
<Controller
name={`schemas.${schemaPos}.environments.${index}.isPublic`}
defaultValue={false}
render={({ field: controllerField }) => (
<Checkbox
label="Is this API accessible from public?"
description="Tick this option if your environment can be accessed without VPN"
isChecked={controllerField.value}
id={`api-schema-${schemaPos}-env-${index}-internal`}
{...controllerField}
/>
)}
/>
</GridItem>
</Grid>
</CardBody>
</Card>
</StackItem>
))}
</Stack>
</StackItem>
<StackItem>
<Button
variant="link"
icon={<PlusIcon size="sm" />}
className="pf-u-p-0 pf-u-mb-lg"
onClick={handleAddNewEnvironment}
>
Add Environment
</Button>
</StackItem>
</Stack>
);
}
Example #5
Source File: EnvHeaderFormSection.tsx From one-platform with MIT License | 4 votes |
EnvHeaderFormSection = ({ schemaPos, envPos }: Props): JSX.Element => {
const { control, watch } = useFormContext<FormData>();
const { fields, append, remove } = useFieldArray({
control,
name: `schemas.${schemaPos}.environments.${envPos}.headers`,
});
const headerFields = watch(`schemas.${schemaPos}.environments.${envPos}.headers`);
const handleRemoveHeader = (indexToRemove: number) => {
remove(indexToRemove);
};
return (
<Stack hasGutter>
<StackItem style={{ marginBottom: 0 }}>
<Split>
<SplitItem isFilled>
<p className="pf-u-font-weight-bold" style={{ marginBottom: '-1rem' }}>
Headers
</p>
</SplitItem>
<SplitItem>
<Button
icon={<PlusIcon />}
variant="link"
isSmall
className="pf-u-mb-xs"
onClick={() => append({ id: undefined, value: '', key: '' })}
>
Add Header
</Button>
</SplitItem>
</Split>
</StackItem>
{fields.map((field, index) => (
<StackItem key={field.id}>
<Split hasGutter>
<SplitItem isFilled>
<Controller
name={`schemas.${schemaPos}.environments.${envPos}.headers.${index}.key`}
control={control}
defaultValue=""
render={({ field: controllerField, fieldState: { error } }) => (
<FormGroup
fieldId={`headers.${index}.key`}
isRequired
validated={error ? 'error' : 'success'}
helperTextInvalid={error?.message}
>
<TextInput
aria-label="header name"
placeholder="Content-Type"
{...controllerField}
/>
</FormGroup>
)}
/>
</SplitItem>
<SplitItem isFilled>
<Controller
name={`schemas.${schemaPos}.environments.${envPos}.headers.${index}.value`}
control={control}
defaultValue=""
render={({ field: controllerField, fieldState: { error } }) => (
<FormGroup
fieldId={`headers.${index}.value`}
isRequired
validated={error ? 'error' : 'success'}
helperTextInvalid={error?.message}
>
<TextInput
aria-label="header url"
isDisabled={Boolean(headerFields?.[index].id)}
placeholder="**********"
type="password"
{...controllerField}
/>
</FormGroup>
)}
/>
</SplitItem>
<SplitItem>
<Button
variant="secondary"
onClick={callbackify(handleRemoveHeader, index)}
className={styles['trash-button']}
>
<TrashIcon />
</Button>
</SplitItem>
</Split>
</StackItem>
))}
</Stack>
);
}
Example #6
Source File: Step2.tsx From one-platform with MIT License | 4 votes |
export default function ConfigureSearchStep2 ( { onNext, onBack, onReset }: IConfigureSearchStepProps ) {
const { control, handleSubmit, setValue, getValues, formState: { isValid } } = useForm<Step2>( {
mode: 'onBlur',
resolver: yupResolver( formSchema ),
} );
const headers = useFieldArray({
control,
name: 'apiHeaders',
} );
const { actions, state } = useStateMachine({ nextStep: saveState });
const formData = state.formData;
const authLocations = [ 'header' ];
const authTypes = ['Basic', 'Bearer', 'apikey'];
const [isOpenAuthLocationDropdown, setIsOpenAuthLocationDrowdown] = useState(false);
const toggleAuthLocationDropdown = useCallback(
(force = false) => {
setIsOpenAuthLocationDrowdown(!isOpenAuthLocationDropdown && force);
},
[isOpenAuthLocationDropdown]
);
const selectAuthLocation = useCallback(
({ target }) => {
setValue('authorization.location', target.dataset.value);
toggleAuthLocationDropdown(true);
},
[setValue, toggleAuthLocationDropdown]
);
const saveAndNext = useCallback((data: Step2) => {
if ( !isEmpty( data.authorization ) ) {
data.authorization.key = 'Authorization';
}
actions.nextStep({
formData: {
...state.formData,
...data,
},
});
onNext?.();
}, [actions, onNext, state.formData]);
return (
<Form onSubmit={handleSubmit(saveAndNext)}>
<FormSection title="Step 2: Auth and other headers">
<FormGroup
fieldId="apiAuthCredentials"
label="Authorization"
helperText="If the API is authenticated, provide the authorization details"
labelIcon={
<Controller
name="authorization.location"
control={control}
defaultValue={
formData?.authorization?.location || (authLocations[0] as any)
}
render={({ field }) => (
<Dropdown
isOpen={isOpenAuthLocationDropdown}
onSelect={(event) => selectAuthLocation(event)}
toggle={
<BadgeToggle
id="toggle-id"
onToggle={toggleAuthLocationDropdown}
>
{getValues('authorization.location')}
</BadgeToggle>
}
dropdownItems={authLocations.map((location) => (
<DropdownItem key={location} data-value={location}>
{location}
</DropdownItem>
))}
/>
)}
/>
}
>
<Grid>
<GridItem span={2}>
<Controller
name="authorization.authType"
control={control}
defaultValue={formData?.authorization?.authType ?? authTypes[0]}
render={({ field }) => (
<FormSelect
id="apiAuthType"
{...field}
aria-label="Authorization Type"
>
{authTypes.map((authType) => (
<FormSelectOption
key={authType}
value={authType}
label={authType}
/>
))}
</FormSelect>
)}
/>
</GridItem>
<GridItem span={10}>
<Controller
name="authorization.credentials"
control={control}
defaultValue={formData?.authorization?.credentials}
render={({ field }) => (
<TextInput
id="apiAuthCredentials"
aria-label="Authorization Credentials"
placeholder="auth_token"
type="password"
{...field}
/>
)}
/>
</GridItem>
</Grid>
</FormGroup>
<FormGroup
fieldId="apiHeaders"
label="Headers"
helperText="Custom Headers that should be passed to the API Endpoint"
labelIcon={
<Controller
name="authorization.location"
control={control}
defaultValue={formData?.authorization?.location}
render={({ field }) => (
<Button
variant="link"
isInline
onClick={() => headers.append({ key: '', value: '' })}
>
<ion-icon name="add-circle-outline"></ion-icon>
Add
</Button>
)}
/>
}
>
<Stack>
{headers.fields.map(({ id }, index) => (
<StackItem key={id}>
<Grid>
<GridItem span={4}>
<Controller
control={control}
name={`apiHeaders.${index}.key` as const}
defaultValue={
(formData?.apiHeaders?.[index]?.key as any) || ''
}
render={({ field }) => (
<TextInput
{...field}
aria-label="Header Key"
placeholder="header"
/>
)}
/>
</GridItem>
<GridItem span={7}>
<Controller
control={control}
name={`apiHeaders.${index}.value` as const}
defaultValue={
(formData?.apiHeaders?.[index]?.value as any) ?? ''
}
render={({ field }) => (
<TextInput
{...field}
aria-label="Header Value"
placeholder="value"
/>
)}
/>
</GridItem>
<GridItem span={1}>
<Button
variant="link"
isDanger
onClick={() => headers.remove(index)}
>
<ion-icon name="remove-circle-outline"></ion-icon>
</Button>
</GridItem>
</Grid>
</StackItem>
))}
</Stack>
</FormGroup>
</FormSection>
<ActionGroup>
<Button variant="primary" type="submit" isDisabled={!isValid}>
Next
</Button>
<Button variant="secondary" type="button" onClick={onBack}>
Back
</Button>
<Button variant="plain" type="button" onClick={onReset}>
Reset
</Button>
</ActionGroup>
</Form>
);
}
Example #7
Source File: Containers.tsx From react-hook-form-generator with MIT License | 4 votes |
ArrayField: FC<FieldProps<ArrayFieldSchema>> = ({
name,
field,
}) => {
const {
label,
isRequired,
isCollapsable,
itemField,
helperText,
shouldDisplay,
styles = {},
} = field;
const { control, watch } = useFormContext();
const values = watch(name);
const { fields, append, remove } = useFieldArray({ name, control });
const { isOpen, onOpen, onToggle } = useDisclosure(true);
const arrayStyles = useStyles<ArrayFieldStyles>('arrayField', styles);
const errorMessage = useErrorMessage(name, label);
const addItem = () => {
append({});
onOpen();
};
const isVisible = useMemo(() => {
return shouldDisplay ? shouldDisplay(values) : true;
}, [values, shouldDisplay]);
return isVisible ? (
<FormControl
isRequired={isRequired}
isInvalid={!!errorMessage}
{...arrayStyles.control}
>
<Flex {...arrayStyles.toolbar}>
{!!label && (
<FormLabel htmlFor={name} {...arrayStyles.label}>
{label}{' '}
<PseudoBox {...arrayStyles.countText}>({fields.length})</PseudoBox>
</FormLabel>
)}
<ButtonGroup {...arrayStyles.buttonGroup}>
<IconButton
icon="add"
aria-label="Add item"
onClick={addItem}
{...arrayStyles.addButton}
/>
<IconButton
icon="delete"
aria-label="Clear items"
onClick={() => remove()}
{...arrayStyles.clearButton}
/>
{isCollapsable && (
<IconButton
icon={isOpen ? 'view-off' : 'view'}
aria-label={isOpen ? 'Hide items' : 'Show items'}
onClick={onToggle}
{...arrayStyles.collapseButton}
/>
)}
</ButtonGroup>
</Flex>
<Collapse isOpen={isOpen}>
<Stack {...arrayStyles.arrayContainer}>
{fields.map((item, i) => (
<Box
key={item?.id || `${name}[${i}].value`}
{...arrayStyles.itemContainer}
>
{renderField(
[`${name}[${i}].value`, itemField],
item.id,
item.value
)}
<Box {...arrayStyles.deleteItemContainer}>
<IconButton
icon="delete"
aria-label="Delete item"
onClick={() => remove(i)}
{...arrayStyles.deleteButton}
/>
</Box>
</Box>
))}
</Stack>
</Collapse>
{!!helperText && (
<FormHelperText {...arrayStyles.helperText}>
{helperText}
</FormHelperText>
)}
<FormErrorMessage {...arrayStyles.errorMessage}>
{errorMessage}
</FormErrorMessage>
</FormControl>
) : null;
}
Example #8
Source File: NFTTransfer.tsx From homebase-app with MIT License | 4 votes |
NFTTransferForm: React.FC = () => {
const {
control,
getValues,
setValue,
watch,
formState: {errors, touchedFields: touched},
} = useFormContext<NFTTransferFormValues>();
const {fields, append} = useFieldArray({
control,
name: "nftTransferForm.transfers",
});
const values = getValues();
const [isBatch, setIsBatch] = useState(values.nftTransferForm.isBatch);
const [activeTransfer, setActiveTransfer] = React.useState(1);
const daoId = useDAOID();
const {nftHoldings} = useDAOHoldings(daoId);
const handleIsBatchChange = () => {
setIsBatch(!isBatch);
setValue("nftTransferForm.isBatch", !isBatch);
setActiveTransfer(1);
};
const recipientError = (
errors.nftTransferForm?.transfers?.[activeTransfer - 1] as any
)?.recipient;
const {transfers} = watch("nftTransferForm");
const activeAsset = transfers[activeTransfer - 1].asset;
const takenNFTs = transfers.map(
(t) => `${t.asset?.contract}-${t.asset?.token_id}`
);
const nonSelectedNFTs = nftHoldings
? nftHoldings.filter(
(nft) =>
!takenNFTs.includes(`${nft?.token?.contract}-${nft?.token?.token_id}`)
)
: [];
const nftOptions = nonSelectedNFTs.map((n) => n.token);
return (
<DialogContent>
<Grid container direction={"column"} style={{gap: 31}}>
<Grid item>
<BatchBar isBatch={isBatch} stateIsBatch={values.nftTransferForm.isBatch}
handleIsBatchChange={handleIsBatchChange} onClickAdd={() => {
append(emptyTransfer);
setActiveTransfer(activeTransfer + 1);
}} items={values.nftTransferForm.transfers} activeItem={activeTransfer}
setActiveItem={(index: number) => setActiveTransfer(index + 1)}/>
</Grid>
{fields.map(
(field, index) =>
index === activeTransfer - 1 && (
<>
<ProposalFormInput label={"Recipient"}>
<Controller
key={field.id}
name={`nftTransferForm.transfers.${index}.recipient`}
control={control}
render={({field}) => (
<TextField
{...field}
type="string"
placeholder="Type an Address Here"
InputProps={{disableUnderline: true}}
/>
)}
/>
{recipientError &&
touched.nftTransferForm?.transfers?.[activeTransfer - 1]
?.recipient ? (
<ErrorText>{recipientError}</ErrorText>
) : null}
</ProposalFormInput>
<ProposalFormInput label={"NFT ID"}>
<Grid container direction="column">
<Controller
key={field.id}
name={`nftTransferForm.transfers.${index}.asset`}
control={control}
render={({field: {onChange, ...props}}) => (
<AutoCompleteField
PaperComponent={AutoCompletePaper}
options={nftOptions}
getOptionLabel={(option) =>
`${(option as NFTModel).symbol}#${
(option as NFTModel).token_id
}`
}
renderInput={(params) => (
<TextField {...params} label="Select NFT" InputProps={{...params.InputProps, disableUnderline: true}}/>
)}
onChange={(e, data) => onChange(data)}
{...props}
/>
)}
/>
{activeAsset && (
<Grid item>
<Grid container justifyContent="center">
<NFTContainer item>
<NFT
qmHash={activeAsset.artifact_hash}
name={activeAsset.name}
mediaType={activeAsset.mediaType}
/>
</NFTContainer>
</Grid>
</Grid>
)}
</Grid>
</ProposalFormInput>
</>
)
)}
</Grid>
</DialogContent>
);
}
Example #9
Source File: NewTreasuryProposalDialog.tsx From homebase-app with MIT License | 4 votes |
NewTreasuryProposalDialog: React.FC = () => {
const {
control,
getValues,
setValue,
watch,
formState: {errors, touchedFields: touched},
} = useFormContext<TreasuryProposalFormValues>();
const {fields, append} = useFieldArray({
control,
name: "transferForm.transfers",
});
const values = getValues();
const [isBatch, setIsBatch] = useState(values.transferForm.isBatch);
const theme = useTheme();
const isMobileSmall = useMediaQuery(theme.breakpoints.down("sm"));
const [activeTransfer, setActiveTransfer] = React.useState(1);
const daoId = useDAOID();
const {data: daoData} = useDAO(daoId);
const dao = daoData as TreasuryDAO | undefined;
const {tokenHoldings: daoHoldings} = useDAOHoldings(daoId);
const {data: tezosBalance} = useTezosBalance(daoId);
const handleIsBatchChange = () => {
setIsBatch(!isBatch);
setValue("transferForm.isBatch", !isBatch);
setActiveTransfer(1);
};
const recipientError = (
errors.transferForm?.transfers?.[activeTransfer - 1] as any
)?.recipient;
const amountError = (
errors.transferForm?.transfers?.[activeTransfer - 1] as any
)?.amount;
const {transfers} = watch("transferForm");
const currentTransfer = transfers[activeTransfer - 1];
const daoAssets = daoHoldings
? [
...daoHoldings,
{ balance: tezosBalance || new BigNumber(0), token: { symbol: "XTZ" } },
]
: [];
const assetOptions = daoAssets.map((a) => a.token);
const currentAssetBalance = daoAssets.find(asset => asset.token.symbol === currentTransfer.asset?.symbol)
return (
<DialogContent>
<Grid container direction={"column"} style={{gap: 31}}>
<Grid item>
<BatchBar isBatch={isBatch} stateIsBatch={values.transferForm.isBatch}
handleIsBatchChange={handleIsBatchChange} onClickAdd={() => {
append(emptyTransfer);
setActiveTransfer(activeTransfer + 1);
}} items={values.transferForm.transfers} activeItem={activeTransfer}
setActiveItem={(index: number) => setActiveTransfer(index + 1)} />
</Grid>
{fields.map(
(field, index) =>
index === activeTransfer - 1 && (
<>
<ProposalFormInput label={"Recipient"}>
<Controller
key={field.id}
name={`transferForm.transfers.${index}.recipient`}
control={control}
render={({field}) => (
<TextField
{...field}
type="string"
InputProps={{disableUnderline: true}}
placeholder="Type an Address Here"
/>
)}
/>
{recipientError &&
touched.transferForm?.transfers?.[activeTransfer - 1]
?.recipient ? (
<ErrorText>{recipientError}</ErrorText>
) : null}
</ProposalFormInput>
<Grid container alignItems="center" style={{gap: 26}}>
<Grid item xs={isMobileSmall ? 12 : 6}>
<ProposalFormInput label={"Asset"}>
<Controller
key={field.id}
name={`transferForm.transfers.${index}.asset`}
control={control}
render={({field: {onChange, ...props}}) => (
<AutoCompleteField
options={assetOptions || []}
PaperComponent={AutoCompletePaper}
getOptionLabel={(option) =>
(
option as
| Token
| {
symbol: string;
}
).symbol
}
renderInput={(params) => (
<TextField {...params} InputProps={{...params.InputProps, disableUnderline: true}}
label="Select asset"/>
)}
onChange={(e, data) => onChange(data)}
{...props}
/>
)}
/>
</ProposalFormInput>
</Grid>
<Grid item xs={isMobileSmall ? 12 : true}>
<ProposalFormInput label={"Amount"}>
<Controller
key={field.id}
name={`transferForm.transfers.${index}.amount`}
control={control}
render={({field}) => (
<TextField
{...field}
type="tel"
placeholder="0"
InputProps={{
inputProps: {
step: 0.01,
min: dao && dao.data.extra.min_xtz_amount,
max: dao && dao.data.extra.max_xtz_amount,
},
disableUnderline: true,
endAdornment: (
<InputAdornment position="start">
<CurrentAsset
color="textPrimary"
variant="subtitle2"
>
{" "}
{values.transferForm.transfers[
activeTransfer - 1
].asset?.symbol || "-"}
</CurrentAsset>
</InputAdornment>
),
}}
/>
)}
/>
{amountError &&
touched.transferForm?.transfers?.[activeTransfer - 1]
?.amount ? (
<ErrorText>{amountError}</ErrorText>
) : null}
</ProposalFormInput>
</Grid>
<DaoBalance
container
direction="row"
alignItems="center"
justify="space-between"
>
<Grid item xs={6}>
<AmountText>DAO Balance</AmountText>
</Grid>
<Grid item xs={6}>
{daoAssets ? (
<AmountContainer
item
container
direction="row"
justify="flex-end"
>
<AmountText>
{currentAssetBalance?.balance.toString() || "-"}
</AmountText>
<AmountText>
{currentTransfer.asset?.symbol.toString() || "-"}
</AmountText>
</AmountContainer>
) : null}
</Grid>
</DaoBalance>
</Grid>
</>
)
)}
</Grid>
</DialogContent>
);
}
Example #10
Source File: UpdateRegistryDialog.tsx From homebase-app with MIT License | 4 votes |
UpdateRegistryDialog: React.FC = () => {
const [activeItem, setActiveItem] = React.useState(1);
const {
control,
getValues,
setValue,
formState: {errors, touchedFields: touched},
} = useFormContext<RegistryProposalFormValues>();
const {fields, append} = useFieldArray({
control,
name: "registryUpdateForm.list",
});
const values = getValues();
const [isBatch, setIsBatch] = useState(values.registryUpdateForm.isBatch);
const handleIsBatchChange = () => {
setIsBatch(!isBatch);
setValue("registryUpdateForm.isBatch", !isBatch);
setActiveItem(1);
};
const keyError = (errors?.registryUpdateForm?.list?.[activeItem - 1] as any)
?.key;
const valueError = (errors?.registryUpdateForm?.list?.[activeItem - 1] as any)
?.value;
return (
<DialogContent>
<Grid container direction={"column"} style={{gap: 31}}>
<Grid item>
<BatchBar isBatch={isBatch} stateIsBatch={values.registryUpdateForm.isBatch}
handleIsBatchChange={handleIsBatchChange} onClickAdd={() => {
append(emptyItem);
setActiveItem(activeItem + 1);
}} items={values.registryUpdateForm.list} activeItem={activeItem}
setActiveItem={(index: number) => setActiveItem(index + 1)}/>
</Grid>
{fields.map(
(field, index) =>
index === activeItem - 1 && (
<>
<Grid item>
<ProposalFormInput label={"Key"}>
<Controller
key={field.id}
name={`registryUpdateForm.list.${activeItem - 1}.key`}
control={control}
render={({field}) => (
<TextField
{...field}
type="string"
InputProps={{disableUnderline: true}}
placeholder="Type a Key"
/>
)}
/>
{keyError &&
touched.registryUpdateForm?.list?.[activeItem - 1]
?.key ? (
<ErrorText>{keyError}</ErrorText>
) : null}
</ProposalFormInput>
</Grid>
<Grid>
<ProposalFormInput label={"Value"}>
<Controller
key={field.id}
name={`registryUpdateForm.list.${activeItem - 1}.value`}
control={control}
render={({field}) => (
<TextArea
{...field}
multiline
type="string"
rows={6}
placeholder="Type a value"
InputProps={{ disableUnderline: true}}
/>
)}
/>
{valueError &&
touched.registryUpdateForm?.list?.[activeItem - 1]
?.value ? (
<ErrorText>{valueError}</ErrorText>
) : null}
</ProposalFormInput>
</Grid>
</>
)
)}
</Grid>
</DialogContent>
);
}
Example #11
Source File: use-values-field-array.ts From admin with MIT License | 4 votes |
useValuesFieldArray = <TKeyName extends string = "id">(
currencyCodes: string[],
{ control, name, keyName }: UseFieldArrayOptions<TKeyName>,
options: UseValuesFieldArrayOptions = {
defaultAmount: 1000,
defaultCurrencyCode: "usd",
}
) => {
const { defaultAmount } = options
const { fields, append, remove } = useFieldArray<ValuesFormValue, TKeyName>({
control,
name,
keyName,
})
const watchedFields = useWatch({
control,
name,
defaultValue: fields,
})
const selectedCurrencies = watchedFields.map(
(field) => field?.price?.currency_code
)
const availableCurrencies = currencyCodes?.filter(
(currency) => !selectedCurrencies.includes(currency)
)
const controlledFields = fields.map((field, index) => {
return {
...field,
...watchedFields[index],
}
})
const appendPrice = () => {
const newCurrency = availableCurrencies[0]
append({
price: {
currency_code: newCurrency,
amount: defaultAmount,
},
})
}
const deletePrice = (index) => {
return () => {
remove(index)
}
}
return {
fields: controlledFields,
appendPrice,
deletePrice,
availableCurrencies,
selectedCurrencies,
} as const
}
Example #12
Source File: index.tsx From admin with MIT License | 4 votes |
MarkShippedModal: React.FC<MarkShippedModalProps> = ({
orderId,
orderToShip,
fulfillment,
handleCancel,
}) => {
const { control, register, watch } = useForm({})
const [noNotis, setNoNotis] = useState(false)
const {
fields,
append: appendTracking,
remove: removeTracking,
} = useFieldArray({
control,
name: "tracking_numbers",
})
useEffect(() => {
appendTracking({
value: "",
})
}, [])
const watchedFields = watch("tracking_numbers")
// Allows us to listen to onChange events
const trackingNumbers = fields.map((field, index) => ({
...field,
...watchedFields[index],
}))
const markOrderShipped = useAdminCreateShipment(orderId)
const markSwapShipped = useAdminCreateSwapShipment(orderId)
const markClaimShipped = useAdminCreateClaimShipment(orderId)
const notification = useNotification()
const markShipped = () => {
const resourceId =
fulfillment.claim_order_id || fulfillment.swap_id || fulfillment.order_id
const [type] = resourceId.split("_")
const tracking_numbers = trackingNumbers.map(({ value }) => value)
type actionType =
| typeof markOrderShipped
| typeof markSwapShipped
| typeof markClaimShipped
let action: actionType = markOrderShipped
let successText = "Successfully marked order as shipped"
let requestObj
switch (type) {
case "swap":
action = markSwapShipped
requestObj = {
fulfillment_id: fulfillment.id,
swap_id: resourceId,
tracking_numbers,
no_notification: noNotis,
}
successText = "Successfully marked swap as shipped"
break
case "claim":
action = markClaimShipped
requestObj = {
fulfillment_id: fulfillment.id,
claim_id: resourceId,
tracking_numbers,
}
successText = "Successfully marked claim as shipped"
break
default:
requestObj = {
fulfillment_id: fulfillment.id,
tracking_numbers,
no_notification: noNotis,
}
break
}
action.mutate(requestObj, {
onSuccess: () => {
notification("Success", successText, "success")
handleCancel()
},
onError: (err) => notification("Error", getErrorMessage(err), "error"),
})
}
return (
<Modal handleClose={handleCancel}>
<Modal.Body>
<Modal.Header handleClose={handleCancel}>
<span className="inter-xlarge-semibold">
Mark Fulfillment Shipped
</span>
</Modal.Header>
<Modal.Content>
<div className="flex flex-col">
<span className="inter-base-semibold mb-2">Tracking</span>
<div className="flex flex-col space-y-2">
{trackingNumbers.map((tn, index) => (
<Input
key={tn.id}
deletable={index !== 0}
label={index === 0 ? "Tracking number" : ""}
type="text"
placeholder={"Tracking number..."}
name={`tracking_numbers[${index}].value`}
ref={register({
required: "Must be filled",
})}
onDelete={() => removeTracking(index)}
/>
))}
</div>
</div>
<div className="flex w-full justify-end mt-4">
<Button
size="small"
onClick={() => appendTracking({ key: "", value: "" })}
variant="secondary"
disabled={trackingNumbers.some((tn) => !tn.value)}
>
+ Add Additional Tracking Number
</Button>
</div>
</Modal.Content>
<Modal.Footer>
<div className="flex w-full h-8 justify-between">
<div
className="items-center h-full flex cursor-pointer"
onClick={() => setNoNotis(!noNotis)}
>
<div
className={`w-5 h-5 flex justify-center text-grey-0 border-grey-30 border rounded-base ${
!noNotis && "bg-violet-60"
}`}
>
<span className="self-center">
{!noNotis && <CheckIcon size={16} />}
</span>
</div>
<input
id="noNotification"
className="hidden"
name="noNotification"
checked={!noNotis}
type="checkbox"
/>
<span className="ml-3 flex items-center text-grey-90 gap-x-xsmall">
Send notifications
<IconTooltip content="" />
</span>
</div>
<div className="flex">
<Button
variant="ghost"
className="mr-2 w-32 text-small justify-center"
size="large"
onClick={handleCancel}
>
Cancel
</Button>
<Button
size="large"
className="w-32 text-small justify-center"
variant="primary"
onClick={markShipped}
>
Complete
</Button>
</div>
</div>
</Modal.Footer>
</Modal.Body>
</Modal>
)
}
Example #13
Source File: variant-editor.tsx From admin with MIT License | 4 votes |
VariantEditor = ({
variant = defaultVariant,
onSubmit,
onCancel,
title,
optionsMap,
}) => {
const countryOptions = countryData.map((c) => ({
label: c.name,
value: c.alpha2.toLowerCase(),
}))
const { store } = useAdminStore()
const [selectedCountry, setSelectedCountry] = useState(() => {
const defaultCountry = variant.origin_country
? countryOptions.find((cd) => cd.label === variant.origin_country)
: null
return defaultCountry || null
})
const { control, register, reset, watch, handleSubmit } = useForm({
defaultValues: variant,
})
const {
fields: prices,
appendPrice,
deletePrice,
availableCurrencies,
} = usePricesFieldArray(
store?.currencies.map((c) => c.code) || [],
{
control,
name: "prices",
keyName: "indexId",
},
{
defaultAmount: 1000,
defaultCurrencyCode:
store?.default_currency.code || store?.currencies[0].code || "usd",
}
)
const { fields } = useFieldArray({
control,
name: "options",
keyName: "indexId",
})
useEffect(() => {
reset({
...variant,
options: Object.values(optionsMap),
prices: variant?.prices.map((p) => ({
price: { ...p },
})),
})
}, [variant, store])
const handleSave = (data) => {
if (!data.prices) {
focusByName("add-price")
return
}
if (!data.title) {
data.title = data.options.map((o) => o.value).join(" / ")
}
data.prices = data.prices.map(({ price: { currency_code, amount } }) => ({
currency_code,
amount: Math.round(amount),
}))
data.options = data.options.map((option) => ({ ...option }))
data.origin_country = selectedCountry?.label
data.inventory_quantity = parseInt(data.inventory_quantity)
data.weight = data?.weight ? parseInt(data.weight, 10) : undefined
data.height = data?.height ? parseInt(data.height, 10) : undefined
data.width = data?.width ? parseInt(data.width, 10) : undefined
data.length = data?.length ? parseInt(data.length, 10) : undefined
const cleaned = convertEmptyStringToNull(data)
onSubmit(cleaned)
}
watch(["manage_inventory", "allow_backorder"])
const variantTitle = variant?.options
.map((opt) => opt?.value || "")
.join(" / ")
return (
<Modal handleClose={onCancel}>
<Modal.Body>
<Modal.Header handleClose={onCancel}>
<h2 className="inter-xlarge-semibold">
{title}{" "}
{variantTitle && (
<span className="text-grey-50 inter-xlarge-regular">
({variantTitle})
</span>
)}
</h2>
</Modal.Header>
<Modal.Content>
<div className="mb-8">
<label
tabIndex={0}
className="inter-base-semibold mb-4 flex items-center gap-xsmall"
>
{"General"}
</label>
<div className="grid grid-cols-1 gap-y-small">
<Input label="Title" name="title" ref={register} />
{fields.map((field, index) => (
<div key={field.indexId}>
<Input
ref={register({ required: true })}
name={`options[${index}].value`}
required={true}
label={field.title}
defaultValue={field.value}
/>
<input
ref={register()}
type="hidden"
name={`options[${index}].option_id`}
defaultValue={field.option_id}
/>
</div>
))}
</div>
</div>
<div className="mb-8">
<label
tabIndex={0}
className="inter-base-semibold mb-4 flex items-center"
>
{"Prices"}
<span className="text-rose-50 mr-xsmall">*</span>
<IconTooltip content={"Variant prices"} />
</label>
<div className="grid grid-cols-1 gap-y-xsmall">
{prices.map((field, index) => (
<div className="flex items-center" key={field.indexId}>
<div className="w-full">
<Controller
control={control}
key={field.indexId}
name={`prices[${index}].price`}
ref={register()}
defaultValue={field.price}
render={({ onChange, value }) => {
let codes = availableCurrencies
if (value?.currency_code) {
codes = [value?.currency_code, ...availableCurrencies]
}
codes.sort()
return (
<CurrencyInput
currencyCodes={codes}
currentCurrency={value?.currency_code}
size="medium"
readOnly={index === 0}
onChange={(code) =>
onChange({ ...value, currency_code: code })
}
>
<CurrencyInput.AmountInput
label="Amount"
onChange={(amount) =>
onChange({ ...value, amount })
}
amount={value?.amount}
/>
</CurrencyInput>
)
}}
/>
</div>
<Button
variant="ghost"
size="small"
className="ml-8 w-8 h-8 mr-2.5 text-grey-40 hover:text-grey-80 transition-colors"
onClick={deletePrice(index)}
>
<TrashIcon />
</Button>
</div>
))}
</div>
<Button
className="mt-4"
onClick={appendPrice}
size="small"
variant="ghost"
name="add-price"
disabled={availableCurrencies?.length === 0}
>
<PlusIcon size={20} /> Add a price
</Button>
</div>
<div className="mb-8">
<label className="inter-base-semibold flex items-center gap-xsmall">
{"Stock & Inventory"}
<IconTooltip content={"Stock and inventory information"} />
</label>
<div className="w-full mt-4 grid medium:grid-cols-2 grid-cols-1 gap-y-base gap-x-xsmall">
<Input label="SKU" name="sku" placeholder="SKU" ref={register} />
<Input label="EAN" name="ean" placeholder="EAN" ref={register} />
<Input
label="Inventory quantity"
name="inventory_quantity"
placeholder="100"
type="number"
ref={register}
/>
<Input
label="UPC Barcode"
name="barcode"
placeholder="Barcode"
ref={register}
/>
</div>
<div className="flex items-center mt-6 gap-x-large">
<div className="flex item-center gap-x-1.5">
<Checkbox
name="manage_inventory"
label="Manage Inventory"
ref={register}
/>
<IconTooltip
content={
"When checked Medusa will regulate the inventory when orders and returns are made."
}
/>
</div>
<div className="flex item-center gap-x-1.5">
<Checkbox
name="allow_backorder"
ref={register}
label="Allow backorders"
/>
<IconTooltip
content={
"When checked the product will be available for purchase despite the product being sold out."
}
/>
</div>
</div>
</div>
<div className="mb-8">
<label className="inter-base-semibold flex items-center gap-xsmall">
Dimensions <IconTooltip content={"Variant dimensions"} />
</label>
<div className="w-full mt-4 grid medium:grid-cols-2 grid-cols-1 gap-y-base gap-x-xsmall">
<Input
label="Height"
placeholder="Product Height"
name="height"
ref={register}
/>
<Input
label="Width"
placeholder="Product Width"
name="width"
ref={register}
/>
<Input
label="Length"
name="length"
placeholder="Product Length"
ref={register}
/>
<Input
label="Weight"
name="weight"
placeholder="Product Weight"
ref={register}
/>
</div>
</div>
<div className="mb-8">
<label className="inter-base-semibold flex items-center gap-xsmall">
Customs <IconTooltip content={"Variant customs information"} />
</label>
<div className="w-full grid mt-4 medium:grid-cols-2 grid-cols-1 gap-y-base gap-x-xsmall">
<Input
label="MID Code"
placeholder="MID Code"
name="mid_code"
ref={register}
/>
<Input
label="HS Code"
placeholder="HS Code"
name="hs_code"
ref={register}
/>
<Select
enableSearch
label={"Country of origin"}
options={countryOptions}
value={selectedCountry}
onChange={setSelectedCountry}
/>
<Input
label="Material"
name="material"
placeholder="Material"
ref={register}
/>
</div>
</div>
</Modal.Content>
<Modal.Footer>
<div className="flex w-full justify-end gap-x-base">
<Button
className="w-[127px]"
onClick={onCancel}
size="small"
variant="ghost"
>
Cancel
</Button>
<Button
onClick={handleSubmit(handleSave)}
type="submit"
className="w-[127px]"
size="small"
variant="primary"
>
Save
</Button>
</div>
</Modal.Footer>
</Modal.Body>
</Modal>
)
}
Example #14
Source File: EditPiggybankModal.tsx From coindrop with GNU General Public License v3.0 | 4 votes |
EditPiggybankModal: FunctionComponent<Props> = ({ isOpen, onClose }) => {
const [isSubmitting, setIsSubmitting] = useState(false);
const { colors } = useTheme();
const { user } = useUser();
const { colorMode } = useColorMode();
const accentColorLevelInitial = getAccentColorLevelInitial(colorMode);
const accentColorLevelHover = getAccentColorLevelHover(colorMode);
const { push: routerPush, query: { piggybankName } } = useRouter();
const initialPiggybankId = Array.isArray(piggybankName) ? piggybankName[0] : piggybankName;
const { piggybankDbData } = useContext(PublicPiggybankDataContext);
const { avatar_storage_id: currentAvatarStorageId } = piggybankDbData;
const initialPaymentMethodsDataFieldArray = convertPaymentMethodsDataToFieldArray(piggybankDbData.paymentMethods);
const initialAccentColor = piggybankDbData.accentColor ?? 'orange';
const {
register,
handleSubmit,
setValue,
watch,
control,
formState: { isDirty },
} = useForm({
defaultValues: {
piggybankId: initialPiggybankId,
accentColor: initialAccentColor,
website: piggybankDbData.website ?? '',
name: piggybankDbData.name ?? '',
verb: piggybankDbData.verb ?? 'pay',
paymentMethods: sortByIsPreferredThenAlphabetical(initialPaymentMethodsDataFieldArray),
},
});
const paymentMethodsFieldArrayName = "paymentMethods";
const { fields, append, remove } = useFieldArray({
control,
name: paymentMethodsFieldArrayName,
});
const {
accentColor: watchedAccentColor,
piggybankId: watchedPiggybankId,
} = watch(["accentColor", "piggybankId"]);
const isAccentColorDirty = initialAccentColor !== watchedAccentColor;
const isUrlUnchanged = initialPiggybankId === watchedPiggybankId;
const { isPiggybankIdAvailable, setIsAddressTouched } = useContext(AdditionalValidation);
const onSubmit = async (formData) => {
try {
setIsSubmitting(true);
const dataToSubmit = {
...formData,
paymentMethods: convertPaymentMethodsFieldArrayToDbMap(formData.paymentMethods ?? []),
owner_uid: user.id,
avatar_storage_id: currentAvatarStorageId ?? null,
};
if (isUrlUnchanged) {
await db.collection('piggybanks').doc(initialPiggybankId).set(dataToSubmit);
mutate(['publicPiggybankData', initialPiggybankId], dataToSubmit);
} else {
await axios.post(
'/api/createPiggybank',
{
oldPiggybankName: initialPiggybankId,
newPiggybankName: formData.piggybankId,
piggybankData: dataToSubmit,
},
{
headers: {
token: user.token,
},
},
);
try {
await db.collection('piggybanks').doc(initialPiggybankId).delete();
} catch (err) {
console.log('error deleting old Coindrop page');
}
routerPush(`/${formData.piggybankId}`);
}
fetch(`/${initialPiggybankId}`, { headers: { isToForceStaticRegeneration: "true" }});
onClose();
} catch (error) {
setIsSubmitting(false);
// TODO: handle errors
throw error;
}
};
const handleAccentColorChange = (e) => {
e.preventDefault();
setValue("accentColor", e.target.dataset.colorname);
};
useEffect(() => {
register("accentColor");
}, [register]);
const formControlTopMargin = 2;
return (
<Modal
isOpen={isOpen}
onClose={onClose}
size="xl"
closeOnOverlayClick={false}
>
<ModalOverlay />
<ModalContent>
<ModalHeader>Configure</ModalHeader>
<ModalCloseButton />
<form id="configure-coindrop-form" onSubmit={handleSubmit(onSubmit)}>
<ModalBody>
<AvatarInput />
<FormControl
isRequired
mt={formControlTopMargin}
>
<FormLabel htmlFor="input-piggybankId">URL</FormLabel>
<EditUrlInput
register={register}
value={watchedPiggybankId}
/>
</FormControl>
<FormControl
mt={formControlTopMargin}
>
<FormLabel
htmlFor="input-accentColor"
>
Theme
</FormLabel>
<Flex wrap="wrap" justify="center">
{themeColorOptions.map(colorName => {
const isColorSelected = watchedAccentColor === colorName;
const accentColorInitial = colors[colorName][accentColorLevelInitial];
const accentColorHover = colors[colorName][accentColorLevelHover];
return (
<Box
key={colorName}
as="button"
bg={isColorSelected ? accentColorHover : accentColorInitial}
_hover={{
bg: accentColorHover,
}}
w="36px"
h="36px"
borderRadius="50%"
mx={1}
my={1}
onClick={handleAccentColorChange}
data-colorname={colorName}
>
{isColorSelected && (
<CheckIcon color="#FFF" />
)}
</Box>
);
})}
</Flex>
</FormControl>
<FormControl
isRequired
mt={formControlTopMargin}
>
<FormLabel
htmlFor="input-name"
>
Name
</FormLabel>
<Input
id="input-name"
name="name"
ref={register}
/>
</FormControl>
<FormControl
isRequired
mt={formControlTopMargin}
>
<FormLabel
htmlFor="input-verb"
>
Payment action name
</FormLabel>
<Select
id="input-verb"
name="verb"
ref={register}
>
<option value="pay">Pay</option>
<option value="donate to">Donate to</option>
<option value="support">Support</option>
<option value="tip">Tip</option>
</Select>
</FormControl>
<FormControl
mt={formControlTopMargin}
>
<FormLabel
htmlFor="input-website"
>
Website
</FormLabel>
<Input
id="input-website"
name="website"
ref={register}
placeholder="http://"
type="url"
/>
</FormControl>
<FormControl
mt={formControlTopMargin}
>
<FormLabel
htmlFor="input-paymentmethods"
>
Payment Methods
</FormLabel>
<PaymentMethodsInput
fields={fields}
control={control}
register={register}
remove={remove}
append={append}
fieldArrayName={paymentMethodsFieldArrayName}
/>
</FormControl>
</ModalBody>
<Flex
id="modal-footer"
justify="space-between"
m={6}
>
<DeleteButton
piggybankName={initialPiggybankId}
/>
<Flex>
<Button
variant="ghost"
onClick={onClose}
>
Cancel
</Button>
<Button
id="save-configuration-btn"
colorScheme="green"
mx={1}
type="submit"
isLoading={isSubmitting}
loadingText="Saving"
isDisabled={
(
!isDirty
&& !isAccentColorDirty // controlled accentColor field is not showing up in formState.dirtyFields
)
|| !isPiggybankIdAvailable
|| !initialPiggybankId
}
onClick={() => setIsAddressTouched(true)}
>
Save
</Button>
</Flex>
</Flex>
</form>
</ModalContent>
</Modal>
);
}