Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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']
}
Expand Down
28 changes: 28 additions & 0 deletions docs/structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 {@}\" --",
Expand Down
3 changes: 3 additions & 0 deletions src/assets/default-structure.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
{
"global": {
"title": "Untitled"
},
"triggers": {

},
"locales": {

Expand Down
13 changes: 13 additions & 0 deletions src/assets/example-structure.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/components/JSONms.vue
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,8 @@ if (globalStore.session.loggedIn) {
<StructureEditor
ref="structureEditor"
v-model="structure"
:structure-data="structureParsedData"
:user-data="modelStore.userData"
style="flex: 1"
@save="onSaveStructure"
@create="onCreateStructure"
Expand Down
3 changes: 3 additions & 0 deletions src/components/SitePreview.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const structure = defineModel<IStructure>({ required: true });
const emit = defineEmits(['save', 'create', 'change'])
const props = defineProps<{
structureData: IStructureData,
userData: any,
}>();

const structureEditor = ref<InstanceType<typeof StructureEditor> | null>();
Expand Down Expand Up @@ -215,6 +216,8 @@ defineExpose({
<StructureEditor
ref="structureEditor"
v-model="structure"
:structure-data="structureData"
:user-data="userData"
class="fill-height"
columns
@save="onSaveStructureContent"
Expand Down
68 changes: 41 additions & 27 deletions src/components/StructureEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import {computed, onMounted, type Ref, ref, defineExpose, onBeforeUnmount, watch, nextTick} from 'vue';
import { VAceEditor } from 'vue3-ace-editor';
import type { VAceEditorInstance } from 'vue3-ace-editor/types';
import type {IStructure} from '@/interfaces';
import type {IStructure, IStructureData} from '@/interfaces';
import TriggerMenu from '@/components/TriggerMenu.vue';
import {useStructure} from '@/composables/structure';
import '@/plugins/aceeditor';
import {useGlobalStore} from '@/stores/global';
Expand All @@ -12,8 +13,10 @@ import {useModelStore} from "@/stores/model";

const emit = defineEmits(['save', 'create', 'change'])
const structure = defineModel<IStructure>({ 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();
Expand Down Expand Up @@ -456,7 +459,6 @@ watch(() => globalStore.userSettings.data, () => {
/>
</div>
<div class="pa-2 d-flex w-100" style="flex: 0; gap: 0.5rem">

<v-tooltip
text="Toggle local folder synchronization"
location="bottom"
Expand Down Expand Up @@ -488,27 +490,35 @@ watch(() => globalStore.userSettings.data, () => {
</template>
</v-tooltip>
<v-spacer />
<v-tooltip
text="Save (CTRL+S)"
location="bottom"
>
<template #activator="{ props }">
<v-btn
v-if="structure"
v-bind="props"
:loading="structureStates.saving"
:disabled="!canSaveStructure || structureStates.saving || structureStates.saved"
:prepend-icon="!structureStates.saved ? 'mdi-content-save' : 'mdi-check'"
variant="outlined"
color="primary"
size="small"
@mousedown.stop.prevent="onSaveStructure"
>
<span v-if="!structureStates.saved">Save</span>
<span v-else>Saved!</span>
</v-btn>
</template>
</v-tooltip>
<div class="d-flex justify-end" style="gap: 0.5rem">
<TriggerMenu
:model-value="structureData"
:structure="structure"
:user-data="userData"
/>
<v-tooltip
v-if="isFolderSynced(modelStore.structure, 'typescript')"
text="Save (CTRL+S)"
location="bottom"
>
<template #activator="{ props }">
<v-btn
v-if="structure"
v-bind="props"
:loading="structureStates.saving"
:disabled="!canSaveStructure || structureStates.saving || structureStates.saved"
:prepend-icon="!structureStates.saved ? 'mdi-content-save' : 'mdi-check'"
variant="outlined"
color="primary"
size="small"
@mousedown.stop.prevent="onSaveStructure"
>
<span v-if="!structureStates.saved">Save</span>
<span v-else>Saved!</span>
</v-btn>
</template>
</v-tooltip>
</div>
</div>
</div>
</v-tabs-window-item>
Expand All @@ -520,7 +530,9 @@ watch(() => globalStore.userSettings.data, () => {
>
<div v-if="globalStore.userSettings.data.blueprintsIncludeTypings" class="d-flex flex-column" style="flex: 1">
<v-alert tile class="py-4 text-caption">
<strong>Readonly:</strong> Typings are generated automatically.
<div class="text-truncate">
<strong>Readonly:</strong> Typings are generated automatically.
</div>
</v-alert>
<v-ace-editor
ref="blueprintEditorTypings"
Expand All @@ -534,7 +546,9 @@ watch(() => globalStore.userSettings.data, () => {
</div>
<div class="d-flex flex-column" style="flex: 1">
<v-alert tile class="py-4 text-caption">
<strong>Readonly:</strong> Default objects are generated automatically.
<div class="text-truncate">
<strong>Readonly:</strong> Default objects are generated automatically.
</div>
</v-alert>
<v-ace-editor
ref="blueprintEditorDefault"
Expand Down
98 changes: 98 additions & 0 deletions src/components/TriggerMenu.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<script setup lang="ts">
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<IStructureData>({ required: true });
const { structure, userData } = defineProps<{
structure: IStructure,
userData: IStructure,
}>();

const globalStore = useGlobalStore();
const running: Ref<boolean> = ref(false)
const lastTrigger: Ref<ITrigger | null> = 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<any> => {
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);
}
</script>

<template>
<v-menu v-if="triggers.length > 0" location="top right">
<template #activator="{ props }">
<div class="position-relative" style="flex: 1">
<v-btn
variant="outlined"
size="small"
:disabled="running"
:text="firstTrigger.label"
:class="hasManyItems ? ['w-100 pr-12'] : []"
@click="() => run(firstTrigger)"
>
<v-icon :icon="running ? 'mdi-loading' : firstTrigger.icon" :class="running ? ['mdi-spin'] : []" start />
{{ firstTrigger.label }}
</v-btn>
<v-btn
v-if="hasManyItems"
v-bind="props"
:disabled="running"
variant="flat"
color="transparent"
size="small"
class="position-absolute"
style="right: 0; border-top-left-radius: 0; border-bottom-left-radius: 0"
min-width="0"
>
<v-icon icon="mdi-chevron-up" />
</v-btn>
</div>
</template>
<v-list density="compact">
<v-list-item
v-for="trigger in otherTriggers"
:key="trigger.key"
:title="trigger.label"
:prepend-icon="trigger.icon"
@click="() => run(trigger)"
/>
</v-list>
</v-menu>
</template>

10 changes: 10 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
2 changes: 1 addition & 1 deletion src/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> {
static handle(url: string, method = 'GET', body?: any, headers: {[key: string]: any} = {}, params: {[key: string]: any} = {}, cache = true): Promise<any> {
const finalParams: any = {
credentials: 'include',
method,
Expand Down