(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
This commit is contained in:
Dmitry S 2020-09-29 18:31:47 -04:00
parent 2edf64c132
commit bac070de91
3 changed files with 37 additions and 10 deletions

View File

@ -251,21 +251,16 @@ export class TableData extends ActionDispatcher {
return records; 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. * 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. * Properties may include 'id' and any table columns. Returned records are not sorted.
*/ */
public filterRecords(properties: {[key: string]: any}): RowRecord[] { public filterRecords(properties: {[key: string]: any}): RowRecord[] {
const rowIndices: number[] = []; const rowIndices: number[] = this._filterRowIndices(properties);
// 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);
});
// Convert the array of indices to an array of RowRecords. // Convert the array of indices to an array of RowRecords.
const records: RowRecord[] = rowIndices.map(i => ({id: this._rowIdCol[i]})); 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]; 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. * 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. // Stop dispatching actions if we've been deleted. We might also want to clean up in the future.
this._isLoaded = false; 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<T>(targetArray: T[], sourceArray: T[]): void { function reassignArray<T>(targetArray: T[], sourceArray: T[]): void {

View File

@ -66,6 +66,7 @@ export interface IGristUrlState {
embed?: boolean; embed?: boolean;
style?: InterfaceStyle; style?: InterfaceStyle;
compare?: string; compare?: string;
aclUI?: boolean;
}; };
hash?: HashLink; // if present, this specifies an individual row within a section of a page. hash?: HashLink; // if present, this specifies an individual row within a section of a page.
} }
@ -260,6 +261,9 @@ export function decodeUrl(gristConfig: Partial<GristLoadConfig>, location: Locat
if (sp.has('compare')) { if (sp.has('compare')) {
state.params!.compare = sp.get('compare')!; state.params!.compare = sp.get('compare')!;
} }
if (sp.has('aclUI')) {
state.params!.aclUI = isAffirmative(sp.get('aclUI'));
}
if (location.hash) { if (location.hash) {
const hash = location.hash; const hash = location.hash;
const hashParts = hash.split('.'); const hashParts = hash.split('.');

View File

@ -75,6 +75,7 @@ export class GranularAccess {
public update() { public update() {
this._resources = this._docData.getTable('_grist_ACLResources')!; this._resources = this._docData.getTable('_grist_ACLResources')!;
this._ownerOnlyTableIds.clear(); this._ownerOnlyTableIds.clear();
this._onlyOwnersCanModifyStructure = false;
for (const res of this._resources.getRecords()) { for (const res of this._resources.getRecords()) {
const code = String(res.colIds); const code = String(res.colIds);
if (res.tableId && code === '~o') { if (res.tableId && code === '~o') {