(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:
Alex Hall
2022-02-07 16:02:26 +02:00
parent 0f4153dc23
commit 592a43ec36
15 changed files with 193 additions and 111 deletions

View 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());
}
)
];
}
}

View File

@@ -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()),
};

View File

@@ -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];
}
}

View File

@@ -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'),

View File

@@ -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); }),
);
}