Skip to content

Commit 82ecb10

Browse files
authored
feat: Manual save command (#5327)
Closes #5325 ## Description As a user I am sometimes afraid my changes weren't saved. So this is an extra layer of confidence to know that changes are actually saved. https://github.com/user-attachments/assets/9135e225-2c12-450b-b6d0-680d9ecd7454 <img width="207" height="175" alt="image" src="https://github.com/user-attachments/assets/49ecdbad-e191-4088-8046-99b2b55070f7" /> ## Steps for reproduction 1. click button 2. expect xyz ## Code Review - [ ] hi @kof, I need you to do - conceptual review (architecture, feature-correctness) - detailed review (read every line) - test it on preview ## Before requesting a review - [ ] made a self-review - [ ] added inline comments where things may be not obvious (the "why", not "what") ## Before merging - [ ] tested locally and on preview environment (preview dev login: 0000) - [ ] updated [test cases](https://github.com/webstudio-is/webstudio/blob/main/apps/builder/docs/test-cases.md) document - [ ] added tests - [ ] if any new env variables are added, added them to `.env` file
1 parent d5d1fed commit 82ecb10

File tree

4 files changed

+56
-0
lines changed

4 files changed

+56
-0
lines changed

apps/builder/app/builder/features/topbar/menu/menu.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ export const Menu = () => {
142142
<Kbd value={["backspace"]} />
143143
</DropdownMenuItemRightSlot>
144144
</DropdownMenuItem>
145+
<DropdownMenuItem onSelect={() => emitCommand("save")}>
146+
Save
147+
<DropdownMenuItemRightSlot>
148+
<Kbd value={["meta", "s"]} />
149+
</DropdownMenuItemRightSlot>
150+
</DropdownMenuItem>
145151
<DropdownMenuSeparator />
146152
<DropdownMenuItem onSelect={() => emitCommand("togglePreviewMode")}>
147153
Preview

apps/builder/app/builder/shared/commands.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ import { denormalizeSrcProps } from "~/shared/copy-paste/asset-upload";
5656
import { getInstanceLabel } from "./instance-label";
5757
import { $instanceTags } from "../features/style-panel/shared/model";
5858
import { reactPropsToStandardAttributes } from "@webstudio-is/react-sdk";
59+
import { isSyncIdle } from "./sync/sync-server";
5960

6061
export const $styleSourceInputElement = atom<HTMLInputElement | undefined>();
6162

@@ -632,6 +633,22 @@ export const { emitCommand, subscribeCommands } = createCommandsEmitter({
632633
},
633634
},
634635

636+
{
637+
name: "save",
638+
defaultHotkeys: ["meta+s", "ctrl+s"],
639+
handler: async () => {
640+
toast.dismiss("save-success");
641+
try {
642+
await isSyncIdle();
643+
toast.success("Project saved successfully", { id: "save-success" });
644+
} catch (error) {
645+
if (error instanceof Error) {
646+
toast.error(error.message);
647+
}
648+
}
649+
},
650+
},
651+
635652
{
636653
name: "openCommandPanel",
637654
hidden: true,

apps/builder/app/builder/shared/sync/sync-server.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,38 @@ export class ServerSyncStorage implements SyncStorage {
324324
}
325325
}
326326

327+
/**
328+
* Promisify idle state of the queue for a one-off notification when everything is saved.
329+
*/
330+
export const isSyncIdle = () => {
331+
return new Promise<QueueStatus>((resolve, reject) => {
332+
const handle = (status: QueueStatus) => {
333+
if (status.status === "idle") {
334+
resolve(status);
335+
return true;
336+
}
337+
if (status.status === "fatal") {
338+
reject(
339+
new Error(
340+
"Synchronization is in fatal state. Please reload the page or check your internet connection."
341+
)
342+
);
343+
return true;
344+
}
345+
return false;
346+
};
347+
const status = $queueStatus.get();
348+
349+
if (handle(status) === false) {
350+
const unsubscribe = $queueStatus.subscribe((status) => {
351+
if (handle(status)) {
352+
unsubscribe();
353+
}
354+
});
355+
}
356+
});
357+
};
358+
327359
type SyncServerProps = {
328360
projectId: Project["id"];
329361
authPermit: AuthPermit;

packages/design-system/src/components/toast.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,4 +391,5 @@ export const toast = {
391391
hotToast.custom(value, options),
392392
success: (value: JSX.Element | string, options?: Options) =>
393393
hotToast.success(value, options),
394+
dismiss: hotToast.dismiss,
394395
};

0 commit comments

Comments
 (0)