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/new translation html #1394

Merged
merged 11 commits into from
Jan 20, 2025
3 changes: 2 additions & 1 deletion packages/bygger/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@
"react-csv": "^2.2.2",
"react-debounce-input": "^3.3.0",
"react-dom": "^18.3.1",
"react-router-dom": "^6.26.2"
"react-router-dom": "^6.26.2",
"react-simple-wysiwyg": "^3.2.0"
},
"devDependencies": {
"@testing-library/react": "^16.1.0",
Expand Down
18 changes: 18 additions & 0 deletions packages/bygger/src/components/wysiwyg/LinkButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { LinkIcon } from '@navikt/aksel-icons';
import { createButton } from 'react-simple-wysiwyg';

const LinkButton = createButton('Link', <LinkIcon title="Link" fontSize="1.5rem" />, ({ $selection }) => {
if ($selection?.nodeName === 'A') {
document.execCommand('unlink');
} else {
const Selection = document.getSelection()?.toString();
const Uri = prompt('URL', '');
document.execCommand(
'insertHTML',
false,
Uri ? `<a href="${Uri}" target="_blank" rel="noopener noreferrer">${Selection ? Selection : Uri}</a>` : Selection,
);
}
});

export default LinkButton;
9 changes: 9 additions & 0 deletions packages/bygger/src/components/wysiwyg/TextTypeDropdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { createDropdown } from 'react-simple-wysiwyg';

const TextTypeDropdown = createDropdown('Skrifttype', [
['Avsnitt', 'formatBlock', 'P'],
['Overskrift', 'formatBlock', 'H3'],
['Underoverskrift', 'formatBlock', 'H4'],
]);

export default TextTypeDropdown;
52 changes: 52 additions & 0 deletions packages/bygger/src/components/wysiwyg/WysiwygEditor.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { htmlConverter } from '@navikt/skjemadigitalisering-shared-components';
import { useState } from 'react';
import {
BtnBold,
BtnBulletList,
BtnClearFormatting,
BtnNumberedList,
Editor,
EditorProvider,
Toolbar,
} from 'react-simple-wysiwyg';
import LinkButton from './LinkButton';
import TextTypeDropdown from './TextTypeDropdown';

interface Props {
defaultValue?: string;
onBlur: (value: string) => void;
}

const WysiwygEditor = ({ defaultValue = '', onBlur }: Props) => {
const [htmlValue, setHtmlValue] = useState(defaultValue);

const handleChange = (event) => {
magnurh-cx marked this conversation as resolved.
Show resolved Hide resolved
setHtmlValue(event.target.value);
};

const handleBlur = () => {
onBlur(htmlConverter.sanitizeHtmlString(htmlValue, { FORBID_ATTR: ['style'] }));
};

return (
<EditorProvider>
<Editor
value={htmlValue}
onChange={handleChange}
onBlur={handleBlur}
containerProps={{ style: { resize: 'vertical', backgroundColor: 'white', minHeight: 'min-content' } }}
>
<Toolbar>
<TextTypeDropdown />
<BtnBold />
<LinkButton />
<BtnBulletList />
<BtnNumberedList />
<BtnClearFormatting />
</Toolbar>
</Editor>
</EditorProvider>
);
};

export default WysiwygEditor;
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,17 @@ const EditFormTranslationsContext = createContext<EditTranslationsContextValue<F
const EditFormTranslationsProvider = ({ initialChanges, children }: Props) => {
const [state, dispatch] = useReducer(editFormTranslationsReducer, {
errors: [],
state: 'INIT',
status: 'INIT',
changes: {},
});
const { storedTranslations, saveTranslation, loadTranslations } = useFormTranslations();
const feedbackEmit = useFeedbackEmit();

useEffect(() => {
if (initialChanges && state.state === 'INIT') {
if (initialChanges && state.status === 'INIT') {
dispatch({ type: 'INITIALIZE', payload: { initialChanges } });
}
}, [initialChanges, state.state]);
}, [initialChanges, state.status]);

const updateTranslation = (original: FormsApiFormTranslation, lang: TranslationLang, value: string) => {
const { key } = original;
Expand All @@ -55,14 +55,14 @@ const EditFormTranslationsProvider = ({ initialChanges, children }: Props) => {
};

const saveChanges = async () => {
dispatch({ type: 'CLEAR_ERRORS' });
dispatch({ type: 'SAVE_STARTED' });
const { responses, errors } = await saveEachTranslation(
getTranslationsForSaving<FormsApiFormTranslation>(state),
saveTranslation,
);

await loadTranslations();
dispatch({ type: 'SAVED', payload: { errors } });
dispatch({ type: 'SAVE_FINISHED', payload: { errors } });
if (responses.length > 0) {
feedbackEmit.success(`${responses.length} oversettelser ble lagret.`);
}
Expand All @@ -81,7 +81,7 @@ const EditFormTranslationsProvider = ({ initialChanges, children }: Props) => {
storedTranslations,
updateTranslation,
errors: state.errors,
editState: state.state,
editState: state.status,
saveChanges,
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,18 @@ const EditGlobalTranslationsContext =
const EditGlobalTranslationsProvider = ({ initialChanges, children }: Props) => {
const [state, dispatch] = useReducer(editGlobalTranslationsReducer, {
errors: [],
state: 'INIT',
status: 'INIT',
new: createDefaultGlobalTranslation(),
changes: {},
});
const { storedTranslations, loadTranslations, createNewTranslation, saveTranslation } = useGlobalTranslations();
const feedbackEmit = useFeedbackEmit();

useEffect(() => {
if (initialChanges && state.state === 'INIT') {
if (initialChanges && state.status === 'INIT') {
dispatch({ type: 'INITIALIZE', payload: { initialChanges } });
}
}, [initialChanges, state.state]);
}, [initialChanges, state.status]);

const updateTranslation = (original: FormsApiGlobalTranslation, lang: TranslationLang, value: string) => {
const { key } = original;
Expand Down Expand Up @@ -86,7 +86,7 @@ const EditGlobalTranslationsProvider = ({ initialChanges, children }: Props) =>
if (validationError) {
dispatch({ type: 'VALIDATION_ERROR', payload: { errors: [validationError] } });
} else {
dispatch({ type: 'CLEAR_ERRORS' });
dispatch({ type: 'SAVE_STARTED' });
const { responses, errors } = await saveEachTranslation(
getTranslationsForSaving<FormsApiGlobalTranslation>(state),
saveTranslation,
Expand All @@ -103,7 +103,7 @@ const EditGlobalTranslationsProvider = ({ initialChanges, children }: Props) =>
}

await loadTranslations();
dispatch({ type: 'SAVED', payload: { errors } });
dispatch({ type: 'SAVE_FINISHED', payload: { errors } });
if (responses.length > 0) {
feedbackEmit.success(`${responses.length} oversettelser ble lagret.`);
}
Expand All @@ -123,7 +123,7 @@ const EditGlobalTranslationsProvider = ({ initialChanges, children }: Props) =>
updateTranslation,
errors: state.errors,
newTranslation: state.new,
editState: state.state,
editState: state.status,
updateNewTranslation,
saveChanges,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,23 +1,27 @@
import { FormsApiFormTranslation, TranslationLang } from '@navikt/skjemadigitalisering-shared-domain';
import { TranslationError } from '../utils/errorUtils';
import {
ClearErrorsAction,
generateMap,
getErrors,
getResetChanges,
InitializeAction,
SavedAction,
SaveFinishedAction,
SaveStartedAction,
Status,
UpdateAction,
} from './reducerUtils';

type FormTranslationState = {
changes: Record<string, FormsApiFormTranslation>;
errors: TranslationError[];
state: Status;
status: Status;
};

type FormTranslationAction = InitializeAction | UpdateAction<FormsApiFormTranslation> | ClearErrorsAction | SavedAction;
type FormTranslationAction =
| InitializeAction
| UpdateAction<FormsApiFormTranslation>
| SaveStartedAction
| SaveFinishedAction;

const getUpdatedFormTranslationChanges = (
state: FormTranslationState,
Expand All @@ -38,15 +42,15 @@ const editFormTranslationsReducer = (
? { ...state, changes: generateMap(action.payload.initialChanges) }
: state;
case 'UPDATE':
return { ...state, changes: getUpdatedFormTranslationChanges(state, action.payload), state: 'EDITING' };
case 'CLEAR_ERRORS':
return { ...state, errors: [] };
case 'SAVED':
return { ...state, changes: getUpdatedFormTranslationChanges(state, action.payload), status: 'EDITING' };
case 'SAVE_STARTED':
return { ...state, errors: [], status: 'SAVING' };
case 'SAVE_FINISHED':
return {
...state,
changes: getResetChanges(state, action.payload),
errors: getErrors(state, action.payload),
state: 'SAVED',
status: 'SAVED',
};
default:
throw new Error();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { FormsApiGlobalTranslation, TranslationLang } from '@navikt/skjemadigitalisering-shared-domain';
import { TranslationError } from '../utils/errorUtils';
import {
ClearErrorsAction,
createDefaultGlobalTranslation,
generateMap,
getErrors,
getResetChanges,
InitializeAction,
SavedAction,
SaveFinishedAction,
SaveStartedAction,
Status,
UpdateAction,
} from './reducerUtils';
Expand All @@ -16,7 +16,7 @@ type GlobalTranslationState = {
changes: Record<string, FormsApiGlobalTranslation>;
new: FormsApiGlobalTranslation;
errors: TranslationError[];
state: Status;
status: Status;
};

type UpdateNewAction = {
Expand All @@ -30,8 +30,8 @@ type GlobalTranslationAction =
| UpdateAction<FormsApiGlobalTranslation>
| UpdateNewAction
| ValidationErrorAction
| ClearErrorsAction
| SavedAction;
| SaveStartedAction
| SaveFinishedAction;

const getUpdatedGlobalTranslationChanges = (
state: GlobalTranslationState,
Expand Down Expand Up @@ -73,20 +73,20 @@ const editGlobalTranlationReducer = (
? { ...state, changes: generateMap(action.payload.initialChanges) }
: state;
case 'UPDATE':
return { ...state, changes: getUpdatedGlobalTranslationChanges(state, action.payload), state: 'EDITING' };
return { ...state, changes: getUpdatedGlobalTranslationChanges(state, action.payload), status: 'EDITING' };
case 'UPDATE_NEW':
return { ...state, new: getUpdatedNew(state, action.payload), state: 'EDITING' };
return { ...state, new: getUpdatedNew(state, action.payload), status: 'EDITING' };
case 'VALIDATION_ERROR':
return { ...state, errors: action.payload.errors };
case 'CLEAR_ERRORS':
case 'SAVE_STARTED':
return { ...state, errors: [] };
case 'SAVED':
case 'SAVE_FINISHED':
return {
...state,
changes: getResetChanges(state, action.payload),
new: getResetNew(state, action.payload),
errors: getErrors(state, action.payload),
state: 'SAVED',
status: 'SAVED',
};
default:
throw new Error();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import { TranslationError } from '../utils/errorUtils';
import { State } from './index';

type Status = 'INIT' | 'EDITING' | 'SAVED';
type Status = 'INIT' | 'EDITING' | 'SAVING' | 'SAVED';

type InitializeAction = {
type: 'INITIALIZE';
Expand All @@ -17,8 +17,8 @@ type UpdateAction<Translation> = {
type: 'UPDATE';
payload: { original: Translation; lang: TranslationLang; value: string };
};
type ClearErrorsAction = { type: 'CLEAR_ERRORS' };
type SavedAction = { type: 'SAVED'; payload: { errors: TranslationError[] } };
type SaveStartedAction = { type: 'SAVE_STARTED' };
type SaveFinishedAction = { type: 'SAVE_FINISHED'; payload: { errors: TranslationError[] } };

const createDefaultGlobalTranslation = (tag: TranslationTag = 'skjematekster'): FormsApiGlobalTranslation => ({
key: '',
Expand Down Expand Up @@ -47,4 +47,4 @@ const generateMap = <Translation extends FormsApiTranslation>(values: Translatio
values.reduce((acc, value) => ({ ...acc, [value.key]: value }), {});

export { createDefaultGlobalTranslation, generateMap, getErrors, getResetChanges };
export type { ClearErrorsAction, InitializeAction, SavedAction, Status, UpdateAction };
export type { InitializeAction, SaveFinishedAction, SaveStartedAction, Status, UpdateAction };
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
import { TextField, Textarea } from '@navikt/ds-react';
import { FocusEventHandler } from 'react';
import { Textarea, TextField } from '@navikt/ds-react';
import { FocusEvent } from 'react';
import WysiwygEditor from '../../components/wysiwyg/WysiwygEditor';

interface Props {
label: string;
defaultValue?: string;
isHtml: boolean;
minRows: number;
onChange: FocusEventHandler<HTMLInputElement | HTMLTextAreaElement>;
onChange: (value: string) => void;
error?: string;
}

const TranslationInput = ({ label, defaultValue, minRows, onChange, error }: Props) => {
const TranslationInput = ({ label, defaultValue, isHtml, minRows, onChange, error }: Props) => {
const handleBlur = (event: FocusEvent<HTMLTextAreaElement | HTMLInputElement>) => {
onChange(event.currentTarget.value);
};

if (isHtml) {
return <WysiwygEditor onBlur={onChange} defaultValue={defaultValue} />;
}
if (minRows > 2) {
return (
<Textarea
Expand All @@ -18,12 +27,12 @@ const TranslationInput = ({ label, defaultValue, minRows, onChange, error }: Pro
minRows={minRows}
resize="vertical"
defaultValue={defaultValue}
onBlur={onChange}
onBlur={handleBlur}
error={error}
/>
);
}
return <TextField label={label} hideLabel defaultValue={defaultValue} onBlur={onChange} error={error} />;
return <TextField label={label} hideLabel defaultValue={defaultValue} onBlur={handleBlur} error={error} />;
};

export default TranslationInput;
Loading