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(),
|
numFrozen: this.viewSection.numFrozen.peek(),
|
||||||
disableModify: calcFieldsCondition(copySelection.fields, f => f.disableModify.peek()),
|
disableModify: calcFieldsCondition(copySelection.fields, f => f.disableModify.peek()),
|
||||||
isReadonly: this.gristDoc.isReadonly.get() || this.isPreview,
|
isReadonly: this.gristDoc.isReadonly.get() || this.isPreview,
|
||||||
|
isRaw: this.viewSection.isRaw(),
|
||||||
isFiltered: this.isFiltered(),
|
isFiltered: this.isFiltered(),
|
||||||
isFormula: calcFieldsCondition(copySelection.fields, f => f.column.peek().isRealFormula.peek()),
|
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 * as commands from 'app/client/components/commands';
|
||||||
import {CursorPos} from 'app/client/components/Cursor';
|
import {CursorPos} from 'app/client/components/Cursor';
|
||||||
import {CursorMonitor, ViewCursorPos} from "app/client/components/CursorMonitor";
|
import {CursorMonitor, ViewCursorPos} from "app/client/components/CursorMonitor";
|
||||||
|
import {DataTables} from 'app/client/components/DataTables';
|
||||||
import {DocComm, DocUserAction} from 'app/client/components/DocComm';
|
import {DocComm, DocUserAction} from 'app/client/components/DocComm';
|
||||||
import * as DocConfigTab from 'app/client/components/DocConfigTab';
|
import * as DocConfigTab from 'app/client/components/DocConfigTab';
|
||||||
import {Drafts} from "app/client/components/Drafts";
|
import {Drafts} from "app/client/components/Drafts";
|
||||||
@ -350,6 +351,7 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
dom.domComputed<IDocPage>(this.activeViewId, (viewId) => (
|
dom.domComputed<IDocPage>(this.activeViewId, (viewId) => (
|
||||||
viewId === 'code' ? dom.create((owner) => owner.autoDispose(CodeEditorPanel.create(this))) :
|
viewId === 'code' ? dom.create((owner) => owner.autoDispose(CodeEditorPanel.create(this))) :
|
||||||
viewId === 'acl' ? dom.create((owner) => owner.autoDispose(AccessRules.create(this, 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 :
|
viewId === 'GristDocTour' ? null :
|
||||||
dom.create((owner) => (this._viewLayout = ViewLayout.create(owner, this, viewId)))
|
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 srcTable = await this._getTableData(srcSection);
|
||||||
const query: ClientQuery = {tableId: srcTable.tableId, filters: {}, operations: {}};
|
const query: ClientQuery = {tableId: srcTable.tableId, filters: {}, operations: {}};
|
||||||
if (colId) {
|
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];
|
query.filters[colId] = isList(controller) ? controller.slice(1) : [controller];
|
||||||
} else {
|
} else {
|
||||||
// must be a summary -- otherwise dealt with earlier.
|
// 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()) {
|
for (const srcCol of srcSection.table.peek().groupByColumns.peek()) {
|
||||||
const filterColId = srcCol.summarySource.peek().colId.peek();
|
const filterColId = srcCol.summarySource.peek().colId.peek();
|
||||||
controller = destTable.getValue(cursorPos.rowId, filterColId);
|
controller = destTable.getValue(cursorPos.rowId, filterColId);
|
||||||
query.operations![filterColId] = 'in';
|
query.operations[filterColId] = 'in';
|
||||||
query.filters[filterColId] = isList(controller) ? controller.slice(1) : [controller];
|
query.filters[filterColId] = isList(controller) ? controller.slice(1) : [controller];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -444,8 +444,8 @@ ViewConfigTab.prototype._buildFilterDom = function() {
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
cssRow(
|
grainjsDom.maybe((use) => !use(section.isRaw),
|
||||||
cssTextBtn(
|
() => cssRow(cssTextBtn(
|
||||||
testId('toggle-filter-bar'),
|
testId('toggle-filter-bar'),
|
||||||
grainjsDom.domComputed((use) => {
|
grainjsDom.domComputed((use) => {
|
||||||
const filterBar = use(activeFilterBar);
|
const filterBar = use(activeFilterBar);
|
||||||
@ -457,8 +457,7 @@ ViewConfigTab.prototype._buildFilterDom = function() {
|
|||||||
}),
|
}),
|
||||||
grainjsDom.on('click', () => activeFilterBar(!activeFilterBar.peek())),
|
grainjsDom.on('click', () => activeFilterBar(!activeFilterBar.peek())),
|
||||||
'Toggle Filter Bar',
|
'Toggle Filter Bar',
|
||||||
)
|
))),
|
||||||
),
|
|
||||||
grainjsDom.maybe(hasChangedObs, () => cssRow(
|
grainjsDom.maybe(hasChangedObs, () => cssRow(
|
||||||
cssExtraMarginTop.cls(''),
|
cssExtraMarginTop.cls(''),
|
||||||
testId('save-filter-btns'),
|
testId('save-filter-btns'),
|
||||||
|
@ -43,7 +43,7 @@ function getInstanceConstructor(parentKey: string) {
|
|||||||
return Cons || viewSectionTypes.record;
|
return Cons || viewSectionTypes.record;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ViewSectionHelper extends Disposable {
|
export class ViewSectionHelper extends Disposable {
|
||||||
private _instance = Holder.create<BaseView>(this);
|
private _instance = Holder.create<BaseView>(this);
|
||||||
|
|
||||||
constructor(gristDoc: GristDoc, vs: ViewSectionRec) {
|
constructor(gristDoc: GristDoc, vs: ViewSectionRec) {
|
||||||
@ -183,49 +183,7 @@ export class ViewLayout extends DisposableWithEvents implements IDomComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _buildLeafContent(sectionRowId: number) {
|
private _buildLeafContent(sectionRowId: number) {
|
||||||
// Creating normal section dom
|
return buildViewSectionDom(this.gristDoc, sectionRowId, this._isResizing, this.viewModel);
|
||||||
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); }),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -396,3 +354,56 @@ const cssLayoutBox = styled('div', `
|
|||||||
const cssResizing = styled('div', `
|
const cssResizing = styled('div', `
|
||||||
pointer-events: none;
|
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 {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 {ColumnRec, ValidationRec, ViewRec} from 'app/client/models/DocModel';
|
||||||
import {MANUALSORT} from 'app/common/gristTypes';
|
import {MANUALSORT} from 'app/common/gristTypes';
|
||||||
import * as ko from 'knockout';
|
import * as ko from 'knockout';
|
||||||
@ -12,6 +12,7 @@ export interface TableRec extends IRowModel<"_grist_Tables"> {
|
|||||||
validations: ko.Computed<KoArray<ValidationRec>>;
|
validations: ko.Computed<KoArray<ValidationRec>>;
|
||||||
|
|
||||||
primaryView: ko.Computed<ViewRec>;
|
primaryView: ko.Computed<ViewRec>;
|
||||||
|
rawViewSection: ko.Computed<ViewSectionRec>;
|
||||||
summarySource: ko.Computed<TableRec>;
|
summarySource: ko.Computed<TableRec>;
|
||||||
|
|
||||||
// A Set object of colRefs for all summarySourceCols of table.
|
// 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.validations = recordSet(this, docModel.validations, 'tableRef');
|
||||||
|
|
||||||
this.primaryView = refRecord(docModel.views, this.primaryViewId);
|
this.primaryView = refRecord(docModel.views, this.primaryViewId);
|
||||||
|
this.rawViewSection = refRecord(docModel.viewSections, this.rawViewSectionRef);
|
||||||
this.summarySource = refRecord(docModel.tables, this.summarySourceTable);
|
this.summarySource = refRecord(docModel.tables, this.summarySourceTable);
|
||||||
|
|
||||||
// A Set object of colRefs for all summarySourceCols of this table.
|
// 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.
|
// The default function which is used when the conditional case is true.
|
||||||
// Read may occur for recently disposed sections, must check condition first.
|
// Read may occur for recently disposed sections, must check condition first.
|
||||||
return !this.isDisposed() &&
|
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);
|
this.activeSection = refRecord(docModel.viewSections, this.activeSectionId);
|
||||||
|
@ -47,6 +47,10 @@ export interface ViewSectionRec extends IRowModel<"_grist_Views_section"> {
|
|||||||
tableTitle: ko.Computed<string>;
|
tableTitle: ko.Computed<string>;
|
||||||
titleDef: modelUtil.KoSaveableObservable<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>;
|
borderWidthPx: ko.Computed<string>;
|
||||||
|
|
||||||
layoutSpecObj: modelUtil.ObjObservable<any>;
|
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.borderWidthPx = ko.pureComputed(function() { return this.borderWidth() + 'px'; }, this);
|
||||||
|
|
||||||
this.layoutSpecObj = modelUtil.jsonObservable(this.layoutSpec);
|
this.layoutSpecObj = modelUtil.jsonObservable(this.layoutSpec);
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import {menuItem, menuSubHeader} from 'app/client/ui2018/menus';
|
import {menuItem, menuSubHeader} from 'app/client/ui2018/menus';
|
||||||
|
import {dom} from 'grainjs';
|
||||||
|
|
||||||
interface IFieldOptions {
|
interface IFieldOptions {
|
||||||
useSeparate: () => void;
|
useSeparate: () => void;
|
||||||
@ -6,10 +7,11 @@ interface IFieldOptions {
|
|||||||
revertToCommon: () => void;
|
revertToCommon: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FieldSettingsMenu(useColOptions: boolean, actions: IFieldOptions) {
|
export function FieldSettingsMenu(useColOptions: boolean, disableSeparate: boolean, actions: IFieldOptions) {
|
||||||
|
useColOptions = useColOptions || disableSeparate;
|
||||||
return [
|
return [
|
||||||
menuSubHeader(`Using ${useColOptions ? 'common' : 'separate'} settings`),
|
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.saveAsCommon, 'Save as common settings'),
|
||||||
menuItem(actions.revertToCommon, 'Revert to common settings'),
|
menuItem(actions.revertToCommon, 'Revert to common settings'),
|
||||||
]
|
]
|
||||||
|
@ -39,6 +39,7 @@ export interface IMultiColumnContextMenu {
|
|||||||
numFrozen: number;
|
numFrozen: number;
|
||||||
disableModify: boolean|'mixed'; // If the columns are read-only.
|
disableModify: boolean|'mixed'; // If the columns are read-only.
|
||||||
isReadonly: boolean;
|
isReadonly: boolean;
|
||||||
|
isRaw: boolean;
|
||||||
isFiltered: boolean; // If this view shows a proper subset of all rows in the table.
|
isFiltered: boolean; // If this view shows a proper subset of all rows in the table.
|
||||||
isFormula: boolean|'mixed';
|
isFormula: boolean|'mixed';
|
||||||
columnIndices: number[];
|
columnIndices: number[];
|
||||||
@ -57,10 +58,9 @@ export function calcFieldsCondition(fields: ViewFieldRec[], condition: (f: ViewF
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function ColumnContextMenu(options: IColumnContextMenu) {
|
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 disableForReadonlyColumn = dom.cls('disabled', Boolean(disableModify) || isReadonly);
|
||||||
const disableForReadonlyView = dom.cls('disabled', isReadonly);
|
|
||||||
|
|
||||||
const addToSortLabel = getAddToSortLabel(sortSpec, colId);
|
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')),
|
menuItem(allCommands.sortFilterTabOpen.run, 'More sort options ...', testId('more-sort-options')),
|
||||||
menuDivider({style: 'margin-top: 0;'}),
|
menuDivider({style: 'margin-top: 0;'}),
|
||||||
menuItemCmd(allCommands.renameField, 'Rename column', disableForReadonlyColumn),
|
menuItemCmd(allCommands.renameField, 'Rename column', disableForReadonlyColumn),
|
||||||
menuItemCmd(allCommands.hideField, 'Hide column', disableForReadonlyView),
|
menuItemCmd(allCommands.hideField, 'Hide column', dom.cls('disabled', isReadonly || isRaw)),
|
||||||
freezeMenuItemCmd(options),
|
freezeMenuItemCmd(options),
|
||||||
menuDivider(),
|
menuDivider(),
|
||||||
MultiColumnMenu((options.disableFrozenMenu = true, options)),
|
MultiColumnMenu((options.disableFrozenMenu = true, options)),
|
||||||
|
@ -307,8 +307,14 @@ export class RightPanel extends Disposable {
|
|||||||
testId('right-widget-title')
|
testId('right-widget-title')
|
||||||
)),
|
)),
|
||||||
|
|
||||||
cssRow(primaryButton('Change Widget', this._createPageWidgetPicker()),
|
dom.maybe(
|
||||||
cssRow.cls('-top-space')),
|
(use) => !use(activeSection.isRaw),
|
||||||
|
() => cssRow(
|
||||||
|
primaryButton('Change Widget', this._createPageWidgetPicker()),
|
||||||
|
cssRow.cls('-top-space')
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
cssSeparator(),
|
cssSeparator(),
|
||||||
|
|
||||||
dom.maybe((use) => ['detail', 'single'].includes(use(this._pageWidgetType)!), () => [
|
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(),
|
cssSeparator(),
|
||||||
dom.create(VisibleFieldsConfig, this._gristDoc, activeSection),
|
dom.create(VisibleFieldsConfig, this._gristDoc, activeSection),
|
||||||
]),
|
]),
|
||||||
@ -413,6 +425,7 @@ export class RightPanel extends Disposable {
|
|||||||
dom.hide((use) => !use(use(table).summarySourceTable)),
|
dom.hide((use) => !use(use(table).summarySourceTable)),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
dom.maybe((use) => !use(activeSection.isRaw), () =>
|
||||||
cssButtonRow(primaryButton('Edit Data Selection', this._createPageWidgetPicker(),
|
cssButtonRow(primaryButton('Edit Data Selection', this._createPageWidgetPicker(),
|
||||||
testId('pwc-editDataSelection')),
|
testId('pwc-editDataSelection')),
|
||||||
dom.maybe(
|
dom.maybe(
|
||||||
@ -424,7 +437,7 @@ export class RightPanel extends Disposable {
|
|||||||
testId('detach-button'),
|
testId('detach-button'),
|
||||||
)),
|
)),
|
||||||
cssRow.cls('-top-space'),
|
cssRow.cls('-top-space'),
|
||||||
),
|
)),
|
||||||
|
|
||||||
// TODO: "Advanced settings" is for "on-demand" marking of tables. This should only be shown
|
// 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
|
// 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(),
|
cssSeparator(),
|
||||||
|
|
||||||
|
dom.maybe((use) => !use(activeSection.isRaw), () => [
|
||||||
cssLabel('SELECT BY'),
|
cssLabel('SELECT BY'),
|
||||||
cssRow(
|
cssRow(
|
||||||
select(link, linkOptions, {defaultLabel: 'Select Widget'}),
|
select(link, linkOptions, {defaultLabel: 'Select Widget'}),
|
||||||
testId('right-select-by')
|
testId('right-select-by')
|
||||||
),
|
),
|
||||||
|
]),
|
||||||
|
|
||||||
domComputed((use) => {
|
domComputed((use) => {
|
||||||
const activeSectionRef = activeSection.getRowId();
|
const activeSectionRef = activeSection.getRowId();
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {allCommands} from 'app/client/components/commands';
|
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 {testId} from 'app/client/ui2018/cssVars';
|
||||||
import {menuDivider, menuItemCmd, menuItemLink} from 'app/client/ui2018/menus';
|
import {menuDivider, menuItemCmd, menuItemLink} from 'app/client/ui2018/menus';
|
||||||
import {dom} from 'grainjs';
|
import {dom} from 'grainjs';
|
||||||
@ -7,7 +7,7 @@ import {dom} from 'grainjs';
|
|||||||
/**
|
/**
|
||||||
* Returns a list of menu items for a view section.
|
* 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 viewInstance = viewSection.viewInstance.peek()!;
|
||||||
const gristDoc = viewInstance.gristDoc;
|
const gristDoc = viewInstance.gristDoc;
|
||||||
|
|
||||||
@ -30,6 +30,7 @@ export function makeViewLayoutMenu(viewModel: ViewRec, viewSection: ViewSectionR
|
|||||||
menuDivider(),
|
menuDivider(),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const viewRec = viewSection.view();
|
||||||
return [
|
return [
|
||||||
dom.maybe((use) => ['single'].includes(use(viewSection.parentKey)), () => contextMenu),
|
dom.maybe((use) => ['single'].includes(use(viewSection.parentKey)), () => contextMenu),
|
||||||
menuItemCmd(allCommands.printSection, 'Print widget', testId('print-section')),
|
menuItemCmd(allCommands.printSection, 'Print widget', testId('print-section')),
|
||||||
@ -50,7 +51,7 @@ export function makeViewLayoutMenu(viewModel: ViewRec, viewSection: ViewSectionR
|
|||||||
testId('section-open-configuration')),
|
testId('section-open-configuration')),
|
||||||
),
|
),
|
||||||
menuItemCmd(allCommands.deleteSection, 'Delete widget',
|
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')),
|
testId('section-delete')),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {reportError} from 'app/client/models/AppModel';
|
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 {FilterInfo} from 'app/client/models/entities/ViewSectionRec';
|
||||||
import {CustomComputed} from 'app/client/models/modelUtil';
|
import {CustomComputed} from 'app/client/models/modelUtil';
|
||||||
import {attachColumnFilterMenu} from 'app/client/ui/ColumnFilterMenu';
|
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.
|
// [Filter Icon] (v) (x) - Filter toggle and all the components in the menu.
|
||||||
export function viewSectionMenu(owner: IDisposableOwner, docModel: DocModel, viewSection: ViewSectionRec,
|
export function viewSectionMenu(owner: IDisposableOwner, docModel: DocModel, viewSection: ViewSectionRec,
|
||||||
viewModel: ViewRec, isReadonly: Observable<boolean>) {
|
isReadonly: Observable<boolean>) {
|
||||||
|
|
||||||
const popupControls = new WeakMap<ColumnRec, PopupControl>();
|
const popupControls = new WeakMap<ColumnRec, PopupControl>();
|
||||||
|
|
||||||
@ -84,7 +84,8 @@ export function viewSectionMenu(owner: IDisposableOwner, docModel: DocModel, vie
|
|||||||
// [+] Add filter
|
// [+] Add filter
|
||||||
makeAddFilterButton(viewSection, popupControls),
|
makeAddFilterButton(viewSection, popupControls),
|
||||||
// [+] Toggle filter bar
|
// [+] Toggle filter bar
|
||||||
makeFilterBarToggle(viewSection.activeFilterBar),
|
dom.maybe((use) => !use(viewSection.isRaw),
|
||||||
|
() => makeFilterBarToggle(viewSection.activeFilterBar)),
|
||||||
// Widget options
|
// Widget options
|
||||||
dom.maybe(use => use(viewSection.parentKey) === 'custom', () =>
|
dom.maybe(use => use(viewSection.parentKey) === 'custom', () =>
|
||||||
makeCustomOptions(viewSection)
|
makeCustomOptions(viewSection)
|
||||||
@ -125,7 +126,7 @@ export function viewSectionMenu(owner: IDisposableOwner, docModel: DocModel, vie
|
|||||||
testId('viewLayout'),
|
testId('viewLayout'),
|
||||||
cssFixHeight.cls(''),
|
cssFixHeight.cls(''),
|
||||||
cssDotsIconWrapper(cssIcon('Dots')),
|
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('div.fieldbuilder_settings_button',
|
||||||
dom.testId('FieldBuilder_settings'),
|
dom.testId('FieldBuilder_settings'),
|
||||||
kd.text(() => this.field.useColOptions() ? 'Common' : 'Separate'), ' ▾',
|
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(),
|
useSeparate: () => this.fieldSettingsUseSeparate(),
|
||||||
saveAsCommon: () => this.fieldSettingsSaveAsCommon(),
|
saveAsCommon: () => this.fieldSettingsSaveAsCommon(),
|
||||||
revertToCommon: () => this.fieldSettingsRevertToCommon()
|
revertToCommon: () => this.fieldSettingsRevertToCommon(),
|
||||||
}))
|
},
|
||||||
|
)),
|
||||||
),
|
),
|
||||||
'Field in ',
|
'Field in ',
|
||||||
kd.text(() => this.origColumn.viewFields().all().length),
|
kd.text(() => this.origColumn.viewFields().all().length),
|
||||||
|
@ -10,7 +10,9 @@ import {Document} from 'app/common/UserAPI';
|
|||||||
import clone = require('lodash/clone');
|
import clone = require('lodash/clone');
|
||||||
import pickBy = require('lodash/pickBy');
|
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
|
// What page to show in the user's home area. Defaults to 'workspace' if a workspace is set, and
|
||||||
// to 'all' otherwise.
|
// 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) {
|
function parseDocPage(p: string): IDocPage {
|
||||||
if (['code', 'acl', 'GristDocTour'].includes(p)) {
|
if (SpecialDocPage.guard(p)) {
|
||||||
return p as 'code'|'acl'|'GristDocTour';
|
return p;
|
||||||
}
|
}
|
||||||
return parseInt(p, 10);
|
return parseInt(p, 10);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user