(core) Add dropdown conditions

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
This commit is contained in:
George Gevoian
2024-04-26 16:34:16 -04:00
parent 34c85757f1
commit 3112433a58
86 changed files with 4221 additions and 1060 deletions

View File

@@ -4,13 +4,22 @@ var TextEditor = require('app/client/widgets/TextEditor');
const {Autocomplete} = require('app/client/lib/autocomplete');
const {ACIndexImpl, buildHighlightedDom} = require('app/client/lib/ACIndex');
const {ChoiceItem, cssChoiceList, cssMatchText, cssPlusButton,
cssPlusIcon} = require('app/client/widgets/ChoiceListEditor');
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 {icon} = require('../ui2018/icons');
const t = makeT('ChoiceEditor');
/**
* ChoiceEditor - TextEditor with a dropdown for possible choices.
@@ -18,15 +27,46 @@ const {icon} = require('../ui2018/icons');
function ChoiceEditor(options) {
TextEditor.call(this, options);
this.choices = options.field.widgetOptionsJson.peek().choices || [];
this.choiceOptions = options.field.widgetOptionsJson.peek().choiceOptions || {};
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 = true;
this.enableAddNew = !this.hasDropdownCondition;
}
dispose.makeDisposable(ChoiceEditor);
@@ -66,20 +106,7 @@ ChoiceEditor.prototype.attach = function(cellElem) {
// Don't create autocomplete if readonly.
if (this.options.readonly) { return; }
const acItems = this.choices.map(c => new ChoiceItem(c, false, false));
const acIndex = new ACIndexImpl(acItems);
const acOptions = {
popperOptions: {
placement: 'bottom'
},
menuCssClass: `${menuCssClass} ${cssChoiceList.className} test-autocomplete`,
search: (term) => this.maybeShowAddNew(acIndex.search(term), term),
renderItem: (item, highlightFunc) => this.renderACItem(item, highlightFunc),
getItemText: (item) => item.label,
onClick: () => this.options.commands.fieldEditSave(),
};
this.autocomplete = Autocomplete.create(this, this.textInput, acOptions);
this.autocomplete = Autocomplete.create(this, this.textInput, this._acOptions);
}
/**
@@ -89,11 +116,35 @@ ChoiceEditor.prototype.attach = function(cellElem) {
ChoiceEditor.prototype.prepForSave = async function() {
const selectedItem = this.autocomplete && this.autocomplete.getSelectedItem();
if (selectedItem && selectedItem.isNew) {
const choices = this.options.field.widgetOptionsJson.prop('choices');
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.
*
@@ -103,15 +154,21 @@ 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 (!this.enableAddNew || !trimmedText) { return result; }
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.items.push(addNewItem);
result.extraItems.push(addNewItem);
this.showAddNew = true;
return result;

View File

@@ -2,7 +2,9 @@ import {createGroup} from 'app/client/components/commands';
import {ACIndexImpl, ACItem, ACResults,
buildHighlightedDom, HighlightFunc, normalizeText} from 'app/client/lib/ACIndex';
import {IAutocompleteOptions} from 'app/client/lib/autocomplete';
import {makeT} from 'app/client/lib/localization';
import {IToken, TokenField, tokenFieldStyles} from 'app/client/lib/TokenField';
import {DocData} from 'app/client/models/DocData';
import {colors, testId, theme} from 'app/client/ui2018/cssVars';
import {menuCssClass} from 'app/client/ui2018/menus';
import {createMobileButtons, getButtonMargins} from 'app/client/widgets/EditorButtons';
@@ -10,12 +12,15 @@ import {EditorPlacement} from 'app/client/widgets/EditorPlacement';
import {FieldOptions, NewBaseEditor} from 'app/client/widgets/NewBaseEditor';
import {csvEncodeRow} from 'app/common/csvFormat';
import {CellValue} from "app/common/DocActions";
import {CompiledPredicateFormula, EmptyRecordView} from 'app/common/PredicateFormula';
import {decodeObject, encodeObject} from 'app/plugin/objtypes';
import {ChoiceOptions, getRenderFillColor, getRenderTextColor} from 'app/client/widgets/ChoiceTextBox';
import {choiceToken, cssChoiceACItem, cssChoiceToken} from 'app/client/widgets/ChoiceToken';
import {icon} from 'app/client/ui2018/icons';
import {dom, styled} from 'grainjs';
const t = makeT('ChoiceListEditor');
export class ChoiceItem implements ACItem, IToken {
public cleanText: string = normalizeText(this.label);
constructor(
@@ -38,25 +43,37 @@ export class ChoiceListEditor extends NewBaseEditor {
private _inputSizer!: HTMLElement; // Part of _contentSizer to size the text input
private _alignment: string;
private _widgetOptionsJson = this.options.field.widgetOptionsJson.peek();
private _choices: string[] = this._widgetOptionsJson.choices || [];
private _choicesSet: Set<string> = new Set(this._choices);
private _choiceOptionsByName: ChoiceOptions = this._widgetOptionsJson.choiceOptions || {};
// Whether to include a button to show a new choice.
// TODO: Disable when the user cannot change column configuration.
private _enableAddNew: boolean = true;
private _enableAddNew: boolean;
private _showAddNew: boolean = false;
private _choiceOptionsByName: ChoiceOptions;
private _hasDropdownCondition = Boolean(this.options.field.dropdownCondition.peek()?.text);
private _dropdownConditionError: string | undefined;
constructor(protected options: FieldOptions) {
super(options);
const choices: string[] = options.field.widgetOptionsJson.peek().choices || [];
this._choiceOptionsByName = options.field.widgetOptionsJson
.peek().choiceOptions || {};
const acItems = choices.map(c => new ChoiceItem(c, false, false));
const choiceSet = new Set(choices);
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<ChoiceItem>(acItems);
const acOptions: IAutocompleteOptions<ChoiceItem> = {
menuCssClass: `${menuCssClass} ${cssChoiceList.className} test-autocomplete`,
buildNoItemsMessage: this._buildNoItemsMessage.bind(this),
search: async (term: string) => this._maybeShowAddNew(acIndex.search(term), term),
renderItem: (item, highlightFunc) => this._renderACItem(item, highlightFunc),
getItemText: (item) => item.label,
@@ -65,12 +82,13 @@ export class ChoiceListEditor extends NewBaseEditor {
this.commandGroup = this.autoDispose(createGroup(options.commands, null, true));
this._alignment = options.field.widgetOptionsJson.peek().alignment || 'left';
// If starting to edit by typing in a string, ignore previous tokens.
const cellValue = decodeObject(options.cellValue);
const startLabels: unknown[] = options.editValue !== undefined || !Array.isArray(cellValue) ? [] : cellValue;
const startTokens = startLabels.map(label => new ChoiceItem(
String(label),
!choiceSet.has(String(label)),
!this._choicesSet.has(String(label)),
String(label).trim() === ''
));
@@ -87,7 +105,7 @@ export class ChoiceListEditor extends NewBaseEditor {
cssChoiceToken.cls('-invalid', item.isInvalid),
cssChoiceToken.cls('-blank', item.isBlank),
],
createToken: label => new ChoiceItem(label, !choiceSet.has(label), label.trim() === ''),
createToken: label => new ChoiceItem(label, !this._choicesSet.has(label), label.trim() === ''),
acOptions,
openAutocompleteOnFocus: true,
readonly : options.readonly,
@@ -118,6 +136,8 @@ export class ChoiceListEditor extends NewBaseEditor {
dom.prop('value', options.editValue || ''),
this.commandGroup.attach(),
);
this._enableAddNew = !this._hasDropdownCondition;
}
public attach(cellElem: Element): void {
@@ -150,7 +170,7 @@ export class ChoiceListEditor extends NewBaseEditor {
}
public getTextValue() {
const values = this._tokenField.tokensObs.get().map(t => t.label);
const values = this._tokenField.tokensObs.get().map(token => token.label);
return csvEncodeRow(values, {prettier: true});
}
@@ -164,7 +184,7 @@ export class ChoiceListEditor extends NewBaseEditor {
*/
public async prepForSave() {
const tokens = this._tokenField.tokensObs.get();
const newChoices = tokens.filter(t => t.isNew).map(t => t.label);
const newChoices = tokens.filter(({isNew}) => isNew).map(({label}) => label);
if (newChoices.length > 0) {
const choices = this.options.field.widgetOptionsJson.prop('choices');
await choices.saveOnly([...(choices.peek() || []), ...new Set(newChoices)]);
@@ -218,6 +238,30 @@ export class ChoiceListEditor extends NewBaseEditor {
this._textInput.style.width = this._inputSizer.getBoundingClientRect().width + 'px';
}
private _buildDropdownConditionFilter() {
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,
});
}
private _buildNoItemsMessage(): string {
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.
*
@@ -225,15 +269,21 @@ export class ChoiceListEditor extends NewBaseEditor {
*/
private _maybeShowAddNew(result: ACResults<ChoiceItem>, text: string): ACResults<ChoiceItem> {
this._showAddNew = false;
if (!this._enableAddNew) {
return result;
}
const trimmedText = text.trim();
if (!this._enableAddNew || !trimmedText) { return result; }
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.items.push(addNewItem);
result.extraItems.push(addNewItem);
this._showAddNew = true;
return result;
@@ -259,6 +309,24 @@ export class ChoiceListEditor extends NewBaseEditor {
}
}
export interface GetACFilterFuncParams {
dropdownConditionCompiled: CompiledPredicateFormula;
docData: DocData;
tableId: string;
rowId: number;
}
export function buildDropdownConditionFilter(
params: GetACFilterFuncParams
): (item: ChoiceItem) => boolean {
const {dropdownConditionCompiled, docData, tableId, rowId} = params;
const table = docData.getTable(tableId);
if (!table) { throw new Error(`Table ${tableId} not found`); }
const rec = table.getRecord(rowId) || new EmptyRecordView();
return (item: ChoiceItem) => dropdownConditionCompiled({rec, choice: item.label});
}
const cssCellEditor = styled('div', `
background-color: ${theme.cellEditorBg};
font-family: var(--grist-font-family-data);

View File

@@ -605,7 +605,6 @@ const cssButtonRow = styled('div', `
gap: 8px;
display: flex;
margin-top: 8px;
margin-bottom: 16px;
`);
const cssDeleteButton = styled('div', `

View File

@@ -3,6 +3,7 @@ import {
FormOptionsSortConfig,
FormSelectConfig,
} from 'app/client/components/Forms/FormConfig';
import {DropdownConditionConfig} from 'app/client/components/DropdownConditionConfig';
import {makeT} from 'app/client/lib/localization';
import {DataRowModel} from 'app/client/models/DataRowModel';
import {ViewFieldRec} from 'app/client/models/entities/ViewFieldRec';
@@ -82,11 +83,15 @@ export class ChoiceTextBox extends NTextBox {
return [
super.buildConfigDom(),
this.buildChoicesConfigDom(),
dom.create(DropdownConditionConfig, this.field),
];
}
public buildTransformConfigDom() {
return this.buildConfigDom();
return [
super.buildConfigDom(),
this.buildChoicesConfigDom(),
];
}
public buildFormConfigDom() {

View File

@@ -4,7 +4,8 @@ import {ColumnRec} from 'app/client/models/DocModel';
import {KoSaveableObservable} from 'app/client/models/modelUtil';
import {RuleOwner} from 'app/client/models/RuleOwner';
import {Style} from 'app/client/models/Styles';
import {cssFieldFormula} from 'app/client/ui/FieldConfig';
import {buildHighlightedCode} from 'app/client/ui/CodeHighlight';
import {cssFieldFormula} from 'app/client/ui/RightPanelStyles';
import {withInfoTooltip} from 'app/client/ui/tooltips';
import {textButton} from 'app/client/ui2018/buttons';
import {ColorOption, colorSelect} from 'app/client/ui2018/ColorSelect';
@@ -180,10 +181,11 @@ export class ConditionalStyle extends Disposable {
column: ColumnRec,
hasError: Observable<boolean>
) {
return cssFieldFormula(
return dom.create(buildHighlightedCode,
formula,
{ gristTheme: this._gristDoc.currentTheme, maxLines: 1 },
{ maxLines: 1 },
dom.cls('formula_field_sidepane'),
dom.cls(cssFieldFormula.className),
dom.cls(cssErrorBorder.className, hasError),
{ tabIndex: '-1' },
dom.on('focus', (_, refElem) => {

View File

@@ -10,8 +10,8 @@ import {dom, styled} from 'grainjs';
export function createMobileButtons(commands: IEditorCommandGroup) {
// TODO A better check may be to detect a physical keyboard or touch support.
return isDesktop() ? null : [
cssCancelBtn(cssIconWrap(cssFinishIcon('CrossSmall')), dom.on('click', commands.fieldEditCancel)),
cssSaveBtn(cssIconWrap(cssFinishIcon('Tick')), dom.on('click', commands.fieldEditSaveHere)),
cssCancelBtn(cssIconWrap(cssFinishIcon('CrossSmall')), dom.on('mousedown', commands.fieldEditCancel)),
cssSaveBtn(cssIconWrap(cssFinishIcon('Tick')), dom.on('mousedown', commands.fieldEditSaveHere)),
];
}

View File

@@ -9,12 +9,13 @@ import {ColumnRec, ViewFieldRec} from 'app/client/models/DocModel';
import {ChatMessage} from 'app/client/models/entities/ColumnRec';
import {HAS_FORMULA_ASSISTANT, WHICH_FORMULA_ASSISTANT} from 'app/client/models/features';
import {getLoginOrSignupUrl, urlState} from 'app/client/models/gristUrlState';
import {buildHighlightedCode} from 'app/client/ui/CodeHighlight';
import {buildCodeHighlighter, buildHighlightedCode} from 'app/client/ui/CodeHighlight';
import {autoGrow} from 'app/client/ui/forms';
import {sanitizeHTML} from 'app/client/ui/sanitizeHTML';
import {createUserImage} from 'app/client/ui/UserImage';
import {basicButton, bigPrimaryButtonLink, primaryButton} from 'app/client/ui2018/buttons';
import {theme, vars} from 'app/client/ui2018/cssVars';
import {gristThemeObs} from 'app/client/ui2018/theme';
import {icon} from 'app/client/ui2018/icons';
import {cssLink} from 'app/client/ui2018/links';
import {loadingDots} from 'app/client/ui2018/loaders';
@@ -1009,26 +1010,19 @@ class ChatHistory extends Disposable {
* Renders the message as markdown if possible, otherwise as a code block.
*/
private _render(message: string, ...args: DomElementArg[]) {
const doc = this._options.gristDoc;
if (this.supportsMarkdown()) {
return dom('div',
(el) => subscribeElem(el, doc.currentTheme, () => {
(el) => subscribeElem(el, gristThemeObs(), async () => {
const highlightCode = await buildCodeHighlighter({maxLines: 60});
const content = sanitizeHTML(marked(message, {
highlight: (code) => {
const codeBlock = buildHighlightedCode(code, {
gristTheme: doc.currentTheme,
maxLines: 60,
});
return codeBlock.innerHTML;
},
highlight: (code) => highlightCode(code)
}));
el.innerHTML = content;
}),
...args
);
} else {
return buildHighlightedCode(message, {
gristTheme: doc.currentTheme,
return dom.create(buildHighlightedCode, message, {
maxLines: 100,
});
}

View File

@@ -74,15 +74,13 @@ export class FormulaEditor extends NewBaseEditor {
this._aceEditor = AceEditor.create({
// A bit awkward, but we need to assume calcSize is not used until attach() has been called
// and _editorPlacement created.
column: options.column,
calcSize: this._calcSize.bind(this),
gristDoc: options.gristDoc,
saveValueOnBlurEvent: !options.readonly,
editorState : this.editorState,
readonly: options.readonly
readonly: options.readonly,
getSuggestions: this._getSuggestions.bind(this),
});
// For editable editor we will grab the cursor when we are in the formula editing mode.
const cursorCommands = options.readonly ? {} : { setCursor: this._onSetCursor };
const isActive = Computed.create(this, use => Boolean(use(editingFormula)));
@@ -201,10 +199,7 @@ export class FormulaEditor extends NewBaseEditor {
cssFormulaEditor.cls('-detached', this.isDetached),
dom('div.formula_editor.formula_field_edit', testId('formula-editor'),
this._aceEditor.buildDom((aceObj: any) => {
aceObj.setFontSize(11);
aceObj.setHighlightActiveLine(false);
aceObj.getSession().setUseWrapMode(false);
aceObj.renderer.setPadding(0);
initializeAceOptions(aceObj);
const val = initialValue;
const pos = Math.min(options.cursorPos, val.length);
this._aceEditor.setValue(val, pos);
@@ -405,6 +400,17 @@ export class FormulaEditor extends NewBaseEditor {
return result;
}
private _getSuggestions(prefix: string) {
const section = this.options.gristDoc.viewModel.activeSection();
// If section is disposed or is pointing to an empty row, don't try to autocomplete.
if (!section?.getRowId()) { return []; }
const tableId = section.table().tableId();
const columnId = this.options.column.colId();
const rowId = section.activeRowId();
return this.options.gristDoc.docComm.autocomplete(prefix, tableId, columnId, rowId);
}
// TODO: update regexes to unicode?
private _onSetCursor(row?: DataRowModel, col?: ViewFieldRec) {
// Don't do anything when we are readonly.
@@ -714,6 +720,13 @@ export function createFormulaErrorObs(owner: MultiHolder, gristDoc: GristDoc, or
return errorMessage;
}
export function initializeAceOptions(aceObj: any) {
aceObj.setFontSize(11);
aceObj.setHighlightActiveLine(false);
aceObj.getSession().setUseWrapMode(false);
aceObj.renderer.setPadding(0);
}
const cssCollapseIcon = styled(icon, `
margin: -3px 4px 0 4px;
--icon-color: ${colors.slate};

View File

@@ -3,6 +3,7 @@ import {
FormOptionsSortConfig,
FormSelectConfig
} from 'app/client/components/Forms/FormConfig';
import {DropdownConditionConfig} from 'app/client/components/DropdownConditionConfig';
import {makeT} from 'app/client/lib/localization';
import {DataRowModel} from 'app/client/models/DataRowModel';
import {TableRec} from 'app/client/models/DocModel';
@@ -55,6 +56,7 @@ export class Reference extends NTextBox {
public buildConfigDom() {
return [
this.buildTransformConfigDom(),
dom.create(DropdownConditionConfig, this.field),
cssLabel(t('CELL FORMAT')),
super.buildConfigDom(),
];

View File

@@ -11,7 +11,6 @@ import { nocaseEqual, ReferenceUtils } from 'app/client/lib/ReferenceUtils';
import { undef } from 'app/common/gutil';
import { styled } from 'grainjs';
/**
* A ReferenceEditor offers an autocomplete of choices from the referenced table.
*/
@@ -28,7 +27,12 @@ export class ReferenceEditor extends NTextEditor {
this._utils = new ReferenceUtils(options.field, docData);
const vcol = this._utils.visibleColModel;
this._enableAddNew = vcol && !vcol.isRealFormula() && !!vcol.colId();
this._enableAddNew = (
vcol &&
!vcol.isRealFormula() &&
!!vcol.colId() &&
!this._utils.hasDropdownCondition
);
// Decorate the editor to look like a reference column value (with a "link" icon).
// But not on readonly mode - here we will reuse default decoration
@@ -65,7 +69,8 @@ export class ReferenceEditor extends NTextEditor {
// don't create autocomplete for readonly mode
if (this.options.readonly) { return; }
this._autocomplete = this.autoDispose(new Autocomplete<ICellItem>(this.textInput, {
menuCssClass: menuCssClass + ' ' + cssRefList.className,
menuCssClass: `${menuCssClass} ${cssRefList.className} test-autocomplete`,
buildNoItemsMessage: () => this._utils.buildNoItemsMessage(),
search: this._doSearch.bind(this),
renderItem: this._renderItem.bind(this),
getItemText: (item) => item.text,
@@ -110,7 +115,7 @@ export class ReferenceEditor extends NTextEditor {
* Also see: prepForSave.
*/
private async _doSearch(text: string): Promise<ACResults<ICellItem>> {
const result = this._utils.autocompleteSearch(text);
const result = this._utils.autocompleteSearch(text, this.options.rowId);
this._showAddNew = false;
if (!this._enableAddNew || !text) { return result; }
@@ -120,7 +125,7 @@ export class ReferenceEditor extends NTextEditor {
return result;
}
result.items.push({rowId: 'new', text, cleanText});
result.extraItems.push({rowId: 'new', text, cleanText});
this._showAddNew = true;
return result;

View File

@@ -59,10 +59,16 @@ export class ReferenceListEditor extends NewBaseEditor {
this._utils = new ReferenceUtils(options.field, docData);
const vcol = this._utils.visibleColModel;
this._enableAddNew = vcol && !vcol.isRealFormula() && !!vcol.colId();
this._enableAddNew = (
vcol &&
!vcol.isRealFormula() &&
!!vcol.colId() &&
!this._utils.hasDropdownCondition
);
const acOptions: IAutocompleteOptions<ReferenceItem> = {
menuCssClass: `${menuCssClass} ${cssRefList.className}`,
menuCssClass: `${menuCssClass} ${cssRefList.className} test-autocomplete`,
buildNoItemsMessage: () => this._utils.buildNoItemsMessage(),
search: this._doSearch.bind(this),
renderItem: this._renderItem.bind(this),
getItemText: (item) => item.text,
@@ -166,12 +172,14 @@ export class ReferenceListEditor extends NewBaseEditor {
}
public getCellValue(): CellValue {
const rowIds = this._tokenField.tokensObs.get().map(t => typeof t.rowId === 'number' ? t.rowId : t.text);
const rowIds = this._tokenField.tokensObs.get()
.map(token => typeof token.rowId === 'number' ? token.rowId : token.text);
return encodeObject(rowIds);
}
public getTextValue(): string {
const rowIds = this._tokenField.tokensObs.get().map(t => typeof t.rowId === 'number' ? String(t.rowId) : t.text);
const rowIds = this._tokenField.tokensObs.get()
.map(token => typeof token.rowId === 'number' ? String(token.rowId) : token.text);
return csvEncodeRow(rowIds, {prettier: true});
}
@@ -184,19 +192,19 @@ export class ReferenceListEditor extends NewBaseEditor {
*/
public async prepForSave() {
const tokens = this._tokenField.tokensObs.get();
const newValues = tokens.filter(t => t.rowId === 'new');
const newValues = tokens.filter(({rowId})=> rowId === 'new');
if (newValues.length === 0) { return; }
// Add the new items to the referenced table.
const colInfo = {[this._utils.visibleColId]: newValues.map(t => t.text)};
const colInfo = {[this._utils.visibleColId]: newValues.map(({text}) => text)};
const rowIds = await this._utils.tableData.sendTableAction(
["BulkAddRecord", new Array(newValues.length).fill(null), colInfo]
);
// Update the TokenField tokens with the returned row ids.
let i = 0;
const newTokens = tokens.map(t => {
return t.rowId === 'new' ? new ReferenceItem(t.text, rowIds[i++]) : t;
const newTokens = tokens.map(token => {
return token.rowId === 'new' ? new ReferenceItem(token.text, rowIds[i++]) : token;
});
this._tokenField.setTokens(newTokens);
}
@@ -254,11 +262,12 @@ export class ReferenceListEditor extends NewBaseEditor {
* Also see: prepForSave.
*/
private async _doSearch(text: string): Promise<ACResults<ReferenceItem>> {
const {items, selectIndex, highlightFunc} = this._utils.autocompleteSearch(text);
const {items, selectIndex, highlightFunc} = this._utils.autocompleteSearch(text, this.options.rowId);
const result: ACResults<ReferenceItem> = {
selectIndex,
highlightFunc,
items: items.map(i => new ReferenceItem(i.text, i.rowId))
items: items.map(i => new ReferenceItem(i.text, i.rowId)),
extraItems: [],
};
this._showAddNew = false;
@@ -269,7 +278,7 @@ export class ReferenceListEditor extends NewBaseEditor {
return result;
}
result.items.push(new ReferenceItem(text, 'new'));
result.extraItems.push(new ReferenceItem(text, 'new'));
this._showAddNew = true;
return result;