Skip to content

feat/AB#72584_reflect-dashboard-changes-on-templates #963

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: 2.x.x
Choose a base branch
from
Open
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
64 changes: 64 additions & 0 deletions migrations/1706669110852-update-widget-ids.ts
Original file line number Diff line number Diff line change
@@ -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!
*/
};
5 changes: 5 additions & 0 deletions src/models/dashboard.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,6 +67,7 @@ const dashboardSchema = new Schema<Dashboard>(
{
name: String,
structure: mongoose.Schema.Types.Mixed,
deletedWidgets: [String],
showFilter: Boolean,
buttons: [buttonSchema],
archived: {
Expand Down
106 changes: 104 additions & 2 deletions src/schema/mutation/editDashboard.mutation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
Expand All @@ -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,
Expand Down