diff --git a/app/client/components/LinkingState.ts b/app/client/components/LinkingState.ts index 9dfc7249..b81681b3 100644 --- a/app/client/components/LinkingState.ts +++ b/app/client/components/LinkingState.ts @@ -119,12 +119,11 @@ export class LinkingState extends Disposable { } const srcRowId = srcSection.activeRowId(); for (const c of srcSection.table().groupByColumns()) { - const col = c.summarySource(); - const colId = col.colId(); + const colId = c.colId(); const srcValue = srcTableData.getValue(srcRowId as number, colId); result.filters[colId] = [srcValue]; result.operations[colId] = 'in'; - if (isDirectSummary && isListType(col.type())) { + if (isDirectSummary && isListType(c.summarySource().type())) { // If the source groupby column is a ChoiceList or RefList, then null or '' in the summary table // should match against an empty list in the source table. result.operations[colId] = srcValue ? 'intersects' : 'empty'; diff --git a/app/client/models/entities/ViewSectionRec.ts b/app/client/models/entities/ViewSectionRec.ts index b324c0ac..0d1a902e 100644 --- a/app/client/models/entities/ViewSectionRec.ts +++ b/app/client/models/entities/ViewSectionRec.ts @@ -499,10 +499,15 @@ export function createViewSectionRec(this: ViewSectionRec, docModel: DocModel): this._linkingState = Holder.create(this); this.linkingState = this.autoDispose(ko.pureComputed(() => { + if (!this.activeLinkSrcSectionRef()) { + // This view section isn't selecting by anything. + return null; + } try { const config = new LinkConfig(this); return LinkingState.create(this._linkingState, docModel, config); } catch (err) { + console.warn(err); // Dispose old LinkingState in case creating the new one failed. this._linkingState.dispose(); return null; diff --git a/app/client/ui/selectBy.ts b/app/client/ui/selectBy.ts index ee2f4811..e7596c40 100644 --- a/app/client/ui/selectBy.ts +++ b/app/client/ui/selectBy.ts @@ -332,13 +332,21 @@ export class LinkConfig { this.srcSection.table().primaryTableId()); const tgtTableId = (tgtCol ? getReferencedTableId(tgtCol.type()) : this.tgtSection.table().primaryTableId()); + const srcTableSummarySourceTable = this.srcSection.table().summarySourceTable(); + const tgtTableSummarySourceTable = this.tgtSection.table().summarySourceTable(); try { assert(Boolean(this.srcSection.getRowId()), "srcSection was disposed"); assert(!tgtCol || tgtCol.parentId() === this.tgtSection.tableRef(), "tgtCol belongs to wrong table"); assert(!srcCol || srcCol.parentId() === this.srcSection.tableRef(), "srcCol belongs to wrong table"); assert(this.srcSection.getRowId() !== this.tgtSection.getRowId(), "srcSection links to itself"); - assert(tgtTableId, "tgtCol not a valid reference"); - assert(srcTableId, "srcCol not a valid reference"); + + // We usually expect srcTableId and tgtTableId to be non-empty, but there's one exception: + // when linking two summary tables that share a source table (which we can check directly) + // and the source table is hidden by ACL, so its tableId is empty from our perspective. + if (!(srcTableSummarySourceTable !== 0 && srcTableSummarySourceTable === tgtTableSummarySourceTable)) { + assert(tgtTableId, "tgtCol not a valid reference"); + assert(srcTableId, "srcCol not a valid reference"); + } assert(srcTableId === tgtTableId, "mismatched tableIds"); } catch (e) { throw new Error(`LinkConfig invalid: ` + diff --git a/app/server/lib/GranularAccess.ts b/app/server/lib/GranularAccess.ts index 33c6b6cb..c3065b8c 100644 --- a/app/server/lib/GranularAccess.ts +++ b/app/server/lib/GranularAccess.ts @@ -2193,6 +2193,7 @@ export class CensorshipInfo { const columnCode = (tableRef: number, colId: string) => `${tableRef} ${colId}`; const censoredColumnCodes: Set = new Set(); const tableRefToTableId: Map = new Map(); + const tableRefToIndex: Map = new Map(); const uncensoredTables: Set = new Set(); // Scan for forbidden tables. let rec = new RecordView(tables._grist_Tables, undefined); @@ -2202,6 +2203,7 @@ export class CensorshipInfo { const tableId = rec.get('tableId') as string; const tableRef = ids[idx]; tableRefToTableId.set(tableRef, tableId); + tableRefToIndex.set(tableRef, idx); const tableAccess = permInfo.getTableAccess(tableId); if (tableAccess.perms.read === 'deny') { this.censoredTables.add(tableRef); @@ -2254,6 +2256,29 @@ export class CensorshipInfo { !this.censoredColumns.has(rec.get('colRef') as number)) { continue; } this.censoredFields.add(ids[idx]); } + + // Now undo some of the above... + // Specifically, when a summary table is not censored, uncensor the source table's raw view section, + // so that the user can see the source table's title, + // which is used to construct the summary table's title. The section's fields remain censored. + // This would also be a sensible place to uncensor the source tableId, but that causes other problems. + rec = new RecordView(tables._grist_Tables, undefined); + ids = getRowIdsFromDocAction(tables._grist_Tables); + for (let idx = 0; idx < ids.length; idx++) { + rec.index = idx; + const tableRef = ids[idx]; + const sourceTableRef = rec.get('summarySourceTable') as number; + const sourceTableIndex = tableRefToIndex.get(sourceTableRef); + if ( + this.censoredTables.has(tableRef) || + !sourceTableRef || + sourceTableIndex === undefined || + !this.censoredTables.has(sourceTableRef) + ) { continue; } + rec.index = sourceTableIndex; + const rawViewSectionRef = rec.get('rawViewSectionRef') as number; + this.censoredSections.delete(rawViewSectionRef); + } } public apply(a: DataAction) {