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

feat: support array of string for multilang fieldset #911

Merged
merged 2 commits into from
Dec 23, 2024
Merged
Show file tree
Hide file tree
Changes from all 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
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>
);
};
Loading