mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
50af681f47
Summary: The default value of Choice columns is empty string, but ChoiceEditor was saving nulls whenever a blank value was saved. This was causing unexpected updates to trigger values due to the cell value changing internally, even though null and empty string appear the same in the UI. Test Plan: Browser test. Reviewers: jarek Reviewed By: jarek Differential Revision: https://phab.getgrist.com/D4242
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 '';
|
|
} 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;
|