mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(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:
@@ -25,6 +25,8 @@ const {setTestState} = require('app/client/lib/testState');
|
||||
const {ExtraRows} = require('app/client/models/DataTableModelWithDiff');
|
||||
const {createFilterMenu} = require('app/client/ui/ColumnFilterMenu');
|
||||
const {closeRegisteredMenu} = require('app/client/ui2018/menus');
|
||||
const {COMMENTS} = require('app/client/models/features');
|
||||
|
||||
|
||||
/**
|
||||
* BaseView forms the basis for ViewSection classes.
|
||||
@@ -85,6 +87,8 @@ function BaseView(gristDoc, viewSectionModel, options) {
|
||||
this._filteredRowSource.updateFilter(filterFunc);
|
||||
}));
|
||||
|
||||
this.rowSource = this._filteredRowSource;
|
||||
|
||||
// Sorted collection of all rows to show in this view.
|
||||
this.sortedRows = rowset.SortedRowSet.create(this, null, this.tableModel.tableData);
|
||||
|
||||
@@ -238,7 +242,8 @@ BaseView.commonCommands = {
|
||||
showRawData: function() { this.showRawData().catch(reportError); },
|
||||
|
||||
filterByThisCellValue: function() { this.filterByThisCellValue(); },
|
||||
duplicateRows: function() { this._duplicateRows().catch(reportError); }
|
||||
duplicateRows: function() { this._duplicateRows().catch(reportError); },
|
||||
openDiscussion: function() { this.openDiscussionAtCursor(); },
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -288,6 +293,30 @@ BaseView.prototype.activateEditorAtCursor = function(options) {
|
||||
builder.buildEditorDom(this.editRowModel, lazyRow, options || {});
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Opens discussion panel at the cursor position. Returns true if discussion panel was opened.
|
||||
*/
|
||||
BaseView.prototype.openDiscussionAtCursor = function(id) {
|
||||
if (!COMMENTS().get()) { return false; }
|
||||
var builder = this.activeFieldBuilder();
|
||||
if (builder.isEditorActive()) {
|
||||
return false;
|
||||
}
|
||||
var rowId = this.viewData.getRowId(this.cursor.rowIndex());
|
||||
// LazyArrayModel row model which is also used to build the cell dom. Needed since
|
||||
// it may be used as a key to retrieve the cell dom, which is useful for editor placement.
|
||||
var lazyRow = this.getRenderedRowModel(rowId);
|
||||
if (!lazyRow) {
|
||||
// TODO scroll into view. For now, just don't start discussion.
|
||||
return false;
|
||||
}
|
||||
this.editRowModel.assign(rowId);
|
||||
builder.buildDiscussionPopup(this.editRowModel, lazyRow, id);
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Move the floating RowModel for editing to the current cursor position, and return it.
|
||||
*
|
||||
|
||||
@@ -48,6 +48,7 @@ import {isNarrowScreen, mediaSmall, testId} from 'app/client/ui2018/cssVars';
|
||||
import {IconName} from 'app/client/ui2018/IconList';
|
||||
import {invokePrompt} from 'app/client/ui2018/modals';
|
||||
import {FieldEditor} from "app/client/widgets/FieldEditor";
|
||||
import {DiscussionPanel} from 'app/client/widgets/DiscussionEditor';
|
||||
import {MinimalActionGroup} from 'app/common/ActionGroup';
|
||||
import {ClientQuery} from "app/common/ActiveDocAPI";
|
||||
import {CommDocUsage, CommDocUserAction} from 'app/common/CommTypes';
|
||||
@@ -66,6 +67,7 @@ import {
|
||||
bundleChanges,
|
||||
Computed,
|
||||
dom,
|
||||
DomContents,
|
||||
Emitter,
|
||||
fromKo,
|
||||
Holder,
|
||||
@@ -101,11 +103,11 @@ export interface TabOptions {
|
||||
category?: any;
|
||||
}
|
||||
|
||||
const RightPanelTool = StringUnion("none", "docHistory", "validations");
|
||||
const RightPanelTool = StringUnion("none", "docHistory", "validations", "discussion");
|
||||
|
||||
export interface IExtraTool {
|
||||
icon: IconName;
|
||||
label: string;
|
||||
label: DomContents;
|
||||
content: TabContent[]|IDomComponent;
|
||||
}
|
||||
|
||||
@@ -162,6 +164,7 @@ export class GristDoc extends DisposableWithEvents {
|
||||
private _lastOwnActionGroup: ActionGroupWithCursorPos|null = null;
|
||||
private _rightPanelTabs = new Map<string, TabContent[]>();
|
||||
private _docHistory: DocHistory;
|
||||
private _discussionPanel: DiscussionPanel;
|
||||
private _rightPanelTool = createSessionObs(this, "rightPanelTool", "none", RightPanelTool.guard);
|
||||
private _viewLayout: ViewLayout|null = null;
|
||||
private _showGristTour = getUserOrgPrefObs(this.userOrgPrefs, 'showGristTour');
|
||||
@@ -317,6 +320,7 @@ export class GristDoc extends DisposableWithEvents {
|
||||
this._actionLog = this.autoDispose(ActionLog.create({ gristDoc: this }));
|
||||
this._undoStack = this.autoDispose(UndoStack.create(openDocResponse.log, { gristDoc: this }));
|
||||
this._docHistory = DocHistory.create(this, this.docPageModel, this._actionLog);
|
||||
this._discussionPanel = DiscussionPanel.create(this, this);
|
||||
|
||||
// Tap into docData's sendActions method to save the cursor position with every action, so that
|
||||
// undo/redo can jump to the right place.
|
||||
@@ -404,6 +408,7 @@ export class GristDoc extends DisposableWithEvents {
|
||||
return this.docPageModel.currentDocId.get()!;
|
||||
}
|
||||
|
||||
// DEPRECATED This is used only for validation, which is not used anymore.
|
||||
public addOptionsTab(label: string, iconElem: any, contentObj: TabContent[], options: TabOptions): IDisposable {
|
||||
this._rightPanelTabs.set(label, contentObj);
|
||||
// Return a do-nothing disposable, to satisfy the previous interface.
|
||||
@@ -857,7 +862,7 @@ export class GristDoc extends DisposableWithEvents {
|
||||
public async recursiveMoveToCursorPos(
|
||||
cursorPos: CursorPos,
|
||||
setAsActiveSection: boolean,
|
||||
silent: boolean = false): Promise<void> {
|
||||
silent: boolean = false): Promise<boolean> {
|
||||
try {
|
||||
if (!cursorPos.sectionId) { throw new Error('sectionId required'); }
|
||||
if (!cursorPos.rowId) { throw new Error('rowId required'); }
|
||||
@@ -931,11 +936,13 @@ export class GristDoc extends DisposableWithEvents {
|
||||
// even though the cursor is at right place, the scroll could not have yet happened
|
||||
// wait for a bit (scroll is done in a setTimeout 0)
|
||||
await delay(0);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.debug(`_recursiveMoveToCursorPos(${JSON.stringify(cursorPos)}): ${e}`);
|
||||
if (!silent) {
|
||||
throw new UserError('There was a problem finding the desired cell.');
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1051,6 +1058,9 @@ export class GristDoc extends DisposableWithEvents {
|
||||
const content = this._rightPanelTabs.get("Validate Data");
|
||||
return content ? {icon: 'Validation', label: 'Validation Rules', content} : null;
|
||||
}
|
||||
case 'discussion': {
|
||||
return {icon: 'Chat', label: this._discussionPanel.buildMenu(), content: this._discussionPanel};
|
||||
}
|
||||
case 'none':
|
||||
default: {
|
||||
return null;
|
||||
|
||||
@@ -318,6 +318,10 @@ exports.groups = [{
|
||||
name: 'datepickerFocus',
|
||||
keys: ['Up', 'Down'],
|
||||
desc: null, // While editing a date cell, switch keyboard focus to the datepicker
|
||||
}, {
|
||||
name: 'openDiscussion',
|
||||
keys: ['Mod+Alt+M'],
|
||||
desc: 'Comment',
|
||||
}
|
||||
],
|
||||
}, {
|
||||
|
||||
Reference in New Issue
Block a user