mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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
This commit is contained in:
parent
0f4153dc23
commit
592a43ec36
32
app/client/components/DataTables.ts
Normal file
32
app/client/components/DataTables.ts
Normal file
@ -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<ViewSectionRec>(
|
||||
this._gristDoc.viewModel.activeSection,
|
||||
(viewSection) => {
|
||||
if (!viewSection.getRowId()) {
|
||||
return;
|
||||
}
|
||||
ViewSectionHelper.create(this, this._gristDoc, viewSection);
|
||||
return buildViewSectionDom(this._gristDoc, viewSection.getRowId());
|
||||
}
|
||||
)
|
||||
];
|
||||
}
|
||||
}
|
@ -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()),
|
||||
};
|
||||
|
@ -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<IDocPage>(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];
|
||||
}
|
||||
}
|
||||
|
@ -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'),
|
||||
|
@ -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<BaseView>(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<BaseView|null>((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<BaseView|null>(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<boolean> = 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<BaseView|null>((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<BaseView|null>(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); }),
|
||||
);
|
||||
}
|
||||
|
@ -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<KoArray<ValidationRec>>;
|
||||
|
||||
primaryView: ko.Computed<ViewRec>;
|
||||
rawViewSection: ko.Computed<ViewSectionRec>;
|
||||
summarySource: ko.Computed<TableRec>;
|
||||
|
||||
// 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.
|
||||
|
@ -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);
|
||||
|
@ -47,6 +47,10 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section"> {
|
||||
tableTitle: ko.Computed<string>;
|
||||
titleDef: modelUtil.KoSaveableObservable<string>;
|
||||
|
||||
// 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<boolean>;
|
||||
|
||||
borderWidthPx: ko.Computed<string>;
|
||||
|
||||
layoutSpecObj: modelUtil.ObjObservable<any>;
|
||||
@ -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);
|
||||
|
@ -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'),
|
||||
]
|
||||
|
@ -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)),
|
||||
|
@ -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,7 +352,13 @@ export class RightPanel extends Disposable {
|
||||
];
|
||||
}),
|
||||
|
||||
dom.maybe((use) => !use(hasCustomMapping) && use(this._pageWidgetType) !== 'chart', () => [
|
||||
dom.maybe(
|
||||
(use) => !(
|
||||
use(hasCustomMapping) ||
|
||||
use(this._pageWidgetType) === 'chart' ||
|
||||
use(activeSection.isRaw)
|
||||
),
|
||||
() => [
|
||||
cssSeparator(),
|
||||
dom.create(VisibleFieldsConfig, this._gristDoc, activeSection),
|
||||
]),
|
||||
@ -413,6 +425,7 @@ export class RightPanel extends Disposable {
|
||||
dom.hide((use) => !use(use(table).summarySourceTable)),
|
||||
),
|
||||
|
||||
dom.maybe((use) => !use(activeSection.isRaw), () =>
|
||||
cssButtonRow(primaryButton('Edit Data Selection', this._createPageWidgetPicker(),
|
||||
testId('pwc-editDataSelection')),
|
||||
dom.maybe(
|
||||
@ -424,7 +437,7 @@ export class RightPanel extends Disposable {
|
||||
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(),
|
||||
|
||||
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();
|
||||
|
@ -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')),
|
||||
];
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import {reportError} from 'app/client/models/AppModel';
|
||||
import { ColumnRec, DocModel, ViewRec, ViewSectionRec } from 'app/client/models/DocModel';
|
||||
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';
|
||||
@ -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<boolean>) {
|
||||
isReadonly: Observable<boolean>) {
|
||||
|
||||
const popupControls = new WeakMap<ColumnRec, PopupControl>();
|
||||
|
||||
@ -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()))
|
||||
)
|
||||
];
|
||||
}
|
||||
|
@ -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(), {
|
||||
menu(() => FieldSettingsMenu(
|
||||
this.field.useColOptions(),
|
||||
this.field.viewSection().isRaw(),
|
||||
{
|
||||
useSeparate: () => this.fieldSettingsUseSeparate(),
|
||||
saveAsCommon: () => this.fieldSettingsSaveAsCommon(),
|
||||
revertToCommon: () => this.fieldSettingsRevertToCommon()
|
||||
}))
|
||||
revertToCommon: () => this.fieldSettingsRevertToCommon(),
|
||||
},
|
||||
)),
|
||||
),
|
||||
'Field in ',
|
||||
kd.text(() => this.origColumn.viewFields().all().length),
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user