feat: configurable Plan Views for reducing vertical scroll distance (#6415)

* refactor: convert Type API to ES6 module

- Another AMD module bites the dust 🧹

* feat: add initial configurable plan type

- Name change TBD

* feat: add `clipActivityNames` property

- refactor: initialize data to `null`

* refactor: general code cleanup

* feat(WIP): name clipping via clipPath elements

* feat: compose a Gantt Chart using a Plan

- Allows Plans to be dragged into Gantt Charts (name tentative) to create a configurable Activity View

- Clip/Unclip activity names by editing domainObject property

* feat: replace Plan if another is dragged in

- SImilar to Gauges or Scatter Plots, launch a confirmation dialog to replace the existing Plan with another, if another Plan is dragged into the chart.

* test: fix tests, add basic tests for gantt

* tes(e2e): fix plan test

* docs: add TODO

* refactor: clean up more string literals

* style: remove `rx`, increase min width

- round widths to nearest integer

* refactor: extract timeline creation logic

- extracts the logic for creating the timeline into its own component, `ActivityTimeline.vue`. This will save us a lot of re-renders, as we were manually creating elements / clearing them on each tick

* style: fix text y-pos and don't round

* fix: make activities clickable again

* docs: add copyright docs

* feat: swimlane visibility

- configure plan view from inspector

fix: update plans when file changes

- fix gantt chart display in time strips

- code cleanup

* fix: gantt chart embed in time strip

* remove viewBox for now

* fix: make `clipPath` ids more unique

* refactor: more code cleanup

* refactor: more code cleanup

* test: fix existing Plan unit tests

* refactor: rename variables

* fix: respond to code review comments

- Move config manipulation to PlanViewConfiguration.js/.vue

- Variable renames, code refactoring

* fix: unique, reproducible clipPathIds

* fix: only mutate swimlaneVisibility once on init

* fix: really make clipPathId unique this time

* refactor: use default config

* Closes #6113
- Refined CSS class naming and application.
- Set cursor to pointer for Activity elements.
- Added <title> node to Activity elements.
- Styling for selected Activities.
- Better Inspector tab name.

* fix: make Plan creatability configurable and false by default

* test: fix existing tests and add a couple new ones

* Closes #6113
- Now uses SVG <symbol> instead of rect within Activity element.
- Passes in `rowHeight` as a prop from Plan.vue.
- SWIMLANE_PADDING const added and used to create margin at top and bottom
edges of swimlanes.
- Refined styling for selected activities.
- New `$colorGanttSelectedBorder` theme constant.
- Smoke tested in Espresso and Snow themes.

* fix: default swimlaneWidth to clientWidth

* test: fix test

* feat: display selected activity name as header

* fix: remove redundant listener

* refactor: move `examplePlans.js` into `test-data/`

* docs: remove copyright header

* refactor: move `helper.js` into `helper/`

* refactor: `helper.js` -> `planningUtils.js`

* fix: update pathing

* test: add tests for gantt/plan

- add visual tests for gantt / plan

- add test for clicking a single activity and verifying its contents in the inspector

---------

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
This commit is contained in:
Jesse Mazzella
2023-03-16 10:34:31 -07:00
committed by GitHub
parent 0b3e0e7efd
commit ff3a20e446
40 changed files with 2847 additions and 857 deletions

View File

@ -46,7 +46,7 @@ export default class Editor extends EventEmitter {
}
/**
* @returns true if the application is in edit mode, false otherwise.
* @returns {boolean} true if the application is in edit mode, false otherwise.
*/
isEditing() {
return this.editing;

View File

@ -71,7 +71,7 @@ function (
StatusAPI: StatusAPI.default,
TelemetryAPI: TelemetryAPI,
TimeAPI: TimeAPI.default,
TypeRegistry: TypeRegistry,
TypeRegistry: TypeRegistry.default,
UserAPI: UserAPI.default,
AnnotationAPI: AnnotationAPI.default
};

View File

@ -62,6 +62,8 @@ import InMemorySearchProvider from './InMemorySearchProvider';
* @property {Identifier[]} [composition] if
* present, this will be used by the default composition provider
* to load domain objects
* @property {Object.<string, any>} [configuration] A key-value map containing configuration
* settings for this domain object.
* @memberof module:openmct.ObjectAPI~
*/

View File

@ -20,63 +20,25 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(function () {
/**
* A Type describes a kind of domain object that may appear or be
* created within Open MCT.
*
* @param {module:opemct.TypeRegistry~TypeDefinition} definition
* @class Type
* @memberof module:openmct
*/
function Type(definition) {
/**
* A Type describes a kind of domain object that may appear or be
* created within Open MCT.
*
* @param {module:opemct.TypeRegistry~TypeDefinition} definition
* @class Type
* @memberof module:openmct
*/
export default class Type {
constructor(definition) {
this.definition = definition;
if (definition.key) {
this.key = definition.key;
}
}
/**
* Check if a domain object is an instance of this type.
* @param domainObject
* @returns {boolean} true if the domain object is of this type
* @memberof module:openmct.Type#
* @method check
*/
Type.prototype.check = function (domainObject) {
// Depends on assignment from MCT.
return domainObject.type === this.key;
};
/**
* Get a definition for this type that can be registered using the
* legacy bundle format.
* @private
*/
Type.prototype.toLegacyDefinition = function () {
const def = {};
def.name = this.definition.name;
def.cssClass = this.definition.cssClass;
def.description = this.definition.description;
def.properties = this.definition.form;
if (this.definition.initialize) {
def.model = {};
this.definition.initialize(def.model);
}
if (this.definition.creatable) {
def.features = ['creation'];
}
return def;
};
/**
* Create a type definition from a legacy definition.
*/
Type.definitionFromLegacyDefinition = function (legacyDefinition) {
static definitionFromLegacyDefinition(legacyDefinition) {
let definition = {};
definition.name = legacyDefinition.name;
definition.cssClass = legacyDefinition.cssClass;
@ -121,7 +83,39 @@ define(function () {
}
return definition;
};
}
/**
* Check if a domain object is an instance of this type.
* @param domainObject
* @returns {boolean} true if the domain object is of this type
* @memberof module:openmct.Type#
* @method check
*/
check(domainObject) {
// Depends on assignment from MCT.
return domainObject.type === this.key;
}
/**
* Get a definition for this type that can be registered using the
* legacy bundle format.
* @private
*/
toLegacyDefinition() {
const def = {};
def.name = this.definition.name;
def.cssClass = this.definition.cssClass;
def.description = this.definition.description;
def.properties = this.definition.form;
return Type;
});
if (this.definition.initialize) {
def.model = {};
this.definition.initialize(def.model);
}
if (this.definition.creatable) {
def.features = ['creation'];
}
return def;
}
}

View File

@ -19,35 +19,36 @@
* this source code distribution or the Licensing information page available
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(['./Type'], function (Type) {
const UNKNOWN_TYPE = new Type({
key: "unknown",
name: "Unknown Type",
cssClass: "icon-object-unknown"
});
import Type from './Type';
/**
* @typedef TypeDefinition
* @memberof module:openmct.TypeRegistry~
* @property {string} label the name for this type of object
* @property {string} description a longer-form description of this type
* @property {function (object)} [initialize] a function which initializes
* the model for new domain objects of this type
* @property {boolean} [creatable] true if users should be allowed to
* create this type (default: false)
* @property {string} [cssClass] the CSS class to apply for icons
*/
const UNKNOWN_TYPE = new Type({
key: "unknown",
name: "Unknown Type",
cssClass: "icon-object-unknown"
});
/**
* A TypeRegistry maintains the definitions for different types
* that domain objects may have.
* @interface TypeRegistry
* @memberof module:openmct
*/
function TypeRegistry() {
/**
* @typedef TypeDefinition
* @memberof module:openmct.TypeRegistry~
* @property {string} label the name for this type of object
* @property {string} description a longer-form description of this type
* @property {function (object)} [initialize] a function which initializes
* the model for new domain objects of this type
* @property {boolean} [creatable] true if users should be allowed to
* create this type (default: false)
* @property {string} [cssClass] the CSS class to apply for icons
*/
/**
* A TypeRegistry maintains the definitions for different types
* that domain objects may have.
* @interface TypeRegistry
* @memberof module:openmct
*/
export default class TypeRegistry {
constructor() {
this.types = {};
}
/**
* Register a new object type.
*
@ -56,17 +57,16 @@ define(['./Type'], function (Type) {
* @method addType
* @memberof module:openmct.TypeRegistry#
*/
TypeRegistry.prototype.addType = function (typeKey, typeDef) {
addType(typeKey, typeDef) {
this.standardizeType(typeDef);
this.types[typeKey] = new Type(typeDef);
};
}
/**
* Takes a typeDef, standardizes it, and logs warnings about unsupported
* usage.
* @private
*/
TypeRegistry.prototype.standardizeType = function (typeDef) {
standardizeType(typeDef) {
if (Object.prototype.hasOwnProperty.call(typeDef, 'label')) {
if (!typeDef.name) {
typeDef.name = typeDef.label;
@ -74,18 +74,16 @@ define(['./Type'], function (Type) {
delete typeDef.label;
}
};
}
/**
* List keys for all registered types.
* @method listKeys
* @memberof module:openmct.TypeRegistry#
* @returns {string[]} all registered type keys
*/
TypeRegistry.prototype.listKeys = function () {
listKeys() {
return Object.keys(this.types);
};
}
/**
* Retrieve a registered type by its key.
* @method get
@ -93,18 +91,15 @@ define(['./Type'], function (Type) {
* @memberof module:openmct.TypeRegistry#
* @returns {module:openmct.Type} the registered type
*/
TypeRegistry.prototype.get = function (typeKey) {
get(typeKey) {
return this.types[typeKey] || UNKNOWN_TYPE;
};
TypeRegistry.prototype.importLegacyTypes = function (types) {
}
importLegacyTypes(types) {
types.filter((t) => this.get(t.key) === UNKNOWN_TYPE)
.forEach((type) => {
let def = Type.definitionFromLegacyDefinition(type);
this.addType(type.key, def);
});
};
return TypeRegistry;
});
}
}

View File

@ -20,36 +20,36 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/
define(['./TypeRegistry', './Type'], function (TypeRegistry, Type) {
describe('The Type API', function () {
let typeRegistryInstance;
import TypeRegistry from './TypeRegistry';
beforeEach(function () {
typeRegistryInstance = new TypeRegistry ();
typeRegistryInstance.addType('testType', {
name: 'Test Type',
description: 'This is a test type.',
creatable: true
});
});
describe('The Type API', function () {
let typeRegistryInstance;
it('types can be standardized', function () {
typeRegistryInstance.addType('standardizationTestType', {
label: 'Test Type',
description: 'This is a test type.',
creatable: true
});
typeRegistryInstance.standardizeType(typeRegistryInstance.types.standardizationTestType);
expect(typeRegistryInstance.get('standardizationTestType').definition.label).toBeUndefined();
expect(typeRegistryInstance.get('standardizationTestType').definition.name).toBe('Test Type');
});
it('new types are registered successfully and can be retrieved', function () {
expect(typeRegistryInstance.get('testType').definition.name).toBe('Test Type');
});
it('type registry contains new keys', function () {
expect(typeRegistryInstance.listKeys ()).toContain('testType');
beforeEach(function () {
typeRegistryInstance = new TypeRegistry ();
typeRegistryInstance.addType('testType', {
name: 'Test Type',
description: 'This is a test type.',
creatable: true
});
});
it('types can be standardized', function () {
typeRegistryInstance.addType('standardizationTestType', {
label: 'Test Type',
description: 'This is a test type.',
creatable: true
});
typeRegistryInstance.standardizeType(typeRegistryInstance.types.standardizationTestType);
expect(typeRegistryInstance.get('standardizationTestType').definition.label).toBeUndefined();
expect(typeRegistryInstance.get('standardizationTestType').definition.name).toBe('Test Type');
});
it('new types are registered successfully and can be retrieved', function () {
expect(typeRegistryInstance.get('testType').definition.name).toBe('Test Type');
});
it('type registry contains new keys', function () {
expect(typeRegistryInstance.listKeys ()).toContain('testType');
});
});