From 3204078cab486c6fcfeec7d7fd2a45ef63d5e972 Mon Sep 17 00:00:00 2001 From: Pedro Lamas Date: Thu, 7 Dec 2023 17:50:59 +0000 Subject: [PATCH] feat: adds Prompt Dialog support Signed-off-by: Pedro Lamas --- components.d.ts | 1 + src/App.vue | 5 +- .../common/ActionCommandPromptDialog.vue | 69 +++++++++++ src/store/console/actions.ts | 108 ++++++++++++++++-- src/store/console/mutations.ts | 25 +++- src/store/console/state.ts | 5 + src/store/console/types.ts | 27 ++++- 7 files changed, 228 insertions(+), 12 deletions(-) create mode 100644 src/components/common/ActionCommandPromptDialog.vue diff --git a/components.d.ts b/components.d.ts index d153981e3e3..49ad7c9bc9d 100644 --- a/components.d.ts +++ b/components.d.ts @@ -7,6 +7,7 @@ export {} declare module 'vue' { export interface GlobalComponents { + ActionCommandPromptDialog: typeof import('./src/components/common/ActionCommandPromptDialog.vue')['default'] AddInstanceDialog: typeof import('./src/components/common/AddInstanceDialog.vue')['default'] AppAnnouncementDismissMenu: typeof import('./src/components/layout/AppAnnouncementDismissMenu.vue')['default'] AppBar: typeof import('./src/components/layout/AppBar.vue')['default'] diff --git a/src/App.vue b/src/App.vue index 52edf957710..8295f718574 100644 --- a/src/App.vue +++ b/src/App.vue @@ -84,6 +84,7 @@ + @@ -108,6 +109,7 @@ import SpoolSelectionDialog from '@/components/widgets/spoolman/SpoolSelectionDi import type { FlashMessage } from '@/types' import { getFilesFromDataTransfer, hasFilesInDataTransfer } from './util/file-system-entry' import type { ThemeConfig } from '@/store/config/types' +import ActionCommandPromptDialog from './components/common/ActionCommandPromptDialog.vue' @Component({ metaInfo () { @@ -119,7 +121,8 @@ import type { ThemeConfig } from '@/store/config/types' }, components: { SpoolSelectionDialog, - FileSystemDownloadDialog + FileSystemDownloadDialog, + ActionCommandPromptDialog } }) export default class App extends Mixins(StateMixin, FilesMixin, BrowserMixin) { diff --git a/src/components/common/ActionCommandPromptDialog.vue b/src/components/common/ActionCommandPromptDialog.vue new file mode 100644 index 00000000000..9dfe1b49c4e --- /dev/null +++ b/src/components/common/ActionCommandPromptDialog.vue @@ -0,0 +1,69 @@ + + + diff --git a/src/store/console/actions.ts b/src/store/console/actions.ts index 882a6d19969..ad2f43fb9ed 100644 --- a/src/store/console/actions.ts +++ b/src/store/console/actions.ts @@ -1,9 +1,10 @@ import type { ActionTree } from 'vuex' import { Globals } from '@/globals' -import type { ConsoleEntry, ConsoleFilter, ConsoleState } from './types' +import type { ConsoleEntry, ConsoleFilter, ConsoleState, PromptDialogButton, PromptDialogItemButton, PromptDialogItemText } from './types' import type { RootState } from '../types' import { SocketActions } from '@/api/socketActions' import DOMPurify from 'dompurify' +import { takeRightWhile } from 'lodash-es' export const actions: ActionTree = { /** @@ -41,7 +42,7 @@ export const actions: ActionTree = { /** * Add a console entry */ - async onAddConsoleEntry ({ commit }, payload: ConsoleEntry) { + async onAddConsoleEntry ({ commit, dispatch }, payload: ConsoleEntry) { payload.message = DOMPurify.sanitize(payload.message).replace(/(?:\r\n|\r|\n)/g, '
') if (!payload.time || payload.time <= 0) { payload.time = Date.now() / 1000 | 0 @@ -49,21 +50,110 @@ export const actions: ActionTree = { if (!payload.type) { payload.type = 'response' } + if (payload.type === 'response' && payload.message.startsWith('// action:')) { + payload.type = 'action' + } + commit('setConsoleEntry', payload) + + dispatch('onUpdatePromptDialog', payload) }, /** * On a fresh load of the UI, we load prior gcode / console history */ - async onGcodeStore ({ commit }, payload) { + async onGcodeStore ({ commit, dispatch }, payload: { gcode_store?: ConsoleEntry[] }) { if (payload && payload.gcode_store) { - const entries = payload.gcode_store.map((s: ConsoleEntry, i: number) => { - s.message = Globals.CONSOLE_RECEIVE_PREFIX + s.message - s.message = DOMPurify.sanitize(s.message).replace(/(?:\r\n|\r|\n)/g, '
') - s.id = i - return s - }) + const entries = payload.gcode_store + .map((entry, index: number) => { + entry.message = Globals.CONSOLE_RECEIVE_PREFIX + entry.message + + entry.message = DOMPurify.sanitize(entry.message).replace(/(?:\r\n|\r|\n)/g, '
') + + entry.id = index + + if ( + entry.type === 'response' && + entry.message.startsWith('// action:') + ) { + entry.type = 'action' + } + + return entry + }) + commit('setAllEntries', entries) + + const dialogEntries = entries + .filter(entry => ( + entry.type === 'action' && + entry.message.startsWith('// action:prompt_') + )) + + const dialogEntriesAfterEnd = takeRightWhile(dialogEntries, entry => entry.message !== '// action:prompt_end') + const dialogEntriesAfterBegin = takeRightWhile(dialogEntriesAfterEnd, entry => entry.message !== '// action:prompt_begin') + + dialogEntriesAfterBegin + .forEach(entry => dispatch('onUpdatePromptDialog', entry)) + } + }, + + async onUpdatePromptDialog ({ commit }, payload: ConsoleEntry) { + const parsedMessage = ( + payload.type === 'action' && + /^\/\/ action:prompt_([^ ]+)(?: (.+))?/.exec(payload.message) + ) + + if (parsedMessage) { + const [, type, param] = parsedMessage + + switch (type) { + case 'begin': + commit('setResetPromptDialog', param) + break + + case 'text': { + const item: PromptDialogItemText = { + type: 'text', + text: param + } + + commit('setPromptDialogItem', item) + break + } + + case 'button': { + const [text, command, color] = param.split('|') + + const item: PromptDialogItemButton = { + type: 'button', + text, + command, + color + } + + commit('setPromptDialogItem', item) + break + } + + case 'footer_button': { + const [text, command, color] = param.split('|') + + const item: PromptDialogButton = { + text, + command, + color + } + + commit('setPromptDialogFooterButton', item) + + break + } + + case 'show': + case 'end': + commit('setPromptDialogOpen', type === 'show') + } } }, diff --git a/src/store/console/mutations.ts b/src/store/console/mutations.ts index 2b19e61691e..e8edfa5e641 100644 --- a/src/store/console/mutations.ts +++ b/src/store/console/mutations.ts @@ -3,7 +3,7 @@ import { v4 as uuidv4 } from 'uuid' import type { MutationTree } from 'vuex' import { defaultState } from './state' import { Globals } from '@/globals' -import type { ConsoleEntry, ConsoleFilter, ConsoleState } from './types' +import type { ConsoleEntry, ConsoleFilter, ConsoleState, PromptDialogItemButton, PromptDialogItem } from './types' import escapeRegExp from '@/util/escape-regexp' const compileExpression = (filter: ConsoleFilter): RegExp => { @@ -47,6 +47,29 @@ export const mutations: MutationTree = { state.console = payload }, + setResetPromptDialog (state, payload: string) { + const { promptDialog } = defaultState() + + Object.assign(state, { + promptDialog: { + ...promptDialog, + title: payload + } + }) + }, + + setPromptDialogItem (state, payload: PromptDialogItem) { + state.promptDialog.items.push(payload) + }, + + setPromptDialogFooterButton (state, payload: PromptDialogItemButton) { + state.promptDialog.footerButtons.push(payload) + }, + + setPromptDialogOpen (state, payload: boolean) { + state.promptDialog.open = payload + }, + /** * Defines the list of available commands */ diff --git a/src/store/console/state.ts b/src/store/console/state.ts index 7991290b46a..2bf7290e4e9 100644 --- a/src/store/console/state.ts +++ b/src/store/console/state.ts @@ -9,6 +9,11 @@ export const defaultState = (): ConsoleState => { commandHistory: [], autoScroll: true, lastCleared: 0, + promptDialog: { + open: false, + items: [], + footerButtons: [] + }, consoleFilters: [], consoleFiltersRegexp: [] } diff --git a/src/store/console/types.ts b/src/store/console/types.ts index 82f10be3535..df800de4985 100644 --- a/src/store/console/types.ts +++ b/src/store/console/types.ts @@ -7,6 +7,7 @@ export interface ConsoleState { commandHistory: string[]; autoScroll: boolean; lastCleared: number; + promptDialog: PromptDialog; consoleFilters: ConsoleFilter[]; consoleFiltersRegexp: RegExp[]; } @@ -14,7 +15,7 @@ export interface ConsoleState { export interface ConsoleEntry { id?: number; message: string; - type: 'command' | 'response'; + type: 'command' | 'response' | 'action'; time?: number; } @@ -31,3 +32,27 @@ export interface ConsoleFilter { value: string; enabled: boolean; } + +export interface PromptDialog { + open: boolean; + title?: string; + items: PromptDialogItem[]; + footerButtons: PromptDialogButton[] +} + +export type PromptDialogItem = PromptDialogItemText | PromptDialogItemButton + +export interface PromptDialogItemText { + type: 'text'; + text: string; +} + +export interface PromptDialogItemButton extends PromptDialogButton { + type: 'button'; +} + +export interface PromptDialogButton { + text: string; + command?: string; + color?: string; +}