From 94e5083e71a62f34ad404ac617db930ceba922ff Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Tue, 5 Nov 2024 18:18:22 +0100 Subject: [PATCH] rework button action code --- .eslintignore | 5 - .eslintrc.cjs | 146 ------ bun.lockb | Bin 155512 -> 159096 bytes eslint.config.mjs | 143 ++++++ package.json | 21 +- packages/core/src/api/API.ts | 4 +- packages/core/src/config/ButtonConfig.ts | 32 +- .../core/src/config/ButtonConfigValidators.ts | 4 +- .../button/AbstractButtonActionConfig.ts | 29 ++ .../src/fields/button/ButtonActionRunner.ts | 426 +++--------------- .../core/src/fields/button/ButtonField.ts | 12 +- .../actions/CommandButtonActionConfig.ts | 33 ++ .../actions/CreateNoteButtonActionConfig.ts | 54 +++ .../actions/InlineJSButtonActionConfig.ts | 51 +++ .../button/actions/InputButtonActionConfig.ts | 37 ++ .../InsertIntoNoteButtonActionConfig.ts | 59 +++ .../button/actions/JSButtonActionConfig.ts | 49 ++ .../button/actions/OpenButtonActionConfig.ts | 34 ++ .../RegexpReplaceInNoteButtonActionConfig.ts | 44 ++ .../ReplaceInNoteButtonActionConfig.ts | 64 +++ .../actions/ReplaceSelfButtonActionConfig.ts | 72 +++ .../button/actions/SleepButtonActionConfig.ts | 33 ++ .../TemplaterCreateNoteButtonActionConfig.ts | 55 +++ .../UpdateMetadataButtonActionConfig.ts | 64 +++ .../inputFields/InputFieldSvelteWrapper.ts | 1 - .../src/fields/viewFields/fields/MathVF.ts | 1 - .../metadata/ComputedMetadataSubscription.ts | 1 - .../src/metadata/InternalMetadataSources.ts | 2 +- packages/core/src/metadata/MetadataSource.ts | 1 - .../ButtonBuilderModalComponent.svelte | 32 +- .../InlineJsActionSettings.svelte | 4 +- .../bindTargetParser/BindTargetParser.ts | 10 +- .../InputFieldDeclarationValidator.ts | 1 - .../viewFieldParser/ViewFieldParser.ts | 10 +- packages/core/src/utils/ZodUtils.ts | 3 +- .../utils/components/ButtonComponent.svelte | 17 +- packages/core/src/utils/prop/PropPath.ts | 2 +- packages/obsidian/extraTypes/Dataview.d.ts | 4 +- packages/obsidian/extraTypes/JsEngine.d.ts | 134 +++--- packages/obsidian/extraTypes/Templater.d.ts | 4 +- packages/obsidian/extraTypes/obsidian-ex.d.ts | 2 +- packages/obsidian/src/ObsUtils.ts | 3 - packages/obsidian/src/ObsidianInternalAPI.ts | 2 +- .../obsidian/src/ObsidianMetadataSource.ts | 4 +- packages/obsidian/src/cm6/Cm6_Widgets.ts | 1 - packages/obsidian/src/dependencies/Version.ts | 2 +- packages/obsidian/src/main.ts | 1 + .../obsidian/src/playground/PlaygroundView.ts | 2 - packages/publish/src/PublishMetadataSource.ts | 2 +- packages/publish/src/main.ts | 2 +- tests/fields/Button.test.ts | 28 +- 51 files changed, 1066 insertions(+), 681 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc.cjs create mode 100644 eslint.config.mjs create mode 100644 packages/core/src/fields/button/AbstractButtonActionConfig.ts create mode 100644 packages/core/src/fields/button/actions/CommandButtonActionConfig.ts create mode 100644 packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts create mode 100644 packages/core/src/fields/button/actions/InlineJSButtonActionConfig.ts create mode 100644 packages/core/src/fields/button/actions/InputButtonActionConfig.ts create mode 100644 packages/core/src/fields/button/actions/InsertIntoNoteButtonActionConfig.ts create mode 100644 packages/core/src/fields/button/actions/JSButtonActionConfig.ts create mode 100644 packages/core/src/fields/button/actions/OpenButtonActionConfig.ts create mode 100644 packages/core/src/fields/button/actions/RegexpReplaceInNoteButtonActionConfig.ts create mode 100644 packages/core/src/fields/button/actions/ReplaceInNoteButtonActionConfig.ts create mode 100644 packages/core/src/fields/button/actions/ReplaceSelfButtonActionConfig.ts create mode 100644 packages/core/src/fields/button/actions/SleepButtonActionConfig.ts create mode 100644 packages/core/src/fields/button/actions/TemplaterCreateNoteButtonActionConfig.ts create mode 100644 packages/core/src/fields/button/actions/UpdateMetadataButtonActionConfig.ts diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 6857a0ba..00000000 --- a/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -npm node_modules -build -main.js -Publish.js -extraTypes diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index eca9e8c1..00000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,146 +0,0 @@ -const projectConfig = require('./automation/config.json'); - -const overrides = []; - -for (const corePackage of projectConfig.corePackages) { - const patterns = []; - - for (const nonCorePackage of projectConfig.packages) { - patterns.push({ - group: [`packages/${nonCorePackage}/*`], - message: `Core package "${corePackage}" should not import from the non core "${nonCorePackage}" package.`, - }); - } - - overrides.push({ - files: [`packages/${corePackage}/src/**/*.ts`], - rules: { - 'no-restricted-imports': [ - 'error', - { - patterns: patterns, - }, - ], - }, - }); -} - -for (const nonCorePackage of projectConfig.packages) { - const patterns = []; - - for (const otherNonCorePackage of projectConfig.packages) { - if (otherNonCorePackage === nonCorePackage) { - continue; - } - patterns.push({ - group: [`packages/${otherNonCorePackage}/*`], - message: `Non core package "${nonCorePackage}" should not import from the non core "${otherNonCorePackage}" package.`, - }); - } - - overrides.push({ - files: [`packages/${nonCorePackage}/src/**/*.ts`], - rules: { - 'no-restricted-imports': [ - 'error', - { - patterns: patterns, - }, - ], - }, - }); -} - -const flatOverrides = overrides.map(o => ({ - files: o.files, - restrictedImports: o.rules['no-restricted-imports'][1].patterns.map(p => p.group).flat(), -})); - -console.log('Import restrictions:'); -console.log(flatOverrides); - -/** @type {import('eslint').Linter.Config} */ -const config = { - root: true, - parser: '@typescript-eslint/parser', - env: { - node: true, - }, - plugins: ['isaacscript', 'import', 'only-warn', 'no-relative-import-paths'], - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/eslint-recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:@typescript-eslint/recommended-type-checked', - 'plugin:@typescript-eslint/stylistic-type-checked', - 'plugin:svelte/recommended', - 'plugin:svelte/prettier', - ], - parserOptions: { - sourceType: 'module', - tsconfigRootDir: __dirname, - ecmaVersion: 'latest', - project: ['./tsconfig.json', './packages/*/tsconfig.json'], - extraFileExtensions: ['.svelte'], - }, - rules: { - '@typescript-eslint/no-explicit-any': ['warn'], - - '@typescript-eslint/no-unused-vars': [ - 'error', - { argsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_' }, - ], - '@typescript-eslint/consistent-type-imports': [ - 'error', - { prefer: 'type-imports', fixStyle: 'separate-type-imports' }, - ], - - 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], - 'import/order': [ - 'error', - { - 'newlines-between': 'never', - alphabetize: { order: 'asc', orderImportKind: 'asc', caseInsensitive: true }, - }, - ], - - '@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }], - '@typescript-eslint/restrict-template-expressions': 'off', - - 'no-relative-import-paths/no-relative-import-paths': 'error', - - '@typescript-eslint/ban-ts-comment': 'off', - '@typescript-eslint/no-empty-function': 'off', - '@typescript-eslint/no-inferrable-types': 'off', - '@typescript-eslint/explicit-function-return-type': ['warn'], - '@typescript-eslint/require-await': 'off', - }, - overrides: [ - ...overrides, - { - files: ['*.svelte'], - parser: 'svelte-eslint-parser', - parserOptions: { - parser: { - // Specify a parser for each lang. - ts: '@typescript-eslint/parser', - typescript: '@typescript-eslint/parser', - }, - }, - rules: { - '@typescript-eslint/no-unsafe-assignment': 'off', - '@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }], - '@typescript-eslint/no-unused-vars': [ - 'error', - { argsIgnorePattern: '^_', destructuredArrayIgnorePattern: '^_', varsIgnorePattern: '^_|plugin' }, - ], - - 'no-undef': 'off', - - '@typescript-eslint/prefer-nullish-coalescing': 'off', - }, - }, - ], -}; - -module.exports = config; diff --git a/bun.lockb b/bun.lockb index c72355e6b2a7cf781403d25bec6f09181dc9c851..28ac1717dbd6d1119ef18b6b34a9328975ffe900 100755 GIT binary patch delta 35656 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 delta 34097 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 diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..d1b54b93 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,143 @@ +// @ts-check + +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import only_warn from 'eslint-plugin-only-warn'; +import no_relative_import_paths from 'eslint-plugin-no-relative-import-paths'; +import * as plugin_import from 'eslint-plugin-import'; +import eslintPluginSvelte from 'eslint-plugin-svelte'; + +import projectConfig from './automation/config.json' with { type: 'json' }; + +/** @type {{files: string[], rules: Record}[]} */ +const overrides = []; + +for (const corePackage of projectConfig.corePackages) { + const patterns = []; + + for (const nonCorePackage of projectConfig.packages) { + patterns.push({ + group: [`packages/${nonCorePackage}/*`], + message: `Core package "${corePackage}" should not import from the non core "${nonCorePackage}" package.`, + }); + } + + overrides.push({ + files: [`packages/${corePackage}/src/**/*.ts`], + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: patterns, + }, + ], + }, + }); +} + +for (const nonCorePackage of projectConfig.packages) { + const patterns = []; + + for (const otherNonCorePackage of projectConfig.packages) { + if (otherNonCorePackage === nonCorePackage) { + continue; + } + patterns.push({ + group: [`packages/${otherNonCorePackage}/*`], + message: `Non core package "${nonCorePackage}" should not import from the non core "${otherNonCorePackage}" package.`, + }); + } + + overrides.push({ + files: [`packages/${nonCorePackage}/src/**/*.ts`], + rules: { + 'no-restricted-imports': [ + 'error', + { + patterns: patterns, + }, + ], + }, + }); +} + +const flatOverrides = overrides.map(o => ({ + files: o.files, + restrictedImports: o.rules['no-restricted-imports'][1].patterns.map(p => p.group).flat(), +})); + +console.log('Import restrictions:'); +console.log(flatOverrides); + +export default tseslint.config( + { + ignores: ['npm/', 'node_modules/', 'exampleVault/', 'automation/', 'main.js', '**/*.svelte', '**/*.d.ts'], + }, + ...eslintPluginSvelte.configs['flat/recommended'], + ...eslintPluginSvelte.configs['flat/prettier'], + { + files: ['packages/**/*.ts'], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + ], + languageOptions: { + parser: tseslint.parser, + parserOptions: { + project: true, + }, + }, + plugins: { + // @ts-ignore + 'only-warn': only_warn, + 'no-relative-import-paths': no_relative_import_paths, + import: plugin_import, + }, + rules: { + '@typescript-eslint/no-explicit-any': ['warn'], + + '@typescript-eslint/no-unused-vars': [ + 'error', + { + argsIgnorePattern: '^_', + destructuredArrayIgnorePattern: '^_', + varsIgnorePattern: '^_', + caughtErrorsIgnorePattern: '^_', + }, + ], + '@typescript-eslint/consistent-type-imports': [ + 'error', + { prefer: 'type-imports', fixStyle: 'separate-type-imports' }, + ], + + 'import/consistent-type-specifier-style': ['error', 'prefer-top-level'], + 'import/order': [ + 'error', + { + 'newlines-between': 'never', + alphabetize: { order: 'asc', orderImportKind: 'asc', caseInsensitive: true }, + }, + ], + + '@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }], + '@typescript-eslint/restrict-template-expressions': 'off', + + 'no-relative-import-paths/no-relative-import-paths': ['warn', { allowSameFolder: false }], + + '@typescript-eslint/ban-ts-comment': 'off', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-inferrable-types': 'off', + '@typescript-eslint/explicit-function-return-type': ['warn'], + '@typescript-eslint/require-await': 'off', + '@typescript-eslint/no-unused-expressions': [ + 'warn', + { + allowShortCircuit: true, + }, + ], + }, + }, + ...overrides, +); diff --git a/package.json b/package.json index abc56eab..837d12d5 100644 --- a/package.json +++ b/package.json @@ -13,8 +13,8 @@ "test:log": "LOG_TESTS=true bun test --conditions=browser", "format": "prettier --write --plugin prettier-plugin-svelte .", "format:check": "prettier --check --plugin prettier-plugin-svelte .", - "lint": "eslint --max-warnings=0 packages/**", - "lint:fix": "eslint --max-warnings=0 --fix packages/**", + "lint": "eslint --max-warnings=0 packages/** --no-warn-ignored", + "lint:fix": "eslint --max-warnings=0 --fix packages/** --no-warn-ignored", "svelte-check": "svelte-check --compiler-warnings \"unused-export-let:ignore\"", "types": "tsc -p \"./tsconfig.types.json\"", "check": "bun run format:check && bun run tsc && bun run svelte-check && bun run lint && bun run test", @@ -32,17 +32,15 @@ "@elysiajs/cors": "^1.1.1", "@happy-dom/global-registrator": "^14.12.3", "@tsconfig/svelte": "^5.0.4", - "@types/bun": "^1.1.12", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", + "@types/bun": "^1.1.13", "builtin-modules": "^4.0.0", - "elysia": "^1.1.23", + "elysia": "^1.1.24", "esbuild": "^0.24.0", "esbuild-plugin-copy-watch": "^2.3.1", "esbuild-svelte": "^0.8.2", - "eslint": "^8.57.1", + "eslint": "^9.14.0", "eslint-plugin-import": "^2.31.0", - "eslint-plugin-isaacscript": "^3.12.2", + "eslint-plugin-isaacscript": "^4.0.0", "eslint-plugin-no-relative-import-paths": "^1.5.5", "eslint-plugin-only-warn": "^1.1.0", "eslint-plugin-svelte": "^2.46.0", @@ -51,17 +49,18 @@ "string-argv": "^0.3.2", "svelte-check": "^4.0.5", "svelte-preprocess": "^6.0.3", - "tslib": "^2.8.0", + "tslib": "^2.8.1", "typescript": "^5.6.3", + "typescript-eslint": "^8.13.0", "yaml": "^2.6.0" }, "dependencies": { - "@codemirror/legacy-modes": "^6.4.1", + "@codemirror/legacy-modes": "^6.4.2", "@lemons_dev/parsinom": "^0.0.12", "itertools-ts": "^1.27.1", "mathjs": "^13.2.0", "moment": "^2.30.1", - "svelte": "5.1.4", + "svelte": "^5.1.9", "zod": "^3.23.8", "zod-validation-error": "^3.4.0" }, diff --git a/packages/core/src/api/API.ts b/packages/core/src/api/API.ts index f603a952..71d28242 100644 --- a/packages/core/src/api/API.ts +++ b/packages/core/src/api/API.ts @@ -189,7 +189,7 @@ export abstract class API { filePath: string, scope: BindTargetScope | undefined, renderChildType: RenderChildType = RenderChildType.INLINE, - position?: NotePosition | undefined, + position?: NotePosition, honorExcludedSetting: boolean = true, ): FieldMountable { validateAPIArgs( @@ -247,7 +247,7 @@ export abstract class API { filePath: string, scope: BindTargetScope | undefined, renderChildType: RenderChildType = RenderChildType.INLINE, - position?: NotePosition | undefined, + position?: NotePosition, honorExcludedSetting: boolean = true, ): FieldMountable { validateAPIArgs( diff --git a/packages/core/src/config/ButtonConfig.ts b/packages/core/src/config/ButtonConfig.ts index 39b32b3a..24bf605b 100644 --- a/packages/core/src/config/ButtonConfig.ts +++ b/packages/core/src/config/ButtonConfig.ts @@ -102,7 +102,7 @@ export interface InsertIntoNoteButtonAction { templater?: boolean; } -export interface InlineJsButtonAction { +export interface InlineJSButtonAction { type: ButtonActionType.INLINE_JS; code: string; } @@ -120,7 +120,7 @@ export type ButtonAction = | ReplaceSelfButtonAction | RegexpReplaceInNoteButtonAction | InsertIntoNoteButtonAction - | InlineJsButtonAction; + | InlineJSButtonAction; export interface ButtonConfig { label: string; @@ -139,3 +139,31 @@ export interface ButtonContext { isInGroup: boolean; isInline: boolean; } + +export interface ButtonClickContext { + type: ButtonClickType; + shiftKey: boolean; + ctrlKey: boolean; + altKey: boolean; +} + +export enum ButtonClickType { + LEFT = 'left', + MIDDLE = 'middle', +} + +export interface ButtonActionMap { + [ButtonActionType.COMMAND]: CommandButtonAction; + [ButtonActionType.JS]: JSButtonAction; + [ButtonActionType.OPEN]: OpenButtonAction; + [ButtonActionType.INPUT]: InputButtonAction; + [ButtonActionType.SLEEP]: SleepButtonAction; + [ButtonActionType.TEMPLATER_CREATE_NOTE]: TemplaterCreateNoteButtonAction; + [ButtonActionType.UPDATE_METADATA]: UpdateMetadataButtonAction; + [ButtonActionType.CREATE_NOTE]: CreateNoteButtonAction; + [ButtonActionType.REPLACE_IN_NOTE]: ReplaceInNoteButtonAction; + [ButtonActionType.REPLACE_SELF]: ReplaceSelfButtonAction; + [ButtonActionType.REGEXP_REPLACE_IN_NOTE]: RegexpReplaceInNoteButtonAction; + [ButtonActionType.INSERT_INTO_NOTE]: InsertIntoNoteButtonAction; + [ButtonActionType.INLINE_JS]: InlineJSButtonAction; +} diff --git a/packages/core/src/config/ButtonConfigValidators.ts b/packages/core/src/config/ButtonConfigValidators.ts index 274ef036..211c8b6d 100644 --- a/packages/core/src/config/ButtonConfigValidators.ts +++ b/packages/core/src/config/ButtonConfigValidators.ts @@ -3,7 +3,7 @@ import type { ButtonConfig, CommandButtonAction, CreateNoteButtonAction, - InlineJsButtonAction, + InlineJSButtonAction, InputButtonAction, InsertIntoNoteButtonAction, JSButtonAction, @@ -165,7 +165,7 @@ export const V_InsertIntoNoteButtonAction = schemaForType()( +export const V_InlineJsButtonAction = schemaForType()( z.object({ type: z.literal(ButtonActionType.INLINE_JS), code: stringValidator('inlineJS', 'code', 'code string to run'), diff --git a/packages/core/src/fields/button/AbstractButtonActionConfig.ts b/packages/core/src/fields/button/AbstractButtonActionConfig.ts new file mode 100644 index 00000000..66dea10c --- /dev/null +++ b/packages/core/src/fields/button/AbstractButtonActionConfig.ts @@ -0,0 +1,29 @@ +import type { + ButtonActionType, + ButtonClickContext, + ButtonConfig, + ButtonContext, +} from 'packages/core/src/config/ButtonConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; + +export abstract class AbstractButtonActionConfig { + actionType: ButtonActionType; + plugin: IPlugin; + + constructor(actionType: ButtonActionType, plugin: IPlugin) { + this.actionType = actionType; + this.plugin = plugin; + } + + abstract run( + config: ButtonConfig | undefined, + action: T, + filePath: string, + context: ButtonContext, + click: ButtonClickContext, + ): Promise; + + abstract create(): Required; + + abstract getActionLabel(): string; +} diff --git a/packages/core/src/fields/button/ButtonActionRunner.ts b/packages/core/src/fields/button/ButtonActionRunner.ts index d5517a01..73ec8878 100644 --- a/packages/core/src/fields/button/ButtonActionRunner.ts +++ b/packages/core/src/fields/button/ButtonActionRunner.ts @@ -1,36 +1,59 @@ import type { - ButtonAction, + ButtonActionMap, + ButtonClickContext, + ButtonClickType, ButtonConfig, ButtonContext, - CommandButtonAction, - CreateNoteButtonAction, - InlineJsButtonAction, - InputButtonAction, - InsertIntoNoteButtonAction, - JSButtonAction, - OpenButtonAction, - RegexpReplaceInNoteButtonAction, - ReplaceInNoteButtonAction, - ReplaceSelfButtonAction, - SleepButtonAction, - TemplaterCreateNoteButtonAction, - UpdateMetadataButtonAction, } from 'packages/core/src/config/ButtonConfig'; import { ButtonActionType, ButtonStyleType } from 'packages/core/src/config/ButtonConfig'; +import type { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import { CommandButtonActionConfig } from 'packages/core/src/fields/button/actions/CommandButtonActionConfig'; +import { CreateNoteButtonActionConfig } from 'packages/core/src/fields/button/actions/CreateNoteButtonActionConfig'; +import { InlineJSButtonActionConfig } from 'packages/core/src/fields/button/actions/InlineJSButtonActionConfig'; +import { InputButtonActionConfig } from 'packages/core/src/fields/button/actions/InputButtonActionConfig'; +import { InsertIntoNoteButtonActionConfig } from 'packages/core/src/fields/button/actions/InsertIntoNoteButtonActionConfig'; +import { JSButtonActionConfig } from 'packages/core/src/fields/button/actions/JSButtonActionConfig'; +import { OpenButtonActionConfig } from 'packages/core/src/fields/button/actions/OpenButtonActionConfig'; +import { RegexpReplaceInNoteButtonActionConfig } from 'packages/core/src/fields/button/actions/RegexpReplaceInNoteButtonActionConfig'; +import { ReplaceInNoteButtonActionConfig } from 'packages/core/src/fields/button/actions/ReplaceInNoteButtonActionConfig'; +import { ReplaceSelfButtonActionConfig } from 'packages/core/src/fields/button/actions/ReplaceSelfButtonActionConfig'; +import { SleepButtonActionConfig } from 'packages/core/src/fields/button/actions/SleepButtonActionConfig'; +import { TemplaterCreateNoteButtonActionConfig } from 'packages/core/src/fields/button/actions/TemplaterCreateNoteButtonActionConfig'; +import { UpdateMetadataButtonActionConfig } from 'packages/core/src/fields/button/actions/UpdateMetadataButtonActionConfig'; import type { IPlugin } from 'packages/core/src/IPlugin'; import { MDLinkParser } from 'packages/core/src/parsers/MarkdownLinkParser'; -import { ErrorLevel, MetaBindJsError, MetaBindParsingError } from 'packages/core/src/utils/errors/MetaBindErrors'; -import { parseLiteral } from 'packages/core/src/utils/Literal'; -import { ensureFileExtension, expectType, joinPath } from 'packages/core/src/utils/Utils'; +import { ErrorLevel, MetaBindParsingError } from 'packages/core/src/utils/errors/MetaBindErrors'; +type ActionContexts = { + [key in ButtonActionType]: AbstractButtonActionConfig; +}; + +// TODO: rewrite this so that each button action is its own class export class ButtonActionRunner { plugin: IPlugin; + actionContexts: ActionContexts; constructor(plugin: IPlugin) { this.plugin = plugin; + + this.actionContexts = { + [ButtonActionType.COMMAND]: new CommandButtonActionConfig(plugin), + [ButtonActionType.OPEN]: new OpenButtonActionConfig(plugin), + [ButtonActionType.JS]: new JSButtonActionConfig(plugin), + [ButtonActionType.INPUT]: new InputButtonActionConfig(plugin), + [ButtonActionType.SLEEP]: new SleepButtonActionConfig(plugin), + [ButtonActionType.TEMPLATER_CREATE_NOTE]: new TemplaterCreateNoteButtonActionConfig(plugin), + [ButtonActionType.UPDATE_METADATA]: new UpdateMetadataButtonActionConfig(plugin), + [ButtonActionType.CREATE_NOTE]: new CreateNoteButtonActionConfig(plugin), + [ButtonActionType.REPLACE_IN_NOTE]: new ReplaceInNoteButtonActionConfig(plugin), + [ButtonActionType.REPLACE_SELF]: new ReplaceSelfButtonActionConfig(plugin), + [ButtonActionType.REGEXP_REPLACE_IN_NOTE]: new RegexpReplaceInNoteButtonActionConfig(plugin), + [ButtonActionType.INSERT_INTO_NOTE]: new InsertIntoNoteButtonActionConfig(plugin), + [ButtonActionType.INLINE_JS]: new InlineJSButtonActionConfig(plugin), + }; } - resolveFilePath(filePath: string, relativeFilePath?: string | undefined): string { + resolveFilePath(filePath: string, relativeFilePath?: string): string { const targetFilePath = MDLinkParser.isLink(filePath) ? MDLinkParser.parseLink(filePath).target : filePath; const resolvedFilePath = this.plugin.internal.file.getPathByName(targetFilePath, relativeFilePath); if (resolvedFilePath === undefined) { @@ -66,13 +89,18 @@ export class ButtonActionRunner { * @param inline whether the button is inline * @param position the position of the button in the note */ - async runButtonActions(config: ButtonConfig, filePath: string, context: ButtonContext): Promise { + async runButtonActions( + config: ButtonConfig, + filePath: string, + context: ButtonContext, + click: ButtonClickContext, + ): Promise { try { if (config.action) { - await this.plugin.api.buttonActionRunner.runAction(config, config.action, filePath, context); + await this.runAction(config, config.action, filePath, context, click); } else if (config.actions) { for (const action of config.actions) { - await this.plugin.api.buttonActionRunner.runAction(config, action, filePath, context); + await this.runAction(config, action, filePath, context, click); } } else { console.warn('meta-bind | ButtonMDRC >> no action defined'); @@ -90,79 +118,8 @@ export class ButtonActionRunner { * * @param type */ - createDefaultAction(type: ButtonActionType): ButtonAction { - if (type === ButtonActionType.COMMAND) { - return { type: ButtonActionType.COMMAND, command: '' } satisfies Required; - } else if (type === ButtonActionType.OPEN) { - return { type: ButtonActionType.OPEN, link: '', newTab: true } satisfies Required; - } else if (type === ButtonActionType.JS) { - return { type: ButtonActionType.JS, file: '', args: {} } satisfies Required; - } else if (type === ButtonActionType.INPUT) { - return { type: ButtonActionType.INPUT, str: '' } satisfies Required; - } else if (type === ButtonActionType.SLEEP) { - return { type: ButtonActionType.SLEEP, ms: 0 } satisfies Required; - } else if (type === ButtonActionType.TEMPLATER_CREATE_NOTE) { - return { - type: ButtonActionType.TEMPLATER_CREATE_NOTE, - templateFile: '', - folderPath: '/', - fileName: '', - openNote: true, - openIfAlreadyExists: false, - } satisfies Required; - } else if (type === ButtonActionType.UPDATE_METADATA) { - return { - type: ButtonActionType.UPDATE_METADATA, - bindTarget: '', - evaluate: false, - value: '', - } satisfies Required; - } else if (type === ButtonActionType.CREATE_NOTE) { - return { - type: ButtonActionType.CREATE_NOTE, - folderPath: '/', - fileName: 'Untitled', - openNote: true, - openIfAlreadyExists: false, - } satisfies Required; - } else if (type === ButtonActionType.REPLACE_IN_NOTE) { - return { - type: ButtonActionType.REPLACE_IN_NOTE, - fromLine: 0, - toLine: 0, - replacement: 'Replacement text', - templater: false, - } satisfies Required; - } else if (type === ButtonActionType.REPLACE_SELF) { - return { - type: ButtonActionType.REPLACE_SELF, - replacement: 'Replacement text', - templater: false, - } satisfies Required; - } else if (type === ButtonActionType.REGEXP_REPLACE_IN_NOTE) { - return { - type: ButtonActionType.REGEXP_REPLACE_IN_NOTE, - regexp: '([A-Z])\\w+', - replacement: 'Replacement text', - regexpFlags: 'g', - } satisfies Required; - } else if (type === ButtonActionType.INSERT_INTO_NOTE) { - return { - type: ButtonActionType.INSERT_INTO_NOTE, - line: 0, - value: 'Some text', - templater: false, - } satisfies Required; - } else if (type === ButtonActionType.INLINE_JS) { - return { - type: ButtonActionType.INLINE_JS, - code: 'console.log("Hello world")', - } satisfies Required; - } - - expectType(type); - - throw new Error(`Unknown button action type: ${type}`); + createDefaultAction(type: T): Required { + return this.actionContexts[type].create(); } /** @@ -175,282 +132,27 @@ export class ButtonActionRunner { * @param inline whether the button is inline * @param position the position of the button in the note */ - async runAction( + async runAction( config: ButtonConfig | undefined, - action: ButtonAction, + action: ButtonActionMap[T], filePath: string, buttonContext: ButtonContext, + click: ButtonClickContext, ): Promise { - if (action.type === ButtonActionType.COMMAND) { - await this.runCommandAction(action); - return; - } else if (action.type === ButtonActionType.JS) { - await this.runJSAction(config, action, filePath, buttonContext); - return; - } else if (action.type === ButtonActionType.OPEN) { - await this.runOpenAction(action, filePath); - return; - } else if (action.type === ButtonActionType.INPUT) { - await this.runInputAction(action); - return; - } else if (action.type === ButtonActionType.SLEEP) { - await this.runSleepAction(action); - return; - } else if (action.type === ButtonActionType.TEMPLATER_CREATE_NOTE) { - await this.runTemplaterCreateNoteAction(action); - return; - } else if (action.type === ButtonActionType.UPDATE_METADATA) { - await this.runUpdateMetadataAction(action, filePath); - return; - } else if (action.type === ButtonActionType.CREATE_NOTE) { - await this.runCreateNoteAction(action); - return; - } else if (action.type === ButtonActionType.REPLACE_IN_NOTE) { - await this.runReplaceInNoteAction(action, filePath); - return; - } else if (action.type === ButtonActionType.REPLACE_SELF) { - await this.runReplaceSelfAction(action, filePath, buttonContext); - return; - } else if (action.type === ButtonActionType.REGEXP_REPLACE_IN_NOTE) { - await this.runRegexpReplaceInNoteAction(action, filePath); - return; - } else if (action.type === ButtonActionType.INSERT_INTO_NOTE) { - await this.runInsertIntoNoteAction(action, filePath); - return; - } else if (action.type === ButtonActionType.INLINE_JS) { - await this.runInlineJsAction(config, action, filePath, buttonContext); - return; - } - - expectType(action); - - throw new Error(`Unknown button action type`); - } - - async runCommandAction(action: CommandButtonAction): Promise { - this.plugin.internal.executeCommandById(action.command); - } - - async runJSAction( - config: ButtonConfig | undefined, - action: JSButtonAction, - filePath: string, - buttonContext: ButtonContext, - ): Promise { - if (!this.plugin.settings.enableJs) { - throw new MetaBindJsError({ - errorLevel: ErrorLevel.CRITICAL, - effect: "Can't run button action that requires JS evaluation.", - cause: 'JS evaluation is disabled in the plugin settings.', - }); - } - - const configOverrides: Record = { - buttonConfig: structuredClone(config), - args: structuredClone(action.args), - buttonContext: structuredClone(buttonContext), - }; - const unloadCallback = await this.plugin.internal.jsEngineRunFile(action.file, filePath, configOverrides); - unloadCallback(); + const actionType: T = action.type as T; + await this.actionContexts[actionType].run(config, action, filePath, buttonContext, click); } - async runOpenAction(action: OpenButtonAction, filePath: string): Promise { - MDLinkParser.parseLinkOrUrl(action.link).open(this.plugin, filePath, action.newTab ?? false); - } - - async runInputAction(action: InputButtonAction): Promise { - const el = document.activeElement; - if (el && el instanceof HTMLInputElement) { - el.setRangeText(action.str, el.selectionStart!, el.selectionEnd!, 'end'); - el.dispatchEvent(new Event('input', { bubbles: true })); - } - } - - async runSleepAction(action: SleepButtonAction): Promise { - await new Promise(resolve => setTimeout(resolve, action.ms)); - } - - async runTemplaterCreateNoteAction(action: TemplaterCreateNoteButtonAction): Promise { - if (action.openIfAlreadyExists && action.fileName) { - const filePath = ensureFileExtension(joinPath(action.folderPath ?? '', action.fileName), 'md'); - // if the file already exists, open it in the same tab - if (await this.plugin.internal.file.exists(filePath)) { - this.plugin.internal.file.open(filePath, '', false); - return; - } - } - - await this.plugin.internal.createNoteWithTemplater( - action.templateFile, - action.folderPath, - action.fileName, - action.openNote, - ); - } - - async runUpdateMetadataAction(action: UpdateMetadataButtonAction, filePath: string): Promise { - const bindTarget = this.plugin.api.bindTargetParser.fromStringAndValidate(action.bindTarget, filePath); - - if (action.evaluate) { - if (!this.plugin.settings.enableJs) { - throw new MetaBindJsError({ - errorLevel: ErrorLevel.CRITICAL, - effect: "Can't run button action that requires JS evaluation.", - cause: 'JS evaluation is disabled in the plugin settings.', - }); - } - - // eslint-disable-next-line @typescript-eslint/no-implied-eval - const func = new Function('x', 'getMetadata', `return ${action.value};`) as ( - value: unknown, - getMetadata: (bindTarget: string) => unknown, - ) => unknown; - - this.plugin.api.updateMetadata(bindTarget, value => - func(value, bindTarget => { - return this.plugin.api.getMetadata(this.plugin.api.parseBindTarget(bindTarget, filePath)); - }), - ); - } else { - this.plugin.api.setMetadata(bindTarget, parseLiteral(action.value)); - } - } - - async runCreateNoteAction(action: CreateNoteButtonAction): Promise { - if (action.openIfAlreadyExists) { - const filePath = ensureFileExtension(joinPath(action.folderPath ?? '', action.fileName), 'md'); - // if the file already exists, open it in the same tab - if (await this.plugin.internal.file.exists(filePath)) { - this.plugin.internal.file.open(filePath, '', false); - return; - } - } - - await this.plugin.internal.file.create( - action.folderPath ?? '', - action.fileName, - 'md', - action.openNote ?? false, - ); + getActionLabel(type: T): string { + return this.actionContexts[type].getActionLabel(); } - async runReplaceInNoteAction(action: ReplaceInNoteButtonAction, filePath: string): Promise { - if (action.fromLine > action.toLine) { - throw new Error('From line cannot be greater than to line'); - } - - const replacement = action.templater - ? await this.plugin.internal.evaluateTemplaterTemplate(this.resolveFilePath(action.replacement), filePath) - : action.replacement; - - await this.plugin.internal.file.atomicModify(filePath, content => { - let splitContent = content.split('\n'); - - if (action.fromLine < 0 || action.toLine > splitContent.length + 1) { - throw new Error('Line numbers out of bounds'); - } - - splitContent = [ - ...splitContent.slice(0, action.fromLine - 1), - replacement, - ...splitContent.slice(action.toLine), - ]; - - return splitContent.join('\n'); - }); - } - - async runReplaceSelfAction( - action: ReplaceSelfButtonAction, - filePath: string, - buttonContext: ButtonContext, - ): Promise { - if (buttonContext.isInline) { - throw new Error('Replace self action not supported for inline buttons'); - } - - if (buttonContext.position === undefined) { - throw new Error('Position of the button in the note is unknown'); - } - - if (buttonContext.position.lineStart > buttonContext.position.lineEnd) { - throw new Error('Position of the button in the note is invalid'); - } - - const position = buttonContext.position; - - const replacement = action.templater - ? await this.plugin.internal.evaluateTemplaterTemplate(this.resolveFilePath(action.replacement), filePath) - : action.replacement; - - await this.plugin.internal.file.atomicModify(filePath, content => { - let splitContent = content.split('\n'); - - if (position.lineStart < 0 || position.lineEnd > splitContent.length + 1) { - throw new Error('Position of the button in the note is out of bounds'); - } - - splitContent = [ - ...splitContent.slice(0, position.lineStart), - replacement, - ...splitContent.slice(position.lineEnd + 1), - ]; - - return splitContent.join('\n'); - }); - } - - async runRegexpReplaceInNoteAction(action: RegexpReplaceInNoteButtonAction, filePath: string): Promise { - if (action.regexp === '') { - throw new Error('Regexp cannot be empty'); - } - - await this.plugin.internal.file.atomicModify(filePath, content => { - return content.replace(new RegExp(action.regexp, action.regexpFlags ?? 'g'), action.replacement); - }); - } - - async runInsertIntoNoteAction(action: InsertIntoNoteButtonAction, filePath: string): Promise { - const insertString = action.templater - ? await this.plugin.internal.evaluateTemplaterTemplate(this.resolveFilePath(action.value), filePath) - : action.value; - - await this.plugin.internal.file.atomicModify(filePath, content => { - let splitContent = content.split('\n'); - - if (action.line < 1 || action.line > splitContent.length + 1) { - throw new Error('Line number out of bounds'); - } - - splitContent = [ - ...splitContent.slice(0, action.line - 1), - insertString, - ...splitContent.slice(action.line - 1), - ]; - - return splitContent.join('\n'); - }); - } - - async runInlineJsAction( - config: ButtonConfig | undefined, - action: InlineJsButtonAction, - filePath: string, - buttonContext: ButtonContext, - ): Promise { - if (!this.plugin.settings.enableJs) { - throw new MetaBindJsError({ - errorLevel: ErrorLevel.CRITICAL, - effect: "Can't run button action that requires JS evaluation.", - cause: 'JS evaluation is disabled in the plugin settings.', - }); - } - - const configOverrides: Record = { - buttonConfig: structuredClone(config), - buttonContext: structuredClone(buttonContext), + mouseEventToClickContext(event: MouseEvent, type: ButtonClickType): ButtonClickContext { + return { + type: type, + shiftKey: event.shiftKey, + ctrlKey: event.ctrlKey, + altKey: event.altKey, }; - const unloadCallback = await this.plugin.internal.jsEngineRunCode(action.code, filePath, configOverrides); - unloadCallback(); } } diff --git a/packages/core/src/fields/button/ButtonField.ts b/packages/core/src/fields/button/ButtonField.ts index 0589c554..5aaf69bb 100644 --- a/packages/core/src/fields/button/ButtonField.ts +++ b/packages/core/src/fields/button/ButtonField.ts @@ -1,6 +1,7 @@ import type { NotePosition } from 'packages/core/src/config/APIConfigs'; import { RenderChildType } from 'packages/core/src/config/APIConfigs'; import type { ButtonConfig, ButtonContext } from 'packages/core/src/config/ButtonConfig'; +import { ButtonClickType } from 'packages/core/src/config/ButtonConfig'; import type { IPlugin } from 'packages/core/src/IPlugin'; import ButtonComponent from 'packages/core/src/utils/components/ButtonComponent.svelte'; import { Mountable } from 'packages/core/src/utils/Mountable'; @@ -67,11 +68,20 @@ export class ButtonField extends Mountable { variant: this.config.style, label: this.config.label, tooltip: isTruthy(this.config.tooltip) ? this.config.tooltip : this.config.label, - onclick: async (): Promise => { + onclick: async (event: MouseEvent): Promise => { await this.plugin.api.buttonActionRunner.runButtonActions( this.config, this.filePath, this.getContext(), + this.plugin.api.buttonActionRunner.mouseEventToClickContext(event, ButtonClickType.LEFT), + ); + }, + onauxclick: async (event: MouseEvent): Promise => { + await this.plugin.api.buttonActionRunner.runButtonActions( + this.config, + this.filePath, + this.getContext(), + this.plugin.api.buttonActionRunner.mouseEventToClickContext(event, ButtonClickType.MIDDLE), ); }, }, diff --git a/packages/core/src/fields/button/actions/CommandButtonActionConfig.ts b/packages/core/src/fields/button/actions/CommandButtonActionConfig.ts new file mode 100644 index 00000000..5b3e242b --- /dev/null +++ b/packages/core/src/fields/button/actions/CommandButtonActionConfig.ts @@ -0,0 +1,33 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + CommandButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; + +export class CommandButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.COMMAND, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: CommandButtonAction, + _filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + this.plugin.internal.executeCommandById(action.command); + } + + create(): Required { + return { type: ButtonActionType.COMMAND, command: '' }; + } + + getActionLabel(): string { + return 'Run a command'; + } +} diff --git a/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts b/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts new file mode 100644 index 00000000..619cfd50 --- /dev/null +++ b/packages/core/src/fields/button/actions/CreateNoteButtonActionConfig.ts @@ -0,0 +1,54 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + CreateNoteButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; +import { ensureFileExtension, joinPath } from 'packages/core/src/utils/Utils'; + +export class CreateNoteButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.CREATE_NOTE, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: CreateNoteButtonAction, + _filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + if (action.openIfAlreadyExists) { + const filePath = ensureFileExtension(joinPath(action.folderPath ?? '', action.fileName), 'md'); + // if the file already exists, open it in the same tab + if (await this.plugin.internal.file.exists(filePath)) { + this.plugin.internal.file.open(filePath, '', false); + return; + } + } + + await this.plugin.internal.file.create( + action.folderPath ?? '', + action.fileName, + 'md', + action.openNote ?? false, + ); + } + + create(): Required { + return { + type: ButtonActionType.CREATE_NOTE, + folderPath: '/', + fileName: 'Untitled', + openNote: true, + openIfAlreadyExists: false, + }; + } + + getActionLabel(): string { + return 'Create a new note'; + } +} diff --git a/packages/core/src/fields/button/actions/InlineJSButtonActionConfig.ts b/packages/core/src/fields/button/actions/InlineJSButtonActionConfig.ts new file mode 100644 index 00000000..8cb20df4 --- /dev/null +++ b/packages/core/src/fields/button/actions/InlineJSButtonActionConfig.ts @@ -0,0 +1,51 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + InlineJSButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; +import { ErrorLevel, MetaBindJsError } from 'packages/core/src/utils/errors/MetaBindErrors'; + +export class InlineJSButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.INLINE_JS, plugin); + } + + async run( + config: ButtonConfig | undefined, + action: InlineJSButtonAction, + filePath: string, + context: ButtonContext, + click: ButtonClickContext, + ): Promise { + if (!this.plugin.settings.enableJs) { + throw new MetaBindJsError({ + errorLevel: ErrorLevel.CRITICAL, + effect: "Can't run button action that requires JS evaluation.", + cause: 'JS evaluation is disabled in the plugin settings.', + }); + } + + const configOverrides: Record = { + buttonConfig: structuredClone(config), + buttonContext: structuredClone(context), + click: structuredClone(click), + }; + const unloadCallback = await this.plugin.internal.jsEngineRunCode(action.code, filePath, configOverrides); + unloadCallback(); + } + + create(): Required { + return { + type: ButtonActionType.INLINE_JS, + code: 'console.log("Hello world")', + }; + } + + getActionLabel(): string { + return 'Run JavaScript code'; + } +} diff --git a/packages/core/src/fields/button/actions/InputButtonActionConfig.ts b/packages/core/src/fields/button/actions/InputButtonActionConfig.ts new file mode 100644 index 00000000..8ad15dab --- /dev/null +++ b/packages/core/src/fields/button/actions/InputButtonActionConfig.ts @@ -0,0 +1,37 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + InputButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; + +export class InputButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.INPUT, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: InputButtonAction, + _filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + const el = document.activeElement; + if (el && el instanceof HTMLInputElement) { + el.setRangeText(action.str, el.selectionStart!, el.selectionEnd!, 'end'); + el.dispatchEvent(new Event('input', { bubbles: true })); + } + } + + create(): Required { + return { type: ButtonActionType.INPUT, str: '' }; + } + + getActionLabel(): string { + return 'Insert text at cursor'; + } +} diff --git a/packages/core/src/fields/button/actions/InsertIntoNoteButtonActionConfig.ts b/packages/core/src/fields/button/actions/InsertIntoNoteButtonActionConfig.ts new file mode 100644 index 00000000..17a2fc53 --- /dev/null +++ b/packages/core/src/fields/button/actions/InsertIntoNoteButtonActionConfig.ts @@ -0,0 +1,59 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + InsertIntoNoteButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; + +export class InsertIntoNoteButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.INSERT_INTO_NOTE, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: InsertIntoNoteButtonAction, + filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + const insertString = action.templater + ? await this.plugin.internal.evaluateTemplaterTemplate( + this.plugin.api.buttonActionRunner.resolveFilePath(action.value), + filePath, + ) + : action.value; + + await this.plugin.internal.file.atomicModify(filePath, content => { + let splitContent = content.split('\n'); + + if (action.line < 1 || action.line > splitContent.length + 1) { + throw new Error('Line number out of bounds'); + } + + splitContent = [ + ...splitContent.slice(0, action.line - 1), + insertString, + ...splitContent.slice(action.line - 1), + ]; + + return splitContent.join('\n'); + }); + } + + create(): Required { + return { + type: ButtonActionType.INSERT_INTO_NOTE, + line: 0, + value: 'Some text', + templater: false, + }; + } + + getActionLabel(): string { + return 'Insert text into the note'; + } +} diff --git a/packages/core/src/fields/button/actions/JSButtonActionConfig.ts b/packages/core/src/fields/button/actions/JSButtonActionConfig.ts new file mode 100644 index 00000000..65675961 --- /dev/null +++ b/packages/core/src/fields/button/actions/JSButtonActionConfig.ts @@ -0,0 +1,49 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + JSButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; +import { ErrorLevel, MetaBindJsError } from 'packages/core/src/utils/errors/MetaBindErrors'; + +export class JSButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.JS, plugin); + } + + async run( + config: ButtonConfig | undefined, + action: JSButtonAction, + filePath: string, + context: ButtonContext, + click: ButtonClickContext, + ): Promise { + if (!this.plugin.settings.enableJs) { + throw new MetaBindJsError({ + errorLevel: ErrorLevel.CRITICAL, + effect: "Can't run button action that requires JS evaluation.", + cause: 'JS evaluation is disabled in the plugin settings.', + }); + } + + const configOverrides: Record = { + buttonConfig: structuredClone(config), + args: structuredClone(action.args), + buttonContext: structuredClone(context), + click: structuredClone(click), + }; + const unloadCallback = await this.plugin.internal.jsEngineRunFile(action.file, filePath, configOverrides); + unloadCallback(); + } + + create(): Required { + return { type: ButtonActionType.JS, file: '', args: {} }; + } + + getActionLabel(): string { + return 'Run a JavaScript file'; + } +} diff --git a/packages/core/src/fields/button/actions/OpenButtonActionConfig.ts b/packages/core/src/fields/button/actions/OpenButtonActionConfig.ts new file mode 100644 index 00000000..98c4ed0d --- /dev/null +++ b/packages/core/src/fields/button/actions/OpenButtonActionConfig.ts @@ -0,0 +1,34 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + OpenButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; +import { MDLinkParser } from 'packages/core/src/parsers/MarkdownLinkParser'; + +export class OpenButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.OPEN, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: OpenButtonAction, + filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + MDLinkParser.parseLinkOrUrl(action.link).open(this.plugin, filePath, action.newTab ?? false); + } + + create(): Required { + return { type: ButtonActionType.OPEN, link: '', newTab: true }; + } + + getActionLabel(): string { + return 'Open a link'; + } +} diff --git a/packages/core/src/fields/button/actions/RegexpReplaceInNoteButtonActionConfig.ts b/packages/core/src/fields/button/actions/RegexpReplaceInNoteButtonActionConfig.ts new file mode 100644 index 00000000..e222425b --- /dev/null +++ b/packages/core/src/fields/button/actions/RegexpReplaceInNoteButtonActionConfig.ts @@ -0,0 +1,44 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + RegexpReplaceInNoteButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; + +export class RegexpReplaceInNoteButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.REGEXP_REPLACE_IN_NOTE, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: RegexpReplaceInNoteButtonAction, + filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + if (action.regexp === '') { + throw new Error('Regexp cannot be empty'); + } + + await this.plugin.internal.file.atomicModify(filePath, content => { + return content.replace(new RegExp(action.regexp, action.regexpFlags ?? 'g'), action.replacement); + }); + } + + create(): Required { + return { + type: ButtonActionType.REGEXP_REPLACE_IN_NOTE, + regexp: '([A-Z])\\w+', + replacement: 'Replacement text', + regexpFlags: 'g', + }; + } + + getActionLabel(): string { + return 'Replace text in note using regexp'; + } +} diff --git a/packages/core/src/fields/button/actions/ReplaceInNoteButtonActionConfig.ts b/packages/core/src/fields/button/actions/ReplaceInNoteButtonActionConfig.ts new file mode 100644 index 00000000..d33fc2a5 --- /dev/null +++ b/packages/core/src/fields/button/actions/ReplaceInNoteButtonActionConfig.ts @@ -0,0 +1,64 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + ReplaceInNoteButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; + +export class ReplaceInNoteButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.REPLACE_IN_NOTE, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: ReplaceInNoteButtonAction, + filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + if (action.fromLine > action.toLine) { + throw new Error('From line cannot be greater than to line'); + } + + const replacement = action.templater + ? await this.plugin.internal.evaluateTemplaterTemplate( + this.plugin.api.buttonActionRunner.resolveFilePath(action.replacement), + filePath, + ) + : action.replacement; + + await this.plugin.internal.file.atomicModify(filePath, content => { + let splitContent = content.split('\n'); + + if (action.fromLine < 0 || action.toLine > splitContent.length + 1) { + throw new Error('Line numbers out of bounds'); + } + + splitContent = [ + ...splitContent.slice(0, action.fromLine - 1), + replacement, + ...splitContent.slice(action.toLine), + ]; + + return splitContent.join('\n'); + }); + } + + create(): Required { + return { + type: ButtonActionType.REPLACE_IN_NOTE, + fromLine: 0, + toLine: 0, + replacement: 'Replacement text', + templater: false, + }; + } + + getActionLabel(): string { + return 'Replace text in note'; + } +} diff --git a/packages/core/src/fields/button/actions/ReplaceSelfButtonActionConfig.ts b/packages/core/src/fields/button/actions/ReplaceSelfButtonActionConfig.ts new file mode 100644 index 00000000..e83d3ec6 --- /dev/null +++ b/packages/core/src/fields/button/actions/ReplaceSelfButtonActionConfig.ts @@ -0,0 +1,72 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + ReplaceSelfButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; + +export class ReplaceSelfButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.REPLACE_SELF, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: ReplaceSelfButtonAction, + filePath: string, + context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + if (context.isInline) { + throw new Error('Replace self action not supported for inline buttons'); + } + + if (context.position === undefined) { + throw new Error('Position of the button in the note is unknown'); + } + + if (context.position.lineStart > context.position.lineEnd) { + throw new Error('Position of the button in the note is invalid'); + } + + const position = context.position; + + const replacement = action.templater + ? await this.plugin.internal.evaluateTemplaterTemplate( + this.plugin.api.buttonActionRunner.resolveFilePath(action.replacement), + filePath, + ) + : action.replacement; + + await this.plugin.internal.file.atomicModify(filePath, content => { + let splitContent = content.split('\n'); + + if (position.lineStart < 0 || position.lineEnd > splitContent.length + 1) { + throw new Error('Position of the button in the note is out of bounds'); + } + + splitContent = [ + ...splitContent.slice(0, position.lineStart), + replacement, + ...splitContent.slice(position.lineEnd + 1), + ]; + + return splitContent.join('\n'); + }); + } + + create(): Required { + return { + type: ButtonActionType.REPLACE_SELF, + replacement: 'Replacement text', + templater: false, + }; + } + + getActionLabel(): string { + return 'Replace button with text'; + } +} diff --git a/packages/core/src/fields/button/actions/SleepButtonActionConfig.ts b/packages/core/src/fields/button/actions/SleepButtonActionConfig.ts new file mode 100644 index 00000000..95a7f491 --- /dev/null +++ b/packages/core/src/fields/button/actions/SleepButtonActionConfig.ts @@ -0,0 +1,33 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + SleepButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; + +export class SleepButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.SLEEP, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: SleepButtonAction, + _filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + await new Promise(resolve => setTimeout(resolve, action.ms)); + } + + create(): Required { + return { type: ButtonActionType.SLEEP, ms: 0 }; + } + + getActionLabel(): string { + return 'Sleep for some time'; + } +} diff --git a/packages/core/src/fields/button/actions/TemplaterCreateNoteButtonActionConfig.ts b/packages/core/src/fields/button/actions/TemplaterCreateNoteButtonActionConfig.ts new file mode 100644 index 00000000..3f3f1712 --- /dev/null +++ b/packages/core/src/fields/button/actions/TemplaterCreateNoteButtonActionConfig.ts @@ -0,0 +1,55 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + TemplaterCreateNoteButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; +import { ensureFileExtension, joinPath } from 'packages/core/src/utils/Utils'; + +export class TemplaterCreateNoteButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.TEMPLATER_CREATE_NOTE, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: TemplaterCreateNoteButtonAction, + _filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + if (action.openIfAlreadyExists && action.fileName) { + const filePath = ensureFileExtension(joinPath(action.folderPath ?? '', action.fileName), 'md'); + // if the file already exists, open it in the same tab + if (await this.plugin.internal.file.exists(filePath)) { + this.plugin.internal.file.open(filePath, '', false); + return; + } + } + + await this.plugin.internal.createNoteWithTemplater( + action.templateFile, + action.folderPath, + action.fileName, + action.openNote, + ); + } + + create(): Required { + return { + type: ButtonActionType.TEMPLATER_CREATE_NOTE, + templateFile: '', + folderPath: '/', + fileName: '', + openNote: true, + openIfAlreadyExists: false, + }; + } + + getActionLabel(): string { + return 'Create a new note using Templater'; + } +} diff --git a/packages/core/src/fields/button/actions/UpdateMetadataButtonActionConfig.ts b/packages/core/src/fields/button/actions/UpdateMetadataButtonActionConfig.ts new file mode 100644 index 00000000..03863832 --- /dev/null +++ b/packages/core/src/fields/button/actions/UpdateMetadataButtonActionConfig.ts @@ -0,0 +1,64 @@ +import type { + ButtonClickContext, + ButtonConfig, + ButtonContext, + UpdateMetadataButtonAction, +} from 'packages/core/src/config/ButtonConfig'; +import { ButtonActionType } from 'packages/core/src/config/ButtonConfig'; +import { AbstractButtonActionConfig } from 'packages/core/src/fields/button/AbstractButtonActionConfig'; +import type { IPlugin } from 'packages/core/src/IPlugin'; +import { ErrorLevel, MetaBindJsError } from 'packages/core/src/utils/errors/MetaBindErrors'; +import { parseLiteral } from 'packages/core/src/utils/Literal'; + +export class UpdateMetadataButtonActionConfig extends AbstractButtonActionConfig { + constructor(plugin: IPlugin) { + super(ButtonActionType.UPDATE_METADATA, plugin); + } + + async run( + _config: ButtonConfig | undefined, + action: UpdateMetadataButtonAction, + filePath: string, + _context: ButtonContext, + _click: ButtonClickContext, + ): Promise { + const bindTarget = this.plugin.api.bindTargetParser.fromStringAndValidate(action.bindTarget, filePath); + + if (action.evaluate) { + if (!this.plugin.settings.enableJs) { + throw new MetaBindJsError({ + errorLevel: ErrorLevel.CRITICAL, + effect: "Can't run button action that requires JS evaluation.", + cause: 'JS evaluation is disabled in the plugin settings.', + }); + } + + // eslint-disable-next-line @typescript-eslint/no-implied-eval + const func = new Function('x', 'getMetadata', `return ${action.value};`) as ( + value: unknown, + getMetadata: (bindTarget: string) => unknown, + ) => unknown; + + this.plugin.api.updateMetadata(bindTarget, value => + func(value, bindTarget => { + return this.plugin.api.getMetadata(this.plugin.api.parseBindTarget(bindTarget, filePath)); + }), + ); + } else { + this.plugin.api.setMetadata(bindTarget, parseLiteral(action.value)); + } + } + + create(): Required { + return { + type: ButtonActionType.UPDATE_METADATA, + bindTarget: '', + evaluate: false, + value: '', + }; + } + + getActionLabel(): string { + return 'Update metadata'; + } +} diff --git a/packages/core/src/fields/inputFields/InputFieldSvelteWrapper.ts b/packages/core/src/fields/inputFields/InputFieldSvelteWrapper.ts index 6c8f6faf..0885ad79 100644 --- a/packages/core/src/fields/inputFields/InputFieldSvelteWrapper.ts +++ b/packages/core/src/fields/inputFields/InputFieldSvelteWrapper.ts @@ -42,7 +42,6 @@ export class InputFieldSvelteWrapper extends Noti * @param value */ public setValue(value: Value): void { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call this.svelteComponentInstance?.setValue(value); } diff --git a/packages/core/src/fields/viewFields/fields/MathVF.ts b/packages/core/src/fields/viewFields/fields/MathVF.ts index 80a64e17..bf9e3601 100644 --- a/packages/core/src/fields/viewFields/fields/MathVF.ts +++ b/packages/core/src/fields/viewFields/fields/MathVF.ts @@ -79,7 +79,6 @@ export class MathVF extends AbstractViewField { const context = this.buildMathJSContext(); try { - // eslint-disable-next-line const value: unknown = this.expression.evaluate(context); return typeof value === 'string' ? parseLiteral(value) : value; } catch (e) { diff --git a/packages/core/src/metadata/ComputedMetadataSubscription.ts b/packages/core/src/metadata/ComputedMetadataSubscription.ts index 9fc32a8d..8db196ca 100644 --- a/packages/core/src/metadata/ComputedMetadataSubscription.ts +++ b/packages/core/src/metadata/ComputedMetadataSubscription.ts @@ -1,4 +1,3 @@ -// eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents import type { IMetadataSubscription } from 'packages/core/src/metadata/IMetadataSubscription'; import type { MetadataManager } from 'packages/core/src/metadata/MetadataManager'; import type { MetadataSubscription } from 'packages/core/src/metadata/MetadataSubscription'; diff --git a/packages/core/src/metadata/InternalMetadataSources.ts b/packages/core/src/metadata/InternalMetadataSources.ts index 24c16d1a..167ed83e 100644 --- a/packages/core/src/metadata/InternalMetadataSources.ts +++ b/packages/core/src/metadata/InternalMetadataSources.ts @@ -29,7 +29,7 @@ export class InternalMetadataSource extends FilePathMetadataSource { + public syncExternal(_cacheItem: FilePathMetadataCacheItem): void { // Do nothing } } diff --git a/packages/core/src/metadata/MetadataSource.ts b/packages/core/src/metadata/MetadataSource.ts index c83bea10..79204045 100644 --- a/packages/core/src/metadata/MetadataSource.ts +++ b/packages/core/src/metadata/MetadataSource.ts @@ -290,7 +290,6 @@ export abstract class FilePathMetadataSource(actionType); - - return 'CHANGE ME'; + return plugin.api.buttonActionRunner.getActionLabel(actionType); } function openActionContextMenu(index: number, e: MouseEvent): void { diff --git a/packages/core/src/modals/modalContents/buttonBuilder/InlineJsActionSettings.svelte b/packages/core/src/modals/modalContents/buttonBuilder/InlineJsActionSettings.svelte index 64916e18..2976bef8 100644 --- a/packages/core/src/modals/modalContents/buttonBuilder/InlineJsActionSettings.svelte +++ b/packages/core/src/modals/modalContents/buttonBuilder/InlineJsActionSettings.svelte @@ -1,5 +1,5 @@ diff --git a/packages/core/src/parsers/bindTargetParser/BindTargetParser.ts b/packages/core/src/parsers/bindTargetParser/BindTargetParser.ts index aabcf2a4..0eb490bb 100644 --- a/packages/core/src/parsers/bindTargetParser/BindTargetParser.ts +++ b/packages/core/src/parsers/bindTargetParser/BindTargetParser.ts @@ -23,11 +23,7 @@ export class BindTargetParser { return runParser(P_BindTarget, declarationString); } - fromStringAndValidate( - bindTargetString: string, - filePath: string, - scope?: BindTargetScope | undefined, - ): BindTargetDeclaration { + fromStringAndValidate(bindTargetString: string, filePath: string, scope?: BindTargetScope): BindTargetDeclaration { return this.validate(bindTargetString, this.fromString(bindTargetString), filePath, scope); } @@ -56,7 +52,7 @@ export class BindTargetParser { fullDeclaration: string | undefined, unvalidatedBindTargetDeclaration: UnvalidatedBindTargetDeclaration, filePath: string, - scope?: BindTargetScope | undefined, + scope?: BindTargetScope, ): BindTargetDeclaration { const bindTargetDeclaration: BindTargetDeclaration = {} as BindTargetDeclaration; @@ -108,7 +104,7 @@ export class BindTargetParser { return source.resolveBindTargetScope(bindTargetDeclaration, scope, this); } - public resolveScope(bindTarget: BindTargetDeclaration, scope?: BindTargetScope | undefined): BindTargetDeclaration { + public resolveScope(bindTarget: BindTargetDeclaration, scope?: BindTargetScope): BindTargetDeclaration { if (scope === undefined) { throw new ParsingValidationError( ErrorLevel.ERROR, diff --git a/packages/core/src/parsers/inputFieldParser/InputFieldDeclarationValidator.ts b/packages/core/src/parsers/inputFieldParser/InputFieldDeclarationValidator.ts index 93ab469a..9c4e7a6e 100644 --- a/packages/core/src/parsers/inputFieldParser/InputFieldDeclarationValidator.ts +++ b/packages/core/src/parsers/inputFieldParser/InputFieldDeclarationValidator.ts @@ -50,7 +50,6 @@ export class InputFieldDeclarationValidator { const inputFieldType = this.unvalidatedDeclaration.inputFieldType; for (const entry of Object.entries(InputFieldType)) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison if (entry[1] === inputFieldType?.value) { return entry[1]; } diff --git a/packages/core/src/parsers/viewFieldParser/ViewFieldParser.ts b/packages/core/src/parsers/viewFieldParser/ViewFieldParser.ts index c6e5696c..c5960bb4 100644 --- a/packages/core/src/parsers/viewFieldParser/ViewFieldParser.ts +++ b/packages/core/src/parsers/viewFieldParser/ViewFieldParser.ts @@ -41,11 +41,7 @@ export class ViewFieldParser { }; } - fromStringAndValidate( - declarationString: string, - filePath: string, - scope?: BindTargetScope | undefined, - ): ViewFieldDeclaration { + fromStringAndValidate(declarationString: string, filePath: string, scope?: BindTargetScope): ViewFieldDeclaration { return this.validate(this.fromString(declarationString), filePath, scope); } @@ -76,7 +72,7 @@ export class ViewFieldParser { fromSimpleDeclarationAndValidate( simpleDeclaration: SimpleViewFieldDeclaration, filePath: string, - scope?: BindTargetScope | undefined, + scope?: BindTargetScope, ): ViewFieldDeclaration { return this.validate(this.fromSimpleDeclaration(simpleDeclaration), filePath, scope); } @@ -96,7 +92,7 @@ export class ViewFieldParser { validate( unvalidatedDeclaration: UnvalidatedViewFieldDeclaration, filePath: string, - scope?: BindTargetScope | undefined, + scope?: BindTargetScope, ): ViewFieldDeclaration { const validator = new ViewFieldDeclarationValidator(unvalidatedDeclaration, filePath, this.plugin); diff --git a/packages/core/src/utils/ZodUtils.ts b/packages/core/src/utils/ZodUtils.ts index 531098d3..e46b7a93 100644 --- a/packages/core/src/utils/ZodUtils.ts +++ b/packages/core/src/utils/ZodUtils.ts @@ -6,8 +6,7 @@ export function oneOf< A, K1 extends Extract, K2 extends Extract, - R extends A & - ((Required> & { [P in K2]: undefined }) | (Required> & { [P in K1]: undefined })), + R extends A & ((Required> & Record) | (Required> & Record)), >(key1: K1, key2: K2): (arg: A, ctx: RefinementCtx) => arg is R { return (arg, ctx): arg is R => { if ((arg[key1] === undefined) === (arg[key2] === undefined)) { diff --git a/packages/core/src/utils/components/ButtonComponent.svelte b/packages/core/src/utils/components/ButtonComponent.svelte index 90d9ed7e..64450f71 100644 --- a/packages/core/src/utils/components/ButtonComponent.svelte +++ b/packages/core/src/utils/components/ButtonComponent.svelte @@ -1,7 +1,7 @@