Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ Add currency component #50

Merged
merged 7 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"@formatjs/cli": "^6.1.1",
"@formatjs/ts-transformer": "^3.12.0",
"@fortawesome/fontawesome-free": "^6.4.0",
"@open-formulieren/types": "^0.13.0",
"@open-formulieren/types": "^0.14.1",
"@storybook/addon-actions": "^7.3.2",
"@storybook/addon-essentials": "^7.3.2",
"@storybook/addon-interactions": "^7.3.2",
Expand Down
36 changes: 36 additions & 0 deletions src/registry/currency/edit-validation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {IntlShape} from 'react-intl';
import {z} from 'zod';

import {buildCommonSchema} from '@/registry/validation';

// undefined (optional) for unspecified, otherwise a finite numeric value. Note that
// null would be nicer, but formio's schema does not support null for validate.min,
// validate.max or defaultValue
const currencySchema = z.number().finite().optional();

// case for when component.multiple=false
const singleValueSchema = z
.object({multiple: z.literal(false)})
.and(z.object({defaultValue: currencySchema}));

// case for when component.multiple=true
const multipleValueSchema = z
.object({multiple: z.literal(true)})
.and(z.object({defaultValue: currencySchema.array()}));

const defaultValueSchema = singleValueSchema.or(multipleValueSchema);
Viicos marked this conversation as resolved.
Show resolved Hide resolved

