From 66e772bd8af5c3f8e46523ac618a789d60a8125e Mon Sep 17 00:00:00 2001 From: Philipp Reinking Date: Tue, 13 Oct 2026 01:06:26 +0200 Subject: [PATCH 1/7] fix: show when the editor saves, offer Retry, and keep a change through a quick reload MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Editor saves now run through one helper in the notice store. It knows which saves wait or run, keeps a failed save to send again, and names the question by number and text. The workbench shows Saving…, Saved and Not saved with Retry; the red bar gets Retry and wraps. Leaving the page sends waiting saves and asks while a save waits, runs or failed. Co-Authored-By: Claude Opus 5.5 --- resources/js/Jetstream/Banner.test.ts | 27 +++- resources/js/Jetstream/Banner.vue | 20 ++- resources/js/Pages/Forms/Edit.vue | 3 + .../js/components/Factory/Main/Workbench.vue | 2 + .../Factory/Settings/SettingsSave.test.ts | 2 +- .../Factory/Shared/SaveStatus.test.ts | 63 ++++++++ .../components/Factory/Shared/SaveStatus.vue | 90 +++++++++++ .../Factory/Shared/useSaveOnLeave.test.ts | 54 +++++++ .../Factory/Shared/useSaveOnLeave.ts | 21 +++ resources/js/stores/form.ts | 48 +++--- resources/js/stores/logic.ts | 22 +-- resources/js/stores/notice.test.ts | 111 ++++++++++--- resources/js/stores/notice.ts | 94 ++++++++++- resources/js/stores/workbench.test.ts | 13 +- resources/js/stores/workbench.ts | 151 +++++++++--------- 15 files changed, 566 insertions(+), 155 deletions(-) create mode 100644 resources/js/components/Factory/Shared/SaveStatus.test.ts create mode 100644 resources/js/components/Factory/Shared/SaveStatus.vue create mode 100644 resources/js/components/Factory/Shared/useSaveOnLeave.test.ts create mode 100644 resources/js/components/Factory/Shared/useSaveOnLeave.ts diff --git a/resources/js/Jetstream/Banner.test.ts b/resources/js/Jetstream/Banner.test.ts index a0bbee5e..f5133c85 100644 --- a/resources/js/Jetstream/Banner.test.ts +++ b/resources/js/Jetstream/Banner.test.ts @@ -1,8 +1,10 @@ -import { mount } from "@vue/test-utils"; -import { describe, expect, it } from "vitest"; +import { mount, VueWrapper } from "@vue/test-utils"; +import { describe, expect, it, vi } from "vitest"; import Banner from "./Banner.vue"; import { useNotice } from "@/stores/notice"; +const retry = (wrapper: VueWrapper) => + wrapper.findAll("[role=alert] button").find((b) => b.text() === "Retry"); const mountBanner = () => mount(Banner, { global: { mocks: { $page: { props: { jetstream: { flash: {} } } } } }, @@ -14,12 +16,12 @@ describe("banner", () => { // the test pinia stubs actions, so set the state directly useNotice().$patch({ - failedSaves: { "saveBlock:1": "Your change was not saved." }, + failedSaves: { "saveBlock:1": "Your change wasn't saved." }, }); await wrapper.vm.$nextTick(); expect(wrapper.get("[role=alert]").text()).toContain( - "Your change was not saved.", + "Your change wasn't saved.", ); await wrapper.get("button[aria-label=Dismiss]").trigger("click"); @@ -27,4 +29,21 @@ describe("banner", () => { expect(useNotice().message).toBeNull(); expect(wrapper.find("[role=alert]").exists()).toBe(false); }); + + it("offers Retry for a save that can be sent again", async () => { + const wrapper = mountBanner(); + + useNotice().$patch({ + failedSaves: { "saveBlock:1": "Your change wasn't saved." }, + }); + await wrapper.vm.$nextTick(); + expect(retry(wrapper)).toBeUndefined(); + + useNotice().$patch({ retries: { "saveBlock:1": vi.fn() } }); + await wrapper.vm.$nextTick(); + await retry(wrapper)!.trigger("click"); + + expect(useNotice().retrySaves).toHaveBeenCalled(); + wrapper.unmount(); + }); }); diff --git a/resources/js/Jetstream/Banner.vue b/resources/js/Jetstream/Banner.vue index b4e6b569..f1bc4a15 100644 --- a/resources/js/Jetstream/Banner.vue +++ b/resources/js/Jetstream/Banner.vue @@ -12,7 +12,7 @@
+

{{ message }}

-
+
+