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

Legg til autofullfør i nytt tekstfelt #2604

Merged
merged 2 commits into from
Mar 17, 2025
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
11 changes: 11 additions & 0 deletions e2e/meldinger.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,3 +84,14 @@ test('Journalfore dialog', async ({ page }) => {
expect(newRows).toEqual(existingRows + 1);
await expect(journalposterTable.getByText(saksId)).toBeVisible();
});
test('Autocomplete textarea', async ({ page }) => {
await page.goto('/new/person');
const textarea = page.getByRole('textbox');

await textarea.clear();

await textarea.pressSequentially('hei ');

const replacedText = 'Hei, Aremark';
await expect(textarea).toHaveText(replacedText);
});
6 changes: 3 additions & 3 deletions src/components/PersonLinje/index.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ChevronDownIcon, ChevronUpIcon, PersonIcon } from '@navikt/aksel-icons';
import { BodyShort, Box, Button, CopyButton, HStack, Label, Skeleton, VStack } from '@navikt/ds-react';
import { Suspense, useState } from 'react';
import { Kjonn, type KodeBeskrivelse } from 'src/app/personside/visittkort-v2/PersondataDomain';
import config from 'src/config';
import { usePersonData } from 'src/lib/clients/modiapersonoversikt-api';
import { Kjonn, type KodeBeskrivelseKjonn } from 'src/lib/types/modiapersonoversikt-api';
import useHotkey from 'src/utils/hooks/use-hotkey';
import { useClickAway } from 'src/utils/hooks/useClickAway';
import { twMerge } from 'tailwind-merge';
Expand All @@ -13,7 +13,7 @@ import { PersonBadges } from './Badges';
import { PersonlinjeDetails } from './Details';
import { Sikkerhetstiltak } from './Sikkerhetstiltak';

