mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Raw renames
Summary: A new way for renaming tables. - There is a new popup to rename section (where you can also rename the table) - Renaming/Deleting page doesn't modify/delete the table. - Renaming table can rename a page if the names match (and the page contains a section with that table). - User can rename table in Raw Data UI in two ways - either on the listing or by using the section name popup - As before, there is no way to change tableId - it is derived from a table name. - When the section name is empty the table name is shown instead. - White space for section name is allowed (to discuss) - so the user can just paste ' '. - Empty name for a page is not allowed (but white space is). - Some bugs related to deleting tables with attached summary tables (and with undoing this operation) were fixed (but not all of them yet). Test Plan: Updated tests. Reviewers: georgegevoian Reviewed By: georgegevoian Subscribers: georgegevoian Differential Revision: https://phab.getgrist.com/D3360
This commit is contained in:
@@ -62,7 +62,7 @@ function BaseView(gristDoc, viewSectionModel, options) {
|
||||
if (this.viewSection.table().summarySourceTable()) {
|
||||
const groupGetter = this.tableModel.tableData.getRowPropFunc('group');
|
||||
this._mainRowSource = rowset.BaseFilteredRowSource.create(this,
|
||||
rowId => !gristTypes.isEmptyList(groupGetter(rowId)));
|
||||
rowId => !groupGetter || !gristTypes.isEmptyList(groupGetter(rowId)));
|
||||
this._mainRowSource.subscribeTo(this._queryRowSource);
|
||||
} else {
|
||||
this._mainRowSource = this._queryRowSource;
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
var _ = require('underscore');
|
||||
var ko = require('knockout');
|
||||
var BackboneEvents = require('backbone').Events;
|
||||
|
||||
var dispose = require('../lib/dispose');
|
||||
var dom = require('../lib/dom');
|
||||
var kd = require('../lib/koDom');
|
||||
|
||||
// Rather than require the whole of highlight.js, require just the core with the one language we
|
||||
// need, to keep our bundle smaller and the build faster.
|
||||
var hljs = require('highlight.js/lib/highlight');
|
||||
hljs.registerLanguage('python', require('highlight.js/lib/languages/python'));
|
||||
|
||||
function CodeEditorPanel(gristDoc) {
|
||||
this._gristDoc = gristDoc;
|
||||
this._schema = ko.observable('');
|
||||
this._denied = ko.observable(false);
|
||||
|
||||
this.listenTo(this._gristDoc, 'schemaUpdateAction', this.onSchemaAction);
|
||||
this.onSchemaAction(); // Fetch the schema to initialize
|
||||
}
|
||||
dispose.makeDisposable(CodeEditorPanel);
|
||||
_.extend(CodeEditorPanel.prototype, BackboneEvents);
|
||||
|
||||
CodeEditorPanel.prototype.buildDom = function() {
|
||||
// The tabIndex enables the element to gain focus, and the .clipboard class prevents the
|
||||
// Clipboard module from re-grabbing it. This is a quick fix for the issue where clipboard
|
||||
// interferes with text selection. TODO it should be possible for the Clipboard to never
|
||||
// interfere with text selection even for un-focusable elements.
|
||||
return dom('div.g-code-panel.clipboard',
|
||||
{tabIndex: "-1"},
|
||||
kd.maybe(this._denied, () => dom('div.g-code-panel-denied',
|
||||
dom('h2', kd.text('Access denied')),
|
||||
dom('div', kd.text('Code View is available only when you have full document access.')),
|
||||
)),
|
||||
kd.scope(this._schema, function(schema) {
|
||||
// The reason to scope and rebuild instead of using `kd.text(schema)` is because
|
||||
// hljs.highlightBlock(elem) replaces `elem` with a whole new dom tree.
|
||||
if (!schema) { return null; }
|
||||
return dom(
|
||||
'code.g-code-viewer.python',
|
||||
schema,
|
||||
dom.hide,
|
||||
dom.defer(function(elem) {
|
||||
hljs.highlightBlock(elem);
|
||||
dom.show(elem);
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
CodeEditorPanel.prototype.onSchemaAction = async function(actions) {
|
||||
try {
|
||||
const schema = await this._gristDoc.docComm.fetchTableSchema();
|
||||
if (!this.isDisposed()) {
|
||||
this._schema(schema);
|
||||
this._denied(false);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!String(err).match(/Cannot view code/)) {
|
||||
throw err;
|
||||
}
|
||||
if (!this.isDisposed()) {
|
||||
this._schema('');
|
||||
this._denied(true);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = CodeEditorPanel;
|
||||
64
app/client/components/CodeEditorPanel.ts
Normal file
64
app/client/components/CodeEditorPanel.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import {GristDoc} from 'app/client/components/GristDoc';
|
||||
import {reportError} from 'app/client/models/errors';
|
||||
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
|
||||
import {dom, Observable} from 'grainjs';
|
||||
|
||||
// Rather than require the whole of highlight.js, require just the core with the one language we
|
||||
// need, to keep our bundle smaller and the build faster.
|
||||
const hljs = require('highlight.js/lib/highlight');
|
||||
hljs.registerLanguage('python', require('highlight.js/lib/languages/python'));
|
||||
|
||||
export class CodeEditorPanel extends DisposableWithEvents {
|
||||
private _schema = Observable.create(this, '');
|
||||
private _denied = Observable.create(this, false);
|
||||
constructor(private _gristDoc: GristDoc) {
|
||||
super();
|
||||
this.listenTo(_gristDoc, 'schemaUpdateAction', this._onSchemaAction.bind(this));
|
||||
this._onSchemaAction().catch(reportError); // Fetch the schema to initialize
|
||||
}
|
||||
|
||||
public buildDom() {
|
||||
// The tabIndex enables the element to gain focus, and the .clipboard class prevents the
|
||||
// Clipboard module from re-grabbing it. This is a quick fix for the issue where clipboard
|
||||
// interferes with text selection. TODO it should be possible for the Clipboard to never
|
||||
// interfere with text selection even for un-focusable elements.
|
||||
return dom('div.g-code-panel.clipboard',
|
||||
{tabIndex: "-1"},
|
||||
dom.maybe(this._denied, () => dom('div.g-code-panel-denied',
|
||||
dom('h2', dom.text('Access denied')),
|
||||
dom('div', dom.text('Code View is available only when you have full document access.')),
|
||||
)),
|
||||
dom.maybe(this._schema, (schema) => {
|
||||
// The reason to scope and rebuild instead of using `kd.text(schema)` is because
|
||||
// hljs.highlightBlock(elem) replaces `elem` with a whole new dom tree.
|
||||
const elem = dom('code.g-code-viewer',
|
||||
dom.text(schema),
|
||||
dom.hide(true)
|
||||
);
|
||||
setTimeout(() => {
|
||||
hljs.highlightBlock(elem);
|
||||
dom.showElem(elem, true);
|
||||
});
|
||||
return elem;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private async _onSchemaAction() {
|
||||
try {
|
||||
const schema = await this._gristDoc.docComm.fetchTableSchema();
|
||||
if (!this.isDisposed()) {
|
||||
this._schema.set(schema);
|
||||
this._denied.set(false);
|
||||
}
|
||||
} catch (err) {
|
||||
if (!String(err).match(/Cannot view code/)) {
|
||||
throw err;
|
||||
}
|
||||
if (!this.isDisposed()) {
|
||||
this._schema.set('');
|
||||
this._denied.set(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,10 @@ export class CursorMonitor extends Disposable {
|
||||
this._whenCursorHasChangedStoreInMemory(doc);
|
||||
}
|
||||
|
||||
public clear() {
|
||||
this._store.clear(this._key);
|
||||
}
|
||||
|
||||
private _whenCursorHasChangedStoreInMemory(doc: GristDoc) {
|
||||
// whenever current position changes, store it in the memory
|
||||
this.autoDispose(doc.cursorPosition.addListener(pos => {
|
||||
@@ -62,8 +66,7 @@ export class CursorMonitor extends Disposable {
|
||||
private _whenDocumentLoadsRestorePosition(doc: GristDoc) {
|
||||
// if doc was opened with a hash link, don't restore last position
|
||||
if (doc.hasCustomNav.get()) {
|
||||
this._restored = true;
|
||||
return;
|
||||
return this._abortRestore();
|
||||
}
|
||||
|
||||
// if we are on raw data view, we need to set the position manually
|
||||
@@ -85,7 +88,9 @@ export class CursorMonitor extends Disposable {
|
||||
// set that we already restored the position, as some view is shown to the user
|
||||
this._restored = true;
|
||||
const viewId = doc.activeViewId.get();
|
||||
if (!isViewDocPage(viewId)) { return; }
|
||||
if (!isViewDocPage(viewId)) {
|
||||
return this._abortRestore();
|
||||
}
|
||||
const position = this._readPosition(viewId);
|
||||
if (position) {
|
||||
// Ignore error with finding desired cell.
|
||||
@@ -93,6 +98,11 @@ export class CursorMonitor extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
private _abortRestore() {
|
||||
this.clear();
|
||||
this._restored = true;
|
||||
}
|
||||
|
||||
private _storePosition(pos: ViewCursorPos) {
|
||||
this._store.update(this._key, pos);
|
||||
}
|
||||
|
||||
@@ -5,27 +5,32 @@ import {setTestState} from 'app/client/lib/testState';
|
||||
import {TableRec} from 'app/client/models/DocModel';
|
||||
import {docListHeader, docMenuTrigger} from 'app/client/ui/DocMenuCss';
|
||||
import {showTransientTooltip} from 'app/client/ui/tooltips';
|
||||
import {buildTableName} from 'app/client/ui/WidgetTitle';
|
||||
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 {Disposable, dom, fromKo, makeTestId, MultiHolder, styled} from 'grainjs';
|
||||
import {Computed, Disposable, dom, fromKo, makeTestId, Observable, styled} from 'grainjs';
|
||||
|
||||
const testId = makeTestId('test-raw-data-');
|
||||
|
||||
export class DataTables extends Disposable {
|
||||
private _tables: Observable<TableRec[]>;
|
||||
private _view: Observable<string | null>;
|
||||
constructor(private _gristDoc: GristDoc) {
|
||||
super();
|
||||
// Remove tables that we don't have access to. ACL will remove tableId from those tables.
|
||||
this._tables = Computed.create(this, use =>
|
||||
use(_gristDoc.docModel.rawTables.getObservable())
|
||||
.filter(t => Boolean(use(t.tableId))));
|
||||
// Get the user id, to remember selected layout on the next visit.
|
||||
const userId = this._gristDoc.app.topAppModel.appObs.get()?.currentUser?.id ?? 0;
|
||||
this._view = this.autoDispose(localStorageObs(`u=${userId}:raw:viewType`, "list"));
|
||||
}
|
||||
|
||||
public buildDom() {
|
||||
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"));
|
||||
return container(
|
||||
dom.autoDispose(holder),
|
||||
cssTableList(
|
||||
/*************** List section **********/
|
||||
testId('list'),
|
||||
@@ -33,7 +38,7 @@ export class DataTables extends Disposable {
|
||||
docListHeader('Raw data tables'),
|
||||
cssSwitch(
|
||||
buttonSelect<any>(
|
||||
view,
|
||||
this._view,
|
||||
[
|
||||
{value: 'card', icon: 'TypeTable'},
|
||||
{value: 'list', icon: 'TypeCardList'},
|
||||
@@ -44,49 +49,65 @@ export class DataTables extends Disposable {
|
||||
)
|
||||
),
|
||||
cssList(
|
||||
cssList.cls(use => `-${use(view)}`),
|
||||
dom.forEach(fromKo(this._gristDoc.docModel.allTables.getObservable()), tableRec =>
|
||||
cssList.cls(use => `-${use(this._view)}`),
|
||||
dom.forEach(this._tables, tableRec =>
|
||||
cssItem(
|
||||
testId('table'),
|
||||
cssItemContent(
|
||||
cssIcon('TypeTable',
|
||||
// Element to click in tests.
|
||||
dom.domComputed(use => `table-id-${use(tableRec.tableId)}`)
|
||||
),
|
||||
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()});
|
||||
})
|
||||
)
|
||||
),
|
||||
cssLeft(
|
||||
dom.domComputed(tableRec.tableId, (tableId) =>
|
||||
cssGreenIcon(
|
||||
'TypeTable',
|
||||
testId(`table-id-${tableId}`)
|
||||
)
|
||||
),
|
||||
),
|
||||
cssDots(docMenuTrigger(
|
||||
testId('table-dots'),
|
||||
icon('Dots'),
|
||||
menu(() => this._menuItems(tableRec), {placement: 'bottom-start'}),
|
||||
dom.on('click', (ev) => { ev.stopPropagation(); ev.preventDefault(); }),
|
||||
)),
|
||||
cssMiddle(
|
||||
css60(
|
||||
testId('table-title'),
|
||||
dom.domComputed(fromKo(tableRec.rawViewSectionRef), vsRef => {
|
||||
if (!vsRef) {
|
||||
// Some very old documents might not have rawViewSection.
|
||||
return dom('span', dom.text(tableRec.tableNameDef));
|
||||
} else {
|
||||
return dom('div', // to disable flex grow in the widget
|
||||
dom.domComputed(fromKo(tableRec.rawViewSection), vs =>
|
||||
dom.update(
|
||||
buildTableName(vs, testId('widget-title')),
|
||||
dom.on('click', (ev) => { ev.stopPropagation(); ev.preventDefault(); }),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}),
|
||||
),
|
||||
css40(
|
||||
cssIdHoverWrapper(
|
||||
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()});
|
||||
})
|
||||
)
|
||||
),
|
||||
),
|
||||
cssRight(
|
||||
docMenuTrigger(
|
||||
testId('table-menu'),
|
||||
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) {
|
||||
@@ -101,17 +122,17 @@ export class DataTables extends Disposable {
|
||||
);
|
||||
}
|
||||
|
||||
private _menuItems(t: TableRec) {
|
||||
private _menuItems(table: 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),
|
||||
() => this._removeTable(table),
|
||||
'Remove',
|
||||
testId('menu-remove'),
|
||||
dom.cls('disabled', use => use(isReadonly) || use(docModel.allTables.getObservable()).length <= 1 )
|
||||
dom.cls('disabled', use => use(isReadonly) || (
|
||||
// Can't delete last user table, unless it is a hidden table.
|
||||
use(docModel.allTables.getObservable()).length <= 1 && !use(table.isHidden)
|
||||
))
|
||||
),
|
||||
dom.maybe(isReadonly, () => menuText('You do not have edit access to this document')),
|
||||
];
|
||||
@@ -124,10 +145,6 @@ export class DataTables extends Disposable {
|
||||
}
|
||||
confirmModal(`Delete ${t.tableId()} data, and remove it from all pages?`, 'Delete', doRemove);
|
||||
}
|
||||
|
||||
// private async _renameTable(t: TableRec) {
|
||||
// // TODO:
|
||||
// }
|
||||
}
|
||||
|
||||
const container = styled('div', `
|
||||
@@ -169,23 +186,6 @@ const cssList = styled('div', `
|
||||
}
|
||||
`);
|
||||
|
||||
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;
|
||||
@@ -197,7 +197,7 @@ const cssItem = styled('div', `
|
||||
border-color: ${css.colors.slate};
|
||||
}
|
||||
.${cssList.className}-list & {
|
||||
height: calc(1em * 40/13); /* 40px for 13px font */
|
||||
min-height: calc(1em * 40/13); /* 40px for 13px font */
|
||||
}
|
||||
.${cssList.className}-card & {
|
||||
width: 300px;
|
||||
@@ -216,67 +216,69 @@ const cssItem = styled('div', `
|
||||
}
|
||||
`);
|
||||
|
||||
const cssIcon = styled(icon, `
|
||||
--icon-color: ${css.colors.lightGreen};
|
||||
margin-left: 12px;
|
||||
// Holds icon in top left corner
|
||||
const cssLeft = styled('div', `
|
||||
padding-top: 11px;
|
||||
padding-left: 12px;
|
||||
margin-right: 8px;
|
||||
align-self: flex-start;
|
||||
display: flex;
|
||||
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;
|
||||
const cssMiddle = styled('div', `
|
||||
flex-grow: 1;
|
||||
min-width: 0px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
margin-top: 6px;
|
||||
margin-bottom: 4px;
|
||||
.${cssList.className}-card & {
|
||||
margin: 0px:
|
||||
}
|
||||
`);
|
||||
|
||||
const css60 = styled('div', `
|
||||
min-width: min(240px, 100%);
|
||||
display: flex;
|
||||
flex: 6;
|
||||
`);
|
||||
|
||||
const css40 = styled('div', `
|
||||
min-width: min(240px, 100%);
|
||||
flex: 4;
|
||||
display: flex;
|
||||
`);
|
||||
|
||||
|
||||
// Holds dots menu (which is 24px x 24px, but has its own 4px right margin)
|
||||
const cssRight = styled('div', `
|
||||
padding-right: 8px;
|
||||
margin-left: 8px;
|
||||
align-self: center;
|
||||
display: flex;
|
||||
flex: none;
|
||||
`);
|
||||
|
||||
const cssGreenIcon = styled(icon, `
|
||||
--icon-color: ${css.colors.lightGreen};
|
||||
`);
|
||||
|
||||
const cssLine = styled('span', `
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
`);
|
||||
|
||||
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, `
|
||||
const cssIdHoverWrapper = styled('div', `
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
cursor: default;
|
||||
align-items: baseline;
|
||||
color: ${css.colors.slate};
|
||||
transition: background 0.05s;
|
||||
padding: 1px 2px;
|
||||
line-height: 18px;
|
||||
&:hover {
|
||||
background: ${css.colors.lightGrey};
|
||||
}
|
||||
@@ -301,11 +303,6 @@ const cssUpperCase = styled('span', `
|
||||
white-space: nowrap;
|
||||
`);
|
||||
|
||||
const cssDots = styled('div', `
|
||||
flex: none;
|
||||
margin-right: 8px;
|
||||
`);
|
||||
|
||||
const cssTableList = styled('div', `
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
flex-shrink: 0;
|
||||
padding: 0;
|
||||
margin-top: -4px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.grist-single-record__menu__count {
|
||||
|
||||
@@ -66,7 +66,10 @@ export class EditorMonitor extends Disposable {
|
||||
*/
|
||||
private async _listenToReload(doc: GristDoc) {
|
||||
// don't restore on readonly mode or when there is custom nav
|
||||
if (doc.isReadonly.get() || doc.hasCustomNav.get()) { return; }
|
||||
if (doc.isReadonly.get() || doc.hasCustomNav.get()) {
|
||||
this._store.clear();
|
||||
return;
|
||||
}
|
||||
// if we are on raw data view, we need to set the position manually
|
||||
// as currentView observable will not be changed.
|
||||
if (doc.activeViewId.get() === 'data') {
|
||||
@@ -86,7 +89,10 @@ export class EditorMonitor extends Disposable {
|
||||
this._restored = true;
|
||||
const viewId = doc.activeViewId.get();
|
||||
// if view wasn't rendered (page is displaying history or code view) do nothing
|
||||
if (!isViewDocPage(viewId)) { return; }
|
||||
if (!isViewDocPage(viewId)) {
|
||||
this._store.clear();
|
||||
return;
|
||||
}
|
||||
const lastEdit = this._store.readValue();
|
||||
if (lastEdit) {
|
||||
// set the cursor at right cell
|
||||
|
||||
@@ -7,7 +7,7 @@ import {AccessRules} from 'app/client/aclui/AccessRules';
|
||||
import {ActionLog} from 'app/client/components/ActionLog';
|
||||
import * as BaseView from 'app/client/components/BaseView';
|
||||
import {isNumericLike, isNumericOnly} from 'app/client/components/ChartView';
|
||||
import * as CodeEditorPanel from 'app/client/components/CodeEditorPanel';
|
||||
import {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";
|
||||
@@ -380,9 +380,9 @@ export class GristDoc extends DisposableWithEvents {
|
||||
return cssViewContentPane(testId('gristdoc'),
|
||||
cssViewContentPane.cls("-contents", use => use(this.activeViewId) === 'data'),
|
||||
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(RawData.create(this, this))) :
|
||||
viewId === 'code' ? dom.create(CodeEditorPanel, this) :
|
||||
viewId === 'acl' ? dom.create(AccessRules, this) :
|
||||
viewId === 'data' ? dom.create(RawData, this) :
|
||||
viewId === 'GristDocTour' ? null :
|
||||
dom.create((owner) => (this._viewLayout = ViewLayout.create(owner, this, viewId)))
|
||||
)),
|
||||
@@ -413,12 +413,14 @@ export class GristDoc extends DisposableWithEvents {
|
||||
* null, then moves to a position best suited for optActionGroup (not yet implemented).
|
||||
*/
|
||||
public async moveToCursorPos(cursorPos?: CursorPos, optActionGroup?: MinimalActionGroup): Promise<void> {
|
||||
if (!cursorPos || cursorPos.sectionId == null) {
|
||||
if (!cursorPos || !cursorPos.sectionId) {
|
||||
// TODO We could come up with a suitable cursorPos here based on the action itself.
|
||||
// This should only come up if trying to undo/redo after reloading a page (since the cursorPos
|
||||
// associated with the action is only stored in memory of the current JS process).
|
||||
// A function like `getCursorPosForActionGroup(ag)` would also be useful to jump to the best
|
||||
// place from any action in the action log.
|
||||
// When user deletes table from Raw Data view, the section id will be 0 and undoing that
|
||||
// operation will move cursor to the empty section row (with id 0).
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -787,6 +789,9 @@ export class GristDoc extends DisposableWithEvents {
|
||||
if (!cursorPos.sectionId) { throw new Error('sectionId required'); }
|
||||
if (!cursorPos.rowId) { throw new Error('rowId required'); }
|
||||
const section = this.docModel.viewSections.getRowModel(cursorPos.sectionId);
|
||||
if (!section.id.peek()) {
|
||||
throw new Error(`Section ${cursorPos.sectionId} does not exist`);
|
||||
}
|
||||
const srcSection = section.linkSrcSection.peek();
|
||||
if (srcSection.id.peek()) {
|
||||
// We're in a linked section, so we need to recurse to make sure the row we want
|
||||
@@ -867,6 +872,17 @@ export class GristDoc extends DisposableWithEvents {
|
||||
view?.activateEditorAtCursor(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renames table. Method exposed primarily for tests.
|
||||
*/
|
||||
public async renameTable(tableId: string, newTableName: string) {
|
||||
const tableRec = this.docModel.allTables.all().find(t => t.tableId.peek() === tableId);
|
||||
if (!tableRec) {
|
||||
throw new UserError(`No table with id ${tableId}`);
|
||||
}
|
||||
await tableRec.tableName.saveOnly(newTableName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for a view to be ready
|
||||
*/
|
||||
|
||||
@@ -114,6 +114,9 @@ export class LinkingState extends Disposable {
|
||||
_update();
|
||||
function _update() {
|
||||
const result: FilterColValues = {filters: {}, operations: {}};
|
||||
if (srcSection.isDisposed()) {
|
||||
return result;
|
||||
}
|
||||
const srcRowId = srcSection.activeRowId();
|
||||
for (const c of srcSection.table().groupByColumns()) {
|
||||
const col = c.summarySource();
|
||||
|
||||
@@ -22,8 +22,22 @@ export class RawData extends Disposable {
|
||||
this.autoDispose(commands.createGroup(commandGroup, this, true));
|
||||
this._lightboxVisible = Computed.create(this, use => {
|
||||
const section = use(this._gristDoc.viewModel.activeSection);
|
||||
return Boolean(section.getRowId());
|
||||
return Boolean(use(section.id)) && use(section.isRaw);
|
||||
});
|
||||
// When we are disposed, we want to clear active section in the viewModel we got (which is an empty model)
|
||||
// to not restore the section when user will come back to Raw Data page.
|
||||
// But by the time we are gone (disposed), active view will be changed, so here we will save the reference.
|
||||
// TODO: empty view should rather have id = 0, not undefined. Should be fixed soon.
|
||||
const emptyView = this._gristDoc.docModel.views.rowModels.find(x => x.id.peek() === undefined);
|
||||
this.autoDispose(this._gristDoc.activeViewId.addListener(() => {
|
||||
emptyView?.activeSectionId(0);
|
||||
}));
|
||||
// Whenever we close lightbox, clear cursor monitor state.
|
||||
this.autoDispose(this._lightboxVisible.addListener(state => {
|
||||
if (!state) {
|
||||
this._gristDoc.cursorMonitor.clear();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public buildDom() {
|
||||
@@ -39,7 +53,8 @@ export class RawData extends Disposable {
|
||||
),
|
||||
/*************** Lightbox section **********/
|
||||
dom.domComputedOwned(fromKo(this._gristDoc.viewModel.activeSection), (owner, viewSection) => {
|
||||
if (!viewSection.getRowId()) {
|
||||
const sectionId = viewSection.getRowId();
|
||||
if (!sectionId || !viewSection.isRaw.peek()) {
|
||||
return null;
|
||||
}
|
||||
ViewSectionHelper.create(owner, this._gristDoc, viewSection);
|
||||
@@ -51,7 +66,7 @@ export class RawData extends Disposable {
|
||||
sectionRowId: viewSection.getRowId(),
|
||||
draggable: false,
|
||||
focusable: false,
|
||||
onRename: this._renameSection.bind(this)
|
||||
widgetNameHidden: true
|
||||
})
|
||||
),
|
||||
cssCloseButton('CrossBig',
|
||||
@@ -68,12 +83,6 @@ export class RawData extends Disposable {
|
||||
private _close() {
|
||||
this._gristDoc.viewModel.activeSectionId(0);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
const cssContainer = styled('div', `
|
||||
|
||||
@@ -15,32 +15,15 @@
|
||||
color: var(--grist-color-slate);
|
||||
font-size: var(--grist-small-font-size);
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.viewsection_titletext {
|
||||
cursor: text;
|
||||
overflow: hidden;
|
||||
}
|
||||
.viewsection_titletext_container {
|
||||
height: max-content;
|
||||
}
|
||||
|
||||
.viewsection_content {
|
||||
background-color: #ffffff;
|
||||
overflow: visible;
|
||||
margin: 12px;
|
||||
}
|
||||
|
||||
.viewsection_title_colorbox {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 8px;
|
||||
margin: auto .5rem auto 0;
|
||||
box-shadow: inset 0px 0px 5px rgba(0,0,0,0.5);
|
||||
}
|
||||
|
||||
/* TODO should be switched to use new icon */
|
||||
.viewsection_drag_indicator {
|
||||
visibility: hidden;
|
||||
|
||||
@@ -14,15 +14,15 @@ import {ViewRec, ViewSectionRec} from 'app/client/models/DocModel';
|
||||
import {reportError} from 'app/client/models/errors';
|
||||
import {filterBar} from 'app/client/ui/FilterBar';
|
||||
import {viewSectionMenu} from 'app/client/ui/ViewSectionMenu';
|
||||
import {buildWidgetTitle} from 'app/client/ui/WidgetTitle';
|
||||
import {colors, mediaSmall, testId} from 'app/client/ui2018/cssVars';
|
||||
import {editableLabel} from 'app/client/ui2018/editableLabel';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {DisposableWithEvents} from 'app/common/DisposableWithEvents';
|
||||
import {mod} from 'app/common/gutil';
|
||||
import {computedArray, Disposable, dom, fromKo, Holder, IDomComponent, styled, subscribe} from 'grainjs';
|
||||
import {Observable} from 'grainjs';
|
||||
import * as ko from 'knockout';
|
||||
import * as _ from 'underscore';
|
||||
import {icon} from 'app/client/ui2018/icons';
|
||||
import {computedArray, Disposable, dom, fromKo, Holder, IDomComponent, styled, subscribe} from 'grainjs';
|
||||
|
||||
// tslint:disable:no-console
|
||||
|
||||
@@ -275,11 +275,11 @@ export function buildViewSectionDom(options: {
|
||||
isResizing?: Observable<boolean>
|
||||
viewModel?: ViewRec,
|
||||
// Should show drag anchor.
|
||||
draggable?: boolean /* defaults to true */
|
||||
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
|
||||
focusable?: boolean, /* defaults to true */
|
||||
tableNameHidden?: boolean,
|
||||
widgetNameHidden?: boolean,
|
||||
}) {
|
||||
const isResizing = options.isResizing ?? Observable.create(null, false);
|
||||
const {gristDoc, sectionRowId, viewModel, draggable = true, focusable = true} = options;
|
||||
@@ -301,13 +301,7 @@ export function buildViewSectionDom(options: {
|
||||
),
|
||||
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'),
|
||||
)),
|
||||
),
|
||||
buildWidgetTitle(vs, options, testId('viewsection-title'), cssTestClick(testId("viewsection-blank"))),
|
||||
viewInstance.buildTitleControls(),
|
||||
dom('span.viewsection_buttons',
|
||||
dom.create(viewSectionMenu, gristDoc.docModel, vs, gristDoc.isReadonly)
|
||||
@@ -332,6 +326,11 @@ export function buildViewSectionDom(options: {
|
||||
);
|
||||
}
|
||||
|
||||
// With new widgetPopup it is hard to click on viewSection without a activating it, hence we
|
||||
// add a little blank space to use in test.
|
||||
const cssTestClick = styled(`div`, `
|
||||
min-width: 1px;
|
||||
`);
|
||||
|
||||
const cssSigmaIcon = styled(icon, `
|
||||
bottom: 1px;
|
||||
|
||||
Reference in New Issue
Block a user