From 0760724c27b4448d7f47795a163c06770ba38515 Mon Sep 17 00:00:00 2001 From: Danny Coulombe Date: Sat, 20 Dec 2025 06:21:19 -0500 Subject: [PATCH 1/3] - Fix field with null value considered an object ending up with wrong default value - Unable to update newly added admin settings - Can't save newly created structure - Change documentation title to YAML Structure - Have default or fallback icon for projects --- docs/structure.md | 8 ++-- package.json | 2 +- src/components/StructureEditor.vue | 3 +- src/components/StructureSelector.vue | 12 ++++- src/composables/structure.ts | 7 ++- src/composables/user-data.ts | 4 +- src/stores/global.ts | 65 +++++++++++++++------------- src/utils.ts | 52 +++++++++++----------- 8 files changed, 85 insertions(+), 68 deletions(-) diff --git a/docs/structure.md b/docs/structure.md index 27579af..89dfd52 100644 --- a/docs/structure.md +++ b/docs/structure.md @@ -1,9 +1,9 @@ -# YAML Interface +# YAML Structure --- Let's get started on building your ideal admin panel with JSON.ms! ## Global Configuration -Let's start with the global configuration. You can copy paste and adjust the following code in your _YAML_ interface: +Let's start with the global configuration. You can copy paste and adjust the following code in your _YAML_ structure: ```yaml global: @@ -20,7 +20,7 @@ These settings allows to create an admin panel that aligns with your brand and u ## Locales -JSON.ms allows you to manage locales for translating the fields that will be saved in your application. You can easily define multiple locales in a customizable list of key/value pairs within the YAML interface. If no locales are provided, JSON.ms will default to "`en-US`." This feature enables you to ensure that the data fields are accurately translated, accommodating users who speak different languages and enhancing the overall usability of your application. +JSON.ms allows you to manage locales for translating the fields that will be saved in your application. You can easily define multiple locales in a customizable list of key/value pairs within the YAML structure. If no locales are provided, JSON.ms will default to "`en-US`." This feature enables you to ensure that the data fields are accurately translated, accommodating users who speak different languages and enhancing the overall usability of your application. ```yaml locales: @@ -404,7 +404,7 @@ Here's an example of a field using an enum: ## Schemas -Reusable schemas (fieldset) are a powerful feature that allows you to define a set of predefined fields that can be referenced multiple times throughout your interface. This promotes consistency in your data definitions and reduces redundancy, making your code cleaner and easier to maintain. +Reusable schemas (fieldset) are a powerful feature that allows you to define a set of predefined fields that can be referenced multiple times throughout your structure. This promotes consistency in your data definitions and reduces redundancy, making your code cleaner and easier to maintain. ```yaml schemas: diff --git a/package.json b/package.json index 786aefd..d3e984f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@json.ms/www", "private": true, "type": "module", - "version": "1.3.1", + "version": "1.3.2", "scripts": { "dev": "vite --host", "build": "run-p type-check \"build-only {@}\" --", diff --git a/src/components/StructureEditor.vue b/src/components/StructureEditor.vue index b5f6ea7..73b590b 100644 --- a/src/components/StructureEditor.vue +++ b/src/components/StructureEditor.vue @@ -662,7 +662,6 @@ watch(() => globalStore.userSettings.data, () => { size="small" /> @@ -671,7 +670,7 @@ watch(() => globalStore.userSettings.data, () => { v-if="structure" v-bind="props" :loading="structureStates.saving" - :disabled="!canSaveStructure || ((!modelStore.structure.server_url || !globalStore.session.loggedIn) && !isFolderSynced(modelStore.structure)) || structureStates.saving || structureStates.saved" + :disabled="!canSaveStructure || (!globalStore.session.loggedIn && !isFolderSynced(modelStore.structure)) || structureStates.saving || structureStates.saved" :prepend-icon="!structureStates.saved ? 'mdi-content-save' : 'mdi-check'" variant="outlined" color="primary" diff --git a/src/components/StructureSelector.vue b/src/components/StructureSelector.vue index 4230da9..fba306b 100644 --- a/src/components/StructureSelector.vue +++ b/src/components/StructureSelector.vue @@ -85,7 +85,11 @@ const computedStructure = computed((): IStructure => { width="24" height="24" class="mr-1" - /> + > + + @@ -189,7 +193,11 @@ const computedStructure = computed((): IStructure => { width="24" height="24" class="mr-8" - /> + > + + diff --git a/src/composables/structure.ts b/src/composables/structure.ts index 40b5dbf..09b981f 100644 --- a/src/composables/structure.ts +++ b/src/composables/structure.ts @@ -467,7 +467,12 @@ export function useStructure() { const saveStructureSimple = ( structure: IStructure = modelStore.structure, ): Promise => { - const parsedStructureData = getParsedStructure(structure); + const parsedStructureData = getParsedStructure(structure) || { + global: { + title: undefined, + logo: undefined, + } + }; const body = { content: { ...structure }, data: {}, diff --git a/src/composables/user-data.ts b/src/composables/user-data.ts index 5b7333d..b37378e 100644 --- a/src/composables/user-data.ts +++ b/src/composables/user-data.ts @@ -316,7 +316,7 @@ export function useUserData() { } const defaultValue = getFieldDefaultValue(field, locales); - const overrideValue = getDataByPath(override, path, defaultValue); + const overrideValue = cleanProperties(getDataByPath(override, path, defaultValue), defaultValue); // Node if (isFieldType(field, 'node')) { @@ -326,7 +326,7 @@ export function useUserData() { // Files if (isFieldType(field, 'file')) { if (typeof overrideValue === 'object' && overrideValue !== null && typeof overrideValue.path === 'string' && typeof overrideValue.meta === 'object') { - return parent[key] = cleanProperties(overrideValue, defaultValue); + return parent[key] = overrideValue; } else if (Array.isArray(overrideValue)) { parent[key] = []; overrideValue.forEach(value => { diff --git a/src/stores/global.ts b/src/stores/global.ts index 3c5e146..8864512 100644 --- a/src/stores/global.ts +++ b/src/stores/global.ts @@ -11,6 +11,33 @@ import type { IUserSettings } from '@/interfaces'; +const defaultUserSettings: IUserSettings = { + appearanceDarkMode: false, + editorFontSize: 16, + editorLiveUpdate: true, + editorUpdateTimeout: 1000, + editorShowPrintMargin: false, + editorAutoSyncFrom: true, + editorAutoSyncInterval: 1000, + autoCleanData: false, + editorTabSize: 2, + userDataAutoFetch: true, + layoutEditorLocation: 'start', + layoutSitePreviewLocation: 'start', + layoutSitePreviewPadding: true, + layoutSitePreviewKeepRatio: true, + layoutAutoSplit: true, + blueprintsIncludeTypings: true, + blueprintsReadFromData: true, + blueprintsReadFromStructure: true, + blueprintsWriteToData: true, + blueprintsWriteToDefault: true, + blueprintsWriteToIndex: true, + blueprintsWriteToStructure: true, + blueprintsWriteToTypings: true, + blueprintsWriteToSettings: true, +} + export const useGlobalStore = defineStore('global', { state: (): { theme: 'dark' | 'light', @@ -72,32 +99,7 @@ export const useGlobalStore = defineStore('global', { }, userSettings: { visible: false, - data: { - appearanceDarkMode: false, - editorFontSize: 16, - editorLiveUpdate: true, - editorUpdateTimeout: 1000, - editorShowPrintMargin: false, - editorAutoSyncFrom: true, - editorAutoSyncInterval: 1000, - autoCleanData: false, - editorTabSize: 2, - userDataAutoFetch: true, - layoutEditorLocation: 'start', - layoutSitePreviewLocation: 'start', - layoutSitePreviewPadding: true, - layoutSitePreviewKeepRatio: true, - layoutAutoSplit: true, - blueprintsIncludeTypings: true, - blueprintsReadFromData: true, - blueprintsReadFromStructure: true, - blueprintsWriteToData: true, - blueprintsWriteToDefault: true, - blueprintsWriteToIndex: true, - blueprintsWriteToStructure: true, - blueprintsWriteToTypings: true, - blueprintsWriteToSettings: true, - } + data: structuredClone(defaultUserSettings) }, }), actions: { @@ -165,11 +167,12 @@ export const useGlobalStore = defineStore('global', { }, applyUserSettingsData(data: any) { const values: any = {}; - Object.keys(this.userSettings.data).forEach(key => { - // @ts-expect-error Keys are fetched from this.userSettings, so it's all fine... - const item = this.userSettings.data[key]; - if (data[key] !== undefined && typeof data[key] === typeof item) { - values[key] = data[key]; + Object.keys(defaultUserSettings).forEach(key => { + // @ts-expect-error Keys are fetched from defaultUserSettings, so it's all fine... + const originalValue = defaultUserSettings[key]; + values[key] = originalValue + if (typeof data[key] === typeof originalValue) { + values[key] = data[key] !== undefined ? data[key] : originalValue; } }) this.userSettings.data = values; diff --git a/src/utils.ts b/src/utils.ts index 425dca1..5824010 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -679,35 +679,37 @@ export function formatDate(date: Date, format = 'YYYY-MM-DD HH:mm:ss') { .replace('ss', seconds); } +export function valueType(v: any): 'array' | 'object' | 'null' { + if (v === null) return 'null' + if (Array.isArray(v)) return 'array' + if (typeof v === 'object') return 'object' + return 'null' +} + +export function sameType(a: any, b: any): boolean { + return valueType(a) === valueType(b) +} + export function cleanProperties(data: any, defaultValue: any): any { - const result: any = Array.isArray(defaultValue) ? [] : {} + if (Array.isArray(defaultValue)) { + return Array.isArray(data) ? data : [] + } - for (const key in defaultValue) { - const defVal = defaultValue[key] - const dataVal = data?.[key] + if ( + defaultValue !== null && + typeof defaultValue === 'object' + ) { + const result: any = {} - if ( - defVal !== null && - typeof defVal === 'object' && - !Array.isArray(defVal) - ) { - result[key] = - dataVal !== null && - typeof dataVal === 'object' && - !Array.isArray(dataVal) - ? cleanProperties(dataVal, defVal) - : defVal - } else if (Array.isArray(defVal)) { - result[key] = Array.isArray(dataVal) ? dataVal : defVal - } else { - if (dataVal === null || defVal === null) { - result[key] = dataVal !== undefined ? dataVal : defVal - } else { - result[key] = - typeof dataVal === typeof defVal ? dataVal : defVal - } + for (const key in defaultValue) { + const defVal = defaultValue[key] + const dataVal = data?.[key] + + result[key] = cleanProperties(dataVal, defVal) } + + return result } - return result + return sameType(data, defaultValue) ? data : null } From 0c91a7a73cae20662f157b989d860c51909af9ef Mon Sep 17 00:00:00 2001 From: Danny Coulombe Date: Sat, 20 Dec 2025 16:25:35 -0500 Subject: [PATCH 2/3] Support default instance variables for automatic project loading #88 --- .env.example | 84 +++++++++++++++++ src/components/ActionBar.vue | 3 + src/components/JSONms.vue | 41 ++++++--- src/components/Sidebar.vue | 140 ++++++++++++++++------------- src/components/SitePreview.vue | 2 +- src/components/StructureEditor.vue | 27 +++--- src/components/Toolbar.vue | 58 +++++++----- src/interfaces.ts | 66 +++++++++++++- src/stores/global.ts | 97 +++++++++++++++++++- src/stores/model.ts | 6 ++ 10 files changed, 411 insertions(+), 113 deletions(-) diff --git a/.env.example b/.env.example index 17a19d5..d8e7f01 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,87 @@ VITE_SERVER_URL=http://localhost:9200 # If running the demo project VITE_DEMO_PREVIEW_URL=http://localhost:3001 + +# Set the default user settings (user can override them in the UI) +# (Enter data in JSON format) +#VITE_DEFAULT_USER_SETTINGS={"appearanceDarkMode":false,"editorFontSize":16,"editorLiveUpdate":true,"editorUpdateTimeout":1000,"editorShowPrintMargin":false,"editorAutoSyncFrom":true,"editorAutoSyncInterval":1000,"autoCleanData":false,"editorTabSize":2,"userDataAutoFetch":true,"layoutEditorLocation":"start","layoutSitePreviewLocation":"start","layoutSitePreviewPadding":true,"layoutSitePreviewKeepRatio":true,"layoutAutoSplit":true,"blueprintsIncludeTypings":true,"blueprintsReadFromData":true,"blueprintsReadFromStructure":true,"blueprintsWriteToData":true,"blueprintsWriteToDefault":true,"blueprintsWriteToIndex":true,"blueprintsWriteToStructure":true,"blueprintsWriteToTypings":true,"blueprintsWriteToSettings":true} + +# Force user settings (user won't be able to override them from the UI) +# (Enter data in JSON format) +#VITE_FORCE_USER_SETTINGS={"appearanceDarkMode":true} + +# Force a structure data +#VITE_FORCE_STRUCTURE= + +# Inject user data +# (Enter data in JSON format) +#VITE_USER_DATA= + +# UI CONFIGURATION +# -------------------- +# Use the following keys and separated them by a comma to disable many items in the UI. +# +# toolbar +# toolbar.menu +# toolbar.logo +# toolbar.project_selector +# toolbar.project_selector.new +# toolbar.project_selector.delete +# toolbar.advanced +# toolbar.site_preview +# toolbar.site_preview.refresh +# toolbar.site_preview.mobile +# toolbar.site_preview.desktop +# toolbar.site_preview.blank +# toolbar.trigger_menu +# toolbar.info +# toolbar.locale_selector +# toolbar.login +# toolbar.settings +# toolbar.settings.edit_json +# toolbar.settings.fetch_data +# toolbar.settings.download_data +# toolbar.settings.migrate_data +# toolbar.settings.clear_data +# toolbar.settings.settings +# toolbar.settings.logout +# +# sidebar +# sidebar.sections +# sidebar.tools +# sidebar.tools.file_manager +# sidebar.advanced +# sidebar.advanced.hash +# sidebar.advanced.server +# sidebar.advanced.upload +# sidebar.tutorial +# sidebar.github +# sidebar.footer +# +# structure +# structure.menu +# structure.menu.structure +# structure.menu.blueprints +# structure.menu.settings +# structure.menu.integration +# structure.trigger_menu +# structure.settings +# structure.settings.endpoint +# structure.settings.permissions_structure +# structure.settings.permissions_admin +# structure.footer +# structure.footer_local_sync +# structure.footer_save +# +# site_preview +# +# data +# data.footer +# data.footer.set_as_default +# data.footer.sync_local +# data.footer.sync_endpoint +# +# documentation +VITE_UI_DISABLE= +#VITE_UI_DISABLE=toolbar.logo,toolbar.login,toolbar.settings.edit_json,toolbar.settings.migrate_data,toolbar.settings.settings,documentation,structure,sidebar.tutorial,sidebar.github,sidebar.advanced + diff --git a/src/components/ActionBar.vue b/src/components/ActionBar.vue index 4da182f..b76e190 100644 --- a/src/components/ActionBar.vue +++ b/src/components/ActionBar.vue @@ -192,17 +192,20 @@ const reset = () => { globalStore.admin.structure && globalStore.userSettings.data.layoutAutoSplit && layoutSize.value.data >= (mobileFrameWidth.value * 2)); +const splitTabs = computed((): boolean => { + if (![globalStore.uiConfig.data, globalStore.uiConfig.documentation].every(item => item)) { + return false; + } + return globalStore.admin.structure && globalStore.userSettings.data.layoutAutoSplit && layoutSize.value.data >= (mobileFrameWidth.value * 2); +}); const tab = computed({ get: () => { + if (globalStore.uiConfig.data && !globalStore.uiConfig.documentation) { + return 'data'; + } + if (!globalStore.uiConfig.data && globalStore.uiConfig.documentation) { + return 'docs'; + } const advancedMode = globalStore.admin.structure && windowWidth.value > 900; if (advancedMode && splitTabs.value) { return globalStore.admin.dataTab === 'data' @@ -316,12 +328,14 @@ useShortcut({ }).listen(); initLayout(); -setUserData({}, true); +const envInitialUserData = import.meta.env.VITE_USER_DATA; +const initialUserData = JSON.parse(envInitialUserData || '{}') || {}; +setUserData(initialUserData, true); onMounted(() => { sitePreview.value?.structureEditor?.scrollToSection(getAvailableSection()); structureEditor.value?.scrollToSection(getAvailableSection()); }) -if (globalStore.userSettings.data.userDataAutoFetch) { +if (!envInitialUserData && globalStore.userSettings.data.userDataAutoFetch) { refreshUserData(); } if (globalStore.session.loggedIn) { @@ -333,6 +347,7 @@ if (globalStore.session.loggedIn) { -