const ukjentKjonn: KodeBeskrivelse<Kjonn> = {
const ukjentKjonn: KodeBeskrivelseKjonn = {
kode: Kjonn.U,
beskrivelse: 'Ukjent kjønn'
};
Expand Down Expand Up @@ -115,7 +115,7 @@ const PersonLinjeContent = () => {
type PersonaliaProps = {
navn: string;
alder?: number;
kjonn: KodeBeskrivelse<'M' | 'K' | 'U'>;
kjonn: KodeBeskrivelseKjonn;
};

const Personalia = ({ navn, alder, kjonn }: PersonaliaProps) => {
Expand Down
171 changes: 171 additions & 0 deletions src/components/melding/AutoCompleteTextarea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import { Alert, HStack, Heading, HelpText, Textarea } from '@navikt/ds-react';
import { type ChangeEvent, type ComponentProps, type KeyboardEvent, useCallback, useState } from 'react';
import { rapporterBruk } from 'src/app/personside/dialogpanel/sendMelding/standardTekster/sokUtils';
import { useStandardTekster } from 'src/lib/clients/skrivestotte';
import { Locale, type Tekst } from 'src/lib/types/skrivestotte';
import { loggEvent } from 'src/utils/logger/frontendLogger';
import { rules } from './autocompleteRules';
import { type AutofullforData, autofullfor, byggAutofullforMap, useAutoFullforData } from './autocompleteUtils';

function AutoTekstTips() {
return (
<HelpText aria-labelledby="autocomplete-tips">
<Heading as="h4" id="autocomplete-tips" size="xsmall">
Autofullfør-tips:
</Heading>
<ul>
<li>foet + mellomrom: Brukers fulle navn</li>
<li>mvh + mellomrom: Signatur</li>
<li>hei + mellomrom: Hei bruker</li>
<li>AAP + mellomrom: arbeidsavklaringspenger</li>
<li>sbt + mellomrom: saksbehandlingstid</li>
<li>nay + mellomrom: Nav Arbeid og ytelser</li>
<li>nfp + mellomrom: Nav Familie- og pensjonsytelser</li>
<li>hi, + mellomrom: Hi, bruker (engelsk)</li>
<li>mvh/aap + nn eller en + mellomrom: autofullfør på nynorsk eller engelsk</li>
<li>fp + mellomrom: foreldrepenger</li>
<li>bm + mellomrom: bidragsmottaker</li>
<li>bp + mellomrom: bidragspliktig</li>
<li>ag + mellomrom: arbeidsgiver</li>
<li>ub + mellomrom: utbetaling</li>
<li>dp + mellomrom: dagpenger</li>
<li>dpv + mellomrom: dagpengevedtak</li>
<li>sp + mellomrom: sykepenger</li>
<li>sosp + mellomrom: søknad om sykepenger</li>
<li>info + mellomrom: informasjon</li>
<li>baut + mellomrom: utvidet barnetrygd</li>
<li>baor + mellomrom: ordinær barnetrygd</li>
<li>aareg + mellomrom: arbeidsgiver- og arbeidstakerregisteret</li>
<li>aev + mellomrom: arbeidsevnevurdering</li>
<li>uft + mellomrom: uføretrygd</li>
</ul>
</HelpText>
);
}

const SPACE = ' ';
const ENTER = 'Enter';

function findWordBoundary(text: string, initialPosition: number): [number, number] {
const words = text.split(/\s/);
const indices: [number, number][] = new Array(words.length);
for (let i = 0; i < words.length; i++) {
const start = i === 0 ? 0 : indices[i - 1][1] + 1;
const end = start + words[i].length;
indices[i] = [start, end];

if (start <= initialPosition && end >= initialPosition) {
return [start, end];
}
}

return [0, 0];
}

function autoFullfor(autofullforData: AutofullforData, parsedText: string) {
const autofullforMap = byggAutofullforMap(
Locale.nb_NO,
autofullforData.enhet,
autofullforData.person,
autofullforData.saksbehandler
);

return autofullfor(parsedText, autofullforMap);
}

function asChangeEvent<T>(event: KeyboardEvent<T>): ChangeEvent<T> {
if (event.target && event.target === event.currentTarget) {
return event as unknown as ChangeEvent<T>;
}
throw new Error('Not equals at all');
}

type Props = ComponentProps<typeof Textarea>;

function AutocompleteTextarea({ onChange, description, ...rest }: Props) {
const autofullforData = useAutoFullforData();
const [feilmelding, settFeilmelding] = useState<string>();
const standardtekster = useStandardTekster();

const onKeyDown: React.KeyboardEventHandler = useCallback(
(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if ([SPACE, ENTER].includes(event.key)) {
const cursorPosition =
event.currentTarget.selectionStart === event.currentTarget.selectionEnd
? event.currentTarget.selectionStart
: -1;
if (cursorPosition >= 0) {
const value = event.currentTarget.value;
const [start, end] = findWordBoundary(value, cursorPosition);

const word = value.substring(start, end).trim();
const replacement = rules.reduce((acc: string, rule) => {
if (acc.match(rule.regex)) {
event.preventDefault();
event.stopPropagation();
loggEvent('Autocomplete', 'Textarea', {
type: acc.toLowerCase()
});
if (rule.type === 'internal') {
settFeilmelding(undefined);
return rule.replacement();
}
if (standardtekster.data) {
const tekst: Tekst = standardtekster.data[rule.externalId];
if (tekst === undefined) {
settFeilmelding(`Ukjent tekst. Kontakt IT: ${rule.externalId}`);
return `${acc} `;
}
const locale = rule.locale || Locale.nb_NO;
const innhold = tekst.innhold[locale];
if (innhold === undefined) {
settFeilmelding(`Fant ikke tekst. Kontakt IT: ${rule.externalId}@${rule.locale}`);
return `${acc} `;
}

rapporterBruk(tekst);
return innhold;
}
settFeilmelding('Tekster ikke lastet enda. Kontakt IT om problemet vedvarer. ');
return `${acc} `;
}
return acc;
}, word);

const fullfortTekst = autofullforData ? autoFullfor(autofullforData, replacement) : replacement;

event.currentTarget.value = [value.substring(0, start), fullfortTekst, value.substring(end)].join(
''
);

event.currentTarget.selectionEnd = cursorPosition + (fullfortTekst.length - word.length);

onChange?.(asChangeEvent(event));
}
}
},
[autofullforData, onChange, standardtekster]
);

return (
<>
<Textarea
onKeyDown={onKeyDown}
description={
<HStack>
{description} <AutoTekstTips />
</HStack>
}
onChange={onChange}
{...rest}
/>
{feilmelding && (
<Alert variant="error" inline size="small">
{feilmelding}
</Alert>
)}
</>
);
}

export default AutocompleteTextarea;
7 changes: 4 additions & 3 deletions src/components/melding/FortsettDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Alert, BodyShort, Box, Button, Checkbox, HStack, Loader, Textarea, VStack } from '@navikt/ds-react';
import { Alert, BodyShort, Box, Button, Checkbox, HStack, Loader, VStack } from '@navikt/ds-react';
import { type ValidationError, useForm } from '@tanstack/react-form';
import { useAtom, useAtomValue } from 'jotai';
import { useCallback, useMemo, useState } from 'react';
Expand All @@ -21,6 +21,7 @@ import { type Temagruppe, temagruppeTekst } from 'src/lib/types/temagruppe';
import { formatterDatoTid } from 'src/utils/date-utils';
import type { z } from 'zod';
import { erJournalfort } from '../Meldinger/List/utils';
import AutocompleteTextarea from './AutoCompleteTextarea';
import { Oppgaveliste, OppgavelisteRadioKnapper } from './OppgavelisteRadioKnapper';
import { meldingsTyperTekst, traadTypeToMeldingsType } from './VelgMeldingsType';
import VelgOppgaveliste from './VelgOppgaveliste';
Expand Down Expand Up @@ -144,7 +145,7 @@ export const FortsettDialog = ({ traad }: Props) => {
>
{(field) => (
<div>
<Textarea
<AutocompleteTextarea
autoFocus
label="Ny melding i dialog"
description={meldingsTypeTekst.beskrivelse}
Expand All @@ -153,7 +154,7 @@ export const FortsettDialog = ({ traad }: Props) => {
error={buildErrorMessage(field.state.meta.errors)}
maxLength={maksLengdeMelding}
resize="vertical"
minRows={5}
minRows={10}
maxRows={15}
/>
{draftStatus && field.state.value.length > 0 && field.state.meta.isDirty && (
Expand Down
7 changes: 4 additions & 3 deletions src/components/melding/NyMelding.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Alert, Button, ErrorMessage, HStack, Textarea, VStack } from '@navikt/ds-react';
import { Alert, Button, ErrorMessage, HStack, VStack } from '@navikt/ds-react';
import { type ValidationError, useForm, useStore } from '@tanstack/react-form';
import { Link } from '@tanstack/react-router';
import { useAtomValue } from 'jotai';
Expand All @@ -24,6 +24,7 @@ import { useSuspendingBrukernavn } from 'src/lib/hooks/useSuspendingBrukernavn';
import { aktivEnhetAtom, usePersonAtomValue } from 'src/lib/state/context';
import type { Temagruppe } from 'src/models/temagrupper';
import type { z } from 'zod';
import AutocompleteTextarea from './AutoCompleteTextarea';

function NyMelding() {
const fnr = usePersonAtomValue();
Expand Down Expand Up @@ -110,15 +111,15 @@ function NyMelding() {
>
{(field) => (
<div>
<Textarea
<AutocompleteTextarea
label={meldingsTypeTekst.tittel}
description={meldingsTypeTekst.beskrivelse}
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
error={buildErrorMessage(field.state.meta.errors)}
maxLength={maksLengdeMelding}
resize="vertical"
minRows={5}
minRows={10}
maxRows={15}
/>
{draftStatus && field.state.value.length > 0 && field.state.meta.isDirty && (
Expand Down
Loading
Loading