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