mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Revealing hidden pages with visible children.
Summary: When a page is hidden, all its nested pages are shown as children of a different page that happens to be before (as in pagePos) that page. This diff shows those pages as CENSORED. Test Plan: Updated Reviewers: alexmojaki Reviewed By: alexmojaki Subscribers: alexmojaki Differential Revision: https://phab.getgrist.com/D3670
This commit is contained in:
@@ -7,7 +7,7 @@ import {server, setupTestSuite} from 'test/nbrowser/testUtils';
|
||||
import values = require('lodash/values');
|
||||
|
||||
describe('Pages', function() {
|
||||
this.timeout(20000);
|
||||
this.timeout(30000);
|
||||
setupTestSuite();
|
||||
let doc: DocCreationInfo;
|
||||
let api: UserAPI;
|
||||
@@ -20,6 +20,124 @@ describe('Pages', function() {
|
||||
api = session.createHomeApi();
|
||||
});
|
||||
|
||||
it('should show censor pages', async () => {
|
||||
// Make a 3 level hierarchy.
|
||||
assert.deepEqual(await gu.getPageTree(), [
|
||||
{
|
||||
label: 'Interactions', children: [
|
||||
{ label: 'Documents' },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'People', children: [
|
||||
{ label: 'User & Leads' },
|
||||
{ label: 'Overview' },
|
||||
]
|
||||
},
|
||||
]);
|
||||
await insertPage(/Overview/, /User & Leads/);
|
||||
assert.deepEqual(await gu.getPageTree(), [
|
||||
{
|
||||
label: 'Interactions', children: [
|
||||
{ label: 'Documents' },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'People', children: [
|
||||
{ label: 'User & Leads', children: [{ label: 'Overview' }] },
|
||||
]
|
||||
},
|
||||
]);
|
||||
const revertAcl = await gu.beginAclTran(api, doc.id);
|
||||
// Update ACL, hide People table from all users.
|
||||
await hideTable("People");
|
||||
// We will be reloaded, but it's not easy to wait for it, so do the refresh manually.
|
||||
await gu.reloadDoc();
|
||||
assert.deepEqual(await gu.getPageTree(), [
|
||||
{
|
||||
label: 'Interactions', children: [
|
||||
{ label: 'Documents'},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'CENSORED', children: [
|
||||
{ label: 'User & Leads', children: [{ label: 'Overview' }] },
|
||||
]
|
||||
},
|
||||
]);
|
||||
|
||||
// Test that we can't click this page.
|
||||
await driver.findContent('.test-treeview-itemHeader', /CENSORED/).click();
|
||||
await gu.waitForServer();
|
||||
assert.equal(await gu.getSectionTitle(), 'INTERACTIONS');
|
||||
|
||||
// Test that we don't have move handler.
|
||||
assert.isFalse(
|
||||
await driver.findContent('.test-treeview-itemHeaderWrapper', /CENSORED/)
|
||||
.find('.test-treeview-handle').isPresent()
|
||||
);
|
||||
|
||||
// Now hide User_Leads
|
||||
await hideTable("User_Leads");
|
||||
await gu.reloadDoc();
|
||||
assert.deepEqual(await gu.getPageTree(), [
|
||||
{
|
||||
label: 'Interactions', children: [
|
||||
{ label: 'Documents'},
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'CENSORED', children: [
|
||||
{ label: 'CENSORED', children: [{ label: 'Overview' }] },
|
||||
]
|
||||
},
|
||||
]);
|
||||
|
||||
// Now hide Overview, and test that whole node is hidden.
|
||||
await hideTable("Overview");
|
||||
await gu.reloadDoc();
|
||||
assert.deepEqual(await gu.getPageTree(), [
|
||||
{
|
||||
label: 'Interactions', children: [
|
||||
{ label: 'Documents'},
|
||||
]
|
||||
}
|
||||
]);
|
||||
|
||||
// Now hide Documents, this is a leaf, so it should be hidden from the start
|
||||
await hideTable("Documents");
|
||||
await gu.reloadDoc();
|
||||
assert.deepEqual(await gu.getPageTree(), [
|
||||
{
|
||||
label: 'Interactions'
|
||||
}
|
||||
]);
|
||||
|
||||
// Now hide Interactions, we should have a blank treeview
|
||||
await hideTable("Interactions");
|
||||
// We can wait for doc to load, because it waits for section.
|
||||
await driver.findWait(".test-treeview-container", 1000);
|
||||
assert.deepEqual(await gu.getPageTree(), []);
|
||||
|
||||
// Rollback
|
||||
await revertAcl();
|
||||
await gu.reloadDoc();
|
||||
assert.deepEqual(await gu.getPageTree(), [
|
||||
{
|
||||
label: 'Interactions', children: [
|
||||
{ label: 'Documents' },
|
||||
]
|
||||
},
|
||||
{
|
||||
label: 'People', children: [
|
||||
{ label: 'User & Leads', children: [{ label: 'Overview' }] },
|
||||
]
|
||||
},
|
||||
]);
|
||||
await gu.undo();
|
||||
});
|
||||
|
||||
|
||||
it('should list all pages in document', async () => {
|
||||
|
||||
// check content of _girst_Pages and _grist_Views
|
||||
@@ -122,8 +240,7 @@ describe('Pages', function() {
|
||||
// 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
|
||||
@@ -403,7 +520,7 @@ describe('Pages', function() {
|
||||
|
||||
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")
|
||||
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');
|
||||
|
||||
@@ -442,7 +559,7 @@ describe('Pages', function() {
|
||||
|
||||
it('should offer a way to delete last tables', async () => {
|
||||
// Create and open new document
|
||||
const docId = await session.tempNewDoc(cleanup, "prompts")
|
||||
const docId = await session.tempNewDoc(cleanup, "prompts");
|
||||
await driver.get(`${server.getHost()}/o/test-grist/doc/${docId}`);
|
||||
await gu.waitForUrl('prompts');
|
||||
|
||||
@@ -482,9 +599,18 @@ describe('Pages', function() {
|
||||
assert.deepEqual(await gu.getSectionTitles(), ['TABLE C', 'TABLE D', 'TABLE1' ]);
|
||||
});
|
||||
|
||||
|
||||
async function hideTable(tableId: string) {
|
||||
await api.applyUserActions(doc.id, [
|
||||
['AddRecord', '_grist_ACLResources', -1, {tableId, colIds: '*'}],
|
||||
['AddRecord', '_grist_ACLRules', null, {
|
||||
resource: -1, aclFormula: '', permissionsText: '-R',
|
||||
}],
|
||||
]);
|
||||
}
|
||||
});
|
||||
|
||||
async function movePage(page: RegExp, target: {before: RegExp}|{after: RegExp}) {
|
||||
async function movePage(page: RegExp, target: {before: RegExp}|{after: RegExp}|{into: RegExp}) {
|
||||
const targetReg = values(target)[0];
|
||||
await driver.withActions(actions => actions
|
||||
.move({origin: driver.findContent('.test-treeview-itemHeader', page)})
|
||||
@@ -496,3 +622,19 @@ async function movePage(page: RegExp, target: {before: RegExp}|{after: RegExp})
|
||||
})
|
||||
.release());
|
||||
}
|
||||
|
||||
|
||||
async function insertPage(page: RegExp, into: RegExp) {
|
||||
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', into),
|
||||
y: 5
|
||||
})
|
||||
.pause(1500) // wait for a target to be highlighted
|
||||
.release()
|
||||
);
|
||||
await gu.waitForServer();
|
||||
}
|
||||
|
||||
@@ -16,7 +16,8 @@ import { FullUser, UserProfile } from 'app/common/LoginSessionAPI';
|
||||
import { resetOrg } from 'app/common/resetOrg';
|
||||
import { UserAction } from 'app/common/DocActions';
|
||||
import { TestState } from 'app/common/TestState';
|
||||
import { Organization as APIOrganization, DocStateComparison, UserAPIImpl, Workspace } from 'app/common/UserAPI';
|
||||
import { Organization as APIOrganization, DocStateComparison,
|
||||
UserAPI, UserAPIImpl, Workspace } from 'app/common/UserAPI';
|
||||
import { Organization } from 'app/gen-server/entity/Organization';
|
||||
import { Product } from 'app/gen-server/entity/Product';
|
||||
import { create } from 'app/server/lib/create';
|
||||
@@ -688,6 +689,11 @@ export async function waitForDocToLoad(timeoutMs: number = 10000): Promise<void>
|
||||
await waitForServer();
|
||||
}
|
||||
|
||||
export async function reloadDoc() {
|
||||
await driver.navigate().refresh();
|
||||
await waitForDocToLoad();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for the doc list to show, to know that workspaces are fetched, and imports enabled.
|
||||
*/
|
||||
@@ -930,6 +936,51 @@ export function getPageNames(): Promise<string[]> {
|
||||
return driver.findAll('.test-docpage-label', (e) => e.getText());
|
||||
}
|
||||
|
||||
export interface PageTree {
|
||||
label: string;
|
||||
children?: PageTree[];
|
||||
}
|
||||
/**
|
||||
* Returns a current page tree as a JSON object.
|
||||
*/
|
||||
export async function getPageTree(): Promise<PageTree[]> {
|
||||
const allPages = await driver.findAll('.test-docpage-label');
|
||||
const root: PageTree = {label: 'root', children: []};
|
||||
const stack: PageTree[] = [root];
|
||||
let current = 0;
|
||||
for(const page of allPages) {
|
||||
const label = await page.getText();
|
||||
const offset = await page.findClosest('.test-treeview-itemHeader').find('.test-treeview-offset');
|
||||
const level = parseInt((await offset.getCssValue('width')).replace("px", "")) / 10;
|
||||
if (level === current) {
|
||||
const parent = stack.pop()!;
|
||||
parent.children ??= [];
|
||||
parent.children.push({label});
|
||||
stack.push(parent);
|
||||
} else if (level > current) {
|
||||
current = level;
|
||||
const child = {label};
|
||||
const grandFather = stack.pop()!;
|
||||
grandFather.children ??= [];
|
||||
const father = grandFather.children[grandFather.children.length - 1];
|
||||
father.children ??= [];
|
||||
father.children.push(child);
|
||||
stack.push(grandFather);
|
||||
stack.push(father);
|
||||
} else {
|
||||
while (level < current) {
|
||||
stack.pop();
|
||||
current--;
|
||||
}
|
||||
const parent = stack.pop()!;
|
||||
parent.children ??= [];
|
||||
parent.children.push({label});
|
||||
stack.push(parent);
|
||||
}
|
||||
}
|
||||
return root.children!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new empty table using the 'Add New' menu.
|
||||
*/
|
||||
@@ -2685,6 +2736,32 @@ export function withComments() {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to revert ACL changes. It first saves the current ACL data, and
|
||||
* then removes everything and adds it back.
|
||||
*/
|
||||
export async function beginAclTran(api: UserAPI, docId: string) {
|
||||
const oldRes = await api.getTable(docId, '_grist_ACLResources');
|
||||
const oldRules = await api.getTable(docId, '_grist_ACLRules');
|
||||
|
||||
return async () => {
|
||||
const newRes = await api.getTable(docId, '_grist_ACLResources');
|
||||
const newRules = await api.getTable(docId, '_grist_ACLRules');
|
||||
const restoreRes = {tableId: oldRes.tableId, colIds: oldRes.colIds};
|
||||
const restoreRules = {
|
||||
resource: oldRules.resource,
|
||||
aclFormula: oldRules.aclFormula,
|
||||
permissionsText: oldRules.permissionsText
|
||||
};
|
||||
await api.applyUserActions(docId, [
|
||||
['BulkRemoveRecord', '_grist_ACLRules', newRules.id],
|
||||
['BulkRemoveRecord', '_grist_ACLResources', newRes.id],
|
||||
['BulkAddRecord', '_grist_ACLResources', oldRes.id, restoreRes],
|
||||
['BulkAddRecord', '_grist_ACLRules', oldRules.id, restoreRules],
|
||||
]);
|
||||
};
|
||||
}
|
||||
|
||||
} // end of namespace gristUtils
|
||||
|
||||
stackWrapOwnMethods(gristUtils);
|
||||
|
||||
Reference in New Issue
Block a user