Skip to content

Commit

Permalink
Fix toast timeout
Browse files Browse the repository at this point in the history
  • Loading branch information
cskrov committed Oct 1, 2024
1 parent 66baeac commit 7230ce9
Show file tree
Hide file tree
Showing 20 changed files with 339 additions and 298 deletions.
8 changes: 6 additions & 2 deletions frontend/src/components/countdown-button/countdown-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@ interface Props extends ButtonProps {

export const CountdownButton = ({ seconds, children, ...props }: Props) => {
const [secondsLeft, setSecondsLeft] = useState(seconds);
const [isExpired, setIsExpired] = useState(false);
const [isExpired, setIsExpired] = useState(seconds <= 0);

useEffect(() => {
if (seconds <= 0) {
return;
}

const interval = setInterval(() => {
setSecondsLeft((value) => {
if (value === 1) {
Expand All @@ -22,7 +26,7 @@ export const CountdownButton = ({ seconds, children, ...props }: Props) => {
}, 1_000);

return () => clearInterval(interval);
}, []);
}, [seconds]);

return (
<Button {...props} disabled={isExpired}>
Expand Down
26 changes: 22 additions & 4 deletions frontend/src/components/oppgavestyring/toasts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Button } from '@navikt/ds-react';
import { styled } from 'styled-components';
import { CountdownButton } from '@app/components/countdown-button/countdown-button';
import { OnChange } from '@app/components/oppgavestyring/types';
import { toast } from '../toast/store';
import { toast } from '@app/components/toast/store';

interface ToastProps {
testId: string;
Expand All @@ -14,15 +14,33 @@ interface ToastProps {
name: string;
}

export const successToast = (props: ToastProps) => toast.success(<Tildelt {...props} />);
interface CountdownToastProps extends ToastProps {
timestamp: number;
}

export const successToast = (props: CountdownToastProps) => toast.success(<Tildelt {...props} />);

const Tildelt = ({ oppgaveId, testId, fromNavIdent, toNavIdent, label, name, onChange }: ToastProps) => (
const Tildelt = ({
oppgaveId,
testId,
fromNavIdent,
toNavIdent,
label,
name,
onChange,
timestamp,
}: CountdownToastProps) => (
<div data-testid={testId} data-oppgaveid={oppgaveId}>
<span>
{label} {name}.
</span>
<ButtonRow>
<CountdownButton size="small" variant="tertiary" onClick={() => onChange(fromNavIdent, toNavIdent)} seconds={10}>
<CountdownButton
size="small"
variant="tertiary"
onClick={() => onChange(fromNavIdent, toNavIdent)}
seconds={Math.floor((timestamp + 10_000 - Date.now()) / 1_000)}
>
Angre
</CountdownButton>
</ButtonRow>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { parseISO } from 'date-fns';
import { useCallback } from 'react';
import { getFixedCacheKey } from '@app/components/behandling/behandlingsdialog/medunderskriver/helpers';
import { errorToast, successToast } from '@app/components/oppgavestyring/toasts';
Expand Down Expand Up @@ -32,7 +33,8 @@ export const useSetMedunderskriver = (
: `satt til ${formatEmployeeNameAndIdFallback(toMedunderskriver, 'ingen')}`;

try {
await setMedunderskriver({ oppgaveId, employee: toMedunderskriver });
const { modified } = await setMedunderskriver({ oppgaveId, employee: toMedunderskriver }).unwrap();
const timestamp = parseISO(modified).getTime();

successToast({
testId: 'oppgave-set-medunderskriver-success-toast',
Expand All @@ -42,6 +44,7 @@ export const useSetMedunderskriver = (
toNavIdent,
onChange,
name,
timestamp,
});
} catch {
errorToast({
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/oppgavestyring/use-set-rol.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { parseISO } from 'date-fns';
import { useCallback } from 'react';
import { getFixedCacheKey } from '@app/components/behandling/behandlingsdialog/medunderskriver/helpers';
import { errorToast, successToast } from '@app/components/oppgavestyring/toasts';
Expand All @@ -19,7 +20,8 @@ export const useSetRol = (oppgaveId: string, rol: INavEmployee[] = EMPTY_MEDUNDE
const name = toROL === null ? 'fjernet' : `satt til ${formatEmployeeNameAndIdFallback(toROL, 'felles kø')}`;

try {
await setRol({ oppgaveId, employee: toROL });
const { modified } = await setRol({ oppgaveId, employee: toROL }).unwrap();
const timestamp = parseISO(modified).getTime();

successToast({
testId: 'oppgave-set-rol-success-toast',
Expand All @@ -29,6 +31,7 @@ export const useSetRol = (oppgaveId: string, rol: INavEmployee[] = EMPTY_MEDUNDE
toNavIdent,
onChange,
name,
timestamp,
});
} catch {
errorToast({
Expand Down
36 changes: 22 additions & 14 deletions frontend/src/components/oppgavestyring/use-tildel.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { Button } from '@navikt/ds-react';
import { parseISO } from 'date-fns';
import { useContext, useState } from 'react';
import { StaticDataContext } from '@app/components/app/static-data-context';
import { OpenOppgavebehandling } from '@app/components/common-table-components/open';
import { CountdownButton } from '@app/components/countdown-button/countdown-button';
import { ActionToast } from '@app/components/toast/action-toast';
import { toast } from '@app/components/toast/store';
import { formatEmployeeNameAndId, formatEmployeeNameAndIdFallback } from '@app/domain/employee-name';
import { useHasRole } from '@app/hooks/use-has-role';
import {
Expand All @@ -17,8 +20,6 @@ import { INavEmployee, Role } from '@app/types/bruker';
import { SaksTypeEnum } from '@app/types/kodeverk';
import { ISakenGjelderResponse, ISaksbehandlerResponse } from '@app/types/oppgavebehandling/response';
import { FradelReason, FradelWithHjemler, FradelWithoutHjemler, ITildelingResponse } from '@app/types/oppgaver';
import { OpenOppgavebehandling } from '../common-table-components/open';
import { toast } from '../toast/store';

interface Props {
oppgaveId: string;
Expand All @@ -29,6 +30,10 @@ interface Props {
sakenGjelder: ISakenGjelderResponse['sakenGjelder'];
}

interface TildeltProps extends Props {
timestamp: number;
}

type UseTildel = [(employee: INavEmployee) => Promise<boolean>, { isLoading: boolean }];
type UseFradel = [(params: FradelWithHjemler | FradelWithoutHjemler) => Promise<boolean>, { isLoading: boolean }];

Expand All @@ -43,15 +48,17 @@ export const useTildel = (oppgaveId: string, oppgaveType: SaksTypeEnum, ytelseId

try {
const { saksbehandler: fromSaksbehandler } = await getSaksbehandler(oppgaveId, true).unwrap();
const { saksbehandler: toSaksbehandler } = await tildel({ oppgaveId, employee }).unwrap();
const { saksbehandler: toSaksbehandler, modified } = await tildel({ oppgaveId, employee }).unwrap();
const sakenGjelder = await getSakenGjelder(oppgaveId, true).unwrap();
const timestamp = parseISO(modified).getTime();
createTildeltToast({
toSaksbehandler,
fromSaksbehandler,
sakenGjelder,
oppgaveId,
oppgaveType,
ytelseId,
timestamp,
});

return true;
Expand Down Expand Up @@ -82,14 +89,7 @@ export const useFradel = (oppgaveId: string, oppgaveType: SaksTypeEnum, ytelseId
getSakenGjelder(oppgaveId, true).unwrap(),
]);
await fradel({ oppgaveId, ...params }).unwrap();
createFradeltToast({
toSaksbehandler: null,
fromSaksbehandler,
sakenGjelder,
oppgaveId,
oppgaveType,
ytelseId,
});
createFradeltToast({ toSaksbehandler: null, fromSaksbehandler, sakenGjelder, oppgaveId, oppgaveType, ytelseId });

return true;
} catch {
Expand All @@ -104,7 +104,15 @@ export const useFradel = (oppgaveId: string, oppgaveType: SaksTypeEnum, ytelseId
return [fradelSaksbehandler, { isLoading }];
};

const Tildelt = ({ oppgaveId, oppgaveType, ytelseId, sakenGjelder, toSaksbehandler, fromSaksbehandler }: Props) => {
const Tildelt = ({
oppgaveId,
oppgaveType,
ytelseId,
sakenGjelder,
toSaksbehandler,
fromSaksbehandler,
timestamp,
}: TildeltProps) => {
const { user } = useContext(StaticDataContext);
const sakenGjelderText = `${sakenGjelder.name ?? 'Navn mangler'} (${sakenGjelder.id})`;
const toSaksbehandlerText = formatEmployeeNameAndIdFallback(toSaksbehandler, 'ukjent saksbehandler');
Expand All @@ -116,7 +124,7 @@ const Tildelt = ({ oppgaveId, oppgaveType, ytelseId, sakenGjelder, toSaksbehandl
const secondary =
fromSaksbehandler !== null || canDeassignOthers ? (
<CountdownButton
seconds={10}
seconds={Math.floor((timestamp + 10_000 - Date.now()) / 1_000)}
variant="tertiary"
size="small"
onClick={() => {
Expand Down Expand Up @@ -177,7 +185,7 @@ const Fradelt = ({ oppgaveId, sakenGjelder, fromSaksbehandler, oppgaveType, ytel
);
};

const createTildeltToast = (props: Props) => {
const createTildeltToast = (props: TildeltProps) => {
if (props.fromSaksbehandler?.navIdent !== props.toSaksbehandler?.navIdent) {
toast.success(<Tildelt {...props} />);
}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/components/toast/action-toast.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { styled } from 'styled-components';
import { sendCloseEvent } from '@app/components/toast/toast/helpers';

interface Props {
children: React.ReactNode;
Expand All @@ -12,7 +13,7 @@ interface Props {
export const ActionToast = ({ children, primary, secondary, attrs }: Props) => (
<div {...attrs}>
<span>{children}</span>
<ButtonRow>
<ButtonRow onClick={({ target }) => sendCloseEvent(target)}>
{secondary}
{primary}
</ButtonRow>
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/components/toast/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export const SLIDE_DURATION = 150;
export const TOAST_TIMEOUT = 20_000;
export const TOAST_DEFAULT_TIMEOUT = 20_000;
60 changes: 28 additions & 32 deletions frontend/src/components/toast/store.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,5 @@
import { TOAST_TIMEOUT } from './constants';
import { NewMessage, ToastType } from './types';

type ListenerFn = (messages: Message[]) => void;

type CloseFn = () => void;

export interface Message extends NewMessage {
id: string;
createdAt: number;
expiresAt: number;
close: () => void;
setExpiresAt: (ms: number) => void;
}
import { TOAST_DEFAULT_TIMEOUT } from '@app/components/toast/constants';
import { CloseFn, ListenerFn, Message, TimedMessage, ToastType, UntimedMessage } from '@app/components/toast/types';

class Store {
private messages: Message[] = [];
Expand All @@ -21,42 +9,50 @@ class Store {
if (!this.listeners.includes(listener)) {
this.listeners.push(listener);
}

listener(this.messages);

return () => this.unsubscribe(listener);
}

public unsubscribe(listener: ListenerFn) {
this.listeners = this.listeners.filter((l) => l !== listener);
}

public success = (message: React.ReactNode, timeout?: number) => this.addMessage(ToastType.SUCCESS, message, timeout);
public error = (message: React.ReactNode, timeout?: number) => this.addMessage(ToastType.ERROR, message, timeout);
public warning = (message: React.ReactNode, timeout?: number) => this.addMessage(ToastType.WARNING, message, timeout);
public info = (message: React.ReactNode, timeout?: number) => this.addMessage(ToastType.INFO, message, timeout);
public success = (content: React.ReactNode, timeout?: number) => this.addMessage(ToastType.SUCCESS, content, timeout);
public error = (content: React.ReactNode, timeout?: number) => this.addMessage(ToastType.ERROR, content, timeout);
public warning = (content: React.ReactNode, timeout?: number) => this.addMessage(ToastType.WARNING, content, timeout);
public info = (content: React.ReactNode, timeout?: number) => this.addMessage(ToastType.INFO, content, timeout);

private notify() {
this.listeners.forEach((listener) => listener(this.messages));
}

private addMessage(type: ToastType, message: React.ReactNode, timeout: number = TOAST_TIMEOUT): CloseFn {
private addMessage(type: ToastType, content: React.ReactNode, timeout = TOAST_DEFAULT_TIMEOUT): CloseFn {
const createdAt = Date.now();
const expiresAt = createdAt + timeout;
const id = crypto.randomUUID();

const setExpiresAt = (ms: number) => this.setExpiresAt(id, ms);

const close: CloseFn = () => this.removeMessage(id);

this.messages = [
...this.messages,
{
type,
message,
id,
createdAt,
expiresAt,
close,
setExpiresAt,
},
Number.isFinite(timeout)
? ({
type,
content,
id,
createdAt,
close,
expiresAt: createdAt + timeout,
setExpiresAt: (ms: number) => this.setExpiresAt(id, ms),
timeout,
} satisfies TimedMessage)
: ({
type,
content,
id,
createdAt,
close,
} satisfies UntimedMessage),
];

this.notify();
Expand Down
Loading

0 comments on commit 7230ce9

Please sign in to comment.