mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
3112433a58
Summary: Dropdown conditions let you specify a predicate formula that's used to filter choices and references in their respective autocomplete dropdown menus. Test Plan: Python and browser tests (WIP). Reviewers: jarek, paulfitz Reviewed By: jarek Subscribers: dsagal, paulfitz Differential Revision: https://phab.getgrist.com/D4235
192 lines
6.1 KiB
JavaScript
192 lines
6.1 KiB
JavaScript
var _ = require('underscore');
|
|
var dispose = require('app/client/lib/dispose');
|
|
var TextEditor = require('app/client/widgets/TextEditor');
|
|
|
|
const {Autocomplete} = require('app/client/lib/autocomplete');
|
|
const {ACIndexImpl, buildHighlightedDom} = require('app/client/lib/ACIndex');
|
|
const {makeT} = require('app/client/lib/localization');
|
|
const {
|
|
buildDropdownConditionFilter,
|
|
ChoiceItem,
|
|
cssChoiceList,
|
|
cssMatchText,
|
|
cssPlusButton,
|
|
cssPlusIcon,
|
|
} = require('app/client/widgets/ChoiceListEditor');
|
|
const {icon} = require('app/client/ui2018/icons');
|
|
const {menuCssClass} = require('app/client/ui2018/menus');
|
|
const {testId, theme} = require('app/client/ui2018/cssVars');
|
|
const {choiceToken, cssChoiceACItem} = require('app/client/widgets/ChoiceToken');
|
|
const {dom, styled} = require('grainjs');
|
|
|
|
const t = makeT('ChoiceEditor');
|
|
|
|
/**
|
|
* ChoiceEditor - TextEditor with a dropdown for possible choices.
|
|
*/
|
|
function ChoiceEditor(options) {
|
|
TextEditor.call(this, options);
|
|
|
|
this.widgetOptionsJson = options.field.widgetOptionsJson;
|
|
this.choices = this.widgetOptionsJson.peek().choices || [];
|
|
this.choicesSet = new Set(this.choices);
|
|
this.choiceOptions = this.widgetOptionsJson.peek().choiceOptions || {};
|
|
|
|
this.hasDropdownCondition = Boolean(options.field.dropdownCondition.peek()?.text);
|
|
this.dropdownConditionError;
|
|
|
|
let acItems = this.choices.map(c => new ChoiceItem(c, false, false));
|
|
if (this.hasDropdownCondition) {
|
|
try {
|
|
const dropdownConditionFilter = this.buildDropdownConditionFilter();
|
|
acItems = acItems.filter((item) => dropdownConditionFilter(item));
|
|
} catch (e) {
|
|
acItems = [];
|
|
this.dropdownConditionError = e.message;
|
|
}
|
|
}
|
|
|
|
const acIndex = new ACIndexImpl(acItems);
|
|
this._acOptions = {
|
|
popperOptions: {
|
|
placement: 'bottom'
|
|
},
|
|
menuCssClass: `${menuCssClass} ${cssChoiceList.className} test-autocomplete`,
|
|
buildNoItemsMessage: this.buildNoItemsMessage.bind(this),
|
|
search: (term) => this.maybeShowAddNew(acIndex.search(term), term),
|
|
renderItem: (item, highlightFunc) => this.renderACItem(item, highlightFunc),
|
|
getItemText: (item) => item.label,
|
|
onClick: () => this.options.commands.fieldEditSave(),
|
|
};
|
|
|
|
if (!options.readonly && options.field.viewSection().parentKey() === "single") {
|
|
this.cellEditorDiv.classList.add(cssChoiceEditor.className);
|
|
this.cellEditorDiv.appendChild(cssChoiceEditIcon('Dropdown'));
|
|
}
|
|
|
|
// Whether to include a button to show a new choice.
|
|
// TODO: Disable when the user cannot change column configuration.
|
|
this.enableAddNew = !this.hasDropdownCondition;
|
|
}
|
|
|
|
dispose.makeDisposable(ChoiceEditor);
|
|
_.extend(ChoiceEditor.prototype, TextEditor.prototype);
|
|
|
|
ChoiceEditor.prototype.getCellValue = function() {
|
|
const selectedItem = this.autocomplete && this.autocomplete.getSelectedItem();
|
|
if (selectedItem) {
|
|
return selectedItem.label;
|
|
} else if (this.textInput.value.trim() === '') {
|
|
return null;
|
|
} else {
|
|
return TextEditor.prototype.getCellValue.call(this);
|
|
}
|
|
}
|
|
|
|
ChoiceEditor.prototype.renderACItem = function(item, highlightFunc) {
|
|
const options = this.choiceOptions[item.label];
|
|
|
|
return cssChoiceACItem(
|
|
(item.isNew ?
|
|
[cssChoiceACItem.cls('-new'), cssPlusButton(cssPlusIcon('Plus')), testId('choice-editor-new-item')] :
|
|
[cssChoiceACItem.cls('-with-new', this.showAddNew)]
|
|
),
|
|
choiceToken(
|
|
buildHighlightedDom(item.label, highlightFunc, cssMatchText),
|
|
options || {},
|
|
dom.style('max-width', '100%'),
|
|
testId('choice-editor-item-label')
|
|
),
|
|
testId('choice-editor-item'),
|
|
);
|
|
}
|
|
|
|
ChoiceEditor.prototype.attach = function(cellElem) {
|
|
TextEditor.prototype.attach.call(this, cellElem);
|
|
// Don't create autocomplete if readonly.
|
|
if (this.options.readonly) { return; }
|
|
|
|
this.autocomplete = Autocomplete.create(this, this.textInput, this._acOptions);
|
|
}
|
|
|
|
/**
|
|
* Updates list of valid choices with any new ones that may have been
|
|
* added from directly inside the editor (via the "add new" item in autocomplete).
|
|
*/
|
|
ChoiceEditor.prototype.prepForSave = async function() {
|
|
const selectedItem = this.autocomplete && this.autocomplete.getSelectedItem();
|
|
if (selectedItem && selectedItem.isNew) {
|
|
const choices = this.widgetOptionsJson.prop('choices');
|
|
await choices.saveOnly([...(choices.peek() || []), selectedItem.label]);
|
|
}
|
|
}
|
|
|
|
ChoiceEditor.prototype.buildDropdownConditionFilter = function() {
|
|
const dropdownConditionCompiled = this.options.field.dropdownConditionCompiled.get();
|
|
if (dropdownConditionCompiled?.kind !== 'success') {
|
|
throw new Error('Dropdown condition is not compiled');
|
|
}
|
|
|
|
return buildDropdownConditionFilter({
|
|
dropdownConditionCompiled: dropdownConditionCompiled.result,
|
|
docData: this.options.gristDoc.docData,
|
|
tableId: this.options.field.tableId(),
|
|
rowId: this.options.rowId,
|
|
});
|
|
}
|
|
|
|
ChoiceEditor.prototype.buildNoItemsMessage = function() {
|
|
if (this.dropdownConditionError) {
|
|
return t('Error in dropdown condition');
|
|
} else if (this.hasDropdownCondition) {
|
|
return t('No choices matching condition');
|
|
} else {
|
|
return t('No choices to select');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If the search text does not match anything exactly, adds 'new' item to it.
|
|
*
|
|
* Also see: prepForSave.
|
|
*/
|
|
ChoiceEditor.prototype.maybeShowAddNew = function(result, text) {
|
|
// TODO: This logic is also mostly duplicated in ChoiceListEditor and ReferenceEditor.
|
|
// See if there's anything common we can factor out and re-use.
|
|
this.showAddNew = false;
|
|
if (!this.enableAddNew) {
|
|
return result;
|
|
}
|
|
|
|
const trimmedText = text.trim();
|
|
if (!trimmedText || this.choicesSet.has(trimmedText)) {
|
|
return result;
|
|
}
|
|
|
|
const addNewItem = new ChoiceItem(trimmedText, false, false, true);
|
|
if (result.items.find((item) => item.cleanText === addNewItem.cleanText)) {
|
|
return result;
|
|
}
|
|
|
|
result.extraItems.push(addNewItem);
|
|
this.showAddNew = true;
|
|
|
|
return result;
|
|
}
|
|
|
|
const cssChoiceEditIcon = styled(icon, `
|
|
background-color: ${theme.lightText};
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
margin: 3px 3px 0 3px;
|
|
`);
|
|
|
|
const cssChoiceEditor = styled('div', `
|
|
& > .celleditor_text_editor, & > .celleditor_content_measure {
|
|
padding-left: 18px;
|
|
}
|
|
`);
|
|
|
|
module.exports = ChoiceEditor;
|