mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +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:
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); }),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user