From 17556c1227f63df84b9600b7b6cc23cae80c5f69 Mon Sep 17 00:00:00 2001 From: x1unix Date: Sat, 14 Dec 2024 00:30:55 -0500 Subject: [PATCH 1/5] feat: track if workspace is dirty --- web/src/store/workspace/dispatchers/snippet.ts | 3 +++ web/src/store/workspace/reducers.ts | 3 +++ web/src/store/workspace/state.ts | 5 +++++ 3 files changed, 11 insertions(+) diff --git a/web/src/store/workspace/dispatchers/snippet.ts b/web/src/store/workspace/dispatchers/snippet.ts index 94b7013c..ba3da79a 100644 --- a/web/src/store/workspace/dispatchers/snippet.ts +++ b/web/src/store/workspace/dispatchers/snippet.ts @@ -121,6 +121,8 @@ export const dispatchShareSnippet = () => async (dispatch: DispatchFn, getState: return } + console.log('isDirty:', workspace.dirty) + dispatch(newLoadingAction()) dispatch( newAddNotificationAction({ @@ -149,6 +151,7 @@ export const dispatchShareSnippet = () => async (dispatch: DispatchFn, getState: type: WorkspaceAction.WORKSPACE_IMPORT, payload: { ...workspace, + dirty: false, snippet: { id: snippetID, }, diff --git a/web/src/store/workspace/reducers.ts b/web/src/store/workspace/reducers.ts index aa15bcb6..33559d08 100644 --- a/web/src/store/workspace/reducers.ts +++ b/web/src/store/workspace/reducers.ts @@ -11,6 +11,7 @@ export const reducers = mapByAction( const addedFiles = Object.fromEntries(items.map(({ filename, content }) => [filename, content])) return { ...rest, + dirty: true, selectedFile: items[0].filename, files: { ...files, @@ -25,6 +26,7 @@ export const reducers = mapByAction( const { files = {}, ...rest } = s return { ...rest, + dirty: true, files: { ...files, [filename]: content, @@ -69,6 +71,7 @@ export const reducers = mapByAction( const { [filename]: _, ...restFiles } = files return { ...rest, + dirty: true, selectedFile: newSelectedFile, files: restFiles, } diff --git a/web/src/store/workspace/state.ts b/web/src/store/workspace/state.ts index b69366f6..62dbfa18 100644 --- a/web/src/store/workspace/state.ts +++ b/web/src/store/workspace/state.ts @@ -48,6 +48,11 @@ export interface WorkspaceState { * Key-value pair of file names and their content. */ files?: Record + + /** + * Indicates whether any of workspace files were changed. + */ + dirty?: boolean } export const initialWorkspaceState: WorkspaceState = { From cbe7c255860394e832063abeb6370fd04cde088e Mon Sep 17 00:00:00 2001 From: x1unix Date: Sat, 14 Dec 2024 01:11:04 -0500 Subject: [PATCH 2/5] feat: dont allow sharing unchanged snippet or during progress --- .../store/workspace/dispatchers/snippet.ts | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/web/src/store/workspace/dispatchers/snippet.ts b/web/src/store/workspace/dispatchers/snippet.ts index ba3da79a..e563a6f1 100644 --- a/web/src/store/workspace/dispatchers/snippet.ts +++ b/web/src/store/workspace/dispatchers/snippet.ts @@ -12,7 +12,7 @@ import { import { newLoadingAction, newErrorAction, newUIStateChangeAction } from '~/store/actions/ui' import { type SnippetLoadPayload, WorkspaceAction, type BulkFileUpdatePayload } from '../actions' import { loadWorkspaceState } from '../config' -import { getDefaultWorkspaceState } from '../state' +import { type WorkspaceState, getDefaultWorkspaceState } from '../state' /** * Dispatch snippet load from a predefined source. @@ -104,9 +104,29 @@ export const dispatchLoadSnippet = } } +const workspaceHasChanges = (state: WorkspaceState) => { + if (state.snippet?.loading) { + return false + } + + if (!state.snippet?.id) { + return true + } + + return !!state.dirty +} + +const workspaceNotChangedNotificationID = 'WS_NOT_CHANGED' + export const dispatchShareSnippet = () => async (dispatch: DispatchFn, getState: StateProvider) => { const notificationId = newNotificationId() - const { workspace } = getState() + const { workspace, status } = getState() + + if (status?.loading) { + // Prevent sharing during any kind of loading progress. + // This also prevents concurrent share process. + return + } if (!workspace.files) { dispatch( @@ -121,7 +141,27 @@ export const dispatchShareSnippet = () => async (dispatch: DispatchFn, getState: return } - console.log('isDirty:', workspace.dirty) + if (!workspaceHasChanges(workspace)) { + // Prevent from sharing already shared shippets + dispatch( + newAddNotificationAction({ + id: workspaceNotChangedNotificationID, + type: NotificationType.Info, + canDismiss: true, + title: 'Share snippet', + description: 'You haven\'t made any changes to a snippet. Please edit any file before sharing.', + actions: [ + { + label: 'OK', + key: 'ok', + primary: true, + onClick: () => newRemoveNotificationAction(workspaceNotChangedNotificationID), + }, + ] + }) + ) + return + } dispatch(newLoadingAction()) dispatch( From 8bf98d43f86c248acadc70344768354b53507542 Mon Sep 17 00:00:00 2001 From: x1unix Date: Sat, 14 Dec 2024 01:11:40 -0500 Subject: [PATCH 3/5] feat: share on Ctrl/Meta+S hotkey --- .../workspace/CodeEditor/utils/commands.ts | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/web/src/components/features/workspace/CodeEditor/utils/commands.ts b/web/src/components/features/workspace/CodeEditor/utils/commands.ts index a917c822..43ceed91 100644 --- a/web/src/components/features/workspace/CodeEditor/utils/commands.ts +++ b/web/src/components/features/workspace/CodeEditor/utils/commands.ts @@ -1,6 +1,6 @@ import * as monaco from 'monaco-editor' import { runFileDispatcher, type StateDispatch } from '~/store' -import { dispatchFormatFile, dispatchResetWorkspace } from '~/store/workspace' +import { dispatchFormatFile, dispatchResetWorkspace, dispatchShareSnippet } from '~/store/workspace' /** * MonacoDIContainer is undocumented DI service container of monaco editor. @@ -39,7 +39,21 @@ export const attachCustomCommands = (editorInstance: monaco.editor.IStandaloneCo ) } +const debounced = (fn: (arg: TArg) => void, delay: number) => { + let tid: ReturnType | undefined = undefined + + return (arg: TArg) => { + if (tid) { + clearTimeout(tid) + } + + tid = setTimeout(fn, delay, arg) + } +} + export const registerEditorActions = (editor: monaco.editor.IStandaloneCodeEditor, dispatcher: StateDispatch) => { + const dispatchDebounce = debounced(dispatcher, 750) + const actions = [ { id: 'clear', @@ -67,6 +81,15 @@ export const registerEditorActions = (editor: monaco.editor.IStandaloneCodeEdito dispatcher(dispatchFormatFile()) }, }, + { + id: 'share', + label: 'Share Snippet', + contextMenuGroupId: 'navigation', + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS ], + run: () => { + dispatchDebounce(dispatchShareSnippet()) + }, + }, ] actions.forEach((action) => editor.addAction(action)) From 72db6201e9592bb400b19c34c4f3aeb332c7b523 Mon Sep 17 00:00:00 2001 From: x1unix Date: Sat, 14 Dec 2024 01:16:56 -0500 Subject: [PATCH 4/5] fix: remove notification after share --- web/src/store/workspace/dispatchers/snippet.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/web/src/store/workspace/dispatchers/snippet.ts b/web/src/store/workspace/dispatchers/snippet.ts index e563a6f1..6fae037b 100644 --- a/web/src/store/workspace/dispatchers/snippet.ts +++ b/web/src/store/workspace/dispatchers/snippet.ts @@ -163,6 +163,7 @@ export const dispatchShareSnippet = () => async (dispatch: DispatchFn, getState: return } + dispatch(newRemoveNotificationAction(workspaceNotChangedNotificationID)) dispatch(newLoadingAction()) dispatch( newAddNotificationAction({ From 6427d593d2ba51c3f63a4adad9022904e8152355 Mon Sep 17 00:00:00 2001 From: x1unix Date: Sat, 14 Dec 2024 02:52:25 -0500 Subject: [PATCH 5/5] fix: lint --- .../features/workspace/CodeEditor/utils/commands.ts | 4 ++-- web/src/store/workspace/dispatchers/snippet.ts | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/web/src/components/features/workspace/CodeEditor/utils/commands.ts b/web/src/components/features/workspace/CodeEditor/utils/commands.ts index 43ceed91..e278d6d5 100644 --- a/web/src/components/features/workspace/CodeEditor/utils/commands.ts +++ b/web/src/components/features/workspace/CodeEditor/utils/commands.ts @@ -40,7 +40,7 @@ export const attachCustomCommands = (editorInstance: monaco.editor.IStandaloneCo } const debounced = (fn: (arg: TArg) => void, delay: number) => { - let tid: ReturnType | undefined = undefined + let tid: ReturnType | undefined return (arg: TArg) => { if (tid) { @@ -85,7 +85,7 @@ export const registerEditorActions = (editor: monaco.editor.IStandaloneCodeEdito id: 'share', label: 'Share Snippet', contextMenuGroupId: 'navigation', - keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS ], + keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS], run: () => { dispatchDebounce(dispatchShareSnippet()) }, diff --git a/web/src/store/workspace/dispatchers/snippet.ts b/web/src/store/workspace/dispatchers/snippet.ts index 6fae037b..cf627d07 100644 --- a/web/src/store/workspace/dispatchers/snippet.ts +++ b/web/src/store/workspace/dispatchers/snippet.ts @@ -149,7 +149,7 @@ export const dispatchShareSnippet = () => async (dispatch: DispatchFn, getState: type: NotificationType.Info, canDismiss: true, title: 'Share snippet', - description: 'You haven\'t made any changes to a snippet. Please edit any file before sharing.', + description: "You haven't made any changes to a snippet. Please edit any file before sharing.", actions: [ { label: 'OK', @@ -157,8 +157,8 @@ export const dispatchShareSnippet = () => async (dispatch: DispatchFn, getState: primary: true, onClick: () => newRemoveNotificationAction(workspaceNotChangedNotificationID), }, - ] - }) + ], + }), ) return }