diff --git a/CHANGELOG.md b/CHANGELOG.md index 62122ce4..bd7a59dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,14 @@ a version is cut. ### Fixed +- **One confirmation message instead of two.** Saving a new client, system user or role showed the + same green "Client created." twice, stacked. So did deleting one. It was only ever cosmetic — + nothing happened twice — but it read as though something had, which is the last thing a + confirmation should do. Saves that stay on the same screen, such as the email settings, were never + affected. + ([#1675](https://github.com/projectsend/projectsend/issues/1675), reported and diagnosed by + [@denkfabrik-li](https://github.com/denkfabrik-li)) + - **Connecting a provider to an account that already has one.** Signing in with Google, Microsoft or a custom provider worked, but attaching one to an existing account did not: the **Connect** button on Settings → Connected accounts appeared to do nothing at all. The button asks the server in the diff --git a/resources/js/components/toaster.tsx b/resources/js/components/toaster.tsx index 0dbf8ae3..ef2523f5 100644 --- a/resources/js/components/toaster.tsx +++ b/resources/js/components/toaster.tsx @@ -15,11 +15,26 @@ interface Toast { const DURATION = 4500; +/** + * The flash object most recently turned into toasts, across remounts. + * + * Both of the component's sources can hand over the *same* visit's flash: + * the layout (and the Toaster inside it) remounts whenever a flashed + * redirect lands on a different page component — create → edit, delete → + * index — and then the mount-time read and the router `success` event + * each fire once for one flash, stacking every "Client created." twice. + * Identity comparison is the dedup that cannot over-trigger: a repeat of + * the same action produces an identical *message* but never the identical + * *object*, so deliberate back-to-back toasts still both show. + */ +let shownFlash: SharedData['flash'] | null = null; + /** * App-wide flash toasts. Reads the `flash` shared prop and shows a toast * after any Inertia visit that carried one (created/updated/deleted…). * Uses the router `success` event so two identical messages in a row still - * each toast. Mounted once in the app layout. + * each toast. Mounted in the app layout, which remounts it — see the note on + * `shownFlash` above. */ export function Toaster() { const { t } = useTranslation(); @@ -36,16 +51,20 @@ export function Toaster() { const dismiss = (id: number) => setToasts((current) => current.filter((toast) => toast.id !== id)); + const pushFlash = (flash: SharedData['flash'] | null | undefined) => { + if (!flash || flash === shownFlash) return; + shownFlash = flash; + push('success', flash.success); + push('error', flash.error); + }; + useEffect(() => { // A flash present on the very first render (e.g. a server redirect on load). - push('success', page.props.flash?.success); - push('error', page.props.flash?.error); + pushFlash(page.props.flash); // And every subsequent successful visit. const stop = router.on('success', (event) => { - const flash = (event.detail.page.props as unknown as SharedData).flash; - push('success', flash?.success); - push('error', flash?.error); + pushFlash((event.detail.page.props as unknown as SharedData).flash); }); return () => stop();