(core) Comments

Summary:
First iteration for comments system for Grist.
- Comments are stored in a generic metatable `_grist_Cells`
- Each comment is connected to a particular cell (hence the generic name of the table)
- Access level works naturally for records stored in this table
-- User can add/read comments for cells he can see
-- User can't update/remove comments that he doesn't own, but he can delete them by removing cells (rows/columns)
-- Anonymous users can't see comments at all.
- Each comment can have replies (but replies can't have more replies)

Comments are hidden by default, they can be enabled by COMMENTS=true env variable.
Some things for follow-up
- Avatars, currently the user's profile image is not shown or retrieved from the server
- Virtual rendering for comments list in creator panel. Currently, there is a limit of 200 comments.

Test Plan: New and existing tests

Reviewers: georgegevoian, paulfitz

Reviewed By: georgegevoian

Subscribers: paulfitz

Differential Revision: https://phab.getgrist.com/D3509
This commit is contained in:
Jarosław Sadziński
2022-10-17 11:47:16 +02:00
parent 8be920dd25
commit bfd7243fe2
41 changed files with 2621 additions and 77 deletions

View File

@@ -0,0 +1,43 @@
import {isCensored} from 'app/common/gristTypes';
import * as ko from 'knockout';
import {KoArray} from 'app/client/lib/koArray';
import {jsonObservable} from 'app/client/models/modelUtil';
import * as modelUtil from 'app/client/models/modelUtil';
import {ColumnRec, DocModel, IRowModel, recordSet, refRecord, TableRec} from 'app/client/models/DocModel';
export interface CellRec extends IRowModel<"_grist_Cells"> {
column: ko.Computed<ColumnRec>;
table: ko.Computed<TableRec>;
children: ko.Computed<KoArray<CellRec>>;
hidden: ko.Computed<boolean>;
parent: ko.Computed<CellRec>;
text: modelUtil.KoSaveableObservable<string|undefined>;
userName: modelUtil.KoSaveableObservable<string|undefined>;
timeCreated: modelUtil.KoSaveableObservable<number|undefined>;
timeUpdated: modelUtil.KoSaveableObservable<number|undefined>;
resolved: modelUtil.KoSaveableObservable<boolean|undefined>;
resolvedBy: modelUtil.KoSaveableObservable<string|undefined>;
}
export function createCellRec(this: CellRec, docModel: DocModel): void {
this.hidden = ko.pureComputed(() => isCensored(this.content()));
this.column = refRecord(docModel.columns, this.colRef);
this.table = refRecord(docModel.tables, this.tableRef);
this.parent = refRecord(docModel.cells, this.parentId);
this.children = recordSet(this, docModel.cells, 'parentId');
const properContent = modelUtil.savingComputed({
read: () => this.hidden() ? '{}' : this.content(),
write: (setter, val) => setter(this.content, val)
});
const optionJson = jsonObservable(properContent);
// Comments:
this.text = optionJson.prop('text');
this.userName = optionJson.prop('userName');
this.timeCreated = optionJson.prop('timeCreated');
this.timeUpdated = optionJson.prop('timeUpdated');
this.resolved = optionJson.prop('resolved');
this.resolvedBy = optionJson.prop('resolvedBy');
}

View File

@@ -1,5 +1,6 @@
import {KoArray} from 'app/client/lib/koArray';
import {DocModel, IRowModel, recordSet, refRecord, TableRec, ViewFieldRec} from 'app/client/models/DocModel';
import {CellRec, DocModel, IRowModel, recordSet,
refRecord, TableRec, ViewFieldRec} from 'app/client/models/DocModel';
import {jsonObservable, ObjObservable} from 'app/client/models/modelUtil';
import * as gristTypes from 'app/common/gristTypes';
import {getReferencedTableId} from 'app/common/gristTypes';
@@ -55,7 +56,7 @@ export interface ColumnRec extends IRowModel<"_grist_Tables_column"> {
visibleColModel: ko.Computed<ColumnRec>;
disableModifyBase: ko.Computed<boolean>; // True if column config can't be modified (name, type, etc.)
disableModify: ko.Computed<boolean>; // True if column can't be modified or is being transformed.
disableModify: ko.Computed<boolean>; // True if column can't be modified (is summary) or is being transformed.
disableEditData: ko.Computed<boolean>; // True to disable editing of the data in this column.
isHiddenCol: ko.Computed<boolean>;
@@ -73,6 +74,7 @@ export interface ColumnRec extends IRowModel<"_grist_Tables_column"> {
// (i.e. they aren't actually referenced but they exist in the visible column and are relevant to e.g. autocomplete)
// `formatter` formats actual cell values, e.g. a whole list from the display column.
formatter: ko.Computed<BaseFormatter>;
cells: ko.Computed<KoArray<CellRec>>;
// Helper which adds/removes/updates column's displayCol to match the formula.
saveDisplayFormula(formula: string): Promise<void>|undefined;
@@ -83,6 +85,7 @@ export function createColumnRec(this: ColumnRec, docModel: DocModel): void {
this.widgetOptionsJson = jsonObservable(this.widgetOptions);
this.viewFields = recordSet(this, docModel.viewFields, 'colRef');
this.summarySource = refRecord(docModel.columns, this.summarySourceCol);
this.cells = recordSet(this, docModel.cells, 'colRef');
// Is this an empty column (undecided if formula or data); denoted by an empty formula.
this.isEmpty = ko.pureComputed(() => this.isFormula() && this.formula() === '');