diff --git a/migrations/1706669110852-update-widget-ids.ts b/migrations/1706669110852-update-widget-ids.ts new file mode 100644 index 000000000..f4ebd6e1a --- /dev/null +++ b/migrations/1706669110852-update-widget-ids.ts @@ -0,0 +1,64 @@ +import { v4 as uuidv4 } from 'uuid'; +import { Dashboard, Page } from '@models'; +import { startDatabaseForMigration } from '../src/utils/migrations/database.helper'; +import { isEqual } from 'lodash'; + +/** Updates the ids of each dashboard widget */ +export const up = async () => { + await startDatabaseForMigration(); + + const pages = await Page.find({ + type: 'dashboard', + }) + .populate({ + path: 'content', + model: 'Dashboard', + }) + .populate({ + path: 'contentWithContext.content', + model: 'Dashboard', + }); + + const dashboardsToSave: Dashboard[] = []; + pages.forEach((page) => { + // Updates the ids on the content + const mainDashboard = page.content as Dashboard; + mainDashboard.structure.forEach((widget) => { + widget.id = `widget-${uuidv4()}`; + }); + mainDashboard.markModified('structure'); + dashboardsToSave.push(mainDashboard); + + // For each of the templates, try to match the widgets + // in the main dashboard by the structure settings + page.contentWithContext.forEach((cc) => { + const templateDashboard = cc.content as Dashboard; + templateDashboard.structure.forEach((widget) => { + const mainDashboardWidget = mainDashboard.structure.find((w) => + isEqual(w.settings, widget.settings) + ); + if (mainDashboardWidget) { + widget.id = mainDashboardWidget.id; + } else { + widget.id = `widget-${uuidv4()}`; + } + }); + templateDashboard.markModified('structure'); + dashboardsToSave.push(templateDashboard); + }); + }); + + // Save all dashboards + await Dashboard.bulkSave(dashboardsToSave); +}; + +/** + * Sample function of down migration + * + * @returns just migrate data. + */ +export const down = async () => { + /* + Code you downgrade script here! + */ +}; diff --git a/src/models/dashboard.model.ts b/src/models/dashboard.model.ts index c1baace21..48e4ec1d0 100644 --- a/src/models/dashboard.model.ts +++ b/src/models/dashboard.model.ts @@ -26,6 +26,10 @@ export interface Dashboard extends Document { createdAt?: Date; modifiedAt?: Date; structure?: any; + // Contains the list of widget ids that have been deleted from the dashboard + // We store this to prevent updates on main dashboards to recreate widgets + // that have been deleted from the templates + deletedWidgets?: string[]; showFilter?: boolean; buttons?: Button[]; archived: boolean; @@ -63,6 +67,7 @@ const dashboardSchema = new Schema( { name: String, structure: mongoose.Schema.Types.Mixed, + deletedWidgets: [String], showFilter: Boolean, buttons: [buttonSchema], archived: { diff --git a/src/schema/mutation/editDashboard.mutation.ts b/src/schema/mutation/editDashboard.mutation.ts index 3804396bf..82fef0736 100644 --- a/src/schema/mutation/editDashboard.mutation.ts +++ b/src/schema/mutation/editDashboard.mutation.ts @@ -9,7 +9,7 @@ import GraphQLJSON from 'graphql-type-json'; import { DashboardType } from '../types'; import { Dashboard, Page, Step } from '@models'; import extendAbilityForContent from '@security/extendAbilityForContent'; -import { isEmpty } from 'lodash'; +import { isEmpty, isEqual } from 'lodash'; import { logger } from '@services/logger.service'; import ButtonActionInputType from '@schema/inputs/button-action.input'; import { graphQLAuthCheck } from '@schema/shared'; @@ -76,6 +76,107 @@ export default { context.i18next.t('common.errors.permissionNotGranted') ); } + // Has the ids of the widgets that have been deleted from the dashboard + const removedWidgets: string[] = (dashboard.structure || []) + .filter( + (w: any) => + !(args.structure || []).find((widget) => widget.id === w.id) + ) + .map((w) => w.id); + + // Gets the page that contains the dashboard + const page = await Page.findOne({ + $or: [ + { + contentWithContext: { + $elemMatch: { + content: dashboard.id, + }, + }, + }, + { content: dashboard.id }, + ], + }); + + // If editing a template, the content would be in the contentWithContext array + // otherwise, it would be in the content field + const isEditingTemplate = page.content.toString() !== args.id.toString(); + + // If editing a template, we mark the widgets that changed as modified + // to prevent them from being updated when the main dashboard is updated + if (isEditingTemplate) { + args.structure.forEach((widget: any) => { + // Get the old widget by id + const oldWidget = dashboard.structure.find( + (w: any) => w.id === widget.id + ); + + if (!widget.modified) { + widget.modified = !isEqual(oldWidget?.settings, widget.settings); + } + }); + } else { + // If editing the main dashboard, we update all the templates that inherit from it + const templateDashboards = await Dashboard.find({ + _id: { + $in: page.contentWithContext.map((cc) => cc.content), + }, + }); + + templateDashboards.forEach((template) => { + if ( + !Array.isArray(template.structure) || + !Array.isArray(args.structure) + ) { + return; + } + args.structure.forEach((widget) => { + const widgetIdx = template.structure.findIndex( + (w) => w.id === widget.id + ); + + const templateWidget = + widgetIdx !== -1 ? template.structure[widgetIdx] : null; + + // If not found, it means the widget was just added to the main dashboard + // We should also add it to the template + if ( + !templateWidget && + !template.deletedWidgets.includes(widget.id) + ) { + template.structure.push(widget); + template.markModified('structure'); + return; + } else if (!templateWidget) { + return; + } + + // Only update widgets that haven't been modified from the template + if (!templateWidget.modified) { + template.structure[widgetIdx] = widget; + template.markModified('structure'); + } + }); + + // Remove widgets that were removed from the main dashboard, if not modified + removedWidgets.forEach((id) => { + const widgetIdx = template.structure.findIndex((w) => w.id === id); + if (widgetIdx !== -1 && !template.structure[widgetIdx].modified) { + template.structure.splice(widgetIdx, 1); + template.markModified('structure'); + } + }); + }); + + // Save the templates + await Dashboard.bulkSave(templateDashboards); + } + + // update the deletedWidgets array with the id of the widgets that have been just removed + const updatedDeletedWidgets = [ + ...new Set([...dashboard.deletedWidgets, ...removedWidgets]), + ]; + // do the update on dashboard const updateDashboard: { //modifiedAt?: Date; @@ -93,7 +194,8 @@ export default { filter: { ...dashboard.toObject().filter, ...args.filter }, }, args.buttons && { buttons: args.buttons }, - args.gridOptions && { gridOptions: args.gridOptions } + args.gridOptions && { gridOptions: args.gridOptions }, + isEditingTemplate && { deletedWidgets: updatedDeletedWidgets } ); dashboard = await Dashboard.findByIdAndUpdate(args.id, updateDashboard, { new: true,