mirror of
				https://github.com/gristlabs/grist-core.git
				synced 2025-06-13 20:53:59 +00:00 
			
		
		
		
	(core) Mappings improvements
Summary: - Adding new icon for calendar view (the old one by just bigger) - When there are no columns to map the select box is grayed out - Optional mappings can be cleared now Test Plan: Added Reviewers: JakubSerafin Reviewed By: JakubSerafin Subscribers: JakubSerafin Differential Revision: https://phab.getgrist.com/D4066
This commit is contained in:
		
							parent
							
								
									34f366585d
								
							
						
					
					
						commit
						572279916e
					
				@ -25,7 +25,9 @@ export class ColumnToMapImpl implements Required<ColumnToMap> {
 | 
			
		||||
    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);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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<number|null>[] = 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};
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@ export const widgetTypesMap = new Map<IWidgetType, IWidgetTypeInfo>([
 | 
			
		||||
  ['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.
 | 
			
		||||
 | 
			
		||||
@ -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",
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
  }
 | 
			
		||||
`);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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+');
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										22
									
								
								static/ui-icons/Data Types/TypeCalendar.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								static/ui-icons/Data Types/TypeCalendar.svg
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
			
		||||
<svg
 | 
			
		||||
   xmlns:dc="http://purl.org/dc/elements/1.1/"
 | 
			
		||||
   xmlns:cc="http://creativecommons.org/ns#"
 | 
			
		||||
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
 | 
			
		||||
   xmlns:svg="http://www.w3.org/2000/svg"
 | 
			
		||||
   xmlns="http://www.w3.org/2000/svg"
 | 
			
		||||
   width="16px"
 | 
			
		||||
   height="16px"
 | 
			
		||||
   viewBox="0 0 16 16"
 | 
			
		||||
   version="1.1">
 | 
			
		||||
  <g
 | 
			
		||||
     stroke="none"
 | 
			
		||||
     stroke-width="1"
 | 
			
		||||
     fill="none"
 | 
			
		||||
     fill-rule="evenodd">
 | 
			
		||||
    <path
 | 
			
		||||
       d="M 5.1438183,3.2089992 H 11.143818 V 0.79814978 c 0,-0.29037145 0.243675,-0.49817134 0.5,-0.5 0.256325,-0.001829 0.5,0.21305073 0.5,0.5 V 3.2089992 h 2.129941 c 0.828427,0 1.5,0.6715729 1.5,1.5 v 9.5887298 c 0,0.828427 -0.671573,1.5 -1.5,1.5 H 1.8700589 c -0.8284271,0 -1.49999996,-0.671573 -1.49999996,-1.5 V 4.7089992 c 0,-0.8284271 0.67157286,-1.5 1.49999996,-1.5 H 4.1438183 V 0.79814978 c 0,-0.27614238 0.2044615,-0.50115994 0.5,-0.5 0.2955385,0.00116 0.5,0.2230347 0.5,0.5 z M 1.3700589,7.1198486 H 14.773759 V 4.7089992 c 0,-0.2761424 -0.223858,-0.5 -0.5,-0.5 H 1.8700589 c -0.2761423,0 -0.5,0.2238576 -0.5,0.5 z m 13.4037001,1 H 1.3700589 v 6.1778804 c 0,0.276142 0.2238577,0.5 0.5,0.5 H 14.273759 c 0.276142,0 0.5,-0.223858 0.5,-0.5 z"
 | 
			
		||||
       id="Combined-Shape"
 | 
			
		||||
       style="fill:#000000;fill-rule:nonzero"/>
 | 
			
		||||
  </g>
 | 
			
		||||
</svg>
 | 
			
		||||
| 
		 After Width: | Height: | Size: 1.3 KiB  | 
@ -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(), [
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user