From 473a65a562630449bc8f4cbbfc87a58edb02a530 Mon Sep 17 00:00:00 2001 From: Thijn Date: Tue, 28 Jan 2025 17:59:07 +0100 Subject: [PATCH 1/4] WIP - did most of the requirements for the issue I got an EventBus and its so cool! --- src/entities/menu/menu.ts | 15 +- src/eventBus.js | 2 + src/modals/Modals.vue | 9 + src/modals/menu/EditMenuModal.vue | 27 +- src/modals/menuItem/DeleteMenuItemModal.vue | 116 ++++++++ src/modals/menuItem/EditMenuItemModal.vue | 278 ++++++++++++++++++++ src/services/formatZodErrors.js | 232 ++++++++++++++++ src/store/modules/menu.ts | 2 + src/views/menus/MenuDetail.vue | 219 +++++++++++---- src/views/menus/MenuIndex.vue | 2 +- src/views/menus/MenuList.vue | 6 + 11 files changed, 842 insertions(+), 66 deletions(-) create mode 100644 src/eventBus.js create mode 100644 src/modals/menuItem/DeleteMenuItemModal.vue create mode 100644 src/modals/menuItem/EditMenuItemModal.vue create mode 100644 src/services/formatZodErrors.js diff --git a/src/entities/menu/menu.ts b/src/entities/menu/menu.ts index 2c8fb415..ca2ad151 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().min(1, 'link is verplicht'), + 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().min(1, 'link is verplicht'), + 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..e27bc8f3 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..9561cfb2 100644 --- a/src/modals/menu/EditMenuModal.vue +++ b/src/modals/menu/EditMenuModal.vue @@ -43,13 +43,10 @@ import { getTheme } from '../../services/getTheme.js' :value.sync="menuItem.name" :error="!!inputValidation.fieldErrors?.['name']" :helper-text="inputValidation.fieldErrors?.['name']?.[0]" /> - +
option.position === menuStore.menuItem.position) } }, /** @@ -182,6 +190,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..3cec912b 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 +