mirror of
https://github.com/nasa/openmct.git
synced 2024-12-18 20:57:53 +00:00
Refine display options and add Independent Time Conductor option for Time List view (#7161)
* Apply sort settings immediately - even when in edit mode. * Adds test for sort order * Enable independent time conductor for time list view * Remove time frame duration options. * Remove immediate sorting in edit mode. * Closes #7113 - Color of current events changed to bring more in-line with color conventions. - Changed Time List rgba colors to solids. - Removed bolding on current events text. * Fix tests to include new changes --------- Co-authored-by: Charles Hacskaylo <charlesh88@gmail.com>
This commit is contained in:
parent
a0fd1f0171
commit
ae22920576
@ -116,8 +116,10 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
this.isEditing = this.openmct.editor.isEditing();
|
||||
this.timestamp = this.openmct.time.now();
|
||||
this.openmct.time.on(TIME_CONTEXT_EVENTS.modeChanged, this.setFixedTime);
|
||||
this.updateTimestamp = _.throttle(this.updateTimestamp, 1000);
|
||||
|
||||
this.setTimeContext();
|
||||
this.timestamp = this.timeContext.now();
|
||||
|
||||
this.getPlanDataAndSetConfig(this.domainObject);
|
||||
|
||||
@ -137,8 +139,6 @@ export default {
|
||||
);
|
||||
this.status = this.openmct.status.get(this.domainObject.identifier);
|
||||
|
||||
this.updateTimestamp = _.throttle(this.updateTimestamp, 1000);
|
||||
this.openmct.time.on('tick', this.updateTimestamp);
|
||||
this.openmct.editor.on('isEditing', this.setEditState);
|
||||
|
||||
this.deferAutoScroll = _.debounce(this.deferAutoScroll, 500);
|
||||
@ -150,7 +150,7 @@ export default {
|
||||
this.composition.load();
|
||||
}
|
||||
|
||||
this.setFixedTime(this.openmct.time.getMode());
|
||||
this.setFixedTime(this.timeContext.getMode());
|
||||
},
|
||||
beforeUnmount() {
|
||||
if (this.unlisten) {
|
||||
@ -166,8 +166,7 @@ export default {
|
||||
}
|
||||
|
||||
this.openmct.editor.off('isEditing', this.setEditState);
|
||||
this.openmct.time.off('tick', this.updateTimestamp);
|
||||
this.openmct.time.off(TIME_CONTEXT_EVENTS.modeChanged, this.setFixedTime);
|
||||
this.stopFollowingTimeContext();
|
||||
|
||||
this.$el.parentElement?.removeEventListener('scroll', this.deferAutoScroll, true);
|
||||
if (this.clearAutoScrollDisabledTimer) {
|
||||
@ -180,6 +179,21 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
setTimeContext() {
|
||||
this.stopFollowingTimeContext();
|
||||
this.timeContext = this.openmct.time.getContextForView(this.path);
|
||||
this.followTimeContext();
|
||||
},
|
||||
followTimeContext() {
|
||||
this.timeContext.on(TIME_CONTEXT_EVENTS.modeChanged, this.setFixedTime);
|
||||
this.timeContext.on(TIME_CONTEXT_EVENTS.tick, this.updateTimestamp);
|
||||
},
|
||||
stopFollowingTimeContext() {
|
||||
if (this.timeContext) {
|
||||
this.timeContext.off(TIME_CONTEXT_EVENTS.modeChanged, this.setFixedTime);
|
||||
this.timeContext.off(TIME_CONTEXT_EVENTS.tick, this.updateTimestamp);
|
||||
}
|
||||
},
|
||||
planFileUpdated(selectFile) {
|
||||
this.getPlanData({
|
||||
selectFile,
|
||||
@ -198,7 +212,6 @@ export default {
|
||||
} else {
|
||||
this.filterValue = configuration.filter;
|
||||
this.setSort();
|
||||
this.setViewBounds();
|
||||
this.listActivities();
|
||||
}
|
||||
},
|
||||
@ -208,7 +221,7 @@ export default {
|
||||
},
|
||||
setFixedTime() {
|
||||
this.filterValue = this.domainObject.configuration.filter;
|
||||
this.isFixedTime = !this.openmct.time.isRealTime();
|
||||
this.isFixedTime = !this.timeContext.isRealTime();
|
||||
if (this.isFixedTime) {
|
||||
this.hideAll = false;
|
||||
}
|
||||
@ -269,71 +282,6 @@ export default {
|
||||
getPlanData(domainObject) {
|
||||
this.planData = getValidatedData(domainObject);
|
||||
},
|
||||
setViewBounds() {
|
||||
const pastEventsIndex = this.domainObject.configuration.pastEventsIndex;
|
||||
const currentEventsIndex = this.domainObject.configuration.currentEventsIndex;
|
||||
const futureEventsIndex = this.domainObject.configuration.futureEventsIndex;
|
||||
const pastEventsDuration = this.domainObject.configuration.pastEventsDuration;
|
||||
const pastEventsDurationIndex = this.domainObject.configuration.pastEventsDurationIndex;
|
||||
const futureEventsDuration = this.domainObject.configuration.futureEventsDuration;
|
||||
const futureEventsDurationIndex = this.domainObject.configuration.futureEventsDurationIndex;
|
||||
|
||||
if (pastEventsIndex === 0 && futureEventsIndex === 0 && currentEventsIndex === 0) {
|
||||
this.viewBounds = undefined;
|
||||
this.hideAll = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.hideAll = false;
|
||||
|
||||
if (pastEventsIndex === 1 && futureEventsIndex === 1 && currentEventsIndex === 1) {
|
||||
this.viewBounds = undefined;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
this.viewBounds = {};
|
||||
|
||||
if (pastEventsIndex !== 1) {
|
||||
const pastDurationInMS = this.getDurationInMilliSeconds(
|
||||
pastEventsDuration,
|
||||
pastEventsDurationIndex
|
||||
);
|
||||
this.viewBounds.pastEnd = (timestamp) => {
|
||||
if (pastEventsIndex === 2) {
|
||||
return timestamp - pastDurationInMS;
|
||||
} else if (pastEventsIndex === 0) {
|
||||
return timestamp + 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (futureEventsIndex !== 1) {
|
||||
const futureDurationInMS = this.getDurationInMilliSeconds(
|
||||
futureEventsDuration,
|
||||
futureEventsDurationIndex
|
||||
);
|
||||
this.viewBounds.futureStart = (timestamp) => {
|
||||
if (futureEventsIndex === 2) {
|
||||
return timestamp + futureDurationInMS;
|
||||
} else if (futureEventsIndex === 0) {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
getDurationInMilliSeconds(duration, durationIndex) {
|
||||
if (duration > 0) {
|
||||
if (durationIndex === 0) {
|
||||
return duration * 1000;
|
||||
} else if (durationIndex === 1) {
|
||||
return duration * 60 * 1000;
|
||||
} else if (durationIndex === 2) {
|
||||
return duration * 60 * 60 * 1000;
|
||||
}
|
||||
}
|
||||
},
|
||||
listActivities() {
|
||||
let groups = Object.keys(this.planData);
|
||||
let activities = [];
|
||||
@ -356,18 +304,18 @@ export default {
|
||||
},
|
||||
isActivityInBounds(activity) {
|
||||
const startInBounds =
|
||||
activity.start >= this.openmct.time.bounds()?.start &&
|
||||
activity.start <= this.openmct.time.bounds()?.end;
|
||||
activity.start >= this.timeContext.bounds()?.start &&
|
||||
activity.start <= this.timeContext.bounds()?.end;
|
||||
const endInBounds =
|
||||
activity.end >= this.openmct.time.bounds()?.start &&
|
||||
activity.end <= this.openmct.time.bounds()?.end;
|
||||
activity.end >= this.timeContext.bounds()?.start &&
|
||||
activity.end <= this.timeContext.bounds()?.end;
|
||||
const middleInBounds =
|
||||
activity.start <= this.openmct.time.bounds()?.start &&
|
||||
activity.end >= this.openmct.time.bounds()?.end;
|
||||
activity.start <= this.timeContext.bounds()?.start &&
|
||||
activity.end >= this.timeContext.bounds()?.end;
|
||||
|
||||
return startInBounds || endInBounds || middleInBounds;
|
||||
},
|
||||
filterActivities(activity, index) {
|
||||
filterActivities(activity) {
|
||||
if (this.isEditing) {
|
||||
return true;
|
||||
}
|
||||
@ -381,15 +329,12 @@ export default {
|
||||
return false;
|
||||
}
|
||||
//current event or future start event or past end event
|
||||
const isCurrent = this.timestamp >= activity.start && this.timestamp <= activity.end;
|
||||
const isPast =
|
||||
this.timestamp > activity.end &&
|
||||
(this.viewBounds?.pastEnd === undefined ||
|
||||
activity.end >= this.viewBounds?.pastEnd(this.timestamp));
|
||||
const isFuture =
|
||||
this.timestamp < activity.start &&
|
||||
(this.viewBounds?.futureStart === undefined ||
|
||||
activity.start <= this.viewBounds?.futureStart(this.timestamp));
|
||||
const showCurrentEvents = this.domainObject.configuration.currentEventsIndex > 0;
|
||||
|
||||
const isCurrent =
|
||||
showCurrentEvents && this.timestamp >= activity.start && this.timestamp <= activity.end;
|
||||
const isPast = this.timestamp > activity.end;
|
||||
const isFuture = this.timestamp < activity.start;
|
||||
|
||||
return isCurrent || isPast || isFuture;
|
||||
},
|
||||
|
@ -32,34 +32,15 @@
|
||||
{{ activityOption }}
|
||||
</option>
|
||||
</select>
|
||||
<input
|
||||
v-if="index === 2"
|
||||
v-model="duration"
|
||||
class="c-input c-input--sm"
|
||||
type="text"
|
||||
@change="updateForm('duration')"
|
||||
/>
|
||||
<select v-if="index === 2" v-model="durationIndex" @change="updateForm('durationIndex')">
|
||||
<option
|
||||
v-for="(durationOption, durationKey) in durationOptions"
|
||||
:key="durationKey"
|
||||
:value="durationKey"
|
||||
>
|
||||
{{ durationOption }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-else class="c-inspect-properties__value">
|
||||
{{ activitiesOptions[index] }}
|
||||
<span v-if="index > 1">{{ duration }} {{ durationOptions[durationIndex] }}</span>
|
||||
</div>
|
||||
</li>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
const ACTIVITIES_OPTIONS = ["Don't show", 'Show all', 'Show starts within', 'Show after end for'];
|
||||
|
||||
const DURATION_OPTIONS = ['seconds', 'minutes', 'hours'];
|
||||
const ACTIVITIES_OPTIONS = ["Don't show", 'Show all'];
|
||||
|
||||
export default {
|
||||
inject: ['openmct', 'domainObject'],
|
||||
@ -76,11 +57,8 @@ export default {
|
||||
emits: ['updated'],
|
||||
data() {
|
||||
return {
|
||||
index: this.domainObject.configuration[`${this.prefix}Index`],
|
||||
durationIndex: this.domainObject.configuration[`${this.prefix}DurationIndex`],
|
||||
duration: this.domainObject.configuration[`${this.prefix}Duration`],
|
||||
index: this.domainObject.configuration[`${this.prefix}Index`] % 2, //this is modulo since we previously had more options and index could have been > 1
|
||||
activitiesOptions: ACTIVITIES_OPTIONS,
|
||||
durationOptions: DURATION_OPTIONS,
|
||||
isEditing: this.openmct.editor.isEditing()
|
||||
};
|
||||
},
|
||||
@ -116,7 +94,7 @@ export default {
|
||||
});
|
||||
},
|
||||
isValid() {
|
||||
return this.index < 2 || (this.durationIndex >= 0 && this.duration > 0);
|
||||
return this.index <= 1;
|
||||
},
|
||||
setEditState(isEditing) {
|
||||
this.isEditing = isEditing;
|
||||
|
@ -24,7 +24,7 @@
|
||||
<div class="c-timelist-properties">
|
||||
<div class="c-inspect-properties">
|
||||
<ul class="c-inspect-properties__section">
|
||||
<div class="c-inspect-properties_header" title="'Timeframe options'">Timeframe</div>
|
||||
<div class="c-inspect-properties_header" title="'Display options'">Display Options</div>
|
||||
<li class="c-inspect-properties__row">
|
||||
<div v-if="canEdit" class="c-inspect-properties__hint span-all">
|
||||
These settings don't affect the view while editing, but will be applied after editing is
|
||||
@ -72,17 +72,9 @@ import EventProperties from './EventProperties.vue';
|
||||
import Filtering from './FilteringComponent.vue';
|
||||
|
||||
const EVENT_TYPES = [
|
||||
{
|
||||
label: 'Future Events',
|
||||
prefix: 'futureEvents'
|
||||
},
|
||||
{
|
||||
label: 'Current Events',
|
||||
prefix: 'currentEvents'
|
||||
},
|
||||
{
|
||||
label: 'Past Events',
|
||||
prefix: 'pastEvents'
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -46,6 +46,7 @@ describe('the plugin', function () {
|
||||
let twoHoursPast = now - 1000 * 60 * 60 * 2;
|
||||
let oneHourPast = now - 1000 * 60 * 60;
|
||||
let twoHoursFuture = now + 1000 * 60 * 60 * 2;
|
||||
let threeHoursFuture = now + 1000 * 60 * 60 * 3;
|
||||
let planObject = {
|
||||
identifier: {
|
||||
key: 'test-plan-object',
|
||||
@ -71,6 +72,14 @@ describe('the plugin', function () {
|
||||
type: 'TEST-GROUP',
|
||||
color: 'fuchsia',
|
||||
textColor: 'black'
|
||||
},
|
||||
{
|
||||
name: 'Sed ut perspiciatis two',
|
||||
start: now,
|
||||
end: threeHoursFuture,
|
||||
type: 'TEST-GROUP',
|
||||
color: 'fuchsia',
|
||||
textColor: 'black'
|
||||
}
|
||||
]
|
||||
})
|
||||
@ -85,13 +94,13 @@ describe('the plugin', function () {
|
||||
openmct = createOpenMct({
|
||||
timeSystemKey: 'utc',
|
||||
bounds: {
|
||||
start: twoHoursPast,
|
||||
end: twoHoursFuture
|
||||
start: twoHoursFuture,
|
||||
end: threeHoursFuture
|
||||
}
|
||||
});
|
||||
openmct.time.setMode(FIXED_MODE_KEY, {
|
||||
start: twoHoursPast,
|
||||
end: twoHoursFuture
|
||||
start: twoHoursFuture,
|
||||
end: threeHoursFuture
|
||||
});
|
||||
openmct.install(new TimelistPlugin());
|
||||
|
||||
@ -215,7 +224,7 @@ describe('the plugin', function () {
|
||||
|
||||
it('displays the activities', () => {
|
||||
const items = element.querySelectorAll(LIST_ITEM_CLASS);
|
||||
expect(items.length).toEqual(2);
|
||||
expect(items.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('displays the activity headers', () => {
|
||||
@ -230,14 +239,10 @@ describe('the plugin', function () {
|
||||
const itemEls = element.querySelectorAll(LIST_ITEM_CLASS);
|
||||
const itemValues = itemEls[0].querySelectorAll(LIST_ITEM_VALUE_CLASS);
|
||||
expect(itemValues.length).toEqual(4);
|
||||
expect(itemValues[3].innerHTML.trim()).toEqual(
|
||||
'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua'
|
||||
);
|
||||
expect(itemValues[0].innerHTML.trim()).toEqual(
|
||||
timeFormatter.format(twoHoursPast, TIME_FORMAT)
|
||||
);
|
||||
expect(itemValues[3].innerHTML.trim()).toEqual('Sed ut perspiciatis');
|
||||
expect(itemValues[0].innerHTML.trim()).toEqual(timeFormatter.format(now, TIME_FORMAT));
|
||||
expect(itemValues[1].innerHTML.trim()).toEqual(
|
||||
timeFormatter.format(oneHourPast, TIME_FORMAT)
|
||||
timeFormatter.format(twoHoursFuture, TIME_FORMAT)
|
||||
);
|
||||
|
||||
done();
|
||||
@ -313,7 +318,7 @@ describe('the plugin', function () {
|
||||
type: TIMELIST_TYPE,
|
||||
id: 'test-object',
|
||||
configuration: {
|
||||
sortOrderIndex: 0,
|
||||
sortOrderIndex: 2,
|
||||
futureEventsIndex: 1,
|
||||
futureEventsDurationIndex: 0,
|
||||
futureEventsDuration: 0,
|
||||
@ -350,7 +355,26 @@ describe('the plugin', function () {
|
||||
|
||||
return nextTick(() => {
|
||||
const items = element.querySelectorAll(LIST_ITEM_CLASS);
|
||||
expect(items.length).toEqual(1);
|
||||
expect(items.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it('activities and sorts them correctly', () => {
|
||||
mockComposition.emit('add', planObject);
|
||||
|
||||
return nextTick(() => {
|
||||
const timeFormat = openmct.time.timeSystem().timeFormat;
|
||||
const timeFormatter = openmct.telemetry.getValueFormatter({ format: timeFormat }).formatter;
|
||||
|
||||
const items = element.querySelectorAll(LIST_ITEM_CLASS);
|
||||
expect(items.length).toEqual(2);
|
||||
|
||||
const itemValues = items[1].querySelectorAll(LIST_ITEM_VALUE_CLASS);
|
||||
expect(itemValues[0].innerHTML.trim()).toEqual(timeFormatter.format(now, TIME_FORMAT));
|
||||
expect(itemValues[1].innerHTML.trim()).toEqual(
|
||||
timeFormatter.format(threeHoursFuture, TIME_FORMAT)
|
||||
);
|
||||
expect(itemValues[3].innerHTML.trim()).toEqual('Sed ut perspiciatis two');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -405,7 +429,7 @@ describe('the plugin', function () {
|
||||
|
||||
return nextTick(() => {
|
||||
const items = element.querySelectorAll(LIST_ITEM_CLASS);
|
||||
expect(items.length).toEqual(1);
|
||||
expect(items.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -42,7 +42,6 @@
|
||||
background-color: $colorCurrentBg;
|
||||
border-top: 1px solid $colorCurrentBorder !important;
|
||||
color: $colorCurrentFg;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.--is-future {
|
||||
|
@ -421,10 +421,10 @@ $colorGaugeLimitLow: $colorGaugeLimitHigh;
|
||||
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
|
||||
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
|
||||
// Time Strip and Lists
|
||||
$colorCurrentBg: rgba($colorStatusAlert, 0.3);
|
||||
$colorCurrentFg: pullForward($colorBodyFg, 20%);
|
||||
$colorCurrentBg: $colorTimeRealtimeBg;
|
||||
$colorCurrentFg: $colorTimeRealtimeFg;
|
||||
$colorCurrentBorder: $colorBodyBg;
|
||||
$colorFutureBg: rgba($colorKey, 0.2);
|
||||
$colorFutureBg: #1b5263;
|
||||
$colorFutureFg: $colorCurrentFg;
|
||||
$colorFutureBorder: $colorCurrentBorder;
|
||||
$colorGanttSelectedBorder: rgba(#fff, 0.3);
|
||||
|
@ -424,10 +424,10 @@ $colorGaugeLimitLow: $colorGaugeLimitHigh;
|
||||
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
|
||||
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
|
||||
// Time Strip and Lists
|
||||
$colorCurrentBg: rgba($colorStatusAlert, 0.3);
|
||||
$colorCurrentFg: pullForward($colorBodyFg, 20%);
|
||||
$colorCurrentBorder: #fff;
|
||||
$colorFutureBg: rgba($colorKey, 0.2);
|
||||
$colorCurrentBg: $colorTimeRealtimeBg;
|
||||
$colorCurrentFg: $colorTimeRealtimeFg;
|
||||
$colorCurrentBorder: $colorBodyBg;
|
||||
$colorFutureBg: #1b5263;
|
||||
$colorFutureFg: $colorCurrentFg;
|
||||
$colorFutureBorder: $colorCurrentBorder;
|
||||
$colorGanttSelectedBorder: #fff;
|
||||
|
@ -421,11 +421,11 @@ $colorGaugeLimitLow: $colorGaugeLimitHigh;
|
||||
$transitionTimeGauge: 150ms; // CSS transition time used to smooth needle gauge and meter value transitions
|
||||
$marginGaugeMeterValue: 10%; // Margin between meter value bar and bounds edges
|
||||
// Time Strip and Lists
|
||||
$colorCurrentBg: rgba($colorStatusAlert, 0.3);
|
||||
$colorCurrentFg: pullForward($colorBodyFg, 20%);
|
||||
$colorCurrentBg: #5872bd;
|
||||
$colorCurrentFg: #eee;
|
||||
$colorCurrentBorder: #fff;
|
||||
$colorFutureBg: rgba($colorKey, 0.2);
|
||||
$colorFutureFg: $colorCurrentFg;
|
||||
$colorFutureBg: #c6f0ff;
|
||||
$colorFutureFg: $colorBodyFg;
|
||||
$colorFutureBorder: $colorCurrentBorder;
|
||||
$colorGanttSelectedBorder: #fff;
|
||||
|
||||
|
@ -101,18 +101,11 @@ import NotebookMenuSwitcher from '@/plugins/notebook/components/NotebookMenuSwit
|
||||
import IndependentTimeConductor from '@/plugins/timeConductor/independent/IndependentTimeConductor.vue';
|
||||
|
||||
import tooltipHelpers from '../../api/tooltips/tooltipMixins';
|
||||
import { SupportedViewTypes } from '../../utils/constants.js';
|
||||
import ObjectView from './ObjectView.vue';
|
||||
|
||||
const SIMPLE_CONTENT_TYPES = ['clock', 'timer', 'summary-widget', 'hyperlink', 'conditionWidget'];
|
||||
const CSS_WIDTH_LESS_STR = '--width-less-than-';
|
||||
const SupportedViewTypes = [
|
||||
'plot-stacked',
|
||||
'plot-overlay',
|
||||
'bar-graph.view',
|
||||
'scatter-plot.view',
|
||||
'time-strip.view',
|
||||
'example.imagery'
|
||||
];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -143,15 +143,9 @@ import NotebookMenuSwitcher from '@/plugins/notebook/components/NotebookMenuSwit
|
||||
import IndependentTimeConductor from '@/plugins/timeConductor/independent/IndependentTimeConductor.vue';
|
||||
|
||||
import tooltipHelpers from '../../api/tooltips/tooltipMixins';
|
||||
import { SupportedViewTypes } from '../../utils/constants.js';
|
||||
import ViewSwitcher from './ViewSwitcher.vue';
|
||||
|
||||
const SupportedViewTypes = [
|
||||
'plot-stacked',
|
||||
'plot-overlay',
|
||||
'bar-graph.view',
|
||||
'time-strip.view',
|
||||
'example.imagery'
|
||||
];
|
||||
const PLACEHOLDER_OBJECT = {};
|
||||
|
||||
export default {
|
||||
|
8
src/utils/constants.js
Normal file
8
src/utils/constants.js
Normal file
@ -0,0 +1,8 @@
|
||||
export const SupportedViewTypes = [
|
||||
'plot-stacked',
|
||||
'plot-overlay',
|
||||
'bar-graph.view',
|
||||
'time-strip.view',
|
||||
'example.imagery',
|
||||
'timelist.view'
|
||||
];
|
Loading…
Reference in New Issue
Block a user