(core) updates from grist-core

This commit is contained in:
Paul Fitzpatrick 2022-08-08 09:31:07 -04:00
commit c72ebf61a7
4 changed files with 531 additions and 3 deletions

View File

@ -17,6 +17,11 @@ export interface PageActions {
isReadonly: Observable<boolean>;
}
function isTargetSelected(target: HTMLElement) {
const parentItemHeader = target.closest('.' + itemHeader.className);
return parentItemHeader ? parentItemHeader.classList.contains('selected') : false;
}
// build the dom for a document page entry. It shows an icon (for now the first letter of the name,
// but later we'll support user selected icon), the name and a dots menu containing a "Rename" and
// "Remove" entries. Clicking "Rename" turns the page name into an editable input, which then call
@ -52,7 +57,10 @@ export function buildPageDom(name: Observable<string>, actions: PageActions, ...
domComputed(isRenaming, (isrenaming) => (
isrenaming ?
cssPageItem(
cssPageInitial(dom.text((use) => Array.from(use(name))[0])),
cssPageInitial(
testId('initial'),
dom.text((use) => Array.from(use(name))[0])
),
cssEditorInput(
{
initialValue: name.get() || '',
@ -68,8 +76,15 @@ export function buildPageDom(name: Observable<string>, actions: PageActions, ...
// firefox.
) :
cssPageItem(
cssPageInitial(dom.text((use) => Array.from(use(name))[0])),
cssPageName(dom.text(name), testId('label')),
cssPageInitial(
testId('initial'),
dom.text((use) => Array.from(use(name))[0]),
),
cssPageName(
dom.text(name),
testId('label'),
dom.on('click', (ev) => isTargetSelected(ev.target as HTMLElement) && isRenaming.set(true)),
),
cssPageMenuTrigger(
cssPageIcon('Dots'),
menu(pageMenu, {placement: 'bottom-start', parentSelectorToMark: '.' + itemHeader.className}),
@ -120,6 +135,7 @@ const cssPageName = styled('div', `
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-grow: 1;
.${treeViewContainer.className}-close & {
display: none;
}

BIN
test/fixtures/docs/Pages.grist vendored Normal file

Binary file not shown.

502
test/nbrowser/Pages.ts Normal file
View File

@ -0,0 +1,502 @@
import {DocCreationInfo} from 'app/common/DocListAPI';
import {UserAPI} from 'app/common/UserAPI';
import {assert, driver, Key} from 'mocha-webdriver';
import {Session} from 'test/nbrowser/gristUtils';
import * as gu from 'test/nbrowser/gristUtils';
import {server, setupTestSuite} from 'test/nbrowser/testUtils';
import values = require('lodash/values');
describe('Pages', function() {
this.timeout(20000);
setupTestSuite();
let doc: DocCreationInfo;
let api: UserAPI;
let session: Session;
const cleanup = setupTestSuite({team: true});
before(async () => {
session = await gu.session().teamSite.login();
doc = await session.tempDoc(cleanup, 'Pages.grist');
api = session.createHomeApi();
});
it('should list all pages in document', async () => {
// check content of _girst_Pages and _grist_Views
assert.deepInclude(await api.getTable(doc.id, '_grist_Pages'), {
viewRef: [1, 2, 3, 4, 5],
pagePos: [1, 2, 1.5, 3, 4],
indentation: [0, 0, 1, 1, 1],
});
assert.deepInclude(await api.getTable(doc.id, '_grist_Views'), {
name: ['Interactions', 'People', 'Documents', 'User & Leads', 'Overview'],
id: [1, 2, 3, 4, 5],
});
// load page and check all pages are listed
await driver.get(`${server.getHost()}/o/test-grist/doc/${doc.id}`);
await driver.findWait('.test-treeview-container', 1000);
assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'People', 'User & Leads', 'Overview']);
});
it('should select correct page if /p/<docPage> in the url', async () => {
// show page with viewRef 2
await gu.loadDoc(`/o/test-grist/doc/${doc.id}/p/2`);
assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'People', 'User & Leads', 'Overview']);
assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /People/);
assert.match(await gu.getActiveSectionTitle(), /People/i);
});
it('should select first page if /p/<docPage> is omitted in the url', async () => {
await driver.get(`${server.getHost()}/o/test-grist/doc/${doc.id}`);
await driver.findWait('.test-treeview-container', 1000);
assert.match(await driver.find('.test-treeview-itemHeader').getText(), /Interactions/);
assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Interactions/);
assert.match(await gu.getActiveSectionTitle(), /Interactions/i);
// Check also that this did NOT cause a redirect to include /p/ in the URL.
assert.notMatch(await driver.getCurrentUrl(), /\/p\//);
});
it('clicking page should set /p/<docPage> in the url', async () => {
await driver.get(`${server.getHost()}/o/test-grist/doc/${doc.id}`);
// Wait for data to load.
assert.equal(await driver.findWait('.viewsection_title', 3000).isDisplayed(), true);
await gu.waitForServer();
// Click on a page; check the URL, selected item, and the title of the view section.
await clickPage(/Documents/)
assert.match(await driver.getCurrentUrl(), /\/p\/3/);
assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Documents/);
assert.match(await gu.getActiveSectionTitle(), /Documents/i);
// Click on another page; check the URL, selected item, and the title of the view section.
await clickPage(/People/)
assert.match(await driver.getCurrentUrl(), /\/p\/2/);
assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /People/);
assert.match(await gu.getActiveSectionTitle(), /People/i);
// TODO: Add a check that open-in-new-tab works too.
});
it('should allow renaming table', async () => {
// open dots menu and click rename
await gu.openPageMenu('People');
await driver.find('.test-docpage-rename').doClick();
// do rename
await driver.find('.test-docpage-editor').sendKeys('PeopleRenamed', Key.ENTER);
await gu.waitForServer();
assert.deepEqual(
await gu.getPageNames(),
['Interactions', 'Documents', 'PeopleRenamed', 'User & Leads', 'Overview']
);
// Test that we can delete after remove (there was a bug related to this).
await gu.removePage('PeopleRenamed');
assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'User & Leads', 'Overview']);
// revert changes
await gu.undo(2);
assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'People', 'User & Leads', 'Overview']);
});
it('should allow renaming table when click on page selected label', async () => {
// do rename
await clickPage(/People/)
await driver.findContent('.test-treeview-label', 'People').doClick();
await driver.find('.test-docpage-editor').sendKeys('PeopleRenamed', Key.ENTER);
await gu.waitForServer();
assert.deepEqual(
await gu.getPageNames(),
['Interactions', 'Documents', 'PeopleRenamed', 'User & Leads', 'Overview']
);
// revert changes
await gu.undo(2);
assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'People', 'User & Leads', 'Overview']);
})
it('should not allow blank page name', async () => {
// Begin renaming of People page
await gu.openPageMenu('People');
await driver.find('.test-docpage-rename').doClick();
// Delete page name and check editor's value equals ''
await driver.find('.test-docpage-editor').sendKeys(Key.DELETE);
assert.equal(await driver.find('.test-docpage-editor').value(), '');
// Save blank name
await driver.sendKeys(Key.ENTER);
await gu.waitForServer();
// Check name is still People
assert.include(await gu.getPageNames(), 'People');
});
it('should not change page when clicking the input while renaming page', async () => {
// check that initially People is selected
assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /People/);
// start renaming Documents and click the input
await gu.openPageMenu('Documents');
await driver.find('.test-docpage-rename').doClick();
await driver.find('.test-docpage-editor').click();
// check that People is still the selected page.
assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /People/);
// abord renaming
await driver.find('.test-docpage-editor').sendKeys(Key.ESCAPE);
});
it('should allow moving pages', async () => {
// check initial state
assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'People', 'User & Leads', 'Overview']);
// move page
await movePage(/User & Leads/, {after: /Overview/});
await gu.waitForServer();
assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'People', 'Overview', 'User & Leads']);
// revert changes
await gu.undo();
assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'People', 'User & Leads', 'Overview']);
});
it('moving a page should not extend collapsed page', async () => {
/**
* Here what is really being tested is that TreeModelRecord correctly reuses TreeModelItem,
* because if it wasn't the case, TreeViewComponent would not be able to reuse dom and would
* rebuild dom for all pages causing all page to be expanded.
*/
// let's collapse Interactions
await driver.findContent('.test-treeview-itemHeader', /Interactions/).find('.test-treeview-itemArrow').doClick();
assert.deepEqual(await gu.getPageNames(), ['Interactions', '', 'People', 'User & Leads', 'Overview']);
// let's move
await movePage(/User & Leads/, {after: /Overview/});
await gu.waitForServer();
// check that pages has moved and Interactions remained collapsed
assert.deepEqual(await gu.getPageNames(), ['Interactions', '', 'People', 'Overview', 'User & Leads']);
// revert changes
await gu.undo();
await driver.findContent('.test-treeview-itemHeader', /Interactions/).find('.test-treeview-itemArrow').doClick();
assert.deepEqual(await gu.getPageNames(), ['Interactions', 'Documents', 'People', 'User & Leads', 'Overview']);
});
it('should allow to cycle though pages using shortcuts', async () => {
function nextPage() {
return driver.find('body').sendKeys(Key.chord(Key.ALT, Key.DOWN));
}
function prevPage() {
return driver.find('body').sendKeys(Key.chord(Key.ALT, Key.UP));
}
function selectedPage() {
return driver.find('.test-treeview-itemHeader.selected').getText();
}
// goto page 'Interactions'
await clickPage(/Interactions/);
// check selected page
assert.match(await selectedPage(), /Interactions/);
// prev page
await prevPage();
// check selecte page
assert.match(await selectedPage(), /Overview/);
// prev page
await prevPage();
// check selecte page
assert.match(await selectedPage(), /User & Leads/);
// next page
await nextPage();
// check selected page
assert.match(await selectedPage(), /Overview/);
// next page
await nextPage();
// check selected page
assert.match(await selectedPage(), /Interactions/);
});
it('undo/redo should update url', async () => {
// goto page 'Interactions' and send keys
await clickPage(/Interactions/);
assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Interactions/);
await driver.findContentWait('.gridview_data_row_num', /1/, 2000);
await driver.sendKeys(Key.ENTER, 'Foo', Key.ENTER);
await gu.waitForServer();
assert.deepEqual(await gu.getVisibleGridCells(0, [1]), ['Foo']);
// goto page 'People' and click undo
await clickPage(/People/);
await gu.waitForDocToLoad();
await gu.waitForUrl(/\/p\/2\b/); // check that url match p/2
await gu.undo();
await gu.waitForDocToLoad();
await gu.waitForUrl(/\/p\/1\b/); // check that url match p/1
// check that "Interactions" page is selected
assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Interactions/);
// check that undo worked
assert.deepEqual(await gu.getVisibleGridCells(0, [1]), ['']);
// Click on next test should not trigger renaming input
await driver.findContent('.test-treeview-itemHeader', /People/).doClick();
});
it('Add new page should update url', async () => {
// goto page 'Interactions' and check that url updated
await clickPage(/Interactions/);
await gu.waitForUrl(/\/p\/1\b/);
// Add new Page, check that url updated and page is selected
await gu.addNewPage(/Table/, /New Table/);
await gu.waitForUrl(/\/p\/6\b/);
assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Table1/);
// goto page 'Interactions' and check that url updated and page selectd
await clickPage(/Interactions/);
await gu.waitForUrl(/\/p\/1\b/);
assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Interactions/);
});
it('Removing a page should work', async () => {
// Create and open new document
const docId = await session.tempNewDoc(cleanup, "test-page-removal");
await driver.get(`${server.getHost()}/o/test-grist/doc/${docId}`);
await gu.waitForUrl('test-page-removal');
// Add a new page using Table1
await gu.addNewPage(/Table/, /Table1/);
assert.deepInclude(await api.getTable(docId, '_grist_Tables'), {
tableId: ['Table1'],
primaryViewId: [1],
});
assert.deepInclude(await api.getTable(docId, '_grist_Views'), {
name: ['Table1', 'New page'],
id: [1, 2],
});
assert.deepEqual(await gu.getPageNames(), ['Table1', 'New page']);
// check that the new page is now selected
await gu.waitForUrl(/\/p\/2\b/);
assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /New page/);
// remove new page
await gu.removePage(/New page/);
// check that url has no p/<...> and 'Table1' is now selected
await driver.wait(async () => !/\/p\//.test(await driver.getCurrentUrl()), 2000);
assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Table1/);
// check that corresponding view is removed
assert.deepInclude(await api.getTable(docId, '_grist_Tables'), {
tableId: ['Table1'],
primaryViewId: [1],
});
assert.deepInclude(await api.getTable(docId, '_grist_Views'), {
name: ['Table1'],
id: [1],
});
assert.deepEqual(await gu.getPageNames(), ['Table1']);
// create table Foo and 1 new page using Foo
await api.applyUserActions(docId, [['AddTable', 'Foo', [{id: null, isFormula: true}]]]);
await driver.findContentWait('.test-treeview-itemHeader', /Foo/, 2000);
await gu.addNewPage(/Table/, /Foo/);
assert.deepInclude(await api.getTable(docId, '_grist_Tables'), {
tableId: ['Table1', 'Foo'],
primaryViewId: [1, 2],
});
assert.deepInclude(await api.getTable(docId, '_grist_Views'), {
name: ['Table1', 'Foo', 'New page'],
id: [1, 2, 3],
});
assert.deepEqual(await gu.getPageNames(), ['Table1', 'Foo', 'New page']);
// check that last page is now selected
await gu.waitForUrl(/\/p\/3\b/);
assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /New page/);
// remove table and make sure pages are also removed.
await gu.removeTable('Foo');
// check that Foo and page are removed
assert.deepInclude(await api.getTable(docId, '_grist_Tables'), {
tableId: ['Table1'],
primaryViewId: [1],
});
assert.deepInclude(await api.getTable(docId, '_grist_Views'), {
name: ['Table1'],
id: [1],
});
assert.deepEqual(await gu.getPageNames(), ['Table1']);
});
it('Remove should be disabled for last page', async () => {
// check that Remove is disabled on Table1
assert.isFalse(await gu.canRemovePage('Table1'));
// Adds a new page using Table1
await gu.addNewPage(/Table/, /Table1/);
assert.deepEqual(await gu.getPageNames(), ['Table1', 'New page']);
// Add a new table too.
await gu.addNewTable();
assert.deepEqual(await gu.getPageNames(), ['Table1', 'New page', 'Table2']);
// The "Remove" options should now be available on all three items.
assert.isTrue(await gu.canRemovePage('Table1'));
assert.isTrue(await gu.canRemovePage('New page'));
assert.isTrue(await gu.canRemovePage('Table2'));
// Add Table2 to "New page" (so that it can remain after Table1 is removed below).
await gu.getPageItem('New page').click();
await gu.addNewSection(/Table/, /Table2/);
// Now remove Table1.
await gu.removeTable('Table1');
assert.deepEqual(await gu.getPageNames(), ['New page', 'Table2']);
// Both pages should be removable still.
assert.isTrue(await gu.canRemovePage('New page'));
assert.isTrue(await gu.canRemovePage('Table2'));
// Remove New Page
await gu.removePage('New page');
// Now Table2 should not be removable (since it is the last page).
assert.isFalse(await gu.canRemovePage('Table2'));
});
it('should not throw JS errors when removing the current page without a slug', async () => {
// Create and open new document
const docId = await session.tempNewDoc(cleanup, "test-page-removal-js-error")
await driver.get(`${server.getHost()}/o/test-grist/doc/${docId}`);
await gu.waitForUrl('test-page-removal-js-error');
// Add two additional tables
await gu.addNewTable();
await gu.addNewTable();
assert.deepEqual(await gu.getPageNames(), ['Table1', 'Table2', 'Table3']);
// Open the default page (no p/<...> in the URL)
await driver.get(`${server.getHost()}/o/test-grist/doc/${docId}`);
// Check that Table1 is now selected
await driver.findContentWait('.test-treeview-itemHeader.selected', /Table1/, 2000);
assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Table1/);
// Remove page Table1
await gu.removePage('Table1');
assert.deepEqual(await gu.getPageNames(), ['Table2', 'Table3']);
// Now check that Table2 is selected
assert.match(await driver.find('.test-treeview-itemHeader.selected').getText(), /Table2/);
// Remove page Table2
await gu.removePage('Table2');
assert.deepEqual(await gu.getPageNames(), ['Table3']);
// Check that Table3 is the only page remaining
assert.deepInclude(await api.getTable(docId, '_grist_Views'), {
name: ['Table3'],
id: [3],
});
// Check that no JS errors were thrown
await gu.checkForErrors();
});
it('should offer a way to delete last tables', async () => {
// Create and open new document
const docId = await session.tempNewDoc(cleanup, "prompts")
await driver.get(`${server.getHost()}/o/test-grist/doc/${docId}`);
await gu.waitForUrl('prompts');
// Add two additional tables, with custom names.
await gu.addNewTable('Table B');
await gu.addNewTable('Table C');
await gu.addNewTable('Table Last');
assert.deepEqual(await gu.getPageNames(), ['Table1', 'Table B', 'Table C', 'Table Last']);
await gu.getPageItem('Table C').click();
// In Table C add Table D (a new one) and Table1 widget (existing);
await gu.addNewSection(/Table/, /New Table/, { tableName: "Table D"});
await gu.addNewSection(/Table/, "Table1");
// New table should not be added as a page
assert.deepEqual(await gu.getPageNames(), ['Table1', 'Table B', 'Table C', 'Table Last']);
// Make sure we see proper sections.
assert.deepEqual(await gu.getSectionTitles(), ['TABLE C', 'TABLE D', 'TABLE1']);
const revert = await gu.begin();
// Now removing Table1 page should be done without a prompt (since it is also on Table C)
await gu.removePage("Table1", { expectPrompt : false });
assert.deepEqual(await gu.getPageNames(), ['Table B', 'Table C', 'Table Last']);
// Removing Table B should show prompt (since it is last page)
await gu.removePage("Table B", { expectPrompt : true, tables: ['Table B'] });
assert.deepEqual(await gu.getPageNames(), ['Table C', 'Table Last']);
// Removing page Table C should also show prompt (it is last page for Table1,Table D and TableC)
await gu.getPageItem('Table Last').click();
await gu.getPageItem('Table C').click();
assert.deepEqual(await gu.getSectionTitles(), ['TABLE C', 'TABLE D', 'TABLE1' ]);
await gu.removePage("Table C", { expectPrompt : true, tables: ['Table D', 'Table C', 'Table1'] });
assert.deepEqual(await gu.getPageNames(), ['Table Last']);
await revert();
assert.deepEqual(await gu.getPageNames(), ['Table1', 'Table B', 'Table C', 'Table Last']);
assert.deepEqual(await gu.getSectionTitles(), ['TABLE C', 'TABLE D', 'TABLE1' ]);
});
});
async function movePage(page: RegExp, target: {before: RegExp}|{after: RegExp}) {
const targetReg = values(target)[0];
await driver.withActions(actions => actions
.move({origin: driver.findContent('.test-treeview-itemHeader', page)})
.move({origin: driver.findContent('.test-treeview-itemHeaderWrapper', page)
.find('.test-treeview-handle')})
.press()
.move({origin: driver.findContent('.test-treeview-itemHeader', targetReg),
y: 'after' in target ? 1 : -1
})
.release());
}
function clickPage(name: string|RegExp) {
return driver.findContent('.test-treeview-itemHeader', name).find(".test-docpage-initial").doClick();
}

View File

@ -386,6 +386,11 @@
resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-3.11.2.tgz#699ad86054cc20043c30d66a6fcde30bbf5d3d5e"
integrity sha512-JRDtMPEqXrzfuYAdqbxLot1GvAr/QvicIZAnOAigZaj8xVMhuSJTg/xsv9E1TvyL+wujYhRLx9ZsQ0oFOSmwyA==
"@types/jsesc@3.0.1":
version "3.0.1"
resolved "https://registry.yarnpkg.com/@types/jsesc/-/jsesc-3.0.1.tgz#ed1720ae08eae2f64341452e1693a84324029d99"
integrity sha512-F2g93pJlhV0RlW9uSUAM/hIxywlwlZcuRB/nZ82GaMPaO8mdexYbJ8Qt3UGbUS1M19YFQykEetrWW004M+vPCg==
"@types/json-schema@*", "@types/json-schema@^7.0.8":
version "7.0.11"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.11.tgz#d421b6c527a3037f7c84433fd2c4229e016863d3"
@ -3851,6 +3856,11 @@ jsbn@~0.1.0:
resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513"
integrity sha1-peZUwuWi3rXyAdls77yoDA7y9RM=
jsesc@3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e"
integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==
json-bigint@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/json-bigint/-/json-bigint-1.0.0.tgz#ae547823ac0cad8398667f8cd9ef4730f5b01ff1"