mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(core) updates from grist-core
This commit is contained in:
commit
85f1040439
@ -51,7 +51,7 @@ export class DropdownConditionConfig extends Disposable {
|
|||||||
|
|
||||||
const {recColIds = [], choiceColIds = []} = properties;
|
const {recColIds = [], choiceColIds = []} = properties;
|
||||||
const columns = use(this._columns);
|
const columns = use(this._columns);
|
||||||
const validRecColIds = new Set(columns.map((({colId}) => use(colId))));
|
const validRecColIds = new Set(['id', ...columns.map((({colId}) => use(colId)))]);
|
||||||
const invalidRecColIds = recColIds.filter(colId => !validRecColIds.has(colId));
|
const invalidRecColIds = recColIds.filter(colId => !validRecColIds.has(colId));
|
||||||
if (invalidRecColIds.length > 0) {
|
if (invalidRecColIds.length > 0) {
|
||||||
return t('Invalid columns: {{colIds}}', {colIds: invalidRecColIds.join(', ')});
|
return t('Invalid columns: {{colIds}}', {colIds: invalidRecColIds.join(', ')});
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import * as css from 'app/client/components/FormRendererCss';
|
import * as css from 'app/client/components/FormRendererCss';
|
||||||
|
import {makeT} from 'app/client/lib/localization';
|
||||||
import {FormField} from 'app/client/ui/FormAPI';
|
import {FormField} from 'app/client/ui/FormAPI';
|
||||||
import {sanitizeHTML} from 'app/client/ui/sanitizeHTML';
|
import {sanitizeHTML} from 'app/client/ui/sanitizeHTML';
|
||||||
import {dropdownWithSearch} from 'app/client/ui/searchDropdown';
|
import {dropdownWithSearch} from 'app/client/ui/searchDropdown';
|
||||||
@ -11,6 +12,8 @@ import {IPopupOptions, PopupControl} from 'popweasel';
|
|||||||
|
|
||||||
const testId = makeTestId('test-form-');
|
const testId = makeTestId('test-form-');
|
||||||
|
|
||||||
|
const t = makeT('FormRenderer');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A node in a recursive, tree-like hierarchy comprising the layout of a form.
|
* A node in a recursive, tree-like hierarchy comprising the layout of a form.
|
||||||
*/
|
*/
|
||||||
@ -165,7 +168,7 @@ class SubmitRenderer extends FormRenderer {
|
|||||||
css.error(dom.text(use => use(this.context.error) ?? '')),
|
css.error(dom.text(use => use(this.context.error) ?? '')),
|
||||||
css.submitButtons(
|
css.submitButtons(
|
||||||
css.resetButton(
|
css.resetButton(
|
||||||
'Reset',
|
t('Reset'),
|
||||||
dom.boolAttr('disabled', this.context.disabled),
|
dom.boolAttr('disabled', this.context.disabled),
|
||||||
{type: 'button'},
|
{type: 'button'},
|
||||||
dom.on('click', () => {
|
dom.on('click', () => {
|
||||||
@ -182,7 +185,7 @@ class SubmitRenderer extends FormRenderer {
|
|||||||
dom.boolAttr('disabled', this.context.disabled),
|
dom.boolAttr('disabled', this.context.disabled),
|
||||||
{
|
{
|
||||||
type: 'submit',
|
type: 'submit',
|
||||||
value: this.context.rootLayoutNode.submitText || 'Submit',
|
value: this.context.rootLayoutNode.submitText || t('Submit'),
|
||||||
},
|
},
|
||||||
dom.on('click', () => validateRequiredLists()),
|
dom.on('click', () => validateRequiredLists()),
|
||||||
)
|
)
|
||||||
@ -356,7 +359,7 @@ class DateTimeRenderer extends TextRenderer {
|
|||||||
protected inputType = 'datetime-local';
|
protected inputType = 'datetime-local';
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SELECT_PLACEHOLDER = 'Select...';
|
export const selectPlaceholder = () => t('Select...');
|
||||||
|
|
||||||
class ChoiceRenderer extends BaseFieldRenderer {
|
class ChoiceRenderer extends BaseFieldRenderer {
|
||||||
protected value: Observable<string>;
|
protected value: Observable<string>;
|
||||||
@ -417,7 +420,7 @@ class ChoiceRenderer extends BaseFieldRenderer {
|
|||||||
this._selectElement = css.select(
|
this._selectElement = css.select(
|
||||||
{name: this.name(), required: this.field.options.formRequired},
|
{name: this.name(), required: this.field.options.formRequired},
|
||||||
dom.on('input', (_e, elem) => this.value.set(elem.value)),
|
dom.on('input', (_e, elem) => this.value.set(elem.value)),
|
||||||
dom('option', {value: ''}, SELECT_PLACEHOLDER),
|
dom('option', {value: ''}, selectPlaceholder()),
|
||||||
this._choices.map((choice) => dom('option',
|
this._choices.map((choice) => dom('option',
|
||||||
{value: choice},
|
{value: choice},
|
||||||
dom.prop('selected', use => use(this.value) === choice),
|
dom.prop('selected', use => use(this.value) === choice),
|
||||||
@ -434,18 +437,18 @@ class ChoiceRenderer extends BaseFieldRenderer {
|
|||||||
),
|
),
|
||||||
dom.maybe(use => !use(isXSmallScreenObs()), () =>
|
dom.maybe(use => !use(isXSmallScreenObs()), () =>
|
||||||
css.searchSelect(
|
css.searchSelect(
|
||||||
dom('div', dom.text(use => use(this.value) || SELECT_PLACEHOLDER)),
|
dom('div', dom.text(use => use(this.value) || selectPlaceholder())),
|
||||||
dropdownWithSearch<string>({
|
dropdownWithSearch<string>({
|
||||||
action: (value) => this.value.set(value),
|
action: (value) => this.value.set(value),
|
||||||
options: () => [
|
options: () => [
|
||||||
{label: SELECT_PLACEHOLDER, value: '', placeholder: true},
|
{label: selectPlaceholder(), value: '', placeholder: true},
|
||||||
...this._choices.map((choice) => ({
|
...this._choices.map((choice) => ({
|
||||||
label: choice,
|
label: choice,
|
||||||
value: choice,
|
value: choice,
|
||||||
}),
|
}),
|
||||||
)],
|
)],
|
||||||
onClose: () => { setTimeout(() => this._selectElement.focus()); },
|
onClose: () => { setTimeout(() => this._selectElement.focus()); },
|
||||||
placeholder: 'Search',
|
placeholder: t('Search'),
|
||||||
acOptions: {maxResults: 1000, keepOrder: true, showEmptyItems: true},
|
acOptions: {maxResults: 1000, keepOrder: true, showEmptyItems: true},
|
||||||
popupOptions: {
|
popupOptions: {
|
||||||
trigger: [
|
trigger: [
|
||||||
@ -757,7 +760,7 @@ class RefRenderer extends BaseFieldRenderer {
|
|||||||
dom.on('input', (_e, elem) => this.value.set(elem.value)),
|
dom.on('input', (_e, elem) => this.value.set(elem.value)),
|
||||||
dom('option',
|
dom('option',
|
||||||
{value: ''},
|
{value: ''},
|
||||||
SELECT_PLACEHOLDER,
|
selectPlaceholder(),
|
||||||
dom.prop('selected', use => use(this.value) === ''),
|
dom.prop('selected', use => use(this.value) === ''),
|
||||||
),
|
),
|
||||||
this._choices.map((choice) => dom('option',
|
this._choices.map((choice) => dom('option',
|
||||||
@ -778,12 +781,12 @@ class RefRenderer extends BaseFieldRenderer {
|
|||||||
css.searchSelect(
|
css.searchSelect(
|
||||||
dom('div', dom.text(use => {
|
dom('div', dom.text(use => {
|
||||||
const choice = this._choices.find((c) => String(c[0]) === use(this.value));
|
const choice = this._choices.find((c) => String(c[0]) === use(this.value));
|
||||||
return String(choice?.[1] || SELECT_PLACEHOLDER);
|
return String(choice?.[1] || selectPlaceholder());
|
||||||
})),
|
})),
|
||||||
dropdownWithSearch<string>({
|
dropdownWithSearch<string>({
|
||||||
action: (value) => this.value.set(value),
|
action: (value) => this.value.set(value),
|
||||||
options: () => [
|
options: () => [
|
||||||
{label: SELECT_PLACEHOLDER, value: '', placeholder: true},
|
{label: selectPlaceholder(), value: '', placeholder: true},
|
||||||
...this._choices.map((choice) => ({
|
...this._choices.map((choice) => ({
|
||||||
label: String(choice[1]),
|
label: String(choice[1]),
|
||||||
value: String(choice[0]),
|
value: String(choice[0]),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {FormLayoutNode, SELECT_PLACEHOLDER} from 'app/client/components/FormRenderer';
|
import {FormLayoutNode, selectPlaceholder} from 'app/client/components/FormRenderer';
|
||||||
import {buildEditor} from 'app/client/components/Forms/Editor';
|
import {buildEditor} from 'app/client/components/Forms/Editor';
|
||||||
import {FormView} from 'app/client/components/Forms/FormView';
|
import {FormView} from 'app/client/components/Forms/FormView';
|
||||||
import {BoxModel, ignoreClick} from 'app/client/components/Forms/Model';
|
import {BoxModel, ignoreClick} from 'app/client/components/Forms/Model';
|
||||||
@ -406,7 +406,7 @@ class ChoiceModel extends Question {
|
|||||||
ignoreClick,
|
ignoreClick,
|
||||||
dom.prop('name', use => use(use(this.field).colId)),
|
dom.prop('name', use => use(use(this.field).colId)),
|
||||||
dom('option',
|
dom('option',
|
||||||
SELECT_PLACEHOLDER,
|
selectPlaceholder(),
|
||||||
{value: ''},
|
{value: ''},
|
||||||
),
|
),
|
||||||
dom.forEach(this.choices, (choice) => dom('option',
|
dom.forEach(this.choices, (choice) => dom('option',
|
||||||
@ -616,7 +616,7 @@ class RefModel extends RefListModel {
|
|||||||
ignoreClick,
|
ignoreClick,
|
||||||
dom.prop('name', this.model.colId),
|
dom.prop('name', this.model.colId),
|
||||||
dom('option',
|
dom('option',
|
||||||
SELECT_PLACEHOLDER,
|
selectPlaceholder(),
|
||||||
{value: ''},
|
{value: ''},
|
||||||
),
|
),
|
||||||
dom.forEach(this.options, ({label, value}) => dom('option',
|
dom.forEach(this.options, ({label, value}) => dom('option',
|
||||||
|
@ -172,7 +172,7 @@ export function showTipPopup(
|
|||||||
cssBehavioralPromptHeader(
|
cssBehavioralPromptHeader(
|
||||||
cssHeaderIconAndText(
|
cssHeaderIconAndText(
|
||||||
icon('Idea'),
|
icon('Idea'),
|
||||||
cssHeaderText('TIP'),
|
cssHeaderText(t('TIP')),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
cssBehavioralPromptBody(
|
cssBehavioralPromptBody(
|
||||||
|
@ -309,7 +309,7 @@ class OnBoardingPopupsCtl extends Disposable {
|
|||||||
),
|
),
|
||||||
Buttons(
|
Buttons(
|
||||||
bigBasicButton(
|
bigBasicButton(
|
||||||
'Previous', testId('previous'),
|
t('Previous'), testId('previous'),
|
||||||
dom.on('click', () => this._move(-1)),
|
dom.on('click', () => this._move(-1)),
|
||||||
dom.prop('disabled', isFirstStep),
|
dom.prop('disabled', isFirstStep),
|
||||||
{style: `margin-right: 8px; visibility: ${isFirstStep ? 'hidden' : 'visible'}`},
|
{style: `margin-right: 8px; visibility: ${isFirstStep ? 'hidden' : 'visible'}`},
|
||||||
|
@ -88,7 +88,7 @@ export type RemoveTable = ['RemoveTable', string];
|
|||||||
export type RenameTable = ['RenameTable', string, string];
|
export type RenameTable = ['RenameTable', string, string];
|
||||||
```
|
```
|
||||||
|
|
||||||
Data actions take a numeric `rowId` (or a list of them, for “bulk” actions) and a set of values:
|
Data actions take a string `tableId` and a numeric `rowId` (or a list of them, for “bulk” actions) and a set of values:
|
||||||
|
|
||||||
```
|
```
|
||||||
export interface ColValues { [colId: string]: CellValue; }
|
export interface ColValues { [colId: string]: CellValue; }
|
||||||
|
@ -157,11 +157,11 @@ describe('DropdownConditionEditor', function () {
|
|||||||
'choice\n.Supervisor\n '
|
'choice\n.Supervisor\n '
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
await gu.sendKeys('.Role == "Supervisor" and $Role != "Supervisor"', Key.ENTER);
|
await gu.sendKeys('.Role == "Supervisor" and $Role != "Supervisor" and $id != 2', Key.ENTER);
|
||||||
await gu.waitForServer();
|
await gu.waitForServer();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await driver.find('.test-field-dropdown-condition .ace_line').getAttribute('textContent'),
|
await driver.find('.test-field-dropdown-condition .ace_line').getAttribute('textContent'),
|
||||||
'choice.Role == "Supervisor" and $Role != "Supervisor"\n'
|
'choice.Role == "Supervisor" and $Role != "Supervisor" and $id != 2\n'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check that autocomplete values are filtered.
|
// Check that autocomplete values are filtered.
|
||||||
@ -171,6 +171,23 @@ describe('DropdownConditionEditor', function () {
|
|||||||
'Marie Ziyad',
|
'Marie Ziyad',
|
||||||
]);
|
]);
|
||||||
await gu.sendKeys(Key.ESCAPE);
|
await gu.sendKeys(Key.ESCAPE);
|
||||||
|
|
||||||
|
// Should be no options on row 2 because of $id != 2 part of condition.
|
||||||
|
await gu.getCell(2, 2).click();
|
||||||
|
await gu.sendKeys(Key.ENTER);
|
||||||
|
assert.deepEqual(await driver.findAll('.test-autocomplete li', (el) => el.getText()), [
|
||||||
|
]);
|
||||||
|
await gu.sendKeys(Key.ESCAPE);
|
||||||
|
|
||||||
|
// Row 3 should be like row 1.
|
||||||
|
await gu.getCell(2, 3).click();
|
||||||
|
await gu.sendKeys(Key.ENTER);
|
||||||
|
assert.deepEqual(await driver.findAll('.test-autocomplete li', (el) => el.getText()), [
|
||||||
|
'Pavan Madilyn',
|
||||||
|
'Marie Ziyad',
|
||||||
|
]);
|
||||||
|
await gu.sendKeys(Key.ESCAPE);
|
||||||
|
|
||||||
await gu.getCell(2, 4).click();
|
await gu.getCell(2, 4).click();
|
||||||
await gu.sendKeys(Key.ENTER);
|
await gu.sendKeys(Key.ENTER);
|
||||||
assert.isEmpty(await driver.findAll('.test-autocomplete li', (el) => el.getText()));
|
assert.isEmpty(await driver.findAll('.test-autocomplete li', (el) => el.getText()));
|
||||||
@ -187,7 +204,7 @@ describe('DropdownConditionEditor', function () {
|
|||||||
await gu.setType('Reference List', {apply: true});
|
await gu.setType('Reference List', {apply: true});
|
||||||
assert.equal(
|
assert.equal(
|
||||||
await driver.find('.test-field-dropdown-condition .ace_line').getAttribute('textContent'),
|
await driver.find('.test-field-dropdown-condition .ace_line').getAttribute('textContent'),
|
||||||
'choice.Role == "Supervisor" and $Role != "Supervisor"\n'
|
'choice.Role == "Supervisor" and $Role != "Supervisor" and $id != 2\n'
|
||||||
);
|
);
|
||||||
await gu.getCell(2, 4).click();
|
await gu.getCell(2, 4).click();
|
||||||
await gu.sendKeys(Key.ENTER);
|
await gu.sendKeys(Key.ENTER);
|
||||||
|
Loading…
Reference in New Issue
Block a user