Skip to content

Commit

Permalink
Merge pull request #649 from tuanchauict/change-project-management-di…
Browse files Browse the repository at this point in the history
…alog

Observe change from other opening tab and create Alert dialog
  • Loading branch information
tuanchauict authored Feb 6, 2025
2 parents fb65f73 + f20486c commit 6aa8935
Show file tree
Hide file tree
Showing 14 changed files with 131 additions and 143 deletions.
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

0 comments on commit 6aa8935

Please sign in to comment.