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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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>
                  &nbsp;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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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>
    );
}