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

Observe change from other opening tab and create Alert dialog #649

Merged
merged 2 commits into from
Feb 6, 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
1 change: 1 addition & 0 deletions monosketch-svelte/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
<body>
<div id="app"></div>
<div id="modal"></div>
<div id="alert"></div>
<div id="tooltip"></div>
<script type="module" src="/src/main.ts"></script>
</body>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { ExportShapesHelper } from "$mono/state-manager/export/export-shapes-hel
import { FileMediator } from "$mono/state-manager/onetimeaction/file-mediator";
import type { WorkspaceDao } from "$mono/store-manager/dao/workspace-dao";
import { DEFAULT_NAME } from "$mono/store-manager/dao/workspace-object-dao";
import { modalViewModel } from "$ui/modal/viewmodel";
import { AlertDialog } from "$ui/modal/common/AlertDialog";

/**
* A helper class to handle file-related one-time actions in the application.
Expand All @@ -34,7 +34,7 @@ export class FileRelatedActionsHelper {
) {
this.exportShapesHelper = new ExportShapesHelper(
(shape) => bitmapManager.getBitmap(shape),
shapeClipboardManager.setClipboardText.bind(shapeClipboardManager)
shapeClipboardManager.setClipboardText.bind(shapeClipboardManager),
);
}

Expand Down Expand Up @@ -124,16 +124,26 @@ export class FileRelatedActionsHelper {
const rootGroup = RootGroup(monoFile.root);
const existingProject = this.workspaceDao.getObject(rootGroup.id);
if (existingProject.rootGroup) {
modalViewModel.existingProjectFlow.value = {
projectName: existingProject.name,
lastEditedTimeMillis: existingProject.lastModifiedTimestampMillis,
onReplace: () => {
this.prepareAndApplyNewRoot(RootGroup(monoFile.root.copy({ isIdTemporary: true })), monoFile.extra);
AlertDialog(
{
title: "Project already exists",
message: `A project with the same ID already exists. Do you want to replace it?
- ${existingProject.name}
- Last edited: ${new Date(existingProject.lastModifiedTimestampMillis).toLocaleString()}`,
primaryAction: {
text: "Replace",
onClick: () => {
this.prepareAndApplyNewRoot(rootGroup, monoFile.extra);
},
},
secondaryAction: {
text: "Keep both",
onClick: () => {
this.prepareAndApplyNewRoot(RootGroup(monoFile.root.copy({ isIdTemporary: true })), monoFile.extra);
},
},
},
onKeepBoth: () => {
this.prepareAndApplyNewRoot(rootGroup, monoFile.extra);
}
};
);
} else {
this.prepareAndApplyNewRoot(rootGroup, monoFile.extra);
}
Expand Down
3 changes: 2 additions & 1 deletion monosketch-svelte/src/lib/mono/store-manager/consts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ export const StoreKeys = {
FONT_SIZE: 'font-size',

WORKSPACE: 'workspace',
LAST_OPEN: 'last-open', // deprecated
LAST_MODIFIED_PROJECT_ID: 'last-open', // last opened object id
LAST_MODIFIED_TIME: 'last-modified', // last modified timestamp of the workspace

OBJECT_NAME: 'name',
OBJECT_CONTENT: 'content',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Flow } from "$libs/flow";
import { StorageDocument, StoreKeys } from "$mono/store-manager";
import { WorkspaceObjectDao } from "$mono/store-manager/dao/workspace-object-dao";

Expand All @@ -10,25 +11,26 @@ export class WorkspaceDao {
private readonly workspaceDocument: StorageDocument;
private objectDaos: Map<string, WorkspaceObjectDao>;

private workspaceUpdateMutableFlow: Flow<string> = new Flow('');
workspaceUpdateFlow: Flow<string> = this.workspaceUpdateMutableFlow.immutable();

private constructor(workspaceDocument: StorageDocument) {
this.workspaceDocument = workspaceDocument;
this.objectDaos = new Map<string, WorkspaceObjectDao>();

workspaceDocument.setObserver(StoreKeys.LAST_OPEN, {
onChange: (key, oldValue, newValue) => {
console.log(`Last opened object changed: ${oldValue} -> ${newValue}`);
},
workspaceDocument.setObserver(StoreKeys.LAST_MODIFIED_TIME, () => {
this.workspaceUpdateMutableFlow.value = this.lastOpenedObjectId ?? '';
});
}

// Accessor property for lastOpenedObjectId
get lastOpenedObjectId(): string | null {
return this.workspaceDocument.get(StoreKeys.LAST_OPEN);
return this.workspaceDocument.get(StoreKeys.LAST_MODIFIED_PROJECT_ID);
}

set lastOpenedObjectId(value: string | null) {
if (value !== null) {
this.workspaceDocument.set(StoreKeys.LAST_OPEN, value);
this.workspaceDocument.set(StoreKeys.LAST_MODIFIED_PROJECT_ID, value);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@ import { StorageDocument, StoreKeys } from "$mono/store-manager";
* A dao for an object (aka project or file) in the workspace.
*/
export class WorkspaceObjectDao {
objectId: string;
private objectDocument: StorageDocument;

constructor(objectId: string, workspaceDocument: StorageDocument) {
this.objectId = objectId;
constructor(public objectId: string, private workspaceDocument: StorageDocument) {
this.objectDocument = workspaceDocument.childDocument(objectId);
}

Expand Down Expand Up @@ -63,6 +61,7 @@ export class WorkspaceObjectDao {
// @ts-expect-error toJson
const jsonArray = value.map(connector => connector.toJson());
this.objectDocument.set(StoreKeys.OBJECT_CONNECTORS, JSON.stringify(jsonArray));
this.lastModifiedTimestampMillis = Date.now();
}

get name(): string {
Expand All @@ -84,6 +83,8 @@ export class WorkspaceObjectDao {

set lastModifiedTimestampMillis(value: number) {
this.objectDocument.set(StoreKeys.OBJECT_LAST_MODIFIED, value.toString());
this.workspaceDocument.set(StoreKeys.LAST_MODIFIED_TIME, value.toString());
this.workspaceDocument.set(StoreKeys.LAST_MODIFIED_PROJECT_ID, this.objectId);
}

get lastOpened(): number {
Expand Down
6 changes: 2 additions & 4 deletions monosketch-svelte/src/lib/mono/store-manager/store-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import { MigrateTo2 } from './migrations/migrate2';
/**
* An interface for observing storage change.
*/
export interface StoreObserver {
onChange(key: string, oldValue: string | null, newValue: string | null): void;
}
export type StoreObserver = (key: string, oldValue: string | null, newValue: string | null) => void

/**
* A class for managing storage.
Expand Down Expand Up @@ -82,7 +80,7 @@ export class StoreManager {
private onStorageChange = (event: StorageEvent) => {
const key = event.key;
if (key && this.keyToObserverMap[key]) {
this.keyToObserverMap[key].onChange(key, event.oldValue, event.newValue);
this.keyToObserverMap[key](key, event.oldValue, event.newValue);
}
};
}
Expand Down
10 changes: 4 additions & 6 deletions monosketch-svelte/src/lib/mono/ui-state-manager/theme-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,10 @@ export class AppThemeManager {
this.settingsDocument.set(StoreKeys.THEME_MODE, themeMode);
});

this.settingsDocument.setObserver(StoreKeys.THEME_MODE, {
onChange: (key, oldValue, newValue) => {
if (newValue) {
this.themeManager.setTheme(newValue as ThemeMode);
}
},
this.settingsDocument.setObserver(StoreKeys.THEME_MODE, (_key, _oldValue, newValue) => {
if (newValue) {
this.themeManager.setTheme(newValue as ThemeMode);
}
});
};

Expand Down
12 changes: 0 additions & 12 deletions monosketch-svelte/src/lib/ui/modal/ModalHolder.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,16 @@ import { onDestroy, onMount } from 'svelte';
import { modalViewModel } from './viewmodel';
import { LifecycleOwner } from '$libs/flow';
import KeyboardShortcutModal from './keyboard-shortcut/KeyboardShortcutModal.svelte';
import type { ExistingProjectModel } from "$ui/modal/existing-project/model";
import ExistingProjectDialog from "$ui/modal/existing-project/ExistingProjectDialog.svelte";

let shortcutModal: boolean = false;

let existingProjectModel: ExistingProjectModel | null = null;

const lifecycleOwner = new LifecycleOwner();
onMount(() => {
lifecycleOwner.onStart();

modalViewModel.keyboardShortcutVisibilityStateFlow.observe(lifecycleOwner, (value) => {
shortcutModal = value;
});

modalViewModel.existingProjectFlow.observe(lifecycleOwner, (value) => {
existingProjectModel = value;
});
});

onDestroy(() => {
Expand All @@ -31,7 +23,3 @@ onDestroy(() => {
{#if shortcutModal}
<KeyboardShortcutModal />
{/if}

{#if existingProjectModel}
<ExistingProjectDialog model="{existingProjectModel}" />
{/if}
51 changes: 51 additions & 0 deletions monosketch-svelte/src/lib/ui/modal/common/AlertDialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright (c) 2025, tuanchauict
*/

import Dialog from "$ui/modal/common/Dialog.svelte";

export type ActionButtonModel = {
text: string;
onClick: () => void;
}

export type AlertDialogModel = {
title?: string;
message?: string;
dismissOnClickingOutside?: boolean;
primaryAction?: ActionButtonModel;
secondaryAction?: ActionButtonModel;
onDismiss?: () => void;
}

export function AlertDialog(props: AlertDialogModel) {
const primaryAction = props.primaryAction ? {
text: props.primaryAction.text,
onClick: () => {
props.primaryAction!.onClick();
dialog.$destroy();
},
} : undefined;
const secondaryAction = props.secondaryAction ? {
text: props.secondaryAction.text,
onClick: () => {
props.secondaryAction!.onClick();
dialog.$destroy();
},
} : undefined;

const dialog = new Dialog({
target: document.querySelector('#alert')!,
props: {
model: {
...props,
primaryAction,
secondaryAction,
onDismiss: () => {
props.onDismiss?.();
dialog.$destroy();
}
},
},
});
}
62 changes: 34 additions & 28 deletions monosketch-svelte/src/lib/ui/modal/common/Dialog.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,25 @@
-->

<script lang="ts">
export let title: string;
export let content: string = "";
import { fade, slide } from "svelte/transition";
import type { AlertDialogModel } from "$ui/modal/common/AlertDialog";

export let confirmText: string = "Confirm";
export let cancelText: string = "Cancel";
export let onConfirm: () => void;
export let onCancel: () => void;
export let model: AlertDialogModel;

export let dismissOnClickingOutside: boolean = true;
let title = model.title || '';
let message = model.message || '';
let primaryAction = model.primaryAction;
let secondaryAction = model.secondaryAction;
let dismissOnClickingOutside = model.dismissOnClickingOutside ?? true;
let onDismiss = model.onDismiss || (() => {});

export let onDismiss: () => void;

function handleConfirm() {
onConfirm();
function handlePrimaryAction() {
primaryAction?.onClick();
onDismiss();
}

function handleCancel() {
onCancel();
function handleSecondaryAction() {
secondaryAction?.onClick();
onDismiss();
}

Expand All @@ -31,32 +31,34 @@
}
}
</script>

<div class="dialog-modal" role="button" tabindex="-1"
transition:fade={{ duration: 200 }}
on:click="{handleClickOutside}"
on:keydown="{(e) => e.key === 'Escape' && handleClickOutside(e)}"
>
<div class="modal-content" role="button" tabindex="-1"
transition:slide={{ duration: 200 }}
on:click|preventDefault|stopPropagation on:keydown>
{#if title}
<h2>{title}</h2>
{/if}
{#if content}
<p class:no-title={title.length === 0}>{content}</p>
{#if message}
<p class:no-title={!title}>{message}</p>
{:else }
<slot />
<slot/>
{/if}
{#if primaryAction || secondaryAction}
<div class="actions">
{#if secondaryAction}
<button class="secondary" on:click="{handleSecondaryAction}">{secondaryAction.text}</button>
{/if}
{#if primaryAction}
<button class="primary" on:click="{handlePrimaryAction}">{primaryAction.text}</button>
{/if}
</div>
{/if}
<div class="actions">
{#if cancelText}
<button class="secondary" on:click="{handleCancel}">{cancelText}</button>
{/if}
{#if confirmText}
<button class="primary" on:click="{handleConfirm}">{confirmText}</button>
{/if}
</div>
</div>
</div>

<style lang="scss">
@import "../../../style/variables";

Expand All @@ -78,7 +80,7 @@
min-width: 350px;
max-width: 450px;
border-radius: 5px;
padding: 12px;
padding: 16px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.15);
font-family: $monospaceFont;

Expand Down Expand Up @@ -107,6 +109,7 @@
display: flex;
justify-content: flex-end;
margin-top: 16px;
font-family: $monospaceFont;
}

button {
Expand All @@ -116,8 +119,11 @@
user-select: none;
border-radius: 4px;
border: 1px solid transparent;
padding: 4px 8px;
padding: 4px 10px;
background: none;
min-width: 50px;
font-weight: bold;
font-family: $monospaceFont;

&.primary {
background: var(--primary-action-color);
Expand Down
Loading
Loading