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
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
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,
* 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');
}, 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 { createPlanFromJSON } = require('../../../appActions');
const { addPlanGetInterceptor } = require('../../../helper/planningUtils.js');
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', () => {
let plan;
@ -36,4 +41,14 @@ test.describe('Plan', () => {
test('Displays all plan events', async ({ page }) => {
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 PlanViewConfiguration from '../PlanViewConfiguration';
import { getContrastingColor, getValidatedData } from '../util';
import { getContrastingColor, getValidatedData, getValidatedGroups } from '../util';
import ActivityTimeline from './ActivityTimeline.vue';
const PADDING = 1;
@ -416,7 +416,7 @@ export default {
return currentRow || SWIMLANE_PADDING;
},
generateActivities() {
const groupNames = Object.keys(this.planData);
const groupNames = getValidatedGroups(this.domainObject, this.planData);
if (!groupNames.length) {
return;
@ -430,6 +430,10 @@ export default {
let currentRow = 0;
const rawActivities = this.planData[groupName];
if (rawActivities === undefined) {
return;
}
rawActivities.forEach((rawActivity) => {
if (!this.isActivityInBounds(rawActivity)) {
return;

View File

@ -22,17 +22,7 @@
export function getValidatedData(domainObject) {
const sourceMap = domainObject.sourceMap;
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;
}
const json = getObjectJson(domainObject);
if (
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) {
function cutHex(h, start, end) {
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 TimelineAxis from '../../ui/components/TimeSystemAxis.vue';
import { getValidatedData } from '../plan/util';
import { getValidatedData, getValidatedGroups } from '../plan/util';
import TimelineObjectView from './TimelineObjectView.vue';
const unknownObjectType = {
@ -108,7 +108,8 @@ export default {
let objectPath = [domainObject].concat(this.objectPath.slice());
let rowCount = 0;
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') {
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 ListView from '../../ui/components/List/ListView.vue';
import { getPreciseDuration } from '../../utils/duration';
import { getValidatedData } from '../plan/util';
import { getValidatedData, getValidatedGroups } from '../plan/util';
import { SORT_ORDER_OPTIONS } from './constants';
const SCROLL_TIMEOUT = 10000;
@ -283,10 +283,13 @@ export default {
this.planData = getValidatedData(domainObject);
},
listActivities() {
let groups = Object.keys(this.planData);
let groups = getValidatedGroups(this.domainObject, this.planData);
let activities = [];
groups.forEach((key) => {
if (this.planData[key] === undefined) {
return;
}
// Create new objects so Vue 3 can detect any changes
activities = activities.concat(JSON.parse(JSON.stringify(this.planData[key])));
});