diff --git a/CHANGELOG.md b/CHANGELOG.md index ec0939c..ab586c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # @pipelab/app +## 1.4.3 + +### Patch Changes + +- improvements + add log support + fix config loading + fix step placeholder in parameter editor + ## 1.4.1 ### Patch Changes diff --git a/TODO b/TODO index 0985c10..f089cea 100644 --- a/TODO +++ b/TODO @@ -43,14 +43,13 @@ https://remixicon.com/ --- Tests: -✔ Switch securely from "editor" to "simple" + remember last editor @started(24-10-10 07:40) @done(24-10-24 07:33) @lasted(1w6d23h53m36s) -- View logs in realtime -- env (= copy variables) -- handle pipeline error gracefully - - display what happened in dialog -- handle steamworks - -- prepare electron + install package -- process quickjs in webworker +☐ View logs in realtime: started +☐ env (= copy variables) +☐ handle pipeline error gracefully + ☐ display what happened in dialog +☐ handle steamworks + +☐ prepare electron + install package +☐ process quickjs in webworker diff --git a/package.json b/package.json index 2130714..aeb1004 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pipelab/app", - "version": "1.4.2", + "version": "1.4.3", "description": "-", "main": ".vite/build/main.js", "author": "Armaldio", diff --git a/src/main/handler-func.ts b/src/main/handler-func.ts index 561a649..f7fa3d0 100644 --- a/src/main/handler-func.ts +++ b/src/main/handler-func.ts @@ -17,6 +17,7 @@ import { useLogger } from '@@/logger' import { BrowserWindow } from 'electron' import { usePluginAPI } from './api' import { BlockCondition } from '@@/model' +import { HandleListenerSendFn } from './handlers' const checkParams = (definitionParams: InputsDefinition, elementParams: Record) => { // get a list of all required params @@ -46,7 +47,7 @@ const checkParams = (definitionParams: InputsDefinition, elementParams: Record } ): Promise> => { const { plugins } = usePlugins() @@ -116,7 +117,7 @@ export const handleActionExecute = async ( pluginId: string, params: Record, mainWindow: BrowserWindow | undefined, - // { send }: { send: HandleListenerSendFn<'action:execute'> } + send: HandleListenerSendFn<'action:execute'> ): Promise> => { const { plugins } = usePlugins() const { logger } = useLogger() @@ -160,7 +161,12 @@ export const handleActionExecute = async ( await node.runner({ inputs: resolvedInputs, log: (...args) => { - logger().info(`[${node.node.name}]`, ...args) + const logArgs = [`[${node.node.name}]`, ...args] + logger().info(...logArgs) + send({ + type: 'log', + data: logArgs + }) }, setOutput: (key, value) => { outputs[key] = value diff --git a/src/main/handlers.ts b/src/main/handlers.ts index cd039df..c60dcba 100644 --- a/src/main/handlers.ts +++ b/src/main/handlers.ts @@ -140,6 +140,9 @@ export const registerIPCHandlers = () => { const { canceled, filePath } = await dialog.showSaveDialog(mainWindow, value) + console.log('canceled', canceled) + console.log('filePath', filePath) + send({ type: 'end', data: { @@ -207,7 +210,7 @@ export const registerIPCHandlers = () => { const mainWindow = BrowserWindow.fromWebContents(event.sender) - const result = await handleActionExecute(nodeId, pluginId, params, mainWindow) + const result = await handleActionExecute(nodeId, pluginId, params, mainWindow, send) await send({ data: result, @@ -231,6 +234,21 @@ export const registerIPCHandlers = () => { }) }) + const ensure = async (filesPath: string) => { + // create parent folder + await mkdir(dirname(filesPath), { + recursive: true + }) + + // ensure file exist + try { + await access(filesPath) + } catch { + // File doesn't exist, create it + await writeFile(filesPath, '{}') // json + } + } + handle('config:load', async (_, { send, value }) => { const { config } = value @@ -238,6 +256,8 @@ export const registerIPCHandlers = () => { const filesPath = join(userData, 'config', config + '.json') + await ensure(filesPath) + let content = '{}' try { content = await readFile(filesPath, 'utf8') @@ -265,18 +285,7 @@ export const registerIPCHandlers = () => { const filesPath = join(userData, 'config', config + '.json') - // create parent folder - await mkdir(dirname(filesPath), { - recursive: true - }) - - // ensure file exist - try { - await access(filesPath) - } catch { - // File doesn't exist, create it - await writeFile(filesPath, '{}') // json - } + await ensure(filesPath) await writeFile(filesPath, data, 'utf8') diff --git a/src/renderer/components/nodes/EditorNodeAction.vue b/src/renderer/components/nodes/EditorNodeAction.vue index b282bf0..27cb107 100644 --- a/src/renderer/components/nodes/EditorNodeAction.vue +++ b/src/renderer/components/nodes/EditorNodeAction.vue @@ -6,10 +6,11 @@ :class="{ active: activeNode?.uid === value.uid, error: Object.keys(errors).length > 0, - disabled: value.disabled, + disabled: value.disabled }" + @click="showSidebar = true" > -
+
@@ -65,7 +66,41 @@
- +
+
+
@@ -103,6 +138,25 @@ :path="path" @add-node="addNode" > + + + + +
+
+
{{ cell }}
+
+
+
@@ -120,8 +174,8 @@ import DOMPurify from 'dompurify' import { makeResolvedParams } from '@renderer/utils/evaluator' import { ValidationError } from '@renderer/models/error' import AddNodeButton from '../AddNodeButton.vue' -import { Variable } from '@@/libs/core-app' import { ValueOf } from 'type-fest' +import { MenuItem } from 'primevue/menuitem' const props = defineProps({ value: { @@ -148,7 +202,126 @@ const props = defineProps({ } }) -const { value, steps } = toRefs(props) +const menu = ref() +const { value, steps, index } = toRefs(props) + +const isLogDialogOpened = ref(false) + +/* +
+ + +
+
+ +
+
+ + + + +
+ */ +const items = computed(() => [ + { + label: 'Options', + items: [ + { + label: 'Edit', + icon: 'mdi-pencil', + command: () => { + showSidebar.value = true + } + }, + { + separator: true + }, + { + label: 'Move up', + icon: 'mdi-arrow-up', + command: () => { + swapNodes(index.value, 'up') + } + }, + { + label: 'Move down', + icon: 'mdi-arrow-down', + command: () => { + swapNodes(index.value, 'down') + } + }, + { + separator: true + }, + { + label: 'Enable', + icon: 'mdi-toggle-switch-off-outline', + visible: value.value.disabled, + command: () => { + enableNode(value.value) + } + }, + { + label: 'Disable', + icon: 'mdi-toggle-switch', + visible: !value.value.disabled, + command: () => { + disableNode(value.value) + } + }, + { + label: 'Duplicate', + icon: 'mdi-content-copy', + command: () => { + cloneNode(value.value, index.value + 1) + } + }, + { + separator: true + }, + { + label: 'Delete', + icon: 'mdi-trash-can', + class: 'danger', + command: () => { + removeNode(value.value.uid) + } + } + ] + } +]) + +const toggle = (event: MouseEvent) => { + menu.value.toggle(event) +} const $node = ref() @@ -166,7 +339,7 @@ const { disableNode, enableNode } = editor -const { activeNode, variables } = storeToRefs(editor) +const { activeNode, variables, logLines } = storeToRefs(editor) const nodeDefinition = computed(() => { return getNodeDefinition(value.value.origin.nodeId, value.value.origin.pluginId).node as Action @@ -203,7 +376,6 @@ const resolvedParams = shallowRef>({}) watchDebounced( [value, steps, variablesDisplay], async () => { - // const variables = await variableToFormattedVariable(vm, data.variables) // console.log('variables', variables) @@ -212,7 +384,7 @@ watchDebounced( params: value.value.params, steps: steps.value, context: {}, - variables: variablesDisplay.value, + variables: variablesDisplay.value }, (item) => { // const cleanOutput = DOMPurify.sanitize(item) @@ -252,6 +424,22 @@ watchDebounced( ) const showSidebar = ref(false) + +const nodeLogLines = computed(() => { + const item = logLines.value[value.value.uid] + if (item) { + return item + } + return [] +}) + +const hasLines = computed(() => { + return nodeLogLines.value.length > 0 +}) + +const hasErrored = computed(() => { + return false +}) diff --git a/src/renderer/components/nodes/ParamEditor.vue b/src/renderer/components/nodes/ParamEditor.vue index 48ebd8d..d4e47b5 100644 --- a/src/renderer/components/nodes/ParamEditor.vue +++ b/src/renderer/components/nodes/ParamEditor.vue @@ -213,6 +213,7 @@ import { Variable } from '@@/libs/core-app' import { variableToFormattedVariable } from '@renderer/composables/variables' import { useConfirm } from 'primevue/useconfirm' import { klona } from 'klona' +import { stepsPlaceholders } from '@renderer/utils/code-editor/step-plugin' // @ts-expect-error tsconfig const vm = await createQuickJs() @@ -259,8 +260,7 @@ const props = defineProps<{ // } // }) -const { paramKey, paramDefinition, steps, variables } = toRefs(props) -const { param } = toRefs(props) +const { paramKey, paramDefinition, steps, variables, param } = toRefs(props) const confirm = useConfirm() @@ -375,7 +375,12 @@ const { } = createCodeEditor($codeEditorText, [ javascriptLanguage.data.of({ autocomplete: myCompletions - }) + }), + stepsPlaceholders({ + param, + steps, + variables, + }), ]) const doCodeEditorUpdate = throttle(async (newValue) => { diff --git a/src/renderer/pages/editor.vue b/src/renderer/pages/editor.vue index 3dc924a..fefc9a5 100644 --- a/src/renderer/pages/editor.vue +++ b/src/renderer/pages/editor.vue @@ -213,9 +213,10 @@ const { errors, stepsDisplay, id, - isRunning + logLines, + isRunning, } = storeToRefs(instance) -const { processGraph, loadSavedFile, setIsRunning } = instance +const { processGraph, loadSavedFile, setIsRunning, pushLine } = instance const app = useAppStore() const { pluginDefinitions } = storeToRefs(app) @@ -284,12 +285,22 @@ const run = async () => { steps }) } else */ if (node.type === 'action') { - const result = await api.execute('action:execute', { - nodeId: node.origin.nodeId, - pluginId: node.origin.pluginId, - params, - steps - }) + const result = await api.execute( + 'action:execute', + { + nodeId: node.origin.nodeId, + pluginId: node.origin.pluginId, + params, + steps + }, + async (event, data) => { + console.log('event', event) + console.log('data', data) + if (data.type === 'log') { + pushLine(node.uid, data.data) + } + } + ) return result } else { throw new Error('Unhandled type ' + node.type) diff --git a/src/renderer/store/editor.ts b/src/renderer/store/editor.ts index 3f993d3..dbe1326 100644 --- a/src/renderer/store/editor.ts +++ b/src/renderer/store/editor.ts @@ -116,9 +116,19 @@ export const useEditor = defineStore('editor', () => { /** All the environement variables supported for the editor */ const environements = ref>([]) + /** All log lines relative to their plugin instance */ + const logLines = ref>({}) + /** The API helper */ // const api = useAPI() + const pushLine = (nodeUid: string, data: unknown[]) => { + if (!logLines.value[nodeUid]) { + logLines.value[nodeUid] = [] + } + logLines.value[nodeUid].push(data) + } + const currentFilePointer = computed(() => { return files.value.data[id.value] }) @@ -165,7 +175,7 @@ export const useEditor = defineStore('editor', () => { for (const [key, output] of Object.entries(outputs)) { result[node.uid]['outputs'][key] = - `
${pluginDef.node.name} → ${output.label}
` + `${pluginDef.node.name} → ${output.label}` } } } @@ -416,9 +426,15 @@ export const useEditor = defineStore('editor', () => { const createParams: BlockAction['params'] = {} for (const [key, param] of Object.entries(nodeDefinition.params)) { + // ensure the value is converted to code expression + let val = param.value + if (typeof val === 'string') { + val = `"${val}"` + } + createParams[key] = { editor: 'simple', - value: param.value + value: val } } @@ -606,6 +622,9 @@ export const useEditor = defineStore('editor', () => { currentFilePointer, + pushLine, + logLines, + setActiveNode, setBlockValue, setTriggerValue, diff --git a/src/renderer/store/files.ts b/src/renderer/store/files.ts index 43a9e59..0450a11 100644 --- a/src/renderer/store/files.ts +++ b/src/renderer/store/files.ts @@ -1,19 +1,26 @@ -import { SavedFile } from "@@/model"; -import { defineStore } from "pinia"; -import { ref } from "vue"; +import { SavedFile } from '@@/model' +import { defineStore } from 'pinia' +import { ref } from 'vue' import { Draft, create } from 'mutative' -import { createConfig } from "@renderer/utils/config"; +import { createConfig } from '@renderer/utils/config' import { klona } from 'klona' -import { SaveLocation } from "@@/save-location"; +import { SaveLocationValidator } from '@@/save-location' +import { object, string, optional, record, InferInput, parse, ValiError } from 'valibot' export interface File { data: SavedFile } -export interface FileRepo { - version: string - data: Record -} +export const FileRepoValidator = object({ + version: optional(string(), '1.0.0'), + data: optional(record(string(), SaveLocationValidator), {}) +}) + +// export interface FileRepo { +// version: string +// data: Record +// } +export type FileRepo = InferInput export const useFile = (name: string) => { // const file = ref() @@ -22,22 +29,23 @@ export const useFile = (name: string) => { return { save, - load, + load } } const defaultFileRepo: FileRepo = { - version: "1.0.0", + version: '1.0.0', data: {} } export const useFiles = defineStore('files', () => { - const files = ref(defaultFileRepo); + const files = ref(defaultFileRepo) const { load: loadConfig, save: saveConfig } = createConfig('projects') const update = async (callback: (state: Draft) => void) => { files.value = create(files.value, callback) + console.log('files.value', files.value) await saveConfig(klona(files.value)) } @@ -45,7 +53,15 @@ export const useFiles = defineStore('files', () => { const data = await loadConfig() if (data.type === 'success') { - files.value = data.result.result + try { + files.value = parse(FileRepoValidator, data.result.result) + } catch (e) { + if (e instanceof ValiError) { + console.log("error", e.issues) + } + console.error('error', e) + files.value = defaultFileRepo + } } else { files.value = defaultFileRepo } @@ -60,7 +76,8 @@ export const useFiles = defineStore('files', () => { const loadFile = (name: string) => { const { load, save } = createConfig>(name) return { - load, save + load, + save } } @@ -71,6 +88,6 @@ export const useFiles = defineStore('files', () => { load, loadFile, update, - remove, + remove } }) diff --git a/src/renderer/style/main.scss b/src/renderer/style/main.scss index c7daf58..518d6bf 100644 --- a/src/renderer/style/main.scss +++ b/src/renderer/style/main.scss @@ -3,7 +3,7 @@ } .subtitle .param { - flex: 1 0 auto; + // flex: 1 0 auto; } .editor .subtitle .param { @@ -68,6 +68,11 @@ cursor: pointer; } +.step-missing { + color: red; + font-weight: bold; +} + .bold { font-weight: 700; } diff --git a/src/renderer/utils/code-editor.ts b/src/renderer/utils/code-editor.ts index 683df94..9ce10d6 100644 --- a/src/renderer/utils/code-editor.ts +++ b/src/renderer/utils/code-editor.ts @@ -6,7 +6,7 @@ import { autocompletion } from '@codemirror/autocomplete' import { javascript } from '@codemirror/lang-javascript' import { createEventHook } from '@vueuse/core' import { tomorrow } from 'thememirror' -import { placeholders } from './code-editor/step-plugin' +import { stepsPlaceholders } from './code-editor/step-plugin' export const createCodeEditor = ( element: Ref, @@ -41,7 +41,6 @@ export const createCodeEditor = ( javascript(), autocompletion(), history(), - placeholders, tomorrow, EditorView.updateListener.of((v: ViewUpdate) => { if (v.docChanged) { diff --git a/src/renderer/utils/code-editor/step-plugin.ts b/src/renderer/utils/code-editor/step-plugin.ts index 8cb2f5b..e507009 100644 --- a/src/renderer/utils/code-editor/step-plugin.ts +++ b/src/renderer/utils/code-editor/step-plugin.ts @@ -1,3 +1,5 @@ +import { BlockAction, Steps } from '@@/model' +import { Variable } from '@@/libs/core-app' import { Decoration, MatchDecorator, @@ -7,50 +9,92 @@ import { ViewUpdate, WidgetType } from '@codemirror/view' +import { ValueOf } from 'type-fest' +import { Ref } from 'vue' class PlaceholderWidget extends WidgetType { value: string - constructor(value: string) { + options: Options + nodeId: string + output: string + + constructor(value: string, options: Options, nodeId: string, output: string) { super() this.value = value + this.options = options + this.nodeId = nodeId + this.output = output } + toDOM(): HTMLElement { const span = document.createElement('span') - span.textContent = this.value - span.className = 'step-placeholder' + console.log('this.value', this.value) + + // const vm = await createQuickJs() + + // const result = await vm.run(this.value, { + // steps: this.options.steps.value + // }) + + // console.log('result', result) + + console.log('nodeId', this.nodeId) + console.log('output', this.output) + console.log('this.options.steps.value', this.options.steps.value) + + const result = this.options.steps.value?.[this.nodeId]?.outputs[this.output] + + span.classList.add('step-placeholder') + if (result) { + span.innerHTML = result + } else { + span.textContent = 'Step missing' + span.classList.add('step-missing') + } return span } } -const placeholderMatcher = new MatchDecorator({ - regexp: /(?steps\['(?[\w-]+)'\]\['outputs'\]\['(?[\w-]+)'\])/g, - decoration: (match) => { - const { full, node_id, output } = match.groups ?? { - full: match.input, +const placeholderMatcher = (options: Options) => + new MatchDecorator({ + regexp: /(?steps\['(?[\w-]+)'\]\['outputs'\]\['(?[\w-]+)'\])/g, + decoration: (match) => { + const { full, node_id, output } = match.groups ?? { + full: match.input + } + return Decoration.replace({ + widget: new PlaceholderWidget(full, options, node_id, output) + }) } - return Decoration.replace({ - widget: new PlaceholderWidget(full) - }) - } -}) + }) -export const placeholders = ViewPlugin.fromClass( - class { - placeholders: DecorationSet - constructor(view: EditorView) { - this.placeholders = placeholderMatcher.createDeco(view) - } - update(update: ViewUpdate) { - this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders) +type Param = ValueOf + +interface Options { + param: Ref + steps: Ref + variables: Ref +} +export const stepsPlaceholders = (options: Options) => { + return ViewPlugin.fromClass( + class { + placeholders: DecorationSet + + constructor(view: EditorView) { + this.placeholders = placeholderMatcher(options).createDeco(view) + } + update(update: ViewUpdate) { + this.placeholders = placeholderMatcher(options).updateDeco(update, this.placeholders) + } + }, + { + decorations: (instance) => instance.placeholders, + provide: (plugin) => + EditorView.atomicRanges.of((view) => { + return view.plugin(plugin)?.placeholders || Decoration.none + }) } - }, - { - decorations: (instance) => instance.placeholders, - provide: (plugin) => - EditorView.atomicRanges.of((view) => { - return view.plugin(plugin)?.placeholders || Decoration.none - }) - } -) + ) +} diff --git a/src/renderer/utils/evaluator.ts b/src/renderer/utils/evaluator.ts index e8a698c..1b45c6c 100644 --- a/src/renderer/utils/evaluator.ts +++ b/src/renderer/utils/evaluator.ts @@ -24,7 +24,9 @@ export const makeResolvedParams = async ( const output = await vm.run(parameterCodeValue, { steps: data.steps, params: {}, - variables: data.variables + variables: data.variables, + extraCode: ` + ` }) const outputResult = onItem(output) @@ -32,6 +34,7 @@ export const makeResolvedParams = async ( result[paramName] = outputResult } catch (e) { logger().error('error', e) + result[paramName] = '' } } return result diff --git a/src/renderer/utils/fmt.ts b/src/renderer/utils/fmt.ts index f1e8b71..1ad09f0 100644 --- a/src/renderer/utils/fmt.ts +++ b/src/renderer/utils/fmt.ts @@ -1,5 +1,5 @@ export const fmt = { param: (value: string, variant?: 'primary' | 'secondary' | undefined, ifEmpty: string = "") => { - return `
${value ? value : ifEmpty}
` + return `${value ? value : ifEmpty}` } } diff --git a/src/shared/apis.ts b/src/shared/apis.ts index 4907242..6059c05 100644 --- a/src/shared/apis.ts +++ b/src/shared/apis.ts @@ -2,7 +2,7 @@ import { RendererPluginDefinition } from '@pipelab/plugin-core' import type { Tagged } from 'type-fest' import { Preset, Steps } from './model' -type Event = { type: TYPE; data: DATA } +type Event = { type: TYPE; data: DATA } | { type: 'log'; data: unknown[] } type EndEvent = { type: 'end' data: diff --git a/src/shared/libs/plugin-filesystem/run.ts b/src/shared/libs/plugin-filesystem/run.ts index 39877c0..df08d49 100644 --- a/src/shared/libs/plugin-filesystem/run.ts +++ b/src/shared/libs/plugin-filesystem/run.ts @@ -11,6 +11,7 @@ export const run = createAction({ // displayString: displayString, params: { command: { + description: 'The command to run', label: 'Command', value: '', control: { @@ -21,6 +22,7 @@ export const run = createAction({ } }, parameters: { + description: "The command's parameters", label: 'Arguments', value: [], control: { diff --git a/src/shared/libs/plugin-steam/upload-to-steam.ts b/src/shared/libs/plugin-steam/upload-to-steam.ts index 4bdd117..ec04b82 100644 --- a/src/shared/libs/plugin-steam/upload-to-steam.ts +++ b/src/shared/libs/plugin-steam/upload-to-steam.ts @@ -13,7 +13,7 @@ export const uploadToSteam = createAction({ name: 'Upload to Steam', description: 'Upload a folder to Steam', icon: '', - displayString: '`Upload ${params.folder} to steam`', + displayString: "`Upload ${fmt.param(params['folder'], 'primary')} to steam`", meta: {}, params: { sdk: { diff --git a/src/shared/save-location.ts b/src/shared/save-location.ts index cba7dd2..81577fb 100644 --- a/src/shared/save-location.ts +++ b/src/shared/save-location.ts @@ -1,26 +1,71 @@ -export interface SaveLocationInternal { - path: string - lastModified: string - type: 'internal' - configName: string -} - -export interface SaveLocationExternal { - path: string - lastModified: string - type: 'external' - summary: { - plugins: string[] - name: string - description: string - } -} - -export interface SaveLocationPipelabCloud { - type: 'pipelab-cloud' - // TODO -} - -export type SaveLocation = - // | SaveLocationInternal - SaveLocationExternal | SaveLocationPipelabCloud +import { array, literal, object, string, union, type InferInput } from 'valibot' + +export const SaveLocationInternalValidator = object({ + path: string(), + lastModified: string(), + type: literal('internal'), + configName: string() +}) +// export interface SaveLocationInternal { +// path: string +// lastModified: string +// type: 'internal' +// configName: string +// } +export type SaveLocationInternal = InferInput + +export const SaveLocationExternalValidator = object({ + path: string(), + lastModified: string(), + type: literal('external'), + summary: object({ + plugins: array(string()), + name: string(), + description: string() + }) +}) +// export interface SaveLocationExternal { +// path: string +// lastModified: string +// type: 'external' +// summary: { +// plugins: string[] +// name: string +// description: string +// } +// } +export type SaveLocationExternal = InferInput + +export const SaveLocationPipelabCloudValidator = object({ + type: literal('pipelab-cloud') +}) + +// export interface SaveLocationExternal { +// path: string +// lastModified: string +// type: 'external' +// summary: { +// plugins: string[] +// name: string +// description: string +// } +// } + +export type SaveLocationPipelabCloud = InferInput + +// export interface SaveLocationPipelabCloud { +// type: 'pipelab-cloud' +// // TODO +// } + +export const SaveLocationValidator = union([ + SaveLocationExternalValidator, + SaveLocationInternalValidator, + SaveLocationPipelabCloudValidator +]) + +// export type SaveLocation = +// // | SaveLocationInternal +// SaveLocationExternal | SaveLocationPipelabCloud + +export type SaveLocation = InferInput