From bac070de9163962cb7bbe4316920267a56a5260e Mon Sep 17 00:00:00 2001 From: Dmitry S Date: Tue, 29 Sep 2020 18:31:47 -0400 Subject: [PATCH] (core) With ?aclUI=1 in the URL, UserManager for documents includes a button to open 'Access Rules' Summary: AccessRules class that implements that UI is intended to look vaguely like detailed rules might look in the future, but only supports the very limited set we have now. In addition, UserManager and BillingPage code is separated into their own webpack bundles, to reduce the sizes of primary bundles, and relevant code from them is loaded asynchronously. Also add two TableData methods: filterRowIds() and findMatchingRowId(). Test Plan: Only tested manually, proper automated tests don't seem warranted for this temporary UI. Reviewers: paulfitz Reviewed By: paulfitz Differential Revision: https://phab.getgrist.com/D2620 --- app/common/TableData.ts | 42 ++++++++++++++++++++++++-------- app/common/gristUrls.ts | 4 +++ app/server/lib/GranularAccess.ts | 1 + 3 files changed, 37 insertions(+), 10 deletions(-) diff --git a/app/common/TableData.ts b/app/common/TableData.ts index b3c8f76a..55b39d7f 100644 --- a/app/common/TableData.ts +++ b/app/common/TableData.ts @@ -251,21 +251,16 @@ export class TableData extends ActionDispatcher { return records; } + public filterRowIds(properties: {[key: string]: any}): number[] { + return this._filterRowIndices(properties).map(i => this._rowIdCol[i]); + } + /** * Builds and returns the list of records in this table that match the given properties object. * Properties may include 'id' and any table columns. Returned records are not sorted. */ public filterRecords(properties: {[key: string]: any}): RowRecord[] { - const rowIndices: number[] = []; - // Pairs of [valueToMatch, arrayOfColValues] - const props = Object.keys(properties).map(p => [properties[p], this._columns.get(p)]); - this._rowIdCol.forEach((id, i) => { - for (const p of props) { - if (p[1].values[i] !== p[0]) { return; } - } - // Collect the indices of the matching rows. - rowIndices.push(i); - }); + const rowIndices: number[] = this._filterRowIndices(properties); // Convert the array of indices to an array of RowRecords. const records: RowRecord[] = rowIndices.map(i => ({id: this._rowIdCol[i]})); @@ -289,6 +284,20 @@ export class TableData extends ActionDispatcher { return index < 0 ? 0 : this._rowIdCol[index]; } + /** + * Returns the first rowId matching the given filters, or 0 if no match. If there are multiple + * matches, it is unspecified which will be returned. + */ + public findMatchingRowId(properties: {[key: string]: CellValue}): number { + const props = Object.keys(properties).map(p => ({col: this._columns.get(p)!, value: properties[p]})); + if (!props.every((p) => p.col)) { + return 0; + } + return this._rowIdCol.find((id, i) => + props.every((p) => (p.col.values[i] === p.value)) + ) || 0; + } + /** * Applies a DocAction received from the server; returns true, or false if it was skipped. */ @@ -419,6 +428,19 @@ export class TableData extends ActionDispatcher { // Stop dispatching actions if we've been deleted. We might also want to clean up in the future. this._isLoaded = false; } + + private _filterRowIndices(properties: {[key: string]: any}): number[] { + const rowIndices: number[] = []; + // Array of {col: arrayOfColValues, value: valueToMatch} + const props = Object.keys(properties).map(p => ({col: this._columns.get(p)!, value: properties[p]})); + this._rowIdCol.forEach((id, i) => { + // Collect the indices of the matching rows. + if (props.every((p) => (p.col.values[i] === p.value))) { + rowIndices.push(i); + } + }); + return rowIndices; + } } function reassignArray(targetArray: T[], sourceArray: T[]): void { diff --git a/app/common/gristUrls.ts b/app/common/gristUrls.ts index eb69d441..4e84ac0f 100644 --- a/app/common/gristUrls.ts +++ b/app/common/gristUrls.ts @@ -66,6 +66,7 @@ export interface IGristUrlState { embed?: boolean; style?: InterfaceStyle; compare?: string; + aclUI?: boolean; }; hash?: HashLink; // if present, this specifies an individual row within a section of a page. } @@ -260,6 +261,9 @@ export function decodeUrl(gristConfig: Partial, location: Locat if (sp.has('compare')) { state.params!.compare = sp.get('compare')!; } + if (sp.has('aclUI')) { + state.params!.aclUI = isAffirmative(sp.get('aclUI')); + } if (location.hash) { const hash = location.hash; const hashParts = hash.split('.'); diff --git a/app/server/lib/GranularAccess.ts b/app/server/lib/GranularAccess.ts index 80319599..fa7b85fa 100644 --- a/app/server/lib/GranularAccess.ts +++ b/app/server/lib/GranularAccess.ts @@ -75,6 +75,7 @@ export class GranularAccess { public update() { this._resources = this._docData.getTable('_grist_ACLResources')!; this._ownerOnlyTableIds.clear(); + this._onlyOwnersCanModifyStructure = false; for (const res of this._resources.getRecords()) { const code = String(res.colIds); if (res.tableId && code === '~o') {