From 9d0178c8efa6bccf94fbc55adc2728235108f52c Mon Sep 17 00:00:00 2001 From: Danny Coulombe Date: Mon, 15 Dec 2025 08:53:38 -0500 Subject: [PATCH] Support defining triggers directly in the YAML schema structure #75 --- components.d.ts | 1 + docs/structure.md | 28 +++++++++ package.json | 2 +- src/assets/default-structure.json | 3 + src/assets/example-structure.yaml | 13 ++++ src/components/JSONms.vue | 2 + src/components/SitePreview.vue | 3 + src/components/StructureEditor.vue | 68 +++++++++++++-------- src/components/TriggerMenu.vue | 98 ++++++++++++++++++++++++++++++ src/interfaces.ts | 10 +++ src/services.ts | 2 +- 11 files changed, 201 insertions(+), 29 deletions(-) create mode 100644 src/components/TriggerMenu.vue diff --git a/components.d.ts b/components.d.ts index 2bc9fd5..768edee 100644 --- a/components.d.ts +++ b/components.d.ts @@ -47,6 +47,7 @@ declare module 'vue' { StructureSelector: typeof import('./src/components/StructureSelector.vue')['default'] SyntaxHighlighter: typeof import('./src/components/SyntaxHighlighter.vue')['default'] Toolbar: typeof import('./src/components/Toolbar.vue')['default'] + TriggerMenu: typeof import('./src/components/TriggerMenu.vue')['default'] UserSettingsDialog: typeof import('./src/components/UserSettingsDialog.vue')['default'] VideoPlayer: typeof import('./src/components/VideoPlayer.vue')['default'] } diff --git a/docs/structure.md b/docs/structure.md index b105926..26bc200 100644 --- a/docs/structure.md +++ b/docs/structure.md @@ -428,3 +428,31 @@ Here's an example of a field using an schema: collapsable: true # Make it a collapsable group collapsed: true # Make it collapsed by default ``` + +## Triggers + +Use triggers to define executable actions exposed in the UI. Each trigger becomes an interactive control (button, menu item, etc.) that calls a specific URL when activated. + +```yaml +triggers: + + # Unique trigger identifier + build: + + # Text displayed to the user + label: Build + + # Icon shown next to the label (Material Design Icons) + icon: mdi-play + + # URL called when the trigger is executed + url: https://json.ms/?action=build + + # HTTP method used for the request + method: POST + + # HTTP headers sent with the request + headers: + # (Example) Indicates the payload format sent to the backend + Content-Type: application/json +``` diff --git a/package.json b/package.json index 6315e6d..40e0ac7 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@json.ms/www", "private": true, "type": "module", - "version": "1.2.12", + "version": "1.2.13", "scripts": { "dev": "vite --host", "build": "run-p type-check \"build-only {@}\" --", diff --git a/src/assets/default-structure.json b/src/assets/default-structure.json index fdb6b44..1a0f082 100644 --- a/src/assets/default-structure.json +++ b/src/assets/default-structure.json @@ -1,6 +1,9 @@ { "global": { "title": "Untitled" + }, + "triggers": { + }, "locales": { diff --git a/src/assets/example-structure.yaml b/src/assets/example-structure.yaml index 6e8c121..a404ea4 100644 --- a/src/assets/example-structure.yaml +++ b/src/assets/example-structure.yaml @@ -13,6 +13,19 @@ global: logo: [STRUCTURE_EDITOR_URL]/favicon.ico +# Triggers Configuration: +# Define custom actions that can be triggered from the admin panel UI. +# Each trigger represents an executable action (button, menu item, etc.) +# that points to a URL or endpoint responsible for handling the action. +triggers: + build: + label: Build + icon: mdi-play + url: [STRUCTURE_EDITOR_URL]/build + method: POST + headers: + Content-Type: application/json + # Supported Languages: # Specify the languages your content will support, allowing users # to interact with your content in their preferred language diff --git a/src/components/JSONms.vue b/src/components/JSONms.vue index a8be7c1..b6c7565 100644 --- a/src/components/JSONms.vue +++ b/src/components/JSONms.vue @@ -369,6 +369,8 @@ if (globalStore.session.loggedIn) { ({ required: true }); const emit = defineEmits(['save', 'create', 'change']) const props = defineProps<{ structureData: IStructureData, + userData: any, }>(); const structureEditor = ref | null>(); @@ -215,6 +216,8 @@ defineExpose({ ({ required: true }); -const { columns = false } = defineProps<{ - columns?: boolean +const { columns = false, userData } = defineProps<{ + columns?: boolean, + structureData: IStructureData, + userData: any }>(); const { canSaveStructure, yamlException, structureStates } = useStructure(); const modelStore = useModelStore(); @@ -456,7 +459,6 @@ watch(() => globalStore.userSettings.data, () => { />
- globalStore.userSettings.data, () => { - - - +
+ + + + +
@@ -520,7 +530,9 @@ watch(() => globalStore.userSettings.data, () => { >
- Readonly: Typings are generated automatically. +
+ Readonly: Typings are generated automatically. +
globalStore.userSettings.data, () => {
- Readonly: Default objects are generated automatically. +
+ Readonly: Default objects are generated automatically. +
+import type {IStructure, IStructureData, ITrigger} from "@/interfaces"; +import {computed, ref, type Ref} from "vue"; +import {Services} from "@/services"; +import {useGlobalStore} from "@/stores/global"; + +const structureData = defineModel({ required: true }); +const { structure, userData } = defineProps<{ + structure: IStructure, + userData: IStructure, +}>(); + +const globalStore = useGlobalStore(); +const running: Ref = ref(false) +const lastTrigger: Ref = ref(null) + +const triggers = computed((): ITrigger[] => { + const triggers: ITrigger[] = []; + const keys = Object.keys(structureData.value.triggers); + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + triggers.push({ + key, + ...structureData.value.triggers[key], + }) + } + return triggers; +}) +const hasManyItems = computed((): boolean => { + return triggers.value.length > 1; +}) +const otherTriggers = computed((): ITrigger[] => { + return triggers.value.filter((item: ITrigger) => item !== firstTrigger.value && item.key !== lastTrigger.value?.key); +}) +const firstTrigger = computed((): ITrigger => { + return lastTrigger.value || triggers.value[0] || { + label: '', + url: '', + } +}) + +const run = (trigger: ITrigger): Promise => { + lastTrigger.value = trigger; + running.value = true; + const method = (trigger.method || 'POST').toUpperCase(); + return Services.handle(trigger.url, method, method === 'GET' ? undefined : JSON.stringify({ + structure, + userData, + }), Object.assign({ + 'Content-Type': 'application/json' + }, trigger.headers)) + .catch(globalStore.catchError) + .finally(() => running.value = false); +} + + + + diff --git a/src/interfaces.ts b/src/interfaces.ts index c3ec440..1241c5c 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -183,12 +183,22 @@ export interface ISection { fields: {[key: string]: IField} } +export interface ITrigger { + key?: string + label: string + icon?: string + headers?: {[key: string]: string} + method?: string + url: string +} + export interface IStructureData { global: { title?: string logo?: string preview?: string }, + triggers: {[key: string]: ITrigger}, enums: TEnum, schemas: TSchema, locales: {[key: string]: string} diff --git a/src/services.ts b/src/services.ts index 7746d20..53bd5e0 100644 --- a/src/services.ts +++ b/src/services.ts @@ -44,7 +44,7 @@ export class Services { } } - private static handle(url: string, method = 'GET', body?: any, headers: {[key: string]: any} = {}, params: {[key: string]: any} = {}, cache = true): Promise { + static handle(url: string, method = 'GET', body?: any, headers: {[key: string]: any} = {}, params: {[key: string]: any} = {}, cache = true): Promise { const finalParams: any = { credentials: 'include', method,