diff --git a/app/client/models/ColumnToMap.ts b/app/client/models/ColumnToMap.ts index d7432cf9..988d150d 100644 --- a/app/client/models/ColumnToMap.ts +++ b/app/client/models/ColumnToMap.ts @@ -25,7 +25,9 @@ export class ColumnToMapImpl implements Required { this.description = typeof def === 'string' ? '' : (def.description ?? ''); this.optional = typeof def === 'string' ? false : (def.optional ?? false); this.type = typeof def === 'string' ? 'Any' : (def.type ?? 'Any'); - this.typeDesc = String(UserType.typeDefs[this.type]?.label ?? "any").toLowerCase(); + this.type = this.type.split(',').map(t => t.trim()).filter(Boolean).join(','); + this.typeDesc = this.type.split(',') + .map(t => String(UserType.typeDefs[t]?.label ?? "any").toLowerCase()).join(', '); this.allowMultiple = typeof def === 'string' ? false : (def.allowMultiple ?? false); } diff --git a/app/client/ui/CustomSectionConfig.ts b/app/client/ui/CustomSectionConfig.ts index 8efff470..46aa9b80 100644 --- a/app/client/ui/CustomSectionConfig.ts +++ b/app/client/ui/CustomSectionConfig.ts @@ -1,30 +1,30 @@ import {allCommands} from 'app/client/components/commands'; import {GristDoc} from 'app/client/components/GristDoc'; +import {makeTestId} from 'app/client/lib/domUtils'; import * as kf from 'app/client/lib/koForm'; import {makeT} from 'app/client/lib/localization'; import {ColumnToMapImpl} from 'app/client/models/ColumnToMap'; import {ColumnRec, ViewSectionRec} from 'app/client/models/DocModel'; import {reportError} from 'app/client/models/errors'; import {cssHelp, cssLabel, cssRow, cssSeparator} from 'app/client/ui/RightPanelStyles'; +import {hoverTooltip} from 'app/client/ui/tooltips'; import {cssDragRow, cssFieldEntry, cssFieldLabel} from 'app/client/ui/VisibleFieldsConfig'; import {basicButton, primaryButton, textButton} from 'app/client/ui2018/buttons'; import {theme, vars} from 'app/client/ui2018/cssVars'; import {cssDragger} from 'app/client/ui2018/draggableList'; import {textInput} from 'app/client/ui2018/editableLabel'; -import {IconName} from 'app/client/ui2018/IconList'; import {icon} from 'app/client/ui2018/icons'; import {cssLink} from 'app/client/ui2018/links'; -import {IOptionFull, menu, menuItem, menuText, select} from 'app/client/ui2018/menus'; +import {cssOptionLabel, IOption, IOptionFull, menu, menuItem, menuText, select} from 'app/client/ui2018/menus'; import {AccessLevel, ICustomWidget, isSatisfied} from 'app/common/CustomWidget'; import {GristLoadConfig} from 'app/common/gristUrls'; -import {unwrap} from 'app/common/gutil'; +import {not, unwrap} from 'app/common/gutil'; import { bundleChanges, Computed, Disposable, dom, fromKo, - makeTestId, MultiHolder, Observable, styled, @@ -62,12 +62,50 @@ class ColumnPicker extends Disposable { const value = use(this._value); return Array.isArray(value) ? null : value; }); - properValue.onWrite(value => this._value.set(value)); - const options = Computed.create(this, use => { + properValue.onWrite(value => this._value.set(value || null)); + + const canBeMapped = Computed.create(this, use => { return use(this._section.columns) - .filter(col => this._column.canByMapped(use(col.pureType))) - .map((col) => ({value: col.getRowId(), label: use(col.label), icon: 'FieldColumn' as IconName})); + .filter(col => this._column.canByMapped(use(col.pureType))); + }); + + // This is a HACK, to refresh options only when the menu is opened (or closed) + // and not to track down all the dependencies. Otherwise the select menu won't + // be hidden when option is selected - there is a bug that prevents it from closing + // when list of options is changed. + const refreshTrigger = Observable.create(this, false); + + const options = Computed.create(this, use => { + void use(refreshTrigger); + + const columnsAsOptions: IOption[] = use(canBeMapped) + .map((col) => ({ + value: col.getRowId(), + label: col.label.peek(), + icon: 'FieldColumn', + })); + + // For optional mappings, add 'Blank' option but only if the value is set. + // This option will allow to clear the selection. + if (this._column.optional && properValue.get()) { + columnsAsOptions.push({ + value: 0, + // Another hack. Select doesn't allow to have different label for blank option and the default text. + // So we will render this label ourselves later using `renderOptionArgs`. + label: '', + }); + } + return columnsAsOptions; + }); + + const isDisabled = Computed.create(this, use => { + return use(canBeMapped).length === 0; }); + + const defaultLabel = this._column.typeDesc != "any" + ? t("Pick a {{columnType}} column", {"columnType": this._column.typeDesc}) + : t("Pick a column"); + return [ cssLabel( this._column.title, @@ -78,18 +116,49 @@ class ColumnPicker extends Disposable { this._column.description, testId('help-for-' + this._column.name), ) : null, - cssRow( - select( - properValue, - options, - { - defaultLabel: this._column.typeDesc != "any" - ? t("Pick a {{columnType}} column", {"columnType": this._column.typeDesc}) - : t("Pick a column") - } - ), - testId('mapping-for-' + this._column.name), - ), + dom.maybe(not(isDisabled), () => [ + cssRow( + dom.update( + select( + properValue, + options, + { + defaultLabel, + renderOptionArgs : (opt) => { + // If there is a label, render it. + // Otherwise show the 'Clear selection' label as a greyed out text. + // This is the continuation of the hack from above - were we added an option + // without a label. + return (opt.label) ? null : [ + cssBlank(t("Clear selection")), + testId('clear-selection'), + ]; + } + } + ), + dom.on('click', () => { + // When the menu is opened or closed, refresh the options. + refreshTrigger.set(!refreshTrigger.get()); + }) + ), + testId('mapping-for-' + this._column.name), + testId('enabled'), + ), + ]), + dom.maybe(isDisabled, () => [ + cssRow( + cssDisabledSelect( + Observable.create(this, null), + [], { + disabled: true, + defaultLabel: t("No {{columnType}} columns in table.", {"columnType": this._column.typeDesc}) + } + ), + hoverTooltip(t("No {{columnType}} columns in table.", {"columnType": this._column.typeDesc})), + testId('mapping-for-' + this._column.name), + testId('disabled'), + ), + ]), ]; } } @@ -114,16 +183,30 @@ class ColumnListPicker extends Disposable { }); } private _buildAddColumn() { + + const owner = MultiHolder.create(null); + + const notMapped = Computed.create(owner, use => { + const value = use(this._value) || []; + const mapped = !Array.isArray(value) ? [] : value; + return this._section.columns().filter(col => !mapped.includes(use(col.id))); + }); + + const typedColumns = Computed.create(owner, use => { + return use(notMapped).filter(this._typeFilter(use)); + }); + return [ cssRow( + dom.autoDispose(owner), cssAddMapping( cssAddIcon('Plus'), t("Add") + ' ' + this._column.title, + dom.cls('disabled', use => use(notMapped).length === 0), + testId('disabled', use => use(notMapped).length === 0), menu(() => { - const otherColumns = this._getNotMappedColumns(); - const typedColumns = otherColumns.filter(this._typeFilter()); - const wrongTypeCount = otherColumns.length - typedColumns.length; + const wrongTypeCount = notMapped.get().length - typedColumns.get().length; return [ - ...typedColumns + ...typedColumns.get() .map((col) => menuItem( () => this._addColumn(col), col.label.peek(), @@ -145,7 +228,8 @@ class ColumnListPicker extends Disposable { } // Helper method for filtering columns that can be picked by the widget. - private _typeFilter = (use = unwrap) => (col: ColumnRec) => this._column.canByMapped(use(col.pureType)); + private _typeFilter = (use = unwrap) => (col: ColumnRec|null) => + !col ? false : this._column.canByMapped(use(col.pureType)); private _buildDraggableList(use: UseCBOwner) { return dom.update(kf.draggableList( @@ -159,12 +243,7 @@ class ColumnListPicker extends Disposable { } ), testId('map-list-for-' + this._column.name)); } - private _getNotMappedColumns(): ColumnRec[] { - // Get all columns. - const all = this._section.columns.peek(); - const mapped = this._list(); - return all.filter(col => !mapped.includes(col.id.peek())); - } + private _readItems(use: UseCBOwner): ColumnRec[] { let selectedRefs = (use(this._value) || []) as number[]; // Ignore if configuration was changed from what it was saved. @@ -177,6 +256,7 @@ class ColumnListPicker extends Disposable { // Remove any columns that are no longer there. return selectedRefs.map(s => columnMap.get(s)!).filter(c => Boolean(c)); } + private _renderItem(use: UseCBOwner, field: ColumnRec): any { return cssFieldEntry( cssFieldLabel( @@ -199,7 +279,7 @@ class ColumnListPicker extends Disposable { this._value.set(value); } else { let current = (this._value.get() || []) as number[]; - // Ignore if the saved value is not a number. + // Ignore if the saved value is not a number list. if (!Array.isArray(current)) { current = []; } @@ -224,8 +304,16 @@ class ColumnListPicker extends Disposable { this._value.set(current.filter(c => c != column.id.peek())); } private _addColumn(col: ColumnRec): any { - const current = this._list(); + // Helper to find column model. + const model = (id: number) => this._section.columns().find(c => c.id.peek() === id) || null; + // Get the list of currently mapped columns. + let current = this._list(); + // Add new column. current.push(col.id.peek()); + // Remove those that don't exists anymore. + current = current.filter(c => model(c)); + // And those with wrong type. + current = current.filter(c => this._typeFilter()(model(c))); this._value.set(current); } } @@ -551,8 +639,6 @@ export class CustomSectionConfig extends Disposable { this._widgets.set(wigets); } - - private _accept() { if (this._desiredAccess.get()) { this._currentAccess.set(this._desiredAccess.get()!); @@ -629,6 +715,11 @@ const cssAddMapping = styled('div', ` color: ${theme.controlHoverFg}; --icon-color: ${theme.controlHoverFg}; } + &.disabled { + color: ${theme.lightText}; + --icon-color: ${theme.lightText}; + pointer-events: none; + } `); const cssTextInput = styled(textInput, ` @@ -647,3 +738,11 @@ const cssTextInput = styled(textInput, ` color: ${theme.inputPlaceholderFg}; } `); + +const cssDisabledSelect = styled(select, ` + opacity: unset !important; +`); + +const cssBlank = styled(cssOptionLabel, ` + --grist-option-label-color: ${theme.lightText}; +`); diff --git a/app/client/ui/widgetTypesMap.ts b/app/client/ui/widgetTypesMap.ts index 9e2bc614..4a2c33d6 100644 --- a/app/client/ui/widgetTypesMap.ts +++ b/app/client/ui/widgetTypesMap.ts @@ -8,7 +8,7 @@ export const widgetTypesMap = new Map([ ['detail', {label: 'Card List', icon: 'TypeCardList'}], ['chart', {label: 'Chart', icon: 'TypeChart'}], ['custom', {label: 'Custom', icon: 'TypeCustom'}], - ['custom.calendar', {label: 'Calendar', icon: 'FieldDate'}] + ['custom.calendar', {label: 'Calendar', icon: 'TypeCalendar'}], ]); // Widget type info. diff --git a/app/client/ui2018/IconList.ts b/app/client/ui2018/IconList.ts index 84735fa6..cd9735a1 100644 --- a/app/client/ui2018/IconList.ts +++ b/app/client/ui2018/IconList.ts @@ -4,6 +4,7 @@ export type IconName = "ChartArea" | "ChartKaplan" | "ChartLine" | "ChartPie" | + "TypeCalendar" | "TypeCard" | "TypeCardList" | "TypeCell" | @@ -152,6 +153,7 @@ export const IconList: IconName[] = ["ChartArea", "ChartKaplan", "ChartLine", "ChartPie", + "TypeCalendar", "TypeCard", "TypeCardList", "TypeCell", diff --git a/app/client/ui2018/menus.ts b/app/client/ui2018/menus.ts index 88d6f0c1..7ff22fba 100644 --- a/app/client/ui2018/menus.ts +++ b/app/client/ui2018/menus.ts @@ -534,22 +534,25 @@ export const cssOptionRowIcon = styled(icon, ` } `); -const cssOptionLabel = styled('div', ` +export const cssOptionLabel = styled('div', ` white-space: nowrap; overflow: hidden; text-overflow: ellipsis; + --grist-option-label-color: ${theme.menuItemFg}; + --grist-option-label-color-sel: ${theme.menuItemSelectedFg}; + --grist-option-label-color-disabled: ${theme.menuItemDisabledFg}; .${weasel.cssMenuItem.className} & { - color: ${theme.menuItemFg}; + color: var(--grist-option-label-color); } .${weasel.cssMenuItem.className}-sel & { - color: ${theme.menuItemSelectedFg}; + color: var(--grist-option-label-color-sel); background-color: ${theme.menuItemSelectedBg}; } .${weasel.cssMenuItem.className}.disabled & { - color: ${theme.menuItemDisabledFg}; + color: var(--grist-option-label-color-disabled); } `); diff --git a/static/icons/icons.css b/static/icons/icons.css index 85757bb3..d48fc32d 100644 --- a/static/icons/icons.css +++ b/static/icons/icons.css @@ -5,6 +5,7 @@ --icon-ChartKaplan: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTciIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTYuMTcxMjgwMjgsMS40MDgzMTgwMSBMMC45MTM0OTQ4MSwxLjQwODMxODAxIEMwLjY2ODg4NDI2MywxLjQwODMxODAxIDAuNDcwNTg4MjM1LDEuMTk3NjI4NDkgMC40NzA1ODgyMzUsMC45Mzc3Mjk3NzkgQzAuNDcwNTg4MjM1LDAuNjc3ODMxMDc0IDAuNjY4ODg0MjYzLDAuNDY3MTQxNTQ0IDAuOTEzNDk0ODEsMC40NjcxNDE1NDQgTDcuMDU3MDkzNDMsMC40NjcxNDE1NDQgQzcuMzMzMjM1OCwwLjQ2NzE0MTU0NCA3LjU1NzA5MzQzLDAuNjkwOTk5MTY5IDcuNTU3MDkzNDMsMC45NjcxNDE1NDQgTDcuNTU3MDkzNDMsMi43OTA2NzA5NiBDNy41NTcwOTM0MywzLjA2NjgxMzMzIDcuNzgwOTUxMDUsMy4yOTA2NzA5NiA4LjA1NzA5MzQzLDMuMjkwNjcwOTYgTDExLjQ4NjE1OTIsMy4yOTA2NzA5NiBDMTEuNzYyMzAxNSwzLjI5MDY3MDk2IDExLjk4NjE1OTIsMy41MTQ1Mjg1OCAxMS45ODYxNTkyLDMuNzkwNjcwOTYgTDExLjk4NjE1OTIsNS42MTQyMDAzNyBDMTEuOTg2MTU5Miw1Ljg5MDM0Mjc0IDEyLjIxMDAxNjgsNi4xMTQyMDAzNyAxMi40ODYxNTkyLDYuMTE0MjAwMzcgTDE1LjA4NjUwNTIsNi4xMTQyMDAzNyBDMTUuMzMxMTE1Nyw2LjExNDIwMDM3IDE1LjUyOTQxMTgsNi4zMjQ4ODk5IDE1LjUyOTQxMTgsNi41ODQ3ODg2IEMxNS41Mjk0MTE4LDYuODQ0Njg3MzEgMTUuMzMxMTE1Nyw3LjA1NTM3Njg0IDE1LjA4NjUwNTIsNy4wNTUzNzY4NCBMMTEuNjAwMzQ2LDcuMDU1Mzc2ODQgQzExLjMyNDIwMzYsNy4wNTUzNzY4NCAxMS4xMDAzNDYsNi44MzE1MTkyMSAxMS4xMDAzNDYsNi41NTUzNzY4NCBMMTEuMTAwMzQ2LDQuNzMxODQ3NDMgQzExLjEwMDM0Niw0LjQ1NTcwNTA1IDEwLjg3NjQ4ODQsNC4yMzE4NDc0MyAxMC42MDAzNDYsNC4yMzE4NDc0MyBMNy4xNzEyODAyOCw0LjIzMTg0NzQzIEM2Ljg5NTEzNzksNC4yMzE4NDc0MyA2LjY3MTI4MDI4LDQuMDA3OTg5OCA2LjY3MTI4MDI4LDMuNzMxODQ3NDMgTDYuNjcxMjgwMjgsMS45MDgzMTgwMSBDNi42NzEyODAyOCwxLjYzMjE3NTY0IDYuNDQ3NDIyNjUsMS40MDgzMTgwMSA2LjE3MTI4MDI4LDEuNDA4MzE4MDEgWiBNMi43OTQxMTc2NSwzLjI5MDY3MDk2IEwwLjk0MTE3NjQ3MSwzLjI5MDY3MDk2IEMwLjY4MTI3Nzc2NSwzLjI5MDY3MDk2IDAuNDcwNTg4MjM1LDMuMDc5OTgxNDMgMC40NzA1ODgyMzUsMi44MjAwODI3MiBDMC40NzA1ODgyMzUsMi41NjAxODQwMSAwLjY4MTI3Nzc2NSwyLjM0OTQ5NDQ5IDAuOTQxMTc2NDcxLDIuMzQ5NDk0NDkgTDMuNzM1Mjk0MTIsMi4zNDk0OTQ0OSBDNC4wMTE0MzY0OSwyLjM0OTQ5NDQ5IDQuMjM1Mjk0MTIsMi41NzMzNTIxMSA0LjIzNTI5NDEyLDIuODQ5NDk0NDkgTDQuMjM1Mjk0MTIsNy40OTY1NTMzMSBDNC4yMzUyOTQxMiw3Ljc3MjY5NTY4IDQuNDU5MTUxNzQsNy45OTY1NTMzMSA0LjczNTI5NDEyLDcuOTk2NTUzMzEgTDEwLjMyMzUyOTQsNy45OTY1NTMzMSBDMTAuNTk5NjcxOCw3Ljk5NjU1MzMxIDEwLjgyMzUyOTQsOC4yMjA0MTA5MyAxMC44MjM1Mjk0LDguNDk2NTUzMzEgTDEwLjgyMzUyOTQsMTEuMjYxMjU5MiBDMTAuODIzNTI5NCwxMS41Mzc0MDE2IDExLjA0NzM4NywxMS43NjEyNTkyIDExLjMyMzUyOTQsMTEuNzYxMjU5MiBMMTMuMTQ3MDU4OCwxMS43NjEyNTkyIEMxMy40MjMyMDEyLDExLjc2MTI1OTIgMTMuNjQ3MDU4OCwxMS45ODUxMTY4IDEzLjY0NzA1ODgsMTIuMjYxMjU5MiBMMTMuNjQ3MDU4OCwxNS4wNTUzNzY4IEMxMy42NDcwNTg4LDE1LjMxNTI3NTUgMTMuNDM2MzY5MywxNS41MjU5NjUxIDEzLjE3NjQ3MDYsMTUuNTI1OTY1MSBDMTIuOTE2NTcxOSwxNS41MjU5NjUxIDEyLjcwNTg4MjQsMTUuMzE1Mjc1NSAxMi43MDU4ODI0LDE1LjA1NTM3NjggTDEyLjcwNTg4MjQsMTMuMjAyNDM1NyBDMTIuNzA1ODgyNCwxMi45MjYyOTMzIDEyLjQ4MjAyNDcsMTIuNzAyNDM1NyAxMi4yMDU4ODI0LDEyLjcwMjQzNTcgTDEwLjM4MjM1MjksMTIuNzAyNDM1NyBDMTAuMTA2MjEwNiwxMi43MDI0MzU3IDkuODgyMzUyOTQsMTIuNDc4NTc4IDkuODgyMzUyOTQsMTIuMjAyNDM1NyBMOS44ODIzNTI5NCw5LjQzNzcyOTc4IEM5Ljg4MjM1Mjk0LDkuMTYxNTg3NCA5LjY1ODQ5NTMyLDguOTM3NzI5NzggOS4zODIzNTI5NCw4LjkzNzcyOTc4IEwzLjc5NDExNzY1LDguOTM3NzI5NzggQzMuNTE3OTc1MjcsOC45Mzc3Mjk3OCAzLjI5NDExNzY1LDguNzEzODcyMTUgMy4yOTQxMTc2NSw4LjQzNzcyOTc4IEwzLjI5NDExNzY1LDMuNzkwNjcwOTYgQzMuMjk0MTE3NjUsMy41MTQ1Mjg1OCAzLjA3MDI2MDAyLDMuMjkwNjcwOTYgMi43OTQxMTc2NSwzLjI5MDY3MDk2IFoiIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIvPjwvc3ZnPg=='); --icon-ChartLine: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTMuMTkwOTgzMDEsOCBMNS4wNTI3ODY0LDQuMjc2MzkzMiBDNS4xODcyODU0NCw0LjAwNzM5NTEyIDUuNTI3MTExODEsMy45MTcxNDkzMyA1Ljc3NzM1MDEsNC4wODM5NzQ4NSBMMTEuMzMxNjQ1NCw3Ljc4NjgzODM3IEwxNS4wNjU4Nzg0LDEuMjUxOTMwNTMgQzE1LjIwMjg4MzQsMS4wMTIxNzE4MSAxNS41MDgzMTA3LDAuOTI4ODczNDQ0IDE1Ljc0ODA2OTUsMS4wNjU4Nzg0MyBDMTUuOTg3ODI4MiwxLjIwMjg4MzQxIDE2LjA3MTEyNjYsMS41MDgzMTA3NSAxNS45MzQxMjE2LDEuNzQ4MDY5NDcgTDExLjkzNDEyMTYsOC43NDgwNjk0NyBDMTEuNzkwMzk3NCw4Ljk5OTU4Njc4IDExLjQ2MzY4MjcsOS4wNzY3MTM2NiAxMS4yMjI2NDk5LDguOTE2MDI1MTUgTDUuNjkzOTE1NzksNS4yMzAyMDI0MSBMNC4xMzgyODQyNCw4LjM0MTQ2NTUxIEwxMC40NDU5NTEzLDEyLjg0Njk0MiBMMTUuMTQ2NDQ2Niw4LjE0NjQ0NjYxIEMxNS4zNDE3MDg4LDcuOTUxMTg0NDYgMTUuNjU4MjkxMiw3Ljk1MTE4NDQ2IDE1Ljg1MzU1MzQsOC4xNDY0NDY2MSBDMTYuMDQ4ODE1NSw4LjM0MTcwODc2IDE2LjA0ODgxNTUsOC42NTgyOTEyNCAxNS44NTM1NTM0LDguODUzNTUzMzkgTDEwLjg1MzU1MzQsMTMuODUzNTUzNCBDMTAuNjgwNzIyNywxNC4wMjYzODQxIDEwLjQwODI3MzMsMTQuMDQ4OTMyNyAxMC4yMDkzODA5LDEzLjkwNjg2NjcgTDMuMzM5NzY3NDcsOSBMMC41LDkgQzAuMjIzODU3NjI1LDkgMS4xMTAyMjMwMmUtMTQsOC43NzYxNDIzNyAxLjExMDIyMzAyZS0xNCw4LjUgQzEuMTEwMjIzMDJlLTE0LDguMjIzODU3NjMgMC4yMjM4NTc2MjUsOCAwLjUsOCBMMy4xOTA5ODMwMSw4IFogTTAuOTQ3MjEzNTk1LDE0LjcyMzYwNjggQzAuODIzNzE4OTcxLDE0Ljk3MDU5NiAwLjUyMzM4MjQ1MSwxNS4wNzA3MDgyIDAuMjc2MzkzMjAyLDE0Ljk0NzIxMzYgQzAuMDI5NDAzOTUzNSwxNC44MjM3MTkgLTAuMDcwNzA4MjE5OSwxNC41MjMzODI1IDAuMDUyNzg2NDA0NSwxNC4yNzYzOTMyIEwxLjgwMjc4NjQsMTAuNzc2MzkzMiBDMS45MjYyODEwMywxMC41Mjk0MDQgMi4yMjY2MTc1NSwxMC40MjkyOTE4IDIuNDczNjA2OCwxMC41NTI3ODY0IEMyLjcyMDU5NjA1LDEwLjY3NjI4MSAyLjgyMDcwODIyLDEwLjk3NjYxNzUgMi42OTcyMTM2LDExLjIyMzYwNjggTDAuOTQ3MjEzNTk1LDE0LjcyMzYwNjggWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); --icon-ChartPie: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE0LjUyODEyNzMsMTMuMjgxNjQ4NCBDMTQuMzcyNjg4LDEzLjUwOTY0OTcgMTQuMDYxOTI1OSwxMy41Njg2MjA0IDEzLjgzMzc2ODEsMTMuNDEzNDEwOSBMNy4yMTg3NjgwNyw4LjkxMzQxMDk0IEM3LjA4MTkxMjk3LDguODIwMzEyMjMgNyw4LjY2NTUxOTQ1IDcsOC41IEw3LDAuNSBDNywwLjIyMzg1NzYyNSA3LjIyMzg1NzYzLDEuOTUzOTkyNTJlLTE0IDcuNSwxLjk1Mzk5MjUyZS0xNCBDMTIuMTk0MTQyNCwxLjk1Mzk5MjUyZS0xNCAxNiwzLjgwNTg1NzYzIDE2LDguNSBDMTYsMTAuMjI4OTkzOCAxNS40ODE2NTQ5LDExLjg4Mjk5NTggMTQuNTI4MTI3MywxMy4yODE2NDg0IFogTTE1LDguNSBDMTUsNC41MjYxNTU5MyAxMS45MDg3NzY0LDEuMjczNzM0MjMgOCwxLjAxNjQwNTY0IEw4LDguMjM1NDEwODggTDEzLjk2OTUxLDEyLjI5NjMwMjEgQzE0LjYzOTUzNDIsMTEuMTU3NTgxNSAxNSw5Ljg1NTc3OTE2IDE1LDguNSBaIE0xMC42OTIxMTU2LDEzLjczMDAzNjYgQzEwLjkwOTY5NTYsMTMuNTU5OTk2NyAxMS4yMjM5MjM2LDEzLjU5ODUzNTYgMTEuMzkzOTYzNCwxMy44MTYxMTU2IEMxMS41NjQwMDMzLDE0LjAzMzY5NTYgMTEuNTI1NDY0NCwxNC4zNDc5MjM2IDExLjMwNzg4NDQsMTQuNTE3OTYzNCBDMTAuMDg2MjU5NiwxNS40NzI2NjkzIDguNTgyOTQ4NjMsMTYgNywxNiBDMy4xMzM4NTc2MywxNiA4LjQzNzY5NDk5ZS0xNCwxMi44NjYxNDI0IDguNDM3Njk0OTllLTE0LDkgQzguNDM3Njk0OTllLTE0LDUuOTQwODEwMzkgMS45ODA0ODY2OSwzLjI2MzQ4MDA1IDQuODQ2Mzc2MTUsMi4zMzgxODUyMSBDNS4xMDkxNjE0LDIuMjUzMzQxMSA1LjM5MDk3MDY4LDIuMzk3NTkwOSA1LjQ3NTgxNDc5LDIuNjYwMzc2MTUgQzUuNTYwNjU4OSwyLjkyMzE2MTQgNS40MTY0MDkxLDMuMjA0OTcwNjggNS4xNTM2MjM4NSwzLjI4OTgxNDc5IEMyLjY5Nzc1MTc3LDQuMDgyNzI5NDQgMSw2LjM3Nzg0MzI0IDEsOSBDMSwxMi4zMTM4NTc2IDMuNjg2MTQyMzcsMTUgNywxNSBDOC4zNTc3NDM0MywxNSA5LjY0NDc4MTQsMTQuNTQ4NTMzNSAxMC42OTIxMTU2LDEzLjczMDAzNjYgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); + --icon-TypeCalendar: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiI+PHBhdGggZD0iTSA1LjE0MzgxODMsMy4yMDg5OTkyIEggMTEuMTQzODE4IFYgMC43OTgxNDk3OCBjIDAsLTAuMjkwMzcxNDUgMC4yNDM2NzUsLTAuNDk4MTcxMzQgMC41LC0wLjUgMC4yNTYzMjUsLTAuMDAxODI5IDAuNSwwLjIxMzA1MDczIDAuNSwwLjUgViAzLjIwODk5OTIgaCAyLjEyOTk0MSBjIDAuODI4NDI3LDAgMS41LDAuNjcxNTcyOSAxLjUsMS41IHYgOS41ODg3Mjk4IGMgMCwwLjgyODQyNyAtMC42NzE1NzMsMS41IC0xLjUsMS41IEggMS44NzAwNTg5IGMgLTAuODI4NDI3MSwwIC0xLjQ5OTk5OTk2LC0wLjY3MTU3MyAtMS40OTk5OTk5NiwtMS41IFYgNC43MDg5OTkyIGMgMCwtMC44Mjg0MjcxIDAuNjcxNTcyODYsLTEuNSAxLjQ5OTk5OTk2LC0xLjUgSCA0LjE0MzgxODMgViAwLjc5ODE0OTc4IGMgMCwtMC4yNzYxNDIzOCAwLjIwNDQ2MTUsLTAuNTAxMTU5OTQgMC41LC0wLjUgMC4yOTU1Mzg1LDAuMDAxMTYgMC41LDAuMjIzMDM0NyAwLjUsMC41IHogTSAxLjM3MDA1ODksNy4xMTk4NDg2IEggMTQuNzczNzU5IFYgNC43MDg5OTkyIGMgMCwtMC4yNzYxNDI0IC0wLjIyMzg1OCwtMC41IC0wLjUsLTAuNSBIIDEuODcwMDU4OSBjIC0wLjI3NjE0MjMsMCAtMC41LDAuMjIzODU3NiAtMC41LDAuNSB6IG0gMTMuNDAzNzAwMSwxIEggMS4zNzAwNTg5IHYgNi4xNzc4ODA0IGMgMCwwLjI3NjE0MiAwLjIyMzg1NzcsMC41IDAuNSwwLjUgSCAxNC4yNzM3NTkgYyAwLjI3NjE0MiwwIDAuNSwtMC4yMjM4NTggMC41LC0wLjUgeiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); --icon-TypeCard: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTkuNSw5IEM5LjIyMzg1NzYzLDkgOSw4Ljc3NjE0MjM3IDksOC41IEM5LDguMjIzODU3NjMgOS4yMjM4NTc2Myw4IDkuNSw4IEwxMi41LDggQzEyLjc3NjE0MjQsOCAxMyw4LjIyMzg1NzYzIDEzLDguNSBDMTMsOC43NzYxNDIzNyAxMi43NzYxNDI0LDkgMTIuNSw5IEw5LjUsOSBaIE05LjUsMTIgQzkuMjIzODU3NjMsMTIgOSwxMS43NzYxNDI0IDksMTEuNSBDOSwxMS4yMjM4NTc2IDkuMjIzODU3NjMsMTEgOS41LDExIEwxMi41LDExIEMxMi43NzYxNDI0LDExIDEzLDExLjIyMzg1NzYgMTMsMTEuNSBDMTMsMTEuNzc2MTQyNCAxMi43NzYxNDI0LDEyIDEyLjUsMTIgTDkuNSwxMiBaIE00LjUsMyBDNC43NzYxNDIzNywzIDUsMy4yMjM4NTc2MyA1LDMuNSBDNSwzLjc3NjE0MjM3IDQuNzc2MTQyMzcsNCA0LjUsNCBMMS41LDQgQzEuMjIzODU3NjMsNCAxLDQuMjIzODU3NjMgMSw0LjUgTDEsMTMuNSBDMSwxMy43NzYxNDI0IDEuMjIzODU3NjMsMTQgMS41LDE0IEwxNC41LDE0IEMxNC43NzYxNDI0LDE0IDE1LDEzLjc3NjE0MjQgMTUsMTMuNSBMMTUsNC41IEMxNSw0LjIyMzg1NzYzIDE0Ljc3NjE0MjQsNCAxNC41LDQgTDExLjUsNCBDMTEuMjIzODU3Niw0IDExLDMuNzc2MTQyMzcgMTEsMy41IEMxMSwzLjIyMzg1NzYzIDExLjIyMzg1NzYsMyAxMS41LDMgTDE0LjUsMyBDMTUuMzI4NDI3MSwzIDE2LDMuNjcxNTcyODggMTYsNC41IEwxNiwxMy41IEMxNiwxNC4zMjg0MjcxIDE1LjMyODQyNzEsMTUgMTQuNSwxNSBMMS41LDE1IEMwLjY3MTU3Mjg3NSwxNSAxLjY2NTMzNDU0ZS0xNiwxNC4zMjg0MjcxIDAsMTMuNSBMMCw0LjUgQzEuNjY1MzM0NTRlLTE2LDMuNjcxNTcyODggMC42NzE1NzI4NzUsMyAxLjUsMyBMNC41LDMgWiBNNCw5IEw0LDExIEw2LDExIEw2LDkgTDQsOSBaIE0zLjUsOCBMNi41LDggQzYuNzc2MTQyMzcsOCA3LDguMjIzODU3NjMgNyw4LjUgTDcsMTEuNSBDNywxMS43NzYxNDI0IDYuNzc2MTQyMzcsMTIgNi41LDEyIEwzLjUsMTIgQzMuMjIzODU3NjMsMTIgMywxMS43NzYxNDI0IDMsMTEuNSBMMyw4LjUgQzMsOC4yMjM4NTc2MyAzLjIyMzg1NzYzLDggMy41LDggWiBNOSw1IEw5LDIgQzksMS40NDc3MTUyNSA4LjU1MjI4NDc1LDEgOCwxIEM3LjQ0NzcxNTI1LDEgNywxLjQ0NzcxNTI1IDcsMiBMNyw1IEw5LDUgWiBNOCwwIEM5LjEwNDU2OTUsMCAxMCwwLjg5NTQzMDUgMTAsMiBMMTAsNS41IEMxMCw1Ljc3NjE0MjM3IDkuNzc2MTQyMzcsNiA5LjUsNiBMNi41LDYgQzYuMjIzODU3NjMsNiA2LDUuNzc2MTQyMzcgNiw1LjUgTDYsMiBDNiwwLjg5NTQzMDUgNi44OTU0MzA1LDAgOCwwIFoiIGZpbGw9IiMwMDAiIGZpbGwtcnVsZT0ibm9uemVybyIvPjwvc3ZnPg=='); --icon-TypeCardList: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE0LjIzMjQwODEsMTMgTDE1LjU2NTc0MTUsMTEgTDMuNzY3NTkxODgsMTEgTDIuNDM0MjU4NTUsMTMgTDE0LjIzMjQwODEsMTMgWiBNMTQuNSwxNCBMMS41LDE0IEMxLjEwMDY1MjMxLDE0IDAuODYyNDU2NjEsMTMuNTU0OTI3MyAxLjA4Mzk3NDg1LDEzLjIyMjY0OTkgTDMuMDgzOTc0ODUsMTAuMjIyNjQ5OSBDMy4xNzY3MDc3NCwxMC4wODM1NTA2IDMuMzMyODIzNDEsMTAgMy41LDEwIEwxNi41LDEwIEMxNi44OTkzNDc3LDEwIDE3LjEzNzU0MzQsMTAuNDQ1MDcyNyAxNi45MTYwMjUxLDEwLjc3NzM1MDEgTDE0LjkxNjAyNTEsMTMuNzc3MzUwMSBDMTQuODIzMjkyMywxMy45MTY0NDk0IDE0LjY2NzE3NjYsMTQgMTQuNSwxNCBaIE0xNC4yMzI0MDgxLDMgTDE1LjU2NTc0MTUsMSBMMy43Njc1OTE4OCwxIEwyLjQzNDI1ODU1LDMgTDE0LjIzMjQwODEsMyBaIE0xNC41LDQgTDEuNSw0IEMxLjEwMDY1MjMxLDQgMC44NjI0NTY2MSwzLjU1NDkyNzI3IDEuMDgzOTc0ODUsMy4yMjI2NDk5IEwzLjA4Mzk3NDg1LDAuMjIyNjQ5OTAyIEMzLjE3NjcwNzc0LDAuMDgzNTUwNTY3NyAzLjMzMjgyMzQxLDAgMy41LDAgTDE2LjUsMCBDMTYuODk5MzQ3NywwIDE3LjEzNzU0MzQsMC40NDUwNzI3MzMgMTYuOTE2MDI1MSwwLjc3NzM1MDA5OCBMMTQuOTE2MDI1MSwzLjc3NzM1MDEgQzE0LjgyMzI5MjMsMy45MTY0NDk0MyAxNC42NjcxNzY2LDQgMTQuNSw0IFogTTE0LjIzMjQwODEsOCBMMTUuNTY1NzQxNSw2IEwzLjc2NzU5MTg4LDYgTDIuNDM0MjU4NTUsOCBMMTQuMjMyNDA4MSw4IFogTTE0LjUsOSBMMS41LDkgQzEuMTAwNjUyMzEsOSAwLjg2MjQ1NjYxLDguNTU0OTI3MjcgMS4wODM5NzQ4NSw4LjIyMjY0OTkgTDMuMDgzOTc0ODUsNS4yMjI2NDk5IEMzLjE3NjcwNzc0LDUuMDgzNTUwNTcgMy4zMzI4MjM0MSw1IDMuNSw1IEwxNi41LDUgQzE2Ljg5OTM0NzcsNSAxNy4xMzc1NDM0LDUuNDQ1MDcyNzMgMTYuOTE2MDI1MSw1Ljc3NzM1MDEgTDE0LjkxNjAyNTEsOC43NzczNTAxIEMxNC44MjMyOTIzLDguOTE2NDQ5NDMgMTQuNjY3MTc2Niw5IDE0LjUsOSBaIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMSAxKSIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); --icon-TypeCell: url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTEsNiBMMSwxMCBMMTAsMTAgTDEwLDYgTDEsNiBaIE0xLDUgTDEwLDUgTDEwLDEgTDEuNSwxIEMxLjIyNDE0MjM3LDEgMSwxLjIyNDE0MjM3IDEsMS41IEwxLDUgWiBNMTEsMSBMMTEsMTUgTDE0LjUsMTUgQzE0Ljc3NTg1NzYsMTUgMTUsMTQuNzc1ODU3NiAxNSwxNC41IEwxNSwxLjUgQzE1LDEuMjI0MTQyMzcgMTQuNzc1ODU3NiwxIDE0LjUsMSBMMTEsMSBaIE0xMCwxNSBMMTAsMTEgTDEsMTEgTDEsMTQuNSBDMSwxNC43NzU4NTc2IDEuMjI0MTQyMzcsMTUgMS41LDE1IEwxMCwxNSBaIE0xNC41LDE2IEwxLjUsMTYgQzAuNjcxODU3NjI1LDE2IDAsMTUuMzI4MTQyNCAwLDE0LjUgTDAsMS41IEMwLDAuNjcxODU3NjI1IDAuNjcxODU3NjI1LDAgMS41LDAgTDE0LjUsMCBDMTUuMzI4MTQyNCwwIDE2LDAuNjcxODU3NjI1IDE2LDEuNSBMMTYsMTQuNSBDMTYsMTUuMzI4MTQyNCAxNS4zMjgxNDI0LDE2IDE0LjUsMTYgWiIgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIi8+PC9zdmc+'); diff --git a/static/ui-icons/Data Types/TypeCalendar.svg b/static/ui-icons/Data Types/TypeCalendar.svg new file mode 100644 index 00000000..e99578d2 --- /dev/null +++ b/static/ui-icons/Data Types/TypeCalendar.svg @@ -0,0 +1,22 @@ + + + + + + diff --git a/test/nbrowser/CustomWidgetsConfig.ts b/test/nbrowser/CustomWidgetsConfig.ts index bfa4ea30..3e77f810 100644 --- a/test/nbrowser/CustomWidgetsConfig.ts +++ b/test/nbrowser/CustomWidgetsConfig.ts @@ -356,6 +356,97 @@ describe('CustomWidgetsConfig', function () { await clickOption(TESTER_WIDGET); }); + it('should hide mappings when there is no good column', async () => { + await gu.setWidgetUrl( + createConfigUrl({ + columns: [{name: 'M2', type: 'Date'}], + requiredAccess: 'read table', + }) + ); + + await accept(); + + // Get the drop for M2 mappings. + const mappingsForM2 = () => driver.find(pickerDrop('M2')); + + // Make sure it is disabled. + assert.isTrue(await mappingsForM2().matches('.test-config-widget-disabled')); + // And the text is: + assert.equal(await mappingsForM2().getText(), 'No date columns in table.'); + + // Now add Date column. + await gu.sendActions([['AddVisibleColumn', 'Table1', 'NewCol', {type: 'Date'}]]); + + // Now drop should be enabled. + assert.isFalse(await mappingsForM2().matches('.test-config-widget-disabled')); + assert.isTrue(await mappingsForM2().matches('.test-config-widget-enabled')); + + // And the text is: + assert.equal(await mappingsForM2().getText(), 'Pick a date column'); + + // Expand it and make sure we have NewCol there. + await toggleDrop(pickerDrop('M2')); + assert.deepEqual(await getOptions(), ['NewCol']); + + // Select that column. + await clickOption('NewCol'); + + // Now expand the drop again and make sure we can't clear it. + await toggleDrop(pickerDrop('M2')); + assert.deepEqual(await getOptions(), ['NewCol']); + + // Now remove the column, and make sure that the drop is disabled again. + await driver.sendKeys(Key.ESCAPE); + await gu.sendActions([['RemoveColumn', 'Table1', 'NewCol']]); + + // Make sure it is disabled. + assert.isTrue(await mappingsForM2().matches('.test-config-widget-disabled')); + assert.isFalse(await mappingsForM2().matches('.test-config-widget-enabled')); + assert.equal(await mappingsForM2().getText(), 'No date columns in table.'); + }); + + it('should clear optional mapping', async () => { + const revert = await gu.begin(); + await gu.setWidgetUrl( + createConfigUrl({ + columns: [{name: 'M2', type: 'Date', optional: true}], + requiredAccess: 'read table', + }) + ); + + await accept(); + + // Get the drop for M2 mappings. + const mappingsForM2 = () => driver.find(pickerDrop('M2')); + + // Make sure it is disabled. + assert.isTrue(await mappingsForM2().matches('.test-config-widget-disabled')); + // Now add Date column. + await gu.sendActions([['AddVisibleColumn', 'Table1', 'NewCol', {type: 'Date'}]]); + + // Expand it and make sure we have NewCol there. + await toggleDrop(pickerDrop('M2')); + assert.deepEqual(await getOptions(), ['NewCol']); + + // Select that column. + await clickOption('NewCol'); + + // Make sure widget sees the mapping. + assert.deepEqual(await widget.onRecordsMappings(), {M2: 'NewCol'}); + + // Now expand the drop again and make sure we can clear it. + await toggleDrop(pickerDrop('M2')); + assert.deepEqual(await getOptions(), ['NewCol', 'Clear selection']); + + // Now clear the mapping. + await clickOption('Clear selection'); + assert.equal(await mappingsForM2().getText(), 'Pick a date column'); + + // Make sure widget sees the mapping. + assert.deepEqual(await widget.onRecordsMappings(), {M2: null}); + await revert(); + }); + it('should render columns mapping', async () => { const revert = await gu.begin(); assert.isTrue(await driver.find('.test-vfc-visible-fields-select-all').isPresent()); @@ -759,14 +850,12 @@ describe('CustomWidgetsConfig', function () { await gu.selectSectionByTitle('Widget'); await driver.find(".test-right-tab-pagewidget").click(); // Drop should be empty, - assert.equal(await driver.find(pickerDrop("M1")).getText(), "Pick a text column"); + assert.equal(await driver.find(pickerDrop("M1")).getText(), "No text columns in table."); assert.isEmpty(await getListItems("M2")); - // with no options - await toggleDrop(pickerDrop("M1")); - assert.isEmpty(await getOptions()); - await gu.sendKeys(Key.ESCAPE); + // And drop is disabled. + assert.isTrue(await driver.find(pickerDrop("M1")).matches(".test-config-widget-disabled")); // The same for M2 - await click(pickerAdd("M2")); + assert.isTrue(await driver.find(pickerAdd("M2")).matches(".test-config-widget-disabled")); assert.isEmpty(await getMenuOptions()); assert.deepEqual(await widget.onRecordsMappings(), {M1: null, M2: []}); assert.deepEqual(await widget.onRecords(), [