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:
@@ -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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
54
e2e/test-data/examplePlans/ExamplePlanWithOrderedLanes.json
Normal file
54
e2e/test-data/examplePlans/ExamplePlanWithOrderedLanes.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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])));
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user