const currencySpecific = z.object({
decimalLimit: z.number().int().positive().optional(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the types restrict this to the values 0-9 (maybe 10), so perhaps we should do the same here?

z.union([z.literal(0), z.literal(1), z.literal(2)]) etc. (you can write a nice Array(9).map for this probably).

Copy link
Contributor Author

@Viicos Viicos Nov 8, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed, had to cast as any because it doesn't typecheck somehow

validate: z
.object({
min: currencySchema,
max: currencySchema,
})
.optional(),
});

const schema = (intl: IntlShape) =>
buildCommonSchema(intl).and(defaultValueSchema).and(currencySpecific);

export default schema;
213 changes: 213 additions & 0 deletions src/registry/currency/edit.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import {CurrencyComponentSchema} from '@open-formulieren/types';
import {useFormikContext} from 'formik';
import {FormattedMessage, useIntl} from 'react-intl';

import {
BuilderTabs,
ClearOnHide,
Description,
Hidden,
IsSensitiveData,
Key,
Label,
PresentationConfig,
Registration,
SimpleConditional,
Tooltip,
Translations,
Validate,
useDeriveComponentKey,
} from '@/components/builder';
import {LABELS} from '@/components/builder/messages';
import {Checkbox, NumberField, TabList, TabPanel, Tabs} from '@/components/formio';
import {getErrorNames} from '@/utils/errors';

import {EditFormDefinition} from '../types';

/**
* Form to configure a Formio 'currency' type component.
*/
const EditForm: EditFormDefinition<CurrencyComponentSchema> = () => {
const intl = useIntl();
const [isKeyManuallySetRef, generatedKey] = useDeriveComponentKey();
const {errors} = useFormikContext<CurrencyComponentSchema>();

const erroredFields = Object.keys(errors).length
? getErrorNames<CurrencyComponentSchema>(errors)
: [];
// TODO: pattern match instead of just string inclusion?
// TODO: move into more generically usuable utility when we implement other component
// types
const hasAnyError = (...fieldNames: string[]): boolean => {
if (!erroredFields.length) return false;
return fieldNames.some(name => erroredFields.includes(name));
};

Validate.useManageValidatorsTranslations<CurrencyComponentSchema>(['required', 'min', 'max']);

return (
<Tabs>
<TabList>
<BuilderTabs.Basic
hasErrors={hasAnyError(
'label',
'key',
'description',
'tooltip',
'showInSummary',
'showInEmail',
'showInPDF',
'hidden',
'clearOnHide',
'isSensitiveData',
'defaultValue',
'decimalLimit',
'allowNegative'
)}
/>
<BuilderTabs.Advanced hasErrors={hasAnyError('conditional')} />
<BuilderTabs.Validation hasErrors={hasAnyError('validate')} />
<BuilderTabs.Registration hasErrors={hasAnyError('registration')} />
<BuilderTabs.Translations hasErrors={hasAnyError('openForms.translations')} />
</TabList>

{/* Basic tab */}
<TabPanel>
<Label />
<Key isManuallySetRef={isKeyManuallySetRef} generatedValue={generatedKey} />
<Description />
<Tooltip />
<PresentationConfig />
<Hidden />
<ClearOnHide />
<IsSensitiveData />
<DefaultValue />
<DecimalLimit />
<AllowNegative />
</TabPanel>

{/* Advanced tab */}
<TabPanel>
<SimpleConditional />
</TabPanel>

{/* Validation tab */}
<TabPanel>
<Validate.Required />
<Validate.ValidatorPluginSelect />
<Validate.Min />
<Validate.Max />
<Validate.ValidationErrorTranslations />
</TabPanel>

{/* Registration tab */}
<TabPanel>
<Registration.RegistrationAttributeSelect />
</TabPanel>

{/* Translations */}
<TabPanel>
<Translations.ComponentTranslations<CurrencyComponentSchema>
propertyLabels={{
label: intl.formatMessage(LABELS.label),
description: intl.formatMessage(LABELS.description),
tooltip: intl.formatMessage(LABELS.tooltip),
}}
/>
</TabPanel>
</Tabs>
);
};

EditForm.defaultValues = {
// basic tab
label: '',
key: '',
description: '',
tooltip: '',
showInSummary: true,
showInEmail: false,
showInPDF: true,
hidden: false,
clearOnHide: true,
isSensitiveData: false,
defaultValue: undefined,
decimalLimit: undefined,
allowNegative: false,
currency: 'EUR',
Viicos marked this conversation as resolved.
Show resolved Hide resolved
Viicos marked this conversation as resolved.
Show resolved Hide resolved
// Advanced tab
conditional: {
show: undefined,
when: '',
eq: '',
},
// Validation tab
validate: {
required: false,
plugins: [],
min: undefined,
max: undefined,
},
translatedErrors: {},
// Registration tab
registration: {
attribute: '',
},
};

const DefaultValue: React.FC = () => {
const intl = useIntl();
const tooltip = intl.formatMessage({
description: "Tooltip for 'defaultValue' builder field",
defaultMessage: 'This will be the initial value for this field before user interaction.',
});
return (
<NumberField
name="defaultValue"
label={<FormattedMessage {...LABELS.defaultValue} />}
tooltip={tooltip}
/>
Viicos marked this conversation as resolved.
Show resolved Hide resolved
);
};

const DecimalLimit: React.FC = () => {
const intl = useIntl();
const tooltip = intl.formatMessage({
description: "Tooltip for 'decimalLimit' builder field",
defaultMessage: 'The maximum number of decimal places.',
});
return (
<NumberField
name="decimalLimit"
label={
<FormattedMessage
description="Label for 'decimalLimit' builder field"
defaultMessage="Decimal places"
/>
}
tooltip={tooltip}
/>
);
};

const AllowNegative: React.FC = () => {
const intl = useIntl();
const tooltip = intl.formatMessage({
description: "Tooltip for 'allowNegative' builder field",
defaultMessage: 'Allow negative values.',
});
return (
<Checkbox
name="allowNegative"
label={
<FormattedMessage
description="Label for 'allowNegative' builder field"
defaultMessage="Allow negative values"
/>
}
tooltip={tooltip}
/>
);
};
Viicos marked this conversation as resolved.
Show resolved Hide resolved

export default EditForm;
10 changes: 10 additions & 0 deletions src/registry/currency/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import EditForm from './edit';
import validationSchema from './edit-validation';
import Preview from './preview';

export default {
edit: EditForm,
editSchema: validationSchema,
preview: Preview,
defaultValue: undefined, // formik field value
};
29 changes: 29 additions & 0 deletions src/registry/currency/preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import {CurrencyComponentSchema} from '@open-formulieren/types';

import {NumberField} from '@/components/formio';

import {ComponentPreviewProps} from '../types';

/**
* Show a formio currency component preview.
*
* NOTE: for the time being, this is rendered in the default Formio bootstrap style,
* however at some point this should use the components of
* @open-formulieren/formio-renderer instead for a more accurate preview.
*/
const Preview: React.FC<ComponentPreviewProps<CurrencyComponentSchema>> = ({component}) => {
// FIXME: incorporate decimalLimit and allowNegative
const {key, label, description, tooltip, validate = {}} = component;
const {required = false} = validate;
return (
<NumberField
name={key}
label={label}
description={description}
tooltip={tooltip}
required={required}
/>
);
};

export default Preview;
2 changes: 2 additions & 0 deletions src/registry/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {AnyComponentSchema, FallbackSchema, hasOwnProperty} from '@/types';

import Currency from './currency';
import DateField from './date';
import DateTimeField from './datetime';
import Email from './email';
Expand Down Expand Up @@ -36,6 +37,7 @@ const REGISTRY: Registry = {
phoneNumber: PhoneNumber,
postcode: Postcode,
file: FileUpload,
currency: Currency,
};

export {Fallback};
Expand Down
14 changes: 3 additions & 11 deletions src/registry/number/edit-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,9 @@ import {buildCommonSchema} from '@/registry/validation';
// validate.max or defaultValue
const numberSchema = z.number().finite().optional();

// case for when component.multiple=false
const singleValueSchema = z
.object({multiple: z.literal(false)})
.and(z.object({defaultValue: numberSchema}));

// case for when component.multiple=true
const multipleValueSchema = z
.object({multiple: z.literal(true)})
.and(z.object({defaultValue: numberSchema.array()}));

const defaultValueSchema = singleValueSchema.or(multipleValueSchema);
const defaultValueSchema = z.object({
defaultValue: numberSchema,
});

const numberSpecific = z.object({
decimalLimit: z.number().int().positive().optional(),
Expand Down
2 changes: 1 addition & 1 deletion src/registry/number/preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {NumberField} from '@/components/formio';
import {ComponentPreviewProps} from '../types';

/**
* Show a formio textfield component preview.
* Show a formio number component preview.
*
* NOTE: for the time being, this is rendered in the default Formio bootstrap style,
* however at some point this should use the components of
Expand Down