Skip to content

Commit

Permalink
feat: support array of string for multilang fieldset (#911)
Browse files Browse the repository at this point in the history
  • Loading branch information
jeffreiffers authored Dec 23, 2024
1 parent 04d3b4e commit ada0ffd
Show file tree
Hide file tree
Showing 5 changed files with 124 additions and 74 deletions.
2 changes: 1 addition & 1 deletion libs/types/src/lib/localization.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export interface LocalizedStrings {
[kode: string]: string;
[code: string]: string | string[];
}

export type ISOLanguage = 'nb' | 'nn' | 'no' | 'en';
4 changes: 2 additions & 2 deletions libs/ui/src/lib/button/add.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import styles from './button.module.css';

const AddButton = ({ children = localization.add, ...props }: ButtonProps) => (
<Button
{...props}
variant='tertiary'
className={styles.add}
size='sm'
{...props}
>
<span className={styles.withIcon}>
<PlusCircleIcon fontSize={'1.2rem'} />
<PlusCircleIcon fontSize={'1.3rem'} />
{children}
</span>
</Button>
Expand Down
4 changes: 2 additions & 2 deletions libs/ui/src/lib/button/delete.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import styles from './button.module.css';

const DeleteButton = ({ children = localization.button.delete, ...props }: ButtonProps) => (
<Button
{...props}
variant='tertiary'
color='danger'
size='sm'
{...props}
>
<span className={styles.withIcon}>
<TrashIcon fontSize={'1.2rem'} />
<TrashIcon fontSize={'1.3rem'} />
{children}
</span>
</Button>
Expand Down
4 changes: 2 additions & 2 deletions libs/ui/src/lib/button/edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import styles from './button.module.css';

const EditButton = ({ children = localization.button.edit, ...props }: ButtonProps) => (
<Button
{...props}
variant='tertiary'
size='sm'
{...props}
>
<span className={styles.withIcon}>
<PencilWritingIcon fontSize={'1.2rem'} />
<PencilWritingIcon fontSize={'1.3rem'} />
{children}
</span>
</Button>
Expand Down
184 changes: 117 additions & 67 deletions libs/ui/src/lib/formik-language-fieldset/index.tsx
Original file line number Diff line number Diff line change
@@ -1,125 +1,175 @@
'use client';

import { ReactNode } from 'react';
import { ReactNode, useState } from 'react';
import { localization } from '@catalog-frontend/utils';
import { Fieldset, Button, Box, Paragraph, Textfield } from '@digdir/designsystemet-react';
import { Fieldset, Box, Paragraph, Textfield, ErrorMessage, Chip } from '@digdir/designsystemet-react';
import { FastField, useFormikContext } from 'formik';

import styles from './formik-language-fieldset.module.scss';
import { ISOLanguage, LocalizedStrings } from '@catalog-frontend/types';
import { PlusCircleIcon, TrashIcon } from '@navikt/aksel-icons';
import { TextareaWithPrefix } from '../textarea-with-prefix';
import _ from 'lodash';
import { AddButton, DeleteButton } from '../button';

type LanuguageFieldsetProps = {
legend?: ReactNode;
name: string;
errorMessage?: string;
errorArgs?: object;
requiredLanguages?: Omit<ISOLanguage, 'no'>[];
as?: typeof Textfield | typeof TextareaWithPrefix;
multiple?: boolean;
};

const allowedLanguages = Object.freeze<ISOLanguage[]>(['nb', 'nn', 'en']);

export const FormikLanguageFieldset = ({
legend,
name,
errorMessage,
errorArgs,
requiredLanguages,
as: renderAs = Textfield,
multiple = false,
}: LanuguageFieldsetProps) => {
const { errors, getFieldMeta, setFieldValue } = useFormikContext<Record<string, LocalizedStrings>>();
const { errors, values, getFieldMeta, setFieldValue } = useFormikContext<Record<string, LocalizedStrings>>();
const [textValue, setTextValue] = useState<Record<string, string>>({});

const handleAddLanguage = (lang: string) => {
setFieldValue(`${name}.${lang}`, '');
setFieldValue(`${name}.${lang}`, multiple ? [] : '');
};

const handleRemoveLanguage = (lang: string) => {
setFieldValue(`${name}.${lang}`, undefined);
};

const handleOnChangeTextValue = (value: string, lang: string) => {
setTextValue((prev) => ({ ...prev, ...{ [lang]: value } }));
};

const handleAddTextValue = (lang: string) => {
if (Boolean(textValue[lang]) === true) {
const textValues = [...(values?.[name]?.[lang] as string[]), textValue[lang]];
setFieldValue(`${name}.${lang}`, textValues);
setTextValue((prev) => ({ ...prev, ...{ [lang]: '' } }));
}
};

const handleRemoveTextValue = (index: number, lang: string) => {
const textValues = [...(values?.[name]?.[lang] as string[])];
textValues.splice(index, 1);
setFieldValue(`${name}.${lang}`, textValues);
};

const visibleLanguageFields = allowedLanguages.filter((lang) => {
const metadata = getFieldMeta(`${name}.${lang}`);
return requiredLanguages?.includes(lang) || metadata.value !== undefined;
});

const visibleLanguageButtons = allowedLanguages.filter((lang) => !visibleLanguageFields.includes(lang));
const languagesWithError = allowedLanguages
.filter((lang) => _.get(errors, `${name}.${lang}`))
.map((lang) => localization.language[lang]);

return (
<Fieldset legend={legend} size='sm'>
<Fieldset
legend={legend}
size='sm'
>
{visibleLanguageFields.map((lang) => (
<div
key={lang}
className={styles.languageField}
>
<FastField
as={renderAs}
name={`${name}.${lang}`}
size='sm'
aria-label={localization.language[lang]}

error={errors?.[name]?.[lang]}
{...(renderAs === TextareaWithPrefix
? {
cols: 80,
prefix: (
<>
<Paragraph
size='sm'
variant='short'
>
{localization.language[lang]}
</Paragraph>
{!requiredLanguages?.includes(lang) && (
<Box>
<Button
variant='tertiary'
<div key={lang}>
{multiple ? (
<>
<Box className={styles.languageField}>
<Textfield
size='sm'
aria-label={localization.language[lang]}
prefix={localization.language[lang]}
value={textValue[lang]}
onChange={(e) => handleOnChangeTextValue(e.target.value, lang)}
onKeyDown={(e) => {
if (e.code === 'Enter') {
handleAddTextValue(lang);
}
}}
onBlur={() => handleAddTextValue(lang)}
/>
<AddButton
variant='secondary'
disabled={Boolean(textValue[lang]) === false}
onClick={() => handleAddTextValue(lang)}
/>

<DeleteButton
variant='tertiary'
onClick={() => handleRemoveLanguage(lang)}
/>
</Box>
<Chip.Group size='sm'>
{(values?.[name]?.[lang] as string[] | undefined)?.map((v, i) => (
<Chip.Removable onClick={() => handleRemoveTextValue(i, lang)}>{v}</Chip.Removable>
))}
</Chip.Group>
</>
) : (
<Box className={styles.languageField}>
<FastField
as={renderAs}
name={`${name}.${lang}`}
size='sm'
aria-label={localization.language[lang]}
error={Boolean(_.get(errors, `${name}.${lang}`))}
{...(renderAs === TextareaWithPrefix
? {
cols: 110,
prefix: (
<>
<Paragraph
size='sm'
color='danger'
onClick={() => handleRemoveLanguage(lang)}
variant='short'
>
<TrashIcon
title={localization.icon.trash}
fontSize='1.5rem'
/>
{localization.button.delete}
</Button>
</Box>
)}
</>
),
}
: {
prefix: localization.language[lang],
})}
/>
{!requiredLanguages?.includes(lang) && renderAs !== TextareaWithPrefix && (
<Button
variant='tertiary'
size='sm'
color='danger'
onClick={() => handleRemoveLanguage(lang)}
>
<TrashIcon
title={localization.button.delete}
fontSize='1.5rem'
{localization.language[lang]}
</Paragraph>
{!requiredLanguages?.includes(lang) && (
<Box>
<DeleteButton onClick={() => handleRemoveLanguage(lang)} />
</Box>
)}
</>
),
}
: {
prefix: localization.language[lang],
})}
/>
{localization.button.delete}
</Button>
{!requiredLanguages?.includes(lang) && renderAs !== TextareaWithPrefix && (
<DeleteButton onClick={() => handleRemoveLanguage(lang)} />
)}
</Box>
)}
</div>
))}
<div className={styles.languageButtons}>
{visibleLanguageButtons.map((lang) => (
<Button
<AddButton
key={lang}
variant='tertiary'
color='first'
size='sm'
onClick={() => handleAddLanguage(lang)}
>
<PlusCircleIcon fontSize='1rem' />
{localization.language[lang] ?? '?'}
</Button>
</AddButton>
))}
</div>

{languagesWithError.length > 0 && (
<ErrorMessage
size='sm'
error
>
{errorMessage
? `${localization.formatString(errorMessage, { ...errorArgs, language: languagesWithError.join(', ') })}`
: 'This field ({language}) is required'}
</ErrorMessage>
)}
</Fieldset>
);
};

0 comments on commit ada0ffd

Please sign in to comment.