Allow specification of swimlanes via configuration (#7200)

* Use specified group order for plans

* Allow groupIds to be a function

* Fix typo in if statement

* Check that activities are present for a given group

* Change refresh to emit the new model

* Update domainobject on change

* Revert changes for domainObject

* Revert groupIds as functions. Check if groups are objects with names instead.

* Add e2e test for plan swim lane order

* Address review comments - improve if statement

* Move function to right util helper

* Fix path for imported code

* Remove focused test

* Change the name of the ordered group configuration
This commit is contained in:
Shefali Joshi
2023-12-14 06:19:42 -08:00
committed by GitHub
parent 3520a929a9
commit 250db8d7f9
7 changed files with 170 additions and 18 deletions

View File

@@ -81,6 +81,30 @@ function activitiesWithinTimeBounds(start1, end1, start2, end2) {
); );
} }
/**
* Asserts that the swim lanes / groups in the plan view matches the order of
* groups in the plan data.
* @param {import('@playwright/test').Page} page the page
* @param {object} plan The raw plan json to assert against
* @param {string} objectUrl The URL of the object to assert against (plan or gantt chart)
*/
export async function assertPlanOrderedSwimLanes(page, plan, objectUrl) {
// Switch to the plan view
await page.goto(`${objectUrl}?view=plan.view`);
const planGroups = await page
.locator('.c-plan__contents > div > .c-swimlane__lane-label .c-object-label__name')
.all();
const groups = plan.Groups;
for (let i = 0; i < groups.length; i++) {
// Assert that the order of groups in the plan view matches the order of
// groups in the plan data
const groupName = await planGroups[i].innerText();
expect(groupName).toEqual(groups[i].name);
}
}
/** /**
* Navigate to the plan view, switch to fixed time mode, * Navigate to the plan view, switch to fixed time mode,
* and set the bounds to span all activities. * and set the bounds to span all activities.
@@ -110,3 +134,23 @@ export async function setDraftStatusForPlan(page, plan) {
await window.openmct.status.set(planObject.uuid, 'draft'); await window.openmct.status.set(planObject.uuid, 'draft');
}, plan); }, plan);
} }
export async function addPlanGetInterceptor(page) {
await page.waitForLoadState('load');
await page.evaluate(async () => {
await window.openmct.objects.addGetInterceptor({
appliesTo: (identifier, domainObject) => {
return domainObject && domainObject.type === 'plan';
},
invoke: (identifier, object) => {
if (object) {
object.sourceMap = {
orderedGroups: 'Groups'
};
}
return object;
}
});
});
}

View File

@@ -0,0 +1,54 @@
{
"Groups": [
{
"name": "Group 1"
},
{
"name": "Group 2"
}
],
"Group 2": [
{
"name": "Past event 3",
"start": 1660493208000,
"end": 1660503981000,
"type": "Group 2",
"color": "orange",
"textColor": "white"
},
{
"name": "Past event 4",
"start": 1660579608000,
"end": 1660624108000,
"type": "Group 2",
"color": "orange",
"textColor": "white"
},
{
"name": "Past event 5",
"start": 1660666008000,
"end": 1660681529000,
"type": "Group 2",
"color": "orange",
"textColor": "white"
}
],
"Group 1": [
{
"name": "Past event 1",
"start": 1660320408000,
"end": 1660343797000,
"type": "Group 1",
"color": "orange",
"textColor": "white"
},
{
"name": "Past event 2",
"start": 1660406808000,
"end": 1660429160000,
"type": "Group 1",
"color": "orange",
"textColor": "white"
}
]
}

View File

@@ -21,8 +21,13 @@
*****************************************************************************/ *****************************************************************************/
const { test } = require('../../../pluginFixtures'); const { test } = require('../../../pluginFixtures');
const { createPlanFromJSON } = require('../../../appActions'); const { createPlanFromJSON } = require('../../../appActions');
const { addPlanGetInterceptor } = require('../../../helper/planningUtils.js');
const testPlan1 = require('../../../test-data/examplePlans/ExamplePlan_Small1.json'); const testPlan1 = require('../../../test-data/examplePlans/ExamplePlan_Small1.json');
const { assertPlanActivities } = require('../../../helper/planningUtils'); const testPlanWithOrderedLanes = require('../../../test-data/examplePlans/ExamplePlanWithOrderedLanes.json');
const {
assertPlanActivities,
assertPlanOrderedSwimLanes
} = require('../../../helper/planningUtils');
test.describe('Plan', () => { test.describe('Plan', () => {
let plan; let plan;
@@ -36,4 +41,14 @@ test.describe('Plan', () => {
test('Displays all plan events', async ({ page }) => { test('Displays all plan events', async ({ page }) => {
await assertPlanActivities(page, testPlan1, plan.url); await assertPlanActivities(page, testPlan1, plan.url);
}); });
test('Displays plans with ordered swim lanes configuration', async ({ page }) => {
// Add configuration for swim lanes
await addPlanGetInterceptor(page);
// Create the plan
const planWithSwimLanes = await createPlanFromJSON(page, {
json: testPlanWithOrderedLanes
});
await assertPlanOrderedSwimLanes(page, testPlanWithOrderedLanes, planWithSwimLanes.url);
});
}); });

View File

@@ -59,7 +59,7 @@ import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
import TimelineAxis from '../../../ui/components/TimeSystemAxis.vue'; import TimelineAxis from '../../../ui/components/TimeSystemAxis.vue';
import PlanViewConfiguration from '../PlanViewConfiguration'; import PlanViewConfiguration from '../PlanViewConfiguration';
import { getContrastingColor, getValidatedData } from '../util'; import { getContrastingColor, getValidatedData, getValidatedGroups } from '../util';
import ActivityTimeline from './ActivityTimeline.vue'; import ActivityTimeline from './ActivityTimeline.vue';
const PADDING = 1; const PADDING = 1;
@@ -416,7 +416,7 @@ export default {
return currentRow || SWIMLANE_PADDING; return currentRow || SWIMLANE_PADDING;
}, },
generateActivities() { generateActivities() {
const groupNames = Object.keys(this.planData); const groupNames = getValidatedGroups(this.domainObject, this.planData);
if (!groupNames.length) { if (!groupNames.length) {
return; return;
@@ -430,6 +430,10 @@ export default {
let currentRow = 0; let currentRow = 0;
const rawActivities = this.planData[groupName]; const rawActivities = this.planData[groupName];
if (rawActivities === undefined) {
return;
}
rawActivities.forEach((rawActivity) => { rawActivities.forEach((rawActivity) => {
if (!this.isActivityInBounds(rawActivity)) { if (!this.isActivityInBounds(rawActivity)) {
return; return;

View File

@@ -22,17 +22,7 @@
export function getValidatedData(domainObject) { export function getValidatedData(domainObject) {
const sourceMap = domainObject.sourceMap; const sourceMap = domainObject.sourceMap;
const body = domainObject.selectFile?.body; const json = getObjectJson(domainObject);
let json = {};
if (typeof body === 'string') {
try {
json = JSON.parse(body);
} catch (e) {
return json;
}
} else if (body !== undefined) {
json = body;
}
if ( if (
sourceMap !== undefined && sourceMap !== undefined &&
@@ -69,6 +59,47 @@ export function getValidatedData(domainObject) {
} }
} }
function getObjectJson(domainObject) {
const body = domainObject.selectFile?.body;
let json = {};
if (typeof body === 'string') {
try {
json = JSON.parse(body);
} catch (e) {
return json;
}
} else if (body !== undefined) {
json = body;
}
return json;
}
export function getValidatedGroups(domainObject, planData) {
let orderedGroupNames;
const sourceMap = domainObject.sourceMap;
const json = getObjectJson(domainObject);
if (sourceMap?.orderedGroups) {
const groups = json[sourceMap.orderedGroups];
if (groups.length && typeof groups[0] === 'object') {
//if groups is a list of objects, then get the name property from each group object.
const groupsWithNames = groups.filter(
(groupObj) => groupObj.name !== undefined && groupObj.name !== ''
);
orderedGroupNames = groupsWithNames.map((groupObj) => groupObj.name);
} else {
// Otherwise, groups is likely a list of names, so use that.
orderedGroupNames = groups;
}
}
if (orderedGroupNames === undefined) {
orderedGroupNames = Object.keys(planData);
}
return orderedGroupNames;
}
export function getContrastingColor(hexColor) { export function getContrastingColor(hexColor) {
function cutHex(h, start, end) { function cutHex(h, start, end) {
const hStr = h.charAt(0) === '#' ? h.substring(1, 7) : h; const hStr = h.charAt(0) === '#' ? h.substring(1, 7) : h;

View File

@@ -53,7 +53,7 @@ import _ from 'lodash';
import SwimLane from '@/ui/components/swim-lane/SwimLane.vue'; import SwimLane from '@/ui/components/swim-lane/SwimLane.vue';
import TimelineAxis from '../../ui/components/TimeSystemAxis.vue'; import TimelineAxis from '../../ui/components/TimeSystemAxis.vue';
import { getValidatedData } from '../plan/util'; import { getValidatedData, getValidatedGroups } from '../plan/util';
import TimelineObjectView from './TimelineObjectView.vue'; import TimelineObjectView from './TimelineObjectView.vue';
const unknownObjectType = { const unknownObjectType = {
@@ -108,7 +108,8 @@ export default {
let objectPath = [domainObject].concat(this.objectPath.slice()); let objectPath = [domainObject].concat(this.objectPath.slice());
let rowCount = 0; let rowCount = 0;
if (domainObject.type === 'plan') { if (domainObject.type === 'plan') {
rowCount = Object.keys(getValidatedData(domainObject)).length; const planData = getValidatedData(domainObject);
rowCount = getValidatedGroups(domainObject, planData).length;
} else if (domainObject.type === 'gantt-chart') { } else if (domainObject.type === 'gantt-chart') {
rowCount = Object.keys(domainObject.configuration.swimlaneVisibility).length; rowCount = Object.keys(domainObject.configuration.swimlaneVisibility).length;
} }

View File

@@ -38,7 +38,7 @@ import { v4 as uuid } from 'uuid';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants'; import { TIME_CONTEXT_EVENTS } from '../../api/time/constants';
import ListView from '../../ui/components/List/ListView.vue'; import ListView from '../../ui/components/List/ListView.vue';
import { getPreciseDuration } from '../../utils/duration'; import { getPreciseDuration } from '../../utils/duration';
import { getValidatedData } from '../plan/util'; import { getValidatedData, getValidatedGroups } from '../plan/util';
import { SORT_ORDER_OPTIONS } from './constants'; import { SORT_ORDER_OPTIONS } from './constants';
const SCROLL_TIMEOUT = 10000; const SCROLL_TIMEOUT = 10000;
@@ -283,10 +283,13 @@ export default {
this.planData = getValidatedData(domainObject); this.planData = getValidatedData(domainObject);
}, },
listActivities() { listActivities() {
let groups = Object.keys(this.planData); let groups = getValidatedGroups(this.domainObject, this.planData);
let activities = []; let activities = [];
groups.forEach((key) => { groups.forEach((key) => {
if (this.planData[key] === undefined) {
return;
}
// Create new objects so Vue 3 can detect any changes // Create new objects so Vue 3 can detect any changes
activities = activities.concat(JSON.parse(JSON.stringify(this.planData[key]))); activities = activities.concat(JSON.parse(JSON.stringify(this.planData[key])));
}); });