Skip to content

Commit

Permalink
[O2B-1151] Auto-fill tags in on-call log template (#1409)
Browse files Browse the repository at this point in the history
* Rework tags picker

* [O2B-1151] Auto-fill tags in on-call log template

* First attempt at fixing tests

* Fix broken tests

* Handle null tags observable and fix label

* Reformat code

* Explicit tags mapping
  • Loading branch information
martinboulais authored Mar 1, 2024
1 parent f83fff0 commit e88c69c
Show file tree
Hide file tree
Showing 19 changed files with 138 additions and 115 deletions.
2 changes: 1 addition & 1 deletion lib/database/seeders/20200511151010-tags.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ module.exports = {
{ text: 'TEST-TAG-18' },
{ text: 'TEST-TAG-19' },
{ text: 'TEST-TAG-20' },
{ text: 'TEST-TAG-21' },
{ text: 'oncall' },
{ text: 'TEST-TAG-22' },
{ text: 'TEST-TAG-23' },
{ text: 'TEST-TAG-24' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { h } from '/js/src/index.js';
*
* @param {CombinationOperatorChoiceModel} combinationOperatorModel the model to store the choice's state
* @param {Object} [configuration] the component's configuration
* @param {Object} [configuration.selectorPrefix] a selector prefix used to generate DOM selectors
* @param {string} [configuration.selectorPrefix] a selector prefix used to generate DOM selectors
* @return {Component} the choice component
*/
export const combinationOperatorChoiceComponent = (combinationOperatorModel, configuration) => {
Expand All @@ -29,7 +29,7 @@ export const combinationOperatorChoiceComponent = (combinationOperatorModel, con
return h(
'.form-group-header.flex-row',
// Available options are always an array
combinationOperatorModel.availableOptions.map((option) => h('.form-check.mr2', [
combinationOperatorModel.options.map((option) => h('.form-check.mr2', [
h('input.form-check-input', {
onclick: () => combinationOperatorModel.select(option),
id: `${selectorPrefix}combination-operator-radio-button-${option.value}`,
Expand Down
62 changes: 41 additions & 21 deletions lib/public/components/common/selection/SelectionModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
* granted to it by virtue of its status as an Intergovernmental Organization
* or submit itself to any jurisdiction.
*/
import { Observable } from '/js/src/index.js';
import { Observable, RemoteData } from '/js/src/index.js';

/**
* @typedef SelectionOption A picker option, with the actual value and its string representation
Expand Down Expand Up @@ -42,8 +42,8 @@ export class SelectionModel extends Observable {
const { availableOptions = [], defaultSelection = [], multiple = true, allowEmpty = true } = configuration || {};

/**
* @type {RemoteData|SelectionOption[]}
* @private
* @type {RemoteData<SelectionOption[]>|SelectionOption[]}
* @protected
*/
this._availableOptions = availableOptions;

Expand Down Expand Up @@ -132,17 +132,32 @@ export class SelectionModel extends Observable {
}

/**
* Add the given options to the list of selected ones
* Add the given option or value to the list of selected ones
*
* @param {SelectionOption} option the option to select
* @param {SelectionOption|number|string} option the option to select
* @return {void}
*/
select(option) {
if (!this.isSelected(option)) {
let selectOption;

if (typeof option === 'string' || typeof option === 'number') {
if (this._availableOptions instanceof RemoteData) {
selectOption = this._availableOptions.match({
Success: (options) => options.find(({ value }) => value === option),
Other: () => null,
});
} else {
selectOption = this._availableOptions.find(({ value }) => value === option);
}
} else {
selectOption = option;
}

if (selectOption && !this.isSelected(selectOption)) {
if (this._multiple || this._selectedOptions.length === 0) {
this._selectedOptions.push(option);
this._selectedOptions.push(selectOption);
} else {
this._selectedOptions = [option];
this._selectedOptions = [selectOption];
}

this.notify();
Expand All @@ -154,27 +169,32 @@ export class SelectionModel extends Observable {
*
* Depending on the selector, this may be a filtered subset of all the available options
*
* @return {RemoteData|SelectionOption[]} the list of options
* @return {RemoteData<SelectionOption[], *>|SelectionOption[]} the list of options
*/
get options() {
return this._availableOptions;
}

/**
* Return the list of all the available options
*
* @return {RemoteData|SelectionOption[]} the list of available options
*/
get availableOptions() {
return this._availableOptions;
/**
* Add the default options to the list of given options
*
* @param {SelectionOption[]} options the options to which default selection should be added
* @return {SelectionOption[]} the options list including default options
*/
const addDefaultToOptions = (options) => [
...options,
...this.optionsSelectedByDefault.filter((defaultOption) => !options.find(({ value }) => defaultOption.value === value)),
];

return this._availableOptions instanceof RemoteData
? this._availableOptions.apply({ Success: addDefaultToOptions })
: addDefaultToOptions(this._availableOptions);
}

/**
* Defines the list of available options
*
* @param {RemoteData|SelectionOption[]} availableOptions the new available options
* @param {RemoteData<SelectionOption[], *>|SelectionOption[]} availableOptions the new available options
* @return {void}
*/
set availableOptions(availableOptions) {
setAvailableOptions(availableOptions) {
this._availableOptions = availableOptions;
this.visualChange$.notify();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,22 +61,22 @@ export class SelectionDropdownModel extends SelectionModel {
* @return {RemoteData|SelectionOption[]} the filtered available options
*/
get options() {
if (this._searchInputContent && this.availableOptions) {
if (this._searchInputContent && this._availableOptions) {
// eslint-disable-next-line require-jsdoc
const filter = ({ rawLabel, label, value }) => (rawLabel || label || value)
.toUpperCase()
.includes(this._searchInputContent.toUpperCase());
if (this.availableOptions instanceof RemoteData) {
if (this.availableOptions.isSuccess()) {
return RemoteData.success(this.availableOptions.payload.filter(filter));
} else {
return this.availableOptions;
}

if (this._availableOptions instanceof RemoteData) {
return this._availableOptions.apply({
Success: (availableOptions) => availableOptions.filter(filter),
});
}
return this.availableOptions.filter(filter);

return this._availableOptions.filter(filter);
}

return this.availableOptions;
return this._availableOptions;
}

// eslint-disable-next-line valid-jsdoc
Expand Down
10 changes: 0 additions & 10 deletions lib/public/components/common/selection/picker/PickerModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,16 +38,6 @@ export class PickerModel extends SelectionModel {
this._collapsed = !defaultExpand;
}

/**
* If the picker is collapsed expand it, else collapse it
*
* @return {void}
*/
toggleCollapse() {
this._collapsed = !this._collapsed;
this.notify();
}

/**
* Reset the model to its default state
*
Expand Down
29 changes: 4 additions & 25 deletions lib/public/components/common/selection/picker/picker.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,10 @@
* or submit itself to any jurisdiction.
*/

import { h, iconMinus, iconPlus } from '/js/src/index.js';
import { h } from '/js/src/index.js';

/**
* @typedef PickerConfiguration The configuration of the picker
* @property {number|null} [limit=5] Amount of items to be shown before collapsable block is shown (null will display all the items)
* @property {string} [selector='picker'] if specified, this will be used to customize picker components ids and classes
* @property {number|null} [attributes=null] if specified, picker items will be wrapped within a component with these attributes. If a limit is
* present and the items overflow, the overflowed elements will be wrapped in a separated component with the same attributes
Expand Down Expand Up @@ -65,11 +64,11 @@ const mapOptionsToInput = (pickerModel, pickerOptions, selector) => pickerOption
* @returns {Component} A filterable collapsable picker.
*/
export const picker = (pickerOptions, pickerModel, configuration) => {
const { limit = 5, selector = 'picker', attributes = null, outlineSelection = false, placeHolder = null } = configuration || {};
const { selector = 'picker', attributes = null, placeHolder = null } = configuration || {};

const checkboxes = mapOptionsToInput(pickerModel, pickerOptions, selector);

const selectedPills = pickerModel.selectedOptions.length > 0 && outlineSelection
const selectedPills = pickerModel.selectedOptions.length
? h(
'.flex-row.flex-wrap.g2',
pickerModel.selectedOptions.map(({ label, value }) => h(
Expand All @@ -96,25 +95,5 @@ export const picker = (pickerOptions, pickerModel, configuration) => {
}
};

const toggleFilters = h(
`button.btn.btn-primary.mv1#${selector}ToggleMore`,
{ onclick: () => pickerModel.toggleCollapse() },
...pickerModel.collapsed ? [iconPlus(), ' More'] : [iconMinus(), ' Less'],
);

let alwaysVisible = applyAttributes(checkboxes);
let collapsable = null;
if (limit && checkboxes.length > limit) {
alwaysVisible = applyAttributes(checkboxes.slice(0, limit));
collapsable = [toggleFilters];
if (!pickerModel.collapsed) {
collapsable.push(applyAttributes(checkboxes.slice(limit)));
}
}

return [
header,
alwaysVisible,
collapsable,
];
return [header, applyAttributes(checkboxes)];
};
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,10 @@ export class DetectorSelectionDropdownModel extends SelectionDropdownModel {
_initialize() {
detectorsProvider.getAll().then(
(detectors) => {
this.availableOptions = RemoteData.success(detectors.map(({ name }) => ({ value: name })));
this.setAvailableOptions(RemoteData.success(detectors.map(({ name }) => ({ value: name }))));
},
(errors) => {
this.availableOptions = RemoteData.failure(errors);
this.setAvailableOptions(RemoteData.failure(errors));
},
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,8 @@ export class RunTypesSelectionDropdownModel extends SelectionDropdownModel {
*/
_initialize() {
runTypesProvider.getAll().then(
(runTypes) => {
this.availableOptions = RemoteData.success(runTypes.map(runTypeToOption));
},
(errors) => {
this.availableOptions = RemoteData.failure(errors);
},
(runTypes) => this.setAvailableOptions(RemoteData.success(runTypes.map(runTypeToOption))),
(errors) => this.setAvailableOptions(RemoteData.failure(errors)),
);
}
}
8 changes: 2 additions & 6 deletions lib/public/components/tag/TagPickerModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,8 @@ export class TagPickerModel extends PickerModel {
super({ ...configuration || {}, availableOptions: RemoteData.notAsked() });
const { includeArchived = false } = configuration || {};
(includeArchived ? tagsProvider.getAll() : tagsProvider.getNotArchived()).then(
(tags) => {
this.availableOptions = RemoteData.success(tags.map(tagToOption));
},
(errors) => {
this.availableOptions = RemoteData.failure(errors);
},
(tags) => this.setAvailableOptions(RemoteData.success(tags.map(tagToOption))),
(errors) => this.setAvailableOptions(RemoteData.failure(errors)),
);
}
}
8 changes: 2 additions & 6 deletions lib/public/components/tag/TagSelectionDropdownModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,8 @@ export class TagSelectionDropdownModel extends SelectionDropdownModel {
*/
_initialize() {
(this._includeArchived ? tagsProvider.getAll() : tagsProvider.getNotArchived()).then(
(tags) => {
this.availableOptions = RemoteData.success(tags.map(tagToOption));
},
(errors) => {
this.availableOptions = RemoteData.failure(errors);
},
(tags) => this.setAvailableOptions(RemoteData.success(tags.map(tagToOption))),
(errors) => this.setAvailableOptions(RemoteData.failure(errors)),
);
}
}
12 changes: 4 additions & 8 deletions lib/public/components/tag/tagPicker.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,10 @@ const NO_TAG_TEXT = 'No tags defined, please go here to create them.';
* Return a component representing a picker for a remote data list of tags, handling each possible remote data status
*
* @param {PickerModel} tagPickerModel the model storing the tag picker's state
* @param {PickerConfiguration} [configuration] the remote data tag picker's configuration
*
* @return {Component} the resulting component
*/
export const tagPicker = (tagPickerModel, configuration) => asRemoteData(tagPickerModel.availableOptions).match({
export const tagPicker = (tagPickerModel) => asRemoteData(tagPickerModel.options).match({
NotAsked: () => null,
Loading: () => spinner({
size: 2,
Expand All @@ -37,11 +36,6 @@ export const tagPicker = (tagPickerModel, configuration) => asRemoteData(tagPick
Success: (options) => {
if (options.length > 0) {
// Keep the already checked options, even if they are not available in the list
for (const selected of tagPickerModel.optionsSelectedByDefault) {
if (!options.find(({ value }) => selected.value === value)) {
options.push(selected);
}
}
return picker(
options,
tagPickerModel,
Expand All @@ -56,7 +50,9 @@ export const tagPicker = (tagPickerModel, configuration) => asRemoteData(tagPick
' screen.',
],
),
...configuration,
limit: null,
attributes: { class: 'grid columns-2-lg columns-3-xl g2' },
outlineSelection: true,
},
);
} else {
Expand Down
29 changes: 28 additions & 1 deletion lib/public/views/Logs/Create/OnCallLogTemplate.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ import { detectorsProvider } from '../../../services/detectors/detectorsProvider
*/
const ALICE_SYSTEMS = ['FLP', 'ECS', 'EPN', 'DCS', 'OTHER'];

const SHIFTER_TAGS = {
DCS: 'DCS Shifter',
ECS: 'ECS Shifter',
'QC/PDP': 'QC/PDP Shifter',
SL: 'Shift Leader',
};

/**
* Log template for on-call log
*/
Expand All @@ -43,6 +50,8 @@ export class OnCallLogTemplate extends Observable {
(error) => this._detectors.setCurrent(RemoteData.failure(error)),
);

this._tags = new ObservableData(['oncall']);

/**
* @type {Partial<OnCallLogTemplateFormData>}
*/
Expand All @@ -67,6 +76,15 @@ export class OnCallLogTemplate extends Observable {
...this.formData,
...patch,
};

if (patch.shifterPosition || patch.detectorOrSubsystem) {
const tags = ['oncall', this.formData.detectorOrSubsystem];
if (this.formData.shifterPosition in SHIFTER_TAGS) {
tags.push(SHIFTER_TAGS[this.formData.shifterPosition]);
}
this.tags$.setCurrent(tags);
}

this.notify();
}

Expand All @@ -90,7 +108,7 @@ export class OnCallLogTemplate extends Observable {
* @return {boolean} true if the form is valid
*/
get isValid() {
return Boolean(this.formData.shortDescription.length >= 4 && this.formData.shortDescription.length <= 30
return Boolean(this.formData.shortDescription.length >= 4 && this.formData.shortDescription.length <= 100
&& this.formData.detectorOrSubsystem
&& this.formData.severity
&& this.formData.scope
Expand Down Expand Up @@ -130,4 +148,13 @@ export class OnCallLogTemplate extends Observable {

return textParts.join('\n\n');
}

/**
* Return an observable data of tags to select
*
* @return {ObservableData} tags observable data
*/
get tags$() {
return this._tags;
}
}
Loading

0 comments on commit e88c69c

Please sign in to comment.