mirror of
https://github.com/gristlabs/grist-core.git
synced 2024-10-27 20:44:07 +00:00
(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
This commit is contained in:
parent
2d0978559b
commit
eff78ae2e1
@ -1,32 +1,410 @@
|
|||||||
|
import * as commands from 'app/client/components/commands';
|
||||||
import {GristDoc} from 'app/client/components/GristDoc';
|
import {GristDoc} from 'app/client/components/GristDoc';
|
||||||
|
import {printViewSection} from 'app/client/components/Printing';
|
||||||
import {buildViewSectionDom, ViewSectionHelper} from 'app/client/components/ViewLayout';
|
import {buildViewSectionDom, ViewSectionHelper} from 'app/client/components/ViewLayout';
|
||||||
import {ViewSectionRec} from 'app/client/models/DocModel';
|
import {copyToClipboard} from 'app/client/lib/copyToClipboard';
|
||||||
import {Disposable, dom, domComputed} from 'grainjs';
|
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 {
|
export class DataTables extends Disposable {
|
||||||
|
private _popupVisible = Computed.create(this, use => Boolean(use(this._gristDoc.viewModel.activeSectionId)));
|
||||||
|
|
||||||
constructor(private _gristDoc: GristDoc) {
|
constructor(private _gristDoc: GristDoc) {
|
||||||
super();
|
super();
|
||||||
|
const commandGroup = {
|
||||||
|
cancel: () => { this._close(); },
|
||||||
|
printSection: () => { printViewSection(null, this._gristDoc.viewModel.activeSection()).catch(reportError); },
|
||||||
|
};
|
||||||
|
this.autoDispose(commands.createGroup(commandGroup, this, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
public buildDom() {
|
public buildDom() {
|
||||||
return [
|
const holder = new MultiHolder();
|
||||||
dom(
|
// Get the user id, to remember selected layout on the next visit.
|
||||||
'ul',
|
const userId = this._gristDoc.app.topAppModel.appObs.get()?.currentUser?.id ?? 0;
|
||||||
this._gristDoc.docModel.allTables.all().map(t => dom(
|
const view = holder.autoDispose(localStorageObs(`u=${userId}:raw:viewType`, "list"));
|
||||||
'li', t.rawViewSection().title() || t.tableId(),
|
// Handler to close the lightbox.
|
||||||
dom.on('click', () => this._gristDoc.viewModel.activeSectionId(t.rawViewSection.peek().getRowId())),
|
const close = this._close.bind(this);
|
||||||
))
|
return container(
|
||||||
|
dom.autoDispose(holder),
|
||||||
|
docList(
|
||||||
|
/*************** List section **********/
|
||||||
|
testId('list'),
|
||||||
|
cssBetween(
|
||||||
|
docListHeader('Raw data tables'),
|
||||||
|
cssSwitch(
|
||||||
|
buttonSelect<any>(
|
||||||
|
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<ViewSectionRec>(
|
/*************** Lightbox section **********/
|
||||||
this._gristDoc.viewModel.activeSection,
|
container.cls("-lightbox", this._popupVisible),
|
||||||
(viewSection) => {
|
dom.domComputedOwned(fromKo(this._gristDoc.viewModel.activeSection), (owner, viewSection) => {
|
||||||
if (!viewSection.getRowId()) {
|
if (!viewSection.getRowId()) {
|
||||||
return;
|
return null;
|
||||||
}
|
|
||||||
ViewSectionHelper.create(this, this._gristDoc, viewSection);
|
|
||||||
return buildViewSectionDom(this._gristDoc, viewSection.getRowId());
|
|
||||||
}
|
}
|
||||||
)
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
@ -230,8 +230,12 @@ function GridView(gristDoc, viewSectionModel, isPreview = false) {
|
|||||||
this.onEvent(this.scrollPane, 'scroll', this.onScroll);
|
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));
|
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.
|
// Timer to allow short, otherwise non-actionable clicks on column names to trigger renaming.
|
||||||
this._colClickTime = 0; // Units: milliseconds.
|
this._colClickTime = 0; // Units: milliseconds.
|
||||||
@ -244,6 +248,12 @@ _.extend(GridView.prototype, BaseView.prototype);
|
|||||||
// ======================================================================================
|
// ======================================================================================
|
||||||
// GRID-LEVEL COMMANDS
|
// 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 = {
|
GridView.gridCommands = {
|
||||||
cursorUp: function() {
|
cursorUp: function() {
|
||||||
// This conditional exists so that when users have the cursor in the top row but are not
|
// 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.paste(pasteObj, cutCallback);
|
||||||
await this.scrollToCursor(false);
|
await this.scrollToCursor(false);
|
||||||
},
|
},
|
||||||
cancel: function() { this.clearSelection(); },
|
|
||||||
sortAsc: function() {
|
sortAsc: function() {
|
||||||
sortBy(this.viewSection.activeSortSpec, this.currentColumn().getRowId(), Sort.ASC);
|
sortBy(this.viewSection.activeSortSpec, this.currentColumn().getRowId(), Sort.ASC);
|
||||||
},
|
},
|
||||||
|
@ -355,6 +355,7 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
*/
|
*/
|
||||||
public buildDom() {
|
public buildDom() {
|
||||||
return cssViewContentPane(testId('gristdoc'),
|
return cssViewContentPane(testId('gristdoc'),
|
||||||
|
cssViewContentPane.cls("-contents", use => use(this.activeViewId) === 'data'),
|
||||||
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))) :
|
||||||
@ -884,8 +885,14 @@ export class GristDoc extends DisposableWithEvents {
|
|||||||
private async _switchToSectionId(sectionId: number) {
|
private async _switchToSectionId(sectionId: number) {
|
||||||
const section: ViewSectionRec = this.docModel.viewSections.getRowModel(sectionId);
|
const section: ViewSectionRec = this.docModel.viewSections.getRowModel(sectionId);
|
||||||
const view: ViewRec = section.view.peek();
|
const view: ViewRec = section.view.peek();
|
||||||
await this.openDocPage(view.getRowId());
|
if (!view.id.peek()) {
|
||||||
view.activeSectionId(sectionId); // this.viewModel will reflect this with a delay.
|
// 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.
|
// Returns the value of section.viewInstance() as soon as it is truthy.
|
||||||
return waitObs(section.viewInstance);
|
return waitObs(section.viewInstance);
|
||||||
@ -992,4 +999,7 @@ const cssViewContentPane = styled('div', `
|
|||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&-contents {
|
||||||
|
margin: 0px;
|
||||||
|
}
|
||||||
`);
|
`);
|
||||||
|
@ -37,7 +37,7 @@ export async function printViewSection(layout: any, viewSection: ViewSectionRec)
|
|||||||
|
|
||||||
function prepareToPrint(onOff: boolean) {
|
function prepareToPrint(onOff: boolean) {
|
||||||
// Hide all layout boxes that do NOT contain the section to be printed.
|
// 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)) {
|
if (!box.dom.contains(sectionElem)) {
|
||||||
box.dom.classList.toggle('print-hide', onOff);
|
box.dom.classList.toggle('print-hide', onOff);
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
.disable_viewpane {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -183,7 +183,12 @@ export class ViewLayout extends DisposableWithEvents implements IDomComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private _buildLeafContent(sectionRowId: number) {
|
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<boolean>
|
||||||
|
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<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)),
|
||||||
|
!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<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); }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
const cssSigmaIcon = styled(icon, `
|
const cssSigmaIcon = styled(icon, `
|
||||||
bottom: 1px;
|
bottom: 1px;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
@ -354,56 +423,3 @@ 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); }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@ -63,9 +63,9 @@ export function localStorageBoolObs(key: string, defValue = false): Observable<b
|
|||||||
/**
|
/**
|
||||||
* Helper to create a string observable whose state is stored in localStorage.
|
* Helper to create a string observable whose state is stored in localStorage.
|
||||||
*/
|
*/
|
||||||
export function localStorageObs(key: string): Observable<string|null> {
|
export function localStorageObs(key: string, defaultValue?: string): Observable<string|null> {
|
||||||
const store = getStorage();
|
const store = getStorage();
|
||||||
const obs = Observable.create<string|null>(null, store.getItem(key));
|
const obs = Observable.create<string|null>(null, store.getItem(key) ?? defaultValue ?? null);
|
||||||
obs.addListener((val) => (val === null) ? store.removeItem(key) : store.setItem(key, val));
|
obs.addListener((val) => (val === null) ? store.removeItem(key) : store.setItem(key, val));
|
||||||
return obs;
|
return obs;
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,11 @@ export const docList = styled('div', `
|
|||||||
padding: 32px 24px 24px 24px;
|
padding: 32px 24px 24px 24px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@media print {
|
||||||
|
& {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
export const docListHeader = styled('div', `
|
export const docListHeader = styled('div', `
|
||||||
|
@ -49,7 +49,16 @@ export function tools(owner: Disposable, gristDoc: GristDoc, leftPanelOpen: Obse
|
|||||||
}),
|
}),
|
||||||
testId('access-rules'),
|
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(
|
cssPageEntry(
|
||||||
cssPageLink(cssPageIcon('Log'), cssLinkText('Document History'), testId('log'),
|
cssPageLink(cssPageIcon('Log'), cssLinkText('Document History'), testId('log'),
|
||||||
dom.on('click', () => gristDoc.showTool('docHistory')))
|
dom.on('click', () => gristDoc.showTool('docHistory')))
|
||||||
|
@ -331,6 +331,9 @@ export function cssModalWidth(style: ModalWidth) {
|
|||||||
|
|
||||||
/* CSS styled components */
|
/* 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', `
|
const cssModalDialog = styled('div', `
|
||||||
background-color: white;
|
background-color: white;
|
||||||
min-width: 428px;
|
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', `
|
const cssModalBacker = styled('div', `
|
||||||
position: fixed;
|
position: fixed;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
Loading…
Reference in New Issue
Block a user