Skip to content

Commit

Permalink
Add filtering by metadata (#7388)
Browse files Browse the repository at this point in the history
* Add filtering by metadata. Add new sourceMap property to get a list of properties for metadata filtering.

* Change filter label names

* Add aria-labels

* Closes #7389
- Added a "No filters applied" message for both input areas.
- Added additional detail about how it works in the hint text visible while editing.

* Restore valid state if there is an error

* Fix linting error

* Tests for filtering by metadata

---------

Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
  • Loading branch information
shefalijoshi and charlesh88 authored Jan 28, 2024
1 parent 1fc6056 commit 60e1eeb
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 37 deletions.
23 changes: 22 additions & 1 deletion src/plugins/plan/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
* at runtime from the About dialog for additional information.
*****************************************************************************/

import _ from 'lodash';
export function getValidatedData(domainObject) {
const sourceMap = domainObject.sourceMap;
const json = getObjectJson(domainObject);
Expand All @@ -45,6 +46,16 @@ export function getValidatedData(domainObject) {
groupActivity.end = activity[sourceMap.end];
}

if (Array.isArray(sourceMap.filterMetadata)) {
groupActivity.filterMetadataValues = [];
sourceMap.filterMetadata.forEach((property) => {
const value = _.get(activity, property);
groupActivity.filterMetadataValues.push({
value
});
});
}

if (!mappedJson[groupIdKey]) {
mappedJson[groupIdKey] = [];
}
Expand Down Expand Up @@ -92,14 +103,24 @@ export function getValidatedGroups(domainObject, planData) {
orderedGroupNames = groups;
}
}

if (orderedGroupNames === undefined) {
orderedGroupNames = Object.keys(planData);
}

return orderedGroupNames;
}

export function getFilteredValues(activity) {
let values = [];
if (Array.isArray(activity.filterMetadataValues)) {
values = activity.filterMetadataValues;
} else if (activity?.properties) {
values = Object.values(activity.properties);
}

return values;
}

