From 592a43ec36ce7cf47e4816302b75bb8ce1baf469 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Mon, 7 Feb 2022 16:02:26 +0200 Subject: [PATCH] (core) Initial data tables page Summary: - Added a new special page for viewing raw data widgets: - Implemented in DataTables.ts - Accessible only via the special URL path `/p/data` - Future diffs should make this page prettier and easily accessible - Shows a list of user tables - Clicking on a table name shows its `rawViewSection` by setting `GristDoc.viewModel.activeSectionId`. Note that in this case `GristDoc.viewModel` is an empty record, so this is a bit of a hack, but it works well and causes no known issues. - Added `ViewSectionRec.isRaw` to know if the record represents a raw data widget. - Added various restrictions in the UI for raw data widgets: - 'Delete widget' is disabled in the 3-dot widget menu. - Prevent hiding columns: - "Hide column" in the column context menu is disabled - The "VISIBLE/HIDDEN COLUMNS" section of the right panel > Table > Widget is hidden - The toggle bar isn't configurable to ensure that users know when raw data is filtered: - The filter bar always shows if and only if some filters are present - "Toggle Filter Bar" is hidden in: - Right panel > Table > Sort & Filter - The sort/filter menu next to the three-dot menu for widgets. - Other restrictions in the right panel: - In the Column tab: - 'Use separate settings' is disabled - In the Table tab: - In the Widget subtab: - 'Change Widget' is hidden - In the Data subtab: - 'Edit Data Selection' is hidden - 'SELECT BY' is hidden Test Plan: Tested manually. The behaviour of raw data widgets may still change and they aren't easily visible to users yet. Reviewers: georgegevoian Reviewed By: georgegevoian Differential Revision: https://phab.getgrist.com/D3248 --- app/client/components/DataTables.ts | 32 +++++++ app/client/components/GridView.js | 1 + app/client/components/GristDoc.ts | 6 +- app/client/components/ViewConfigTab.js | 7 +- app/client/components/ViewLayout.ts | 99 +++++++++++--------- app/client/models/entities/TableRec.ts | 4 +- app/client/models/entities/ViewRec.ts | 4 +- app/client/models/entities/ViewSectionRec.ts | 8 ++ app/client/ui/FieldMenus.ts | 6 +- app/client/ui/GridViewMenus.ts | 6 +- app/client/ui/RightPanel.ts | 61 +++++++----- app/client/ui/ViewLayoutMenu.ts | 7 +- app/client/ui/ViewSectionMenu.ts | 37 ++++---- app/client/widgets/FieldBuilder.ts | 14 ++- app/common/gristUrls.ts | 12 ++- 15 files changed, 193 insertions(+), 111 deletions(-) create mode 100644 app/client/components/DataTables.ts diff --git a/app/client/components/DataTables.ts b/app/client/components/DataTables.ts new file mode 100644 index 00000000..885b5fe1 --- /dev/null +++ b/app/client/components/DataTables.ts @@ -0,0 +1,32 @@ +import {GristDoc} from 'app/client/components/GristDoc'; +import {buildViewSectionDom, ViewSectionHelper} from 'app/client/components/ViewLayout'; +import {ViewSectionRec} from 'app/client/models/DocModel'; +import {Disposable, dom, domComputed} from 'grainjs'; + +export class DataTables extends Disposable { + constructor(private _gristDoc: GristDoc) { + super(); + } + + public buildDom() { + return [ + dom( + 'ul', + this._gristDoc.docModel.allTables.all().map(t => dom( + 'li', t.rawViewSection().title() || t.tableId(), + dom.on('click', () => this._gristDoc.viewModel.activeSectionId(t.rawViewSection.peek().getRowId())), + )) + ), + domComputed( + this._gristDoc.viewModel.activeSection, + (viewSection) => { + if (!viewSection.getRowId()) { + return; + } + ViewSectionHelper.create(this, this._gristDoc, viewSection); + return buildViewSectionDom(this._gristDoc, viewSection.getRowId()); + } + ) + ]; + } +} diff --git a/app/client/components/GridView.js b/app/client/components/GridView.js index bce11e1c..ba9a522f 100644 --- a/app/client/components/GridView.js +++ b/app/client/components/GridView.js @@ -1495,6 +1495,7 @@ GridView.prototype._getColumnMenuOptions = function(copySelection) { numFrozen: this.viewSection.numFrozen.peek(), disableModify: calcFieldsCondition(copySelection.fields, f => f.disableModify.peek()), isReadonly: this.gristDoc.isReadonly.get() || this.isPreview, + isRaw: this.viewSection.isRaw(), isFiltered: this.isFiltered(), isFormula: calcFieldsCondition(copySelection.fields, f => f.column.peek().isRealFormula.peek()), }; diff --git a/app/client/components/GristDoc.ts b/app/client/components/GristDoc.ts index a1d0d7b4..0f246e9f 100644 --- a/app/client/components/GristDoc.ts +++ b/app/client/components/GristDoc.ts @@ -11,6 +11,7 @@ import * as CodeEditorPanel from 'app/client/components/CodeEditorPanel'; import * as commands from 'app/client/components/commands'; import {CursorPos} from 'app/client/components/Cursor'; import {CursorMonitor, ViewCursorPos} from "app/client/components/CursorMonitor"; +import {DataTables} from 'app/client/components/DataTables'; import {DocComm, DocUserAction} from 'app/client/components/DocComm'; import * as DocConfigTab from 'app/client/components/DocConfigTab'; import {Drafts} from "app/client/components/Drafts"; @@ -350,6 +351,7 @@ export class GristDoc extends DisposableWithEvents { dom.domComputed(this.activeViewId, (viewId) => ( viewId === 'code' ? dom.create((owner) => owner.autoDispose(CodeEditorPanel.create(this))) : viewId === 'acl' ? dom.create((owner) => owner.autoDispose(AccessRules.create(this, this))) : + viewId === 'data' ? dom.create((owner) => owner.autoDispose(DataTables.create(this, this))) : viewId === 'GristDocTour' ? null : dom.create((owner) => (this._viewLayout = ViewLayout.create(owner, this, viewId))) )), @@ -732,7 +734,7 @@ export class GristDoc extends DisposableWithEvents { const srcTable = await this._getTableData(srcSection); const query: ClientQuery = {tableId: srcTable.tableId, filters: {}, operations: {}}; if (colId) { - query.operations![colId] = isRefListType(section.linkSrcCol.peek().type.peek()) ? 'intersects' : 'in'; + query.operations[colId] = isRefListType(section.linkSrcCol.peek().type.peek()) ? 'intersects' : 'in'; query.filters[colId] = isList(controller) ? controller.slice(1) : [controller]; } else { // must be a summary -- otherwise dealt with earlier. @@ -740,7 +742,7 @@ export class GristDoc extends DisposableWithEvents { for (const srcCol of srcSection.table.peek().groupByColumns.peek()) { const filterColId = srcCol.summarySource.peek().colId.peek(); controller = destTable.getValue(cursorPos.rowId, filterColId); - query.operations![filterColId] = 'in'; + query.operations[filterColId] = 'in'; query.filters[filterColId] = isList(controller) ? controller.slice(1) : [controller]; } } diff --git a/app/client/components/ViewConfigTab.js b/app/client/components/ViewConfigTab.js index eeaf3996..f9d2e2d7 100644 --- a/app/client/components/ViewConfigTab.js +++ b/app/client/components/ViewConfigTab.js @@ -444,8 +444,8 @@ ViewConfigTab.prototype._buildFilterDom = function() { ); }), ), - cssRow( - cssTextBtn( + grainjsDom.maybe((use) => !use(section.isRaw), + () => cssRow(cssTextBtn( testId('toggle-filter-bar'), grainjsDom.domComputed((use) => { const filterBar = use(activeFilterBar); @@ -457,8 +457,7 @@ ViewConfigTab.prototype._buildFilterDom = function() { }), grainjsDom.on('click', () => activeFilterBar(!activeFilterBar.peek())), 'Toggle Filter Bar', - ) - ), + ))), grainjsDom.maybe(hasChangedObs, () => cssRow( cssExtraMarginTop.cls(''), testId('save-filter-btns'), diff --git a/app/client/components/ViewLayout.ts b/app/client/components/ViewLayout.ts index 53cff819..6526a197 100644 --- a/app/client/components/ViewLayout.ts +++ b/app/client/components/ViewLayout.ts @@ -43,7 +43,7 @@ function getInstanceConstructor(parentKey: string) { return Cons || viewSectionTypes.record; } -class ViewSectionHelper extends Disposable { +export class ViewSectionHelper extends Disposable { private _instance = Holder.create(this); constructor(gristDoc: GristDoc, vs: ViewSectionRec) { @@ -183,49 +183,7 @@ export class ViewLayout extends DisposableWithEvents implements IDomComponent { } private _buildLeafContent(sectionRowId: number) { - // Creating normal section dom - const vs: ViewSectionRec = this.docModel.viewSections.getRowModel(sectionRowId); - return dom('div.view_leaf.viewsection_content.flexvbox.flexauto', - testId(`viewlayout-section-${sectionRowId}`), - - cssViewLeaf.cls(''), - cssViewLeafInactive.cls('', (use) => !vs.isDisposed() && !use(vs.hasFocus)), - dom.cls('active_section', vs.hasFocus), - - dom.maybe((use) => use(vs.viewInstance), (viewInstance) => dom('div.viewsection_title.flexhbox', - dom('span.viewsection_drag_indicator.glyphicon.glyphicon-option-vertical', - // Makes element grabbable only if grist is not readonly. - dom.cls('layout_grabbable', (use) => !use(this.gristDoc.isReadonlyKo))), - dom.maybe((use) => use(use(viewInstance.viewSection.table).summarySourceTable), () => - cssSigmaIcon('Pivot', testId('sigma'))), - dom('div.viewsection_titletext_container.flexitem.flexhbox', - dom('span.viewsection_titletext', editableLabel( - fromKo(vs.titleDef), - (val) => vs.titleDef.saveOnly(val), - testId('viewsection-title'), - )), - ), - viewInstance.buildTitleControls(), - dom('span.viewsection_buttons', - dom.create(viewSectionMenu, this.docModel, vs, this.viewModel, this.gristDoc.isReadonly) - ) - )), - dom.maybe(vs.activeFilterBar, () => dom.create(filterBar, vs)), - dom.maybe(vs.viewInstance, (viewInstance) => - dom('div.view_data_pane_container.flexvbox', - cssResizing.cls('', this._isResizing), - dom.maybe(viewInstance.disableEditing, () => - dom('div.disable_viewpane.flexvbox', 'No data') - ), - dom.maybe(viewInstance.isTruncated, () => - dom('div.viewsection_truncated', 'Not all data is shown') - ), - dom.cls((use) => 'viewsection_type_' + use(vs.parentKey)), - viewInstance.viewPane - ) - ), - dom.on('mousedown', () => { this.viewModel.activeSectionId(sectionRowId); }), - ); + return buildViewSectionDom(this.gristDoc, sectionRowId, this._isResizing, this.viewModel); } /** @@ -396,3 +354,56 @@ const cssLayoutBox = styled('div', ` const cssResizing = styled('div', ` pointer-events: none; `); + + +export function buildViewSectionDom( + gristDoc: GristDoc, + sectionRowId: number, + isResizing: Observable = Observable.create(null, false), + viewModel?: ViewRec, +) { + // Creating normal section dom + const vs: ViewSectionRec = gristDoc.docModel.viewSections.getRowModel(sectionRowId); + return dom('div.view_leaf.viewsection_content.flexvbox.flexauto', + testId(`viewlayout-section-${sectionRowId}`), + + cssViewLeaf.cls(''), + cssViewLeafInactive.cls('', (use) => !vs.isDisposed() && !use(vs.hasFocus)), + dom.cls('active_section', vs.hasFocus), + + dom.maybe((use) => use(vs.viewInstance), (viewInstance) => dom('div.viewsection_title.flexhbox', + dom('span.viewsection_drag_indicator.glyphicon.glyphicon-option-vertical', + // Makes element grabbable only if grist is not readonly. + dom.cls('layout_grabbable', (use) => !use(gristDoc.isReadonlyKo))), + dom.maybe((use) => use(use(viewInstance.viewSection.table).summarySourceTable), () => + cssSigmaIcon('Pivot', testId('sigma'))), + dom('div.viewsection_titletext_container.flexitem.flexhbox', + dom('span.viewsection_titletext', editableLabel( + fromKo(vs.titleDef), + (val) => vs.titleDef.saveOnly(val), + testId('viewsection-title'), + )), + ), + viewInstance.buildTitleControls(), + dom('span.viewsection_buttons', + dom.create(viewSectionMenu, gristDoc.docModel, vs, gristDoc.isReadonly) + ) + )), + dom.maybe((use) => use(vs.activeFilterBar) || use(vs.isRaw) && use(vs.activeFilters).length, + () => dom.create(filterBar, vs)), + dom.maybe(vs.viewInstance, (viewInstance) => + dom('div.view_data_pane_container.flexvbox', + cssResizing.cls('', isResizing), + dom.maybe(viewInstance.disableEditing, () => + dom('div.disable_viewpane.flexvbox', 'No data') + ), + dom.maybe(viewInstance.isTruncated, () => + dom('div.viewsection_truncated', 'Not all data is shown') + ), + dom.cls((use) => 'viewsection_type_' + use(vs.parentKey)), + viewInstance.viewPane + ) + ), + dom.on('mousedown', () => { viewModel?.activeSectionId(sectionRowId); }), + ); +} diff --git a/app/client/models/entities/TableRec.ts b/app/client/models/entities/TableRec.ts index 3a2e56fa..1265ded7 100644 --- a/app/client/models/entities/TableRec.ts +++ b/app/client/models/entities/TableRec.ts @@ -1,5 +1,5 @@ import {KoArray} from 'app/client/lib/koArray'; -import {DocModel, IRowModel, recordSet, refRecord} from 'app/client/models/DocModel'; +import {DocModel, IRowModel, recordSet, refRecord, ViewSectionRec} from 'app/client/models/DocModel'; import {ColumnRec, ValidationRec, ViewRec} from 'app/client/models/DocModel'; import {MANUALSORT} from 'app/common/gristTypes'; import * as ko from 'knockout'; @@ -12,6 +12,7 @@ export interface TableRec extends IRowModel<"_grist_Tables"> { validations: ko.Computed>; primaryView: ko.Computed; + rawViewSection: ko.Computed; summarySource: ko.Computed; // A Set object of colRefs for all summarySourceCols of table. @@ -37,6 +38,7 @@ export function createTableRec(this: TableRec, docModel: DocModel): void { this.validations = recordSet(this, docModel.validations, 'tableRef'); this.primaryView = refRecord(docModel.views, this.primaryViewId); + this.rawViewSection = refRecord(docModel.viewSections, this.rawViewSectionRef); this.summarySource = refRecord(docModel.tables, this.summarySourceTable); // A Set object of colRefs for all summarySourceCols of this table. diff --git a/app/client/models/entities/ViewRec.ts b/app/client/models/entities/ViewRec.ts index 70615ad9..183fc849 100644 --- a/app/client/models/entities/ViewRec.ts +++ b/app/client/models/entities/ViewRec.ts @@ -32,7 +32,9 @@ export function createViewRec(this: ViewRec, docModel: DocModel): void { // The default function which is used when the conditional case is true. // Read may occur for recently disposed sections, must check condition first. return !this.isDisposed() && - this.viewSections().all().length > 0 ? this.viewSections().at(0)!.getRowId() : 0; + // `!this.getRowId()` implies that this is an empty (non-existent) view record + // which happens when viewing the raw data tables, in which case the default is no active view section. + this.getRowId() && this.viewSections().all().length > 0 ? this.viewSections().at(0)!.getRowId() : 0; }); this.activeSection = refRecord(docModel.viewSections, this.activeSectionId); diff --git a/app/client/models/entities/ViewSectionRec.ts b/app/client/models/entities/ViewSectionRec.ts index e364ccc5..9269c544 100644 --- a/app/client/models/entities/ViewSectionRec.ts +++ b/app/client/models/entities/ViewSectionRec.ts @@ -47,6 +47,10 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section"> { tableTitle: ko.Computed; titleDef: modelUtil.KoSaveableObservable; + // true if this record is its table's rawViewSection, i.e. a 'raw data view' + // in which case the UI prevents various things like hiding columns or changing the widget type. + isRaw: ko.Computed; + borderWidthPx: ko.Computed; layoutSpecObj: modelUtil.ObjObservable; @@ -276,6 +280,10 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel): ) ); + // true if this record is its table's rawViewSection, i.e. a 'raw data view' + // in which case the UI prevents various things like hiding columns or changing the widget type. + this.isRaw = this.autoDispose(ko.pureComputed(() => this.table().rawViewSectionRef() === this.getRowId())); + this.borderWidthPx = ko.pureComputed(function() { return this.borderWidth() + 'px'; }, this); this.layoutSpecObj = modelUtil.jsonObservable(this.layoutSpec); diff --git a/app/client/ui/FieldMenus.ts b/app/client/ui/FieldMenus.ts index 8df3f8e2..da2715ec 100644 --- a/app/client/ui/FieldMenus.ts +++ b/app/client/ui/FieldMenus.ts @@ -1,4 +1,5 @@ import {menuItem, menuSubHeader} from 'app/client/ui2018/menus'; +import {dom} from 'grainjs'; interface IFieldOptions { useSeparate: () => void; @@ -6,10 +7,11 @@ interface IFieldOptions { revertToCommon: () => void; } -export function FieldSettingsMenu(useColOptions: boolean, actions: IFieldOptions) { +export function FieldSettingsMenu(useColOptions: boolean, disableSeparate: boolean, actions: IFieldOptions) { + useColOptions = useColOptions || disableSeparate; return [ menuSubHeader(`Using ${useColOptions ? 'common' : 'separate'} settings`), - useColOptions ? menuItem(actions.useSeparate, 'Use separate settings') : [ + useColOptions ? menuItem(actions.useSeparate, 'Use separate settings', dom.cls('disabled', disableSeparate)) : [ menuItem(actions.saveAsCommon, 'Save as common settings'), menuItem(actions.revertToCommon, 'Revert to common settings'), ] diff --git a/app/client/ui/GridViewMenus.ts b/app/client/ui/GridViewMenus.ts index 2a416d07..2757b4dc 100644 --- a/app/client/ui/GridViewMenus.ts +++ b/app/client/ui/GridViewMenus.ts @@ -39,6 +39,7 @@ export interface IMultiColumnContextMenu { numFrozen: number; disableModify: boolean|'mixed'; // If the columns are read-only. isReadonly: boolean; + isRaw: boolean; isFiltered: boolean; // If this view shows a proper subset of all rows in the table. isFormula: boolean|'mixed'; columnIndices: number[]; @@ -57,10 +58,9 @@ export function calcFieldsCondition(fields: ViewFieldRec[], condition: (f: ViewF } export function ColumnContextMenu(options: IColumnContextMenu) { - const { disableModify, filterOpenFunc, colId, sortSpec, isReadonly } = options; + const { disableModify, filterOpenFunc, colId, sortSpec, isReadonly, isRaw } = options; const disableForReadonlyColumn = dom.cls('disabled', Boolean(disableModify) || isReadonly); - const disableForReadonlyView = dom.cls('disabled', isReadonly); const addToSortLabel = getAddToSortLabel(sortSpec, colId); @@ -112,7 +112,7 @@ export function ColumnContextMenu(options: IColumnContextMenu) { menuItem(allCommands.sortFilterTabOpen.run, 'More sort options ...', testId('more-sort-options')), menuDivider({style: 'margin-top: 0;'}), menuItemCmd(allCommands.renameField, 'Rename column', disableForReadonlyColumn), - menuItemCmd(allCommands.hideField, 'Hide column', disableForReadonlyView), + menuItemCmd(allCommands.hideField, 'Hide column', dom.cls('disabled', isReadonly || isRaw)), freezeMenuItemCmd(options), menuDivider(), MultiColumnMenu((options.disableFrozenMenu = true, options)), diff --git a/app/client/ui/RightPanel.ts b/app/client/ui/RightPanel.ts index f7ed050b..732575a3 100644 --- a/app/client/ui/RightPanel.ts +++ b/app/client/ui/RightPanel.ts @@ -307,8 +307,14 @@ export class RightPanel extends Disposable { testId('right-widget-title') )), - cssRow(primaryButton('Change Widget', this._createPageWidgetPicker()), - cssRow.cls('-top-space')), + dom.maybe( + (use) => !use(activeSection.isRaw), + () => cssRow( + primaryButton('Change Widget', this._createPageWidgetPicker()), + cssRow.cls('-top-space') + ), + ), + cssSeparator(), dom.maybe((use) => ['detail', 'single'].includes(use(this._pageWidgetType)!), () => [ @@ -346,10 +352,16 @@ export class RightPanel extends Disposable { ]; }), - dom.maybe((use) => !use(hasCustomMapping) && use(this._pageWidgetType) !== 'chart', () => [ - cssSeparator(), - dom.create(VisibleFieldsConfig, this._gristDoc, activeSection), - ]), + dom.maybe( + (use) => !( + use(hasCustomMapping) || + use(this._pageWidgetType) === 'chart' || + use(activeSection.isRaw) + ), + () => [ + cssSeparator(), + dom.create(VisibleFieldsConfig, this._gristDoc, activeSection), + ]), ]); } @@ -413,18 +425,19 @@ export class RightPanel extends Disposable { dom.hide((use) => !use(use(table).summarySourceTable)), ), - cssButtonRow(primaryButton('Edit Data Selection', this._createPageWidgetPicker(), - testId('pwc-editDataSelection')), - dom.maybe( - use => Boolean(use(use(activeSection.table).summarySourceTable)), - () => basicButton( - 'Detach', - dom.on('click', () => this._gristDoc.docData.sendAction( - ["DetachSummaryViewSection", activeSection.getRowId()])), - testId('detach-button'), - )), - cssRow.cls('-top-space'), - ), + dom.maybe((use) => !use(activeSection.isRaw), () => + cssButtonRow(primaryButton('Edit Data Selection', this._createPageWidgetPicker(), + testId('pwc-editDataSelection')), + dom.maybe( + use => Boolean(use(use(activeSection.table).summarySourceTable)), + () => basicButton( + 'Detach', + dom.on('click', () => this._gristDoc.docData.sendAction( + ["DetachSummaryViewSection", activeSection.getRowId()])), + testId('detach-button'), + )), + cssRow.cls('-top-space'), + )), // TODO: "Advanced settings" is for "on-demand" marking of tables. This should only be shown // for raw data tables (once that's supported), should have updated UI, and should possibly @@ -434,11 +447,13 @@ export class RightPanel extends Disposable { )), cssSeparator(), - cssLabel('SELECT BY'), - cssRow( - select(link, linkOptions, {defaultLabel: 'Select Widget'}), - testId('right-select-by') - ), + dom.maybe((use) => !use(activeSection.isRaw), () => [ + cssLabel('SELECT BY'), + cssRow( + select(link, linkOptions, {defaultLabel: 'Select Widget'}), + testId('right-select-by') + ), + ]), domComputed((use) => { const activeSectionRef = activeSection.getRowId(); diff --git a/app/client/ui/ViewLayoutMenu.ts b/app/client/ui/ViewLayoutMenu.ts index 43c3ac9a..d5cc1ab4 100644 --- a/app/client/ui/ViewLayoutMenu.ts +++ b/app/client/ui/ViewLayoutMenu.ts @@ -1,5 +1,5 @@ import {allCommands} from 'app/client/components/commands'; -import {ViewRec, ViewSectionRec} from 'app/client/models/DocModel'; +import {ViewSectionRec} from 'app/client/models/DocModel'; import {testId} from 'app/client/ui2018/cssVars'; import {menuDivider, menuItemCmd, menuItemLink} from 'app/client/ui2018/menus'; import {dom} from 'grainjs'; @@ -7,7 +7,7 @@ import {dom} from 'grainjs'; /** * Returns a list of menu items for a view section. */ -export function makeViewLayoutMenu(viewModel: ViewRec, viewSection: ViewSectionRec, isReadonly: boolean) { +export function makeViewLayoutMenu(viewSection: ViewSectionRec, isReadonly: boolean) { const viewInstance = viewSection.viewInstance.peek()!; const gristDoc = viewInstance.gristDoc; @@ -30,6 +30,7 @@ export function makeViewLayoutMenu(viewModel: ViewRec, viewSection: ViewSectionR menuDivider(), ]; + const viewRec = viewSection.view(); return [ dom.maybe((use) => ['single'].includes(use(viewSection.parentKey)), () => contextMenu), menuItemCmd(allCommands.printSection, 'Print widget', testId('print-section')), @@ -50,7 +51,7 @@ export function makeViewLayoutMenu(viewModel: ViewRec, viewSection: ViewSectionR testId('section-open-configuration')), ), menuItemCmd(allCommands.deleteSection, 'Delete widget', - dom.cls('disabled', viewModel.viewSections().peekLength <= 1 || isReadonly), + dom.cls('disabled', !viewRec.getRowId() || viewRec.viewSections().peekLength <= 1 || isReadonly), testId('section-delete')), ]; } diff --git a/app/client/ui/ViewSectionMenu.ts b/app/client/ui/ViewSectionMenu.ts index ab78cecb..67fb7191 100644 --- a/app/client/ui/ViewSectionMenu.ts +++ b/app/client/ui/ViewSectionMenu.ts @@ -1,18 +1,18 @@ -import { reportError } from 'app/client/models/AppModel'; -import { ColumnRec, DocModel, ViewRec, ViewSectionRec } from 'app/client/models/DocModel'; -import { FilterInfo } from 'app/client/models/entities/ViewSectionRec'; -import { CustomComputed } from 'app/client/models/modelUtil'; -import { attachColumnFilterMenu } from 'app/client/ui/ColumnFilterMenu'; -import { addFilterMenu } from 'app/client/ui/FilterBar'; -import { hoverTooltip } from 'app/client/ui/tooltips'; -import { makeViewLayoutMenu } from 'app/client/ui/ViewLayoutMenu'; -import { basicButton, primaryButton } from 'app/client/ui2018/buttons'; -import { colors, vars } from 'app/client/ui2018/cssVars'; -import { icon } from 'app/client/ui2018/icons'; -import { menu } from 'app/client/ui2018/menus'; -import { Sort } from 'app/common/SortSpec'; -import { Computed, dom, fromKo, IDisposableOwner, makeTestId, Observable, styled } from 'grainjs'; -import { PopupControl } from 'popweasel'; +import {reportError} from 'app/client/models/AppModel'; +import {ColumnRec, DocModel, ViewSectionRec} from 'app/client/models/DocModel'; +import {FilterInfo} from 'app/client/models/entities/ViewSectionRec'; +import {CustomComputed} from 'app/client/models/modelUtil'; +import {attachColumnFilterMenu} from 'app/client/ui/ColumnFilterMenu'; +import {addFilterMenu} from 'app/client/ui/FilterBar'; +import {hoverTooltip} from 'app/client/ui/tooltips'; +import {makeViewLayoutMenu} from 'app/client/ui/ViewLayoutMenu'; +import {basicButton, primaryButton} from 'app/client/ui2018/buttons'; +import {colors, vars} from 'app/client/ui2018/cssVars'; +import {icon} from 'app/client/ui2018/icons'; +import {menu} from 'app/client/ui2018/menus'; +import {Sort} from 'app/common/SortSpec'; +import {Computed, dom, fromKo, IDisposableOwner, makeTestId, Observable, styled} from 'grainjs'; +import {PopupControl} from 'popweasel'; import difference = require('lodash/difference'); const testId = makeTestId('test-section-menu-'); @@ -39,7 +39,7 @@ function doRevert(viewSection: ViewSectionRec) { // [Filter Icon] (v) (x) - Filter toggle and all the components in the menu. export function viewSectionMenu(owner: IDisposableOwner, docModel: DocModel, viewSection: ViewSectionRec, - viewModel: ViewRec, isReadonly: Observable) { + isReadonly: Observable) { const popupControls = new WeakMap(); @@ -84,7 +84,8 @@ export function viewSectionMenu(owner: IDisposableOwner, docModel: DocModel, vie // [+] Add filter makeAddFilterButton(viewSection, popupControls), // [+] Toggle filter bar - makeFilterBarToggle(viewSection.activeFilterBar), + dom.maybe((use) => !use(viewSection.isRaw), + () => makeFilterBarToggle(viewSection.activeFilterBar)), // Widget options dom.maybe(use => use(viewSection.parentKey) === 'custom', () => makeCustomOptions(viewSection) @@ -125,7 +126,7 @@ export function viewSectionMenu(owner: IDisposableOwner, docModel: DocModel, vie testId('viewLayout'), cssFixHeight.cls(''), cssDotsIconWrapper(cssIcon('Dots')), - menu(_ctl => makeViewLayoutMenu(viewModel, viewSection, isReadonly.get())) + menu(_ctl => makeViewLayoutMenu(viewSection, isReadonly.get())) ) ]; } diff --git a/app/client/widgets/FieldBuilder.ts b/app/client/widgets/FieldBuilder.ts index 45c65f73..8c751242 100644 --- a/app/client/widgets/FieldBuilder.ts +++ b/app/client/widgets/FieldBuilder.ts @@ -348,11 +348,15 @@ export class FieldBuilder extends Disposable { dom('div.fieldbuilder_settings_button', dom.testId('FieldBuilder_settings'), kd.text(() => this.field.useColOptions() ? 'Common' : 'Separate'), ' ▾', - menu(ctl => FieldSettingsMenu(this.field.useColOptions(), { - useSeparate: () => this.fieldSettingsUseSeparate(), - saveAsCommon: () => this.fieldSettingsSaveAsCommon(), - revertToCommon: () => this.fieldSettingsRevertToCommon() - })) + menu(() => FieldSettingsMenu( + this.field.useColOptions(), + this.field.viewSection().isRaw(), + { + useSeparate: () => this.fieldSettingsUseSeparate(), + saveAsCommon: () => this.fieldSettingsSaveAsCommon(), + revertToCommon: () => this.fieldSettingsRevertToCommon(), + }, + )), ), 'Field in ', kd.text(() => this.origColumn.viewFields().all().length), diff --git a/app/common/gristUrls.ts b/app/common/gristUrls.ts index 6f8cc9a9..9e4bee52 100644 --- a/app/common/gristUrls.ts +++ b/app/common/gristUrls.ts @@ -10,7 +10,9 @@ import {Document} from 'app/common/UserAPI'; import clone = require('lodash/clone'); import pickBy = require('lodash/pickBy'); -export type IDocPage = number | 'code' | 'acl' | 'GristDocTour'; +const SpecialDocPage = StringUnion('code', 'acl', 'data', 'GristDocTour'); +type SpecialDocPage = typeof SpecialDocPage.type; +export type IDocPage = number | SpecialDocPage; // What page to show in the user's home area. Defaults to 'workspace' if a workspace is set, and // to 'all' otherwise. @@ -344,11 +346,11 @@ export function userOverrideParams(email: string|null, extraState?: IGristUrlSta } /** - * parseDocPage is a noop if p is 'new' or 'code', otherwise parse to integer + * parseDocPage is a noop for special pages, otherwise parse to integer */ -function parseDocPage(p: string) { - if (['code', 'acl', 'GristDocTour'].includes(p)) { - return p as 'code'|'acl'|'GristDocTour'; +function parseDocPage(p: string): IDocPage { + if (SpecialDocPage.guard(p)) { + return p; } return parseInt(p, 10); }