From eff78ae2e1efbcc39690b98ae5ec13a7bb84cd6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jaros=C5=82aw=20Sadzi=C5=84ski?= Date: Fri, 11 Mar 2022 11:48:36 +0100 Subject: [PATCH] (core) New UI for raw data views Summary: Creating new UI for raw data views based on design. - Renaming left for follow up diff - Link in the menu is hidden for now To access raw UI, use /p/data URL. Test Plan: new tests Reviewers: georgegevoian Reviewed By: georgegevoian Subscribers: dsagal Differential Revision: https://phab.getgrist.com/D3306 --- app/client/components/DataTables.ts | 414 +++++++++++++++++++++++++-- app/client/components/GridView.js | 13 +- app/client/components/GristDoc.ts | 14 +- app/client/components/Printing.ts | 2 +- app/client/components/ViewLayout.css | 7 + app/client/components/ViewLayout.ts | 124 ++++---- app/client/lib/localStorageObs.ts | 4 +- app/client/ui/DocMenuCss.ts | 5 + app/client/ui/Tools.ts | 11 +- app/client/ui2018/modals.ts | 6 +- 10 files changed, 517 insertions(+), 83 deletions(-) diff --git a/app/client/components/DataTables.ts b/app/client/components/DataTables.ts index 885b5fe1..ca6fe188 100644 --- a/app/client/components/DataTables.ts +++ b/app/client/components/DataTables.ts @@ -1,32 +1,410 @@ +import * as commands from 'app/client/components/commands'; import {GristDoc} from 'app/client/components/GristDoc'; +import {printViewSection} from 'app/client/components/Printing'; import {buildViewSectionDom, ViewSectionHelper} from 'app/client/components/ViewLayout'; -import {ViewSectionRec} from 'app/client/models/DocModel'; -import {Disposable, dom, domComputed} from 'grainjs'; +import {copyToClipboard} from 'app/client/lib/copyToClipboard'; +import {localStorageObs} from 'app/client/lib/localStorageObs'; +import {setTestState} from 'app/client/lib/testState'; +import {TableRec} from 'app/client/models/DocModel'; +import {reportError} from 'app/client/models/errors'; +import {docList, docListHeader, docMenuTrigger} from 'app/client/ui/DocMenuCss'; +import {showTransientTooltip} from 'app/client/ui/tooltips'; +import {buttonSelect, cssButtonSelect} from 'app/client/ui2018/buttonSelect'; +import * as css from 'app/client/ui2018/cssVars'; +import {icon} from 'app/client/ui2018/icons'; +import {menu, menuItem, menuText} from 'app/client/ui2018/menus'; +import {confirmModal} from 'app/client/ui2018/modals'; +import {Computed, Disposable, dom, fromKo, makeTestId, MultiHolder, styled} from 'grainjs'; + +const testId = makeTestId('test-raw-data-'); export class DataTables extends Disposable { + private _popupVisible = Computed.create(this, use => Boolean(use(this._gristDoc.viewModel.activeSectionId))); + constructor(private _gristDoc: GristDoc) { super(); + const commandGroup = { + cancel: () => { this._close(); }, + printSection: () => { printViewSection(null, this._gristDoc.viewModel.activeSection()).catch(reportError); }, + }; + this.autoDispose(commands.createGroup(commandGroup, this, true)); } 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())), - )) + const holder = new MultiHolder(); + // Get the user id, to remember selected layout on the next visit. + const userId = this._gristDoc.app.topAppModel.appObs.get()?.currentUser?.id ?? 0; + const view = holder.autoDispose(localStorageObs(`u=${userId}:raw:viewType`, "list")); + // Handler to close the lightbox. + const close = this._close.bind(this); + return container( + dom.autoDispose(holder), + docList( + /*************** List section **********/ + testId('list'), + cssBetween( + docListHeader('Raw data tables'), + cssSwitch( + buttonSelect( + view, + [ + {value: 'card', icon: 'TypeTable'}, + {value: 'list', icon: 'TypeCardList'}, + ], + css.testId('view-mode'), + cssButtonSelect.cls("-light") + ) + ) + ), + cssList( + cssList.cls(use => `-${use(view)}`), + dom.forEach(fromKo(this._gristDoc.docModel.allTables.getObservable()), tableRec => + cssItem( + testId('table'), + cssItemContent( + cssIcon('TypeTable'), + cssLabels( + cssTitleLine( + cssLine( + dom.text(use2 => use2(use2(tableRec.rawViewSection).title) || use2(tableRec.tableId)), + testId('table-title'), + ) + ), + cssIdLine( + cssIdLineContent( + cssUpperCase("Table id: "), + cssTableId( + testId('table-id'), + dom.text(tableRec.tableId), + ), + { title : 'Click to copy' }, + dom.on('click', async (e, t) => { + e.stopImmediatePropagation(); + e.preventDefault(); + showTransientTooltip(t, 'Table id copied to clipboard', { + key: 'copy-table-id' + }); + await copyToClipboard(tableRec.tableId.peek()); + setTestState({clipboard: tableRec.tableId.peek()}); + }) + ) + ), + ), + ), + cssDots(docMenuTrigger( + testId('table-dots'), + icon('Dots'), + menu(() => this._menuItems(tableRec), {placement: 'bottom-start'}), + dom.on('click', (ev) => { ev.stopPropagation(); ev.preventDefault(); }), + )), + dom.on('click', () => { + const sectionId = tableRec.rawViewSection.peek().getRowId(); + if (!sectionId) { + throw new Error(`Table ${tableRec.tableId.peek()} doesn't have a raw view section.`); + } + this._gristDoc.viewModel.activeSectionId(sectionId); + }) + ) + ) + ), ), - domComputed( - this._gristDoc.viewModel.activeSection, - (viewSection) => { - if (!viewSection.getRowId()) { - return; - } - ViewSectionHelper.create(this, this._gristDoc, viewSection); - return buildViewSectionDom(this._gristDoc, viewSection.getRowId()); + /*************** Lightbox section **********/ + container.cls("-lightbox", this._popupVisible), + dom.domComputedOwned(fromKo(this._gristDoc.viewModel.activeSection), (owner, viewSection) => { + if (!viewSection.getRowId()) { + return null; } - ) + ViewSectionHelper.create(owner, this._gristDoc, viewSection); + return cssOverlay( + testId('overlay'), + cssSectionWrapper( + buildViewSectionDom({ + gristDoc: this._gristDoc, + sectionRowId: viewSection.getRowId(), + draggable: false, + focusable: false, + onRename: this._renameSection.bind(this) + }) + ), + cssCloseButton('CrossBig', + testId('close-button'), + dom.on('click', close) + ), + // Close the lightbox when user clicks exactly on the overlay. + dom.on('click', (ev, elem) => void (ev.target === elem ? close() : null)) + ); + }), + ); + } + + private _close() { + this._gristDoc.viewModel.activeSectionId(0); + } + + private _menuItems(t: TableRec) { + const {isReadonly, docModel} = this._gristDoc; + return [ + // TODO: in the upcoming diff + // menuItem(() => this._renameTable(t), "Rename", testId('rename'), + // dom.cls('disabled', isReadonly)), + menuItem( + () => this._removeTable(t), + 'Remove', + testId('menu-remove'), + dom.cls('disabled', use => use(isReadonly) || use(docModel.allTables.getObservable()).length <= 1 ) + ), + dom.maybe(isReadonly, () => menuText('You do not have edit access to this document')), ]; } + + private async _renameSection(name: string) { + // here we will rename primary page for active primary viewSection + const primaryViewName = this._gristDoc.viewModel.activeSection.peek().table.peek().primaryView.peek().name; + await primaryViewName.saveOnly(name); + } + + private _removeTable(t: TableRec) { + const {docModel} = this._gristDoc; + function doRemove() { + return docModel.docData.sendAction(['RemoveTable', t.tableId.peek()]); + } + confirmModal(`Delete ${t.tableId()} data, and remove it from all pages?`, 'Delete', doRemove); + } + + // private async _renameTable(t: TableRec) { + // // TODO: + // } } + +const container = styled('div', ` + overflow: hidden; + position: relative; + height: 100%; +`); + +const cssBetween = styled('div', ` + display: flex; + justify-content: space-between; +`); + +// Below styles makes the list view look like a card view +// on smaller screens. + +const cssSwitch = styled('div', ` + @media ${css.mediaXSmall} { + & { + display: none; + } + } +`); + +const cssList = styled('div', ` + display: flex; + &-list { + flex-direction: column; + gap: 8px; + } + &-card { + flex-direction: row; + flex-wrap: wrap; + gap: 24px; + } + @media ${css.mediaSmall} { + & { + gap: 12px !important; + } + } +`); + +const cssItemContent = styled('div', ` + display: flex; + flex: 1; + overflow: hidden; + .${cssList.className}-list & { + align-items: center; + } + .${cssList.className}-card & { + align-items: flex-start; + } + @media ${css.mediaXSmall} { + & { + align-items: flex-start !important; + } + } +`); + +const cssItem = styled('div', ` + display: flex; + align-items: center; + cursor: pointer; + border-radius: 3px; + max-width: 750px; + border: 1px solid ${css.colors.mediumGrey}; + &:hover { + border-color: ${css.colors.slate}; + } + .${cssList.className}-list & { + height: calc(1em * 40/13); /* 40px for 13px font */ + } + .${cssList.className}-card & { + width: 300px; + height: calc(1em * 56/13); /* 56px for 13px font */ + } + @media ${css.mediaSmall} { + .${cssList.className}-card & { + width: calc(50% - 12px); + } + } + @media ${css.mediaXSmall} { + & { + width: 100% !important; + height: calc(1em * 56/13) !important; /* 56px for 13px font */ + } + } +`); + +const cssIcon = styled(icon, ` + --icon-color: ${css.colors.lightGreen}; + margin-left: 12px; + margin-right: 8px; + flex: none; + .${cssList.className}-card & { + margin-top: 1px; + } + @media ${css.mediaXSmall} { + & { + margin-top: 1px; + } + } +`); + +const cssOverflow = styled('div', ` + overflow: hidden; +`); + +const cssLabels = styled(cssOverflow, ` + overflow: hidden; + display: flex; + flex-wrap: wrap; + align-items: center; + flex: 1; +`); + +const cssLine = styled('span', ` + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +`); + +const cssTitleLine = styled(cssOverflow, ` + display: flex; + min-width: 50%; + .${cssList.className}-card & { + flex-basis: 100%; + } + @media ${css.mediaXSmall} { + & { + flex-basis: 100% !important; + } + } +`); + +const cssIdLine = styled(cssOverflow, ` + display: flex; + min-width: 40%; + .${cssList.className}-card & { + flex-basis: 100%; + } +`); + +const cssIdLineContent = styled(cssOverflow, ` + display: flex; + cursor: default; + align-items: baseline; + color: ${css.colors.slate}; + transition: background 0.05s; + padding: 1px 2px; + &:hover { + background: ${css.colors.lightGrey}; + } + @media ${css.mediaSmall} { + & { + padding: 0px 2px !important; + } + } +`); + +const cssTableId = styled(cssLine, ` + font-size: ${css.vars.smallFontSize}; +`); + +const cssUpperCase = styled('span', ` + text-transform: uppercase; + letter-spacing: 0.81px; + font-weight: 500; + font-size: 9px; /* xxsmallFontSize is to small */ + margin-right: 2px; + flex: 0; + white-space: nowrap; +`); + +const cssDots = styled('div', ` + flex: none; + margin-right: 8px; +`); + +const cssOverlay = styled('div', ` + z-index: 10; + background-color: ${css.colors.backdrop}; + inset: 0px; + height: 100%; + width: 100%; + padding: 32px 56px 0px 56px; + position: absolute; + @media ${css.mediaSmall} { + & { + padding: 22px; + padding-top: 30px; + } + } +`); + +const cssSectionWrapper = styled('div', ` + background: white; + height: 100%; + display: flex; + flex-direction: column; + overflow: hidden; + border-radius: 5px; + border-bottom-left-radius: 0px; + border-bottom-right-radius: 0px; + & .viewsection_content { + margin: 0px; + margin-top: 12px; + } + & .viewsection_title { + padding: 0px 12px; + } + & .filter_bar { + margin-left: 6px; + } +`); + +const cssCloseButton = styled(icon, ` + position: absolute; + top: 16px; + right: 16px; + height: 24px; + width: 24px; + cursor: pointer; + --icon-color: ${css.vars.primaryBg}; + &:hover { + --icon-color: ${css.colors.lighterGreen}; + } + @media ${css.mediaSmall} { + & { + top: 6px; + right: 6px; + } + } +`); diff --git a/app/client/components/GridView.js b/app/client/components/GridView.js index 45180654..d99cee96 100644 --- a/app/client/components/GridView.js +++ b/app/client/components/GridView.js @@ -230,8 +230,12 @@ function GridView(gristDoc, viewSectionModel, isPreview = false) { this.onEvent(this.scrollPane, 'scroll', this.onScroll); //-------------------------------------------------- - // Command group implementing all grid level commands. + // Command group implementing all grid level commands (except cancel) this.autoDispose(commands.createGroup(GridView.gridCommands, this, this.viewSection.hasFocus)); + // Cancel command is registered conditionally, only when there is an active + // cell selection. This command is also used by Raw Data Views, to close the Grid popup. + const hasSelection = this.autoDispose(ko.pureComputed(() => !this.cellSelector.isCurrentSelectType(''))); + this.autoDispose(commands.createGroup(GridView.selectionCommands, this, hasSelection)); // Timer to allow short, otherwise non-actionable clicks on column names to trigger renaming. this._colClickTime = 0; // Units: milliseconds. @@ -244,6 +248,12 @@ _.extend(GridView.prototype, BaseView.prototype); // ====================================================================================== // GRID-LEVEL COMMANDS +// Moved out of all commands to support Raw Data Views (which use this command to close +// the Grid popup). +GridView.selectionCommands = { + cancel: function() { this.clearSelection(); } +} + GridView.gridCommands = { cursorUp: function() { // This conditional exists so that when users have the cursor in the top row but are not @@ -302,7 +312,6 @@ GridView.gridCommands = { await this.paste(pasteObj, cutCallback); await this.scrollToCursor(false); }, - cancel: function() { this.clearSelection(); }, sortAsc: function() { sortBy(this.viewSection.activeSortSpec, this.currentColumn().getRowId(), Sort.ASC); }, diff --git a/app/client/components/GristDoc.ts b/app/client/components/GristDoc.ts index 1169feac..b66fca5f 100644 --- a/app/client/components/GristDoc.ts +++ b/app/client/components/GristDoc.ts @@ -355,6 +355,7 @@ export class GristDoc extends DisposableWithEvents { */ public buildDom() { return cssViewContentPane(testId('gristdoc'), + cssViewContentPane.cls("-contents", use => use(this.activeViewId) === 'data'), 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))) : @@ -884,8 +885,14 @@ export class GristDoc extends DisposableWithEvents { private async _switchToSectionId(sectionId: number) { const section: ViewSectionRec = this.docModel.viewSections.getRowModel(sectionId); const view: ViewRec = section.view.peek(); - await this.openDocPage(view.getRowId()); - view.activeSectionId(sectionId); // this.viewModel will reflect this with a delay. + if (!view.id.peek()) { + // This is raw data view + await urlState().pushUrl({docPage: 'data'}); + this.viewModel.activeSectionId(sectionId); + } else { + await this.openDocPage(view.getRowId()); + view.activeSectionId(sectionId); // this.viewModel will reflect this with a delay. + } // Returns the value of section.viewInstance() as soon as it is truthy. return waitObs(section.viewInstance); @@ -992,4 +999,7 @@ const cssViewContentPane = styled('div', ` margin: 0px; } } + &-contents { + margin: 0px; + } `); diff --git a/app/client/components/Printing.ts b/app/client/components/Printing.ts index 5d6e90fa..661746d4 100644 --- a/app/client/components/Printing.ts +++ b/app/client/components/Printing.ts @@ -37,7 +37,7 @@ export async function printViewSection(layout: any, viewSection: ViewSectionRec) function prepareToPrint(onOff: boolean) { // Hide all layout boxes that do NOT contain the section to be printed. - layout.forEachBox((box: any) => { + layout?.forEachBox((box: any) => { if (!box.dom.contains(sectionElem)) { box.dom.classList.toggle('print-hide', onOff); } diff --git a/app/client/components/ViewLayout.css b/app/client/components/ViewLayout.css index 511f91e5..c5585150 100644 --- a/app/client/components/ViewLayout.css +++ b/app/client/components/ViewLayout.css @@ -124,6 +124,13 @@ } } +/* Used by Raw Data UI */ +.active_section--no-indicator > .view_data_pane_container, +.active_section--no-indicator > .view_data_pane_container.viewsection_type_detail { + box-shadow: none; + border-left: 1px solid var(--grist-color-dark-grey); +} + .disable_viewpane { justify-content: center; text-align: center; diff --git a/app/client/components/ViewLayout.ts b/app/client/components/ViewLayout.ts index 6526a197..c36103d7 100644 --- a/app/client/components/ViewLayout.ts +++ b/app/client/components/ViewLayout.ts @@ -183,7 +183,12 @@ export class ViewLayout extends DisposableWithEvents implements IDomComponent { } private _buildLeafContent(sectionRowId: number) { - return buildViewSectionDom(this.gristDoc, sectionRowId, this._isResizing, this.viewModel); + return buildViewSectionDom({ + gristDoc: this.gristDoc, + sectionRowId, + isResizing: this._isResizing, + viewModel: this.viewModel + }); } /** @@ -264,6 +269,70 @@ export class ViewLayout extends DisposableWithEvents implements IDomComponent { } } +export function buildViewSectionDom(options: { + gristDoc: GristDoc, + sectionRowId: number, + isResizing?: Observable + viewModel?: ViewRec, + // Should show drag anchor. + draggable?: boolean /* defaults to true */ + // Should show green bar on the left (but preserves active-section class). + focusable?: boolean /* defaults to true */ + // Custom handler for renaming the section. + onRename?: (name: string) => any +}) { + const isResizing = options.isResizing ?? Observable.create(null, false); + const {gristDoc, sectionRowId, viewModel, draggable = true, focusable = true} = options; + + // 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}`), + !options.isResizing ? dom.autoDispose(isResizing) : null, + cssViewLeaf.cls(''), + cssViewLeafInactive.cls('', (use) => !vs.isDisposed() && !use(vs.hasFocus)), + dom.cls('active_section', vs.hasFocus), + dom.cls('active_section--no-indicator', !focusable), + 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)), + !draggable ? dom.style("visibility", "hidden") : null + ), + 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) => options.onRename ? options.onRename(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); }), + ); +} + + const cssSigmaIcon = styled(icon, ` bottom: 1px; margin-right: 5px; @@ -354,56 +423,3 @@ 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/lib/localStorageObs.ts b/app/client/lib/localStorageObs.ts index e05927c2..a7aa3e97 100644 --- a/app/client/lib/localStorageObs.ts +++ b/app/client/lib/localStorageObs.ts @@ -63,9 +63,9 @@ export function localStorageBoolObs(key: string, defValue = false): Observable { +export function localStorageObs(key: string, defaultValue?: string): Observable { const store = getStorage(); - const obs = Observable.create(null, store.getItem(key)); + const obs = Observable.create(null, store.getItem(key) ?? defaultValue ?? null); obs.addListener((val) => (val === null) ? store.removeItem(key) : store.setItem(key, val)); return obs; } diff --git a/app/client/ui/DocMenuCss.ts b/app/client/ui/DocMenuCss.ts index 2f396373..affdb214 100644 --- a/app/client/ui/DocMenuCss.ts +++ b/app/client/ui/DocMenuCss.ts @@ -21,6 +21,11 @@ export const docList = styled('div', ` padding: 32px 24px 24px 24px; } } + @media print { + & { + display: none; + } + } `); export const docListHeader = styled('div', ` diff --git a/app/client/ui/Tools.ts b/app/client/ui/Tools.ts index 9509f587..21602667 100644 --- a/app/client/ui/Tools.ts +++ b/app/client/ui/Tools.ts @@ -49,7 +49,16 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse }), testId('access-rules'), ), - + // Raw data - for now hidden. + // cssPageEntry( + // cssPageEntry.cls('-selected', (use) => use(gristDoc.activeViewId) === 'data'), + // cssPageLink( + // cssPageIcon('Database'), + // cssLinkText('Raw data'), + // testId('raw'), + // urlState().setLinkUrl({docPage: 'data'}) + // ) + // ), cssPageEntry( cssPageLink(cssPageIcon('Log'), cssLinkText('Document History'), testId('log'), dom.on('click', () => gristDoc.showTool('docHistory'))) diff --git a/app/client/ui2018/modals.ts b/app/client/ui2018/modals.ts index 02f51507..db77f9c9 100644 --- a/app/client/ui2018/modals.ts +++ b/app/client/ui2018/modals.ts @@ -331,6 +331,9 @@ export function cssModalWidth(style: ModalWidth) { /* CSS styled components */ +// For centering, we use 'margin: auto' on the flex item instead of 'justify-content: center' on +// the flex container, to ensure the full item can be scrolled in case of overflow. +// See https://stackoverflow.com/a/33455342/328565 const cssModalDialog = styled('div', ` background-color: white; min-width: 428px; @@ -377,9 +380,6 @@ export const cssModalButtons = styled('div', ` } `); -// For centering, we use 'margin: auto' on the flex item instead of 'justify-content: center' on -// the flex container, to ensure the full item can be scrolled in case of overflow. -// See https://stackoverflow.com/a/33455342/328565 const cssModalBacker = styled('div', ` position: fixed; display: flex;