export function getContrastingColor(hexColor) {
function cutHex(h, start, end) {
const hStr = h.charAt(0) === '#' ? h.substring(1, 7) : h;
Expand Down
39 changes: 32 additions & 7 deletions src/plugins/timelist/TimelistComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { v4 as uuid } from 'uuid';
import { TIME_CONTEXT_EVENTS } from '../../api/time/constants.js';
import ListView from '../../ui/components/List/ListView.vue';
import { getPreciseDuration } from '../../utils/duration.js';
import { getValidatedData, getValidatedGroups } from '../plan/util.js';
import { getFilteredValues, getValidatedData, getValidatedGroups } from '../plan/util.js';
import { SORT_ORDER_OPTIONS } from './constants.js';
const SCROLL_TIMEOUT = 10000;
Expand Down Expand Up @@ -208,22 +208,22 @@ export default {
this.setViewFromConfig(mutatedObject.configuration);
},
setViewFromConfig(configuration) {
this.filterValue = configuration.filter || '';
this.filterMetadataValue = configuration.filterMetadata || '';
if (this.isEditing) {
this.filterValue = configuration.filter;
this.hideAll = false;
this.listActivities();
} else {
this.filterValue = configuration.filter;
this.setSort();
this.listActivities();
}
this.listActivities();
},
updateTimestamp(timestamp) {
//The clock never stops ticking
this.updateTimeStampAndListActivities(timestamp);
},
setFixedTime() {
this.filterValue = this.domainObject.configuration.filter;
this.filterValue = this.domainObject.configuration.filter || '';
this.filterMetadataValue = this.domainObject.configuration.filterMetadata || '';
this.isFixedTime = !this.timeContext.isRealTime();
if (this.isFixedTime) {
this.hideAll = false;
Expand Down Expand Up @@ -326,7 +326,21 @@ export default {
return true;
}
const hasFilterMatch = this.filterByName(activity.name);
let hasNameMatch = false;
let hasMetadataMatch = false;
if (this.filterValue || this.filterMetadataValue) {
if (this.filterValue) {
hasNameMatch = this.filterByName(activity.name);
}
if (this.filterMetadataValue) {
hasMetadataMatch = this.filterByMetadata(activity);
}
} else {
hasNameMatch = true;
hasMetadataMatch = true;
}
const hasFilterMatch = hasNameMatch || hasMetadataMatch;
if (hasFilterMatch === false || this.hideAll === true) {
return false;
}
Expand Down Expand Up @@ -354,6 +368,17 @@ export default {
return regex.test(name.toLowerCase());
});
},
filterByMetadata(activity) {
const filters = this.filterMetadataValue.split(',');
return filters.some((search) => {
const normalized = search.trim().toLowerCase();
const regex = new RegExp(normalized);
const activityValues = getFilteredValues(activity);
return regex.test(activityValues.join().toLowerCase());
});
},
// Add activity classes, increase activity counts by type,
// set indices of the first occurrences of current and future activities - used for scrolling
styleActivity(activity, index) {
Expand Down
91 changes: 74 additions & 17 deletions src/plugins/timelist/inspector/FilteringComponent.vue
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,57 @@
<template>
<li class="c-inspect-properties__row">
<div v-if="canEdit" class="c-inspect-properties__hint span-all">
Filter this view by comma-separated keywords.
Filter this view by comma-separated keywords. Filtering uses an 'OR' method.
</div>
<div class="c-inspect-properties__label" title="Filter by keyword.">Filters</div>
<div v-if="canEdit" class="c-inspect-properties__value" :class="{ 'form-error': hasError }">
<div class="c-inspect-properties__label" aria-label="Activity Names" title="Filter by keyword.">
Activity Names
</div>
<div
v-if="canEdit"
class="c-inspect-properties__value"
:class="{ 'form-error': hasFilterError }"
>
<textarea
v-model="filterValue"
class="c-input--flex"
type="text"
@keydown.enter.exact.stop="forceBlur($event)"
@keyup="updateForm($event, 'filter')"
@keyup="updateNameFilter($event, 'filter')"
></textarea>
</div>
<div v-else class="c-inspect-properties__value">
{{ filterValue }}
<template v-if="filterValue.length > 0">
{{ filterValue }}
</template>
<template v-else> No filters applied </template>
</div>
</li>
<li class="c-inspect-properties__row">
<div
class="c-inspect-properties__label"
aria-label="Meta-data Properties"
title="Filter by keyword."
>
Meta-data Properties
</div>
<div
v-if="canEdit"
class="c-inspect-properties__value"
:class="{ 'form-error': hasMetadataFilterError }"
>
<textarea
v-model="filterMetadataValue"
class="c-input--flex"
type="text"
@keydown.enter.exact.stop="forceBlur($event)"
@keyup="updateMetadataFilter($event, 'filterMetadata')"
></textarea>
</div>
<div v-else class="c-inspect-properties__value">
<template v-if="filterMetadataValue.length > 0">
{{ filterMetadataValue }}
</template>
<template v-else> No filters applied </template>
</div>
</li>
</template>
Expand All @@ -48,7 +85,9 @@ export default {
return {
isEditing: this.openmct.editor.isEditing(),
filterValue: this.domainObject.configuration.filter,
hasError: false
filterMetadataValue: this.domainObject.configuration.filterMetadata,
hasFilterError: false,
hasMetadataFilterError: false
};
},
computed: {
Expand All @@ -65,37 +104,55 @@ export default {
methods: {
setEditState(isEditing) {
this.isEditing = isEditing;
if (!this.isEditing && this.hasError) {
this.filterValue = this.domainObject.configuration.filter;
this.hasError = false;
if (!this.isEditing) {
if (this.hasFilterError) {
this.filterValue = this.domainObject.configuration.filter;
}
if (this.hasMetadataFilterError) {
this.filterMetadataValue = this.domainObject.configuration.filterMetadata;
}
this.hasFilterError = false;
this.hasMetadataFilterError = false;
}
},
forceBlur(event) {
event.target.blur();
},
updateForm(event, property) {
if (!this.isValid()) {
this.hasError = true;
updateNameFilter(event, property) {
if (!this.isValid(this.filterValue)) {
this.hasFilterError = true;
return;
}
this.hasError = false;
this.hasFilterError = false;
this.$emit('updated', {
property,
value: this.filterValue.replace(/,(\s)*$/, '')
});
},
isValid() {
updateMetadataFilter(event, property) {
if (!this.isValid(this.filterMetadataValue)) {
this.hasMetadataFilterError = true;
return;
}
this.hasMetadataFilterError = false;
this.$emit('updated', {
property,
value: this.filterMetadataValue.replace(/,(\s)*$/, '')
});
},
isValid(value) {
// Test for any word character, any whitespace character or comma
if (this.filterValue === '') {
if (value === '') {
return true;
}
const regex = new RegExp(/^([a-zA-Z0-9_\-\s,])+$/g);
return regex.test(this.filterValue);
return regex.test(value);
}
}
};
Expand Down
12 changes: 3 additions & 9 deletions src/plugins/timelist/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,10 @@ export default function () {
initialize: function (domainObject) {
domainObject.configuration = {
sortOrderIndex: 0,
futureEventsIndex: 1,
futureEventsDurationIndex: 0,
futureEventsDuration: 20,
currentEventsIndex: 1,
currentEventsDurationIndex: 0,
currentEventsDuration: 20,
pastEventsIndex: 1,
pastEventsDurationIndex: 0,
pastEventsDuration: 20,
filter: ''
filter: '',
filterMetadata: '',
isCompact: false
};
domainObject.composition = [];
}
Expand Down
Loading

0 comments on commit 60e1eeb

Please sign in to comment.