diff --git a/src/entities/menu/menu.ts b/src/entities/menu/menu.ts index 2c8fb415..f800fd2e 100644 --- a/src/entities/menu/menu.ts +++ b/src/entities/menu/menu.ts @@ -48,7 +48,20 @@ export class Menu implements TMenu { const schema = z.object({ name: z.string().min(1, 'naam is verplicht'), position: z.number().min(0, 'positie moet 0 of hoger zijn'), - items: z.array(z.any()), // At least '[]' + items: z.array(z.object({ + name: z.string().min(1, 'naam is verplicht'), + slug: z.string().min(1, 'slug is verplicht'), + link: z.string(), + description: z.string(), + icon: z.string(), + items: z.array(z.object({ + name: z.string().min(1, 'naam is verplicht'), + slug: z.string().min(1, 'slug is verplicht'), + link: z.string(), + description: z.string(), + icon: z.string(), + })), + })), // At least '[]' }) const result = schema.safeParse({ diff --git a/src/eventBus.js b/src/eventBus.js new file mode 100644 index 00000000..d64d4799 --- /dev/null +++ b/src/eventBus.js @@ -0,0 +1,2 @@ +import Vue from 'vue' +export const EventBus = new Vue() diff --git a/src/modals/Modals.vue b/src/modals/Modals.vue index a316e90c..ae707819 100644 --- a/src/modals/Modals.vue +++ b/src/modals/Modals.vue @@ -28,8 +28,11 @@ import { navigationStore, publicationStore } from './../store/store.js' - + + + + @@ -59,8 +62,11 @@ import EditThemeModal from './theme/EditThemeModal.vue' import PageForm from './page/PageForm.vue' import DeletePage from './page/DeletePage.vue' import AddPageContentsModal from './pageContents/AddPageContents.vue' +// menu import EditMenuModal from './menu/EditMenuModal.vue' import DeleteMenuModal from './menu/DeleteMenuModal.vue' +import EditMenuItemModal from './menuItem/EditMenuItemModal.vue' +import DeleteMenuItemModal from './menuItem/DeleteMenuItemModal.vue' /** * Component that contains all modals used in the application @@ -91,8 +97,11 @@ export default { PageForm, DeletePage, AddPageContentsModal, + // menu EditMenuModal, DeleteMenuModal, + EditMenuItemModal, + DeleteMenuItemModal, }, } diff --git a/src/modals/menu/EditMenuModal.vue b/src/modals/menu/EditMenuModal.vue index eda394f8..b1edeb98 100644 --- a/src/modals/menu/EditMenuModal.vue +++ b/src/modals/menu/EditMenuModal.vue @@ -1,11 +1,9 @@ @@ -77,16 +56,13 @@ import { NcDialog, NcLoadingIcon, NcNoteCard, - NcInputField, + NcSelect, NcTextField, } from '@nextcloud/vue' -import CodeMirror from 'vue-codemirror6' -import { json, jsonParseLinter } from '@codemirror/lang-json' import ContentSaveOutline from 'vue-material-design-icons/ContentSaveOutline.vue' import Cancel from 'vue-material-design-icons/Cancel.vue' import Plus from 'vue-material-design-icons/Plus.vue' -import AutoFix from 'vue-material-design-icons/AutoFix.vue' import { Menu } from '../../entities/index.js' @@ -101,7 +77,7 @@ export default { NcLoadingIcon, NcNoteCard, NcTextField, - NcInputField, + NcSelect, // Icons ContentSaveOutline, Cancel, @@ -114,10 +90,17 @@ export default { position: 0, items: '', }, + menuPositionOptions: { + options: [ + { label: 'rechts boven', position: 0 }, + { label: 'navigatiebalk', position: 1 }, + { label: 'footer', position: 2 }, + ], + value: { label: 'rechts boven', position: 0 }, + }, success: null, loading: false, error: false, - hasUpdated: false, closeDialogTimeout: null, } }, @@ -125,6 +108,7 @@ export default { inputValidation() { const menuItem = new Menu({ ...this.menuItem, + position: this.menuPositionOptions.value?.position, }) const result = menuItem.validate() @@ -139,12 +123,6 @@ export default { mounted() { this.initializeMenuItem() }, - updated() { - if (navigationStore.modal === 'editMenu' && !this.hasUpdated) { - this.initializeMenuItem() - this.hasUpdated = true - } - }, methods: { /** * Initialize menu item data from store @@ -155,6 +133,8 @@ export default { ...menuStore.menuItem, items: typeof menuStore.menuItem.items === 'string' ? menuStore.menuItem.items : JSON.stringify(menuStore.menuItem.items, null, 2), } + + this.menuPositionOptions.value = this.menuPositionOptions.options.find((option) => option.position === menuStore.menuItem.position) } }, /** @@ -163,15 +143,6 @@ export default { closeModal() { navigationStore.setModal(false) clearTimeout(this.closeModalTimeout) - this.success = null - this.loading = false - this.error = false - this.hasUpdated = false - this.menuItem = { - name: '', - position: 0, - items: '', - } }, /** * Save menu item changes @@ -182,6 +153,7 @@ export default { const menuItem = new Menu({ ...this.menuItem, items: this.menuItem.items ? JSON.parse(this.menuItem.items) : [], + position: this.menuPositionOptions.value.position, }) menuStore.saveMenu(menuItem).then(({ response }) => { diff --git a/src/modals/menuItem/DeleteMenuItemModal.vue b/src/modals/menuItem/DeleteMenuItemModal.vue new file mode 100644 index 00000000..ac3b4115 --- /dev/null +++ b/src/modals/menuItem/DeleteMenuItemModal.vue @@ -0,0 +1,116 @@ + + + + + diff --git a/src/modals/menuItem/EditMenuItemModal.vue b/src/modals/menuItem/EditMenuItemModal.vue new file mode 100644 index 00000000..e184d927 --- /dev/null +++ b/src/modals/menuItem/EditMenuItemModal.vue @@ -0,0 +1,278 @@ + + + + + diff --git a/src/services/formatZodErrors.js b/src/services/formatZodErrors.js new file mode 100644 index 00000000..9713a644 --- /dev/null +++ b/src/services/formatZodErrors.js @@ -0,0 +1,232 @@ +export function createZodErrorHandler(result) { + const issues = result?.error?.issues || [] + + const normalizePath = (path) => { + if (typeof path === 'string') { + return path.split('.') + } + return path + } + + /** + * Get a single error message for a specific path + * + * @example + * ```js + * const error = getError('items.0.name') + * console.log(error) // "naam is verplicht" + * ``` + * + * @param {string} path - The path to get the error message for + * @return {string | undefined} The error message or undefined if no error is found + */ + const getError = (path) => { + const normalizedPath = normalizePath(path) + const error = issues.find((issue) => issue.path.join('.') === normalizedPath.join('.')) + return error?.message + } + + /** + * Get all error messages for a specific path + * + * @example + * ```js + * const errors = getErrors('items.0') + * console.log(errors) // ["naam is verplicht", "summary is verplicht", "link is verplicht"] + * ``` + * + * @param {string} path - The path to get the error message for + * @return {string[]} The error messages or undefined if no error is found + */ + const getErrors = (path) => { + const normalizedPath = normalizePath(path) + const errors = issues.filter((issue) => issue.path.join('.') === normalizedPath.join('.')) + return errors.map((error) => error.message) + } + + return { + // methods + getError, + getErrors, + + // properties + success: result.success, + + /** + * A simple list of flattened errors. + * + * @example + * ```json + * [ + * "items.0.name: naam is verplicht", + * "items.0.summary: summary is verplicht", + * "items.0.link: link is verplicht" + * ] + * ``` + * + * @type {string[]} + */ + flatErrorMessages: result?.error ? getFlatErrorMessages(result.error.issues) : [], + + /** + * A grouped list of errors by path. + * + * @example + * ```json + * { + * "items.0.name": ["naam is verplicht"], + * "items.0.summary": ["summary is verplicht"], + * "items.0.link": ["link is verplicht"] + * } + * ``` + * + * @type {object} + */ + groupedErrorsByPath: result?.error ? getGroupedErrorsByPath(result.error.issues) : {}, + + /** + * A nested list of errors by path. + * + * @example + * ```json + * { + * items: { + * 0: { + * name: ["naam is verplicht"], + * summary: ["summary is verplicht"], + * link: ["link is verplicht"] + * } + * } + * } + * ``` + * + * @type {object} + */ + nestedFieldErrors: result?.error ? getNestedFieldErrors(result.error.issues) : {}, + + /** + * A list of errors with path, message, code and type. + * + * @example + * ```json + * [ + * { + * path: "items.0.name", + * message: "naam is verplicht", + * code: "too_small", + * type: "string" + * }, + * { + * path: "items.0.summary", + * message: "summary is verplicht", + * code: "too_small", + * type: "string" + * } + * ] + * ``` + * + * @type {object} + */ + fieldSpecificErrors: result?.error ? getFieldSpecificErrors(result.error.issues) : [], + + /** + * A summary of the errors. + * + * @example + * ```json + * { + * totalErrors: 3, + * errorsByField: { + * "items.0.name": 1, + * "items.0.summary": 1, + * "items.0.link": 1 + * }, + * errorsByType: { + * "too_small": 3 + * } + * } + * ``` + * + * @type {object} + */ + errorSummary: result?.error ? getErrorSummary(result.error.issues) : {}, + } +} + +// BASE FUNCTIONS +const joinPath = (path) => path.join('.') + +// ERROR MAPPERS + +// Function to convert the issues array into error messages +const getFlatErrorMessages = (issues) => { + return issues.map((issue) => `${joinPath(issue.path)}: ${issue.message}`) || [] +} + +// Function to get a grouped errors by path +const getGroupedErrorsByPath = (issues) => { + const groupedErrors = {} + + issues.forEach((issue) => { + const path = joinPath(issue.path) + groupedErrors[path] = groupedErrors[path] || [] + groupedErrors[path].push(issue.message) + }) + + return groupedErrors +} + +// Function to convert the issues array into field errors +const getNestedFieldErrors = (issues) => { + const fieldErrors = {} + + issues.forEach((issue) => { + let currentLevel = fieldErrors + const path = issue.path + + // Traverse the path to build the nested structure + for (let i = 0; i < path.length; i++) { + const key = path[i] + + if (i === path.length - 1) { + // If it's the last element in the path, set the error message + currentLevel[key] = currentLevel[key] || [] + currentLevel[key].push(issue.message) + } else { + // Otherwise, continue traversing the nested structure + currentLevel[key] = currentLevel[key] || {} + currentLevel = currentLevel[key] + } + } + }) + + return fieldErrors +} + +// Function to get a list of errors with path, message, code and type +const getFieldSpecificErrors = (issues) => { + return issues.map((issue) => ({ + path: joinPath(issue.path), + message: issue.message, + code: issue.code, + type: issue.type, + })) +} + +// Function to get a summary of the errors as a number +const getErrorSummary = (issues) => { + const errorsByField = {} + const errorsByType = {} + + issues.forEach((issue) => { + const path = joinPath(issue.path) + errorsByField[path] = (errorsByField[path] || 0) + 1 + errorsByType[issue.code] = (errorsByType[issue.code] || 0) + 1 + }) + + return { + totalErrors: issues.length, + errorsByField, + errorsByType, + } +} diff --git a/src/store/modules/menu.ts b/src/store/modules/menu.ts index 0dec6c39..bfbb3be3 100644 --- a/src/store/modules/menu.ts +++ b/src/store/modules/menu.ts @@ -14,6 +14,7 @@ interface Options { interface MenuStoreState { menuItem: Menu; menuList: Menu[]; + menuItemItemsIndex: number; } /** @@ -23,6 +24,7 @@ export const useMenuStore = defineStore('menu', { state: () => ({ menuItem: null, menuList: [], + menuItemItemsIndex: null, } as MenuStoreState), actions: { /** diff --git a/src/views/menus/MenuDetail.vue b/src/views/menus/MenuDetail.vue index 15d31f3b..74c4689d 100644 --- a/src/views/menus/MenuDetail.vue +++ b/src/views/menus/MenuDetail.vue @@ -1,14 +1,14 @@ - + diff --git a/src/views/menus/MenuList.vue b/src/views/menus/MenuList.vue index e6509169..2d89c664 100644 --- a/src/views/menus/MenuList.vue +++ b/src/views/menus/MenuList.vue @@ -59,6 +59,12 @@ import { navigationStore, menuStore } from '../../store/store.js' Bewerken + + + Menu item toevoegen +