feat: add JSON serialization for workspace comments (#7927)

* feat: basic comment serializer

* bad: temporarily jam new comment classes into array

* chore: implement serializer

* chore: add serialization tests

* chore: JSDoc

* chore: unonly tests
This commit is contained in:
Beka Westberg
2024-03-20 19:40:27 +00:00
committed by GitHub
parent fd1a02ff37
commit 407ff44e18
5 changed files with 316 additions and 0 deletions

View File

@@ -53,6 +53,9 @@ export class WorkspaceComment {
) {
this.id = id && !workspace.getCommentById(id) ? id : idGenerator.genUid();
// TODO: File an issue to remove this once everything is migrated.
workspace.addTopComment(this as AnyDuringMigration);
// TODO(7909): Fire events.
}
@@ -162,6 +165,7 @@ export class WorkspaceComment {
/** Disposes of this comment. */
dispose() {
this.disposing = true;
this.workspace.removeTopComment(this as AnyDuringMigration);
this.disposed = true;
}

View File

@@ -16,6 +16,7 @@ import * as procedures from './serialization/procedures.js';
import * as registry from './serialization/registry.js';
import * as variables from './serialization/variables.js';
import * as workspaces from './serialization/workspaces.js';
import * as workspaceComments from './serialization/workspace_comments.js';
import {ISerializer} from './interfaces/i_serializer.js';
export {
@@ -26,5 +27,6 @@ export {
registry,
variables,
workspaces,
workspaceComments,
ISerializer,
};

View File

@@ -20,3 +20,6 @@ export const PROCEDURES = 75;
* The priority for deserializing blocks.
*/
export const BLOCKS = 50;
/** The priority for deserializing workspace comments. */
export const WORKSPACE_COMMENTS = 25;

View File

@@ -0,0 +1,145 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {ISerializer} from '../interfaces/i_serializer.js';
import {Workspace} from '../workspace.js';
import {WorkspaceSvg} from '../workspace_svg.js';
import * as priorities from './priorities.js';
import {WorkspaceComment} from '../comments/workspace_comment.js';
import {RenderedWorkspaceComment} from '../comments/rendered_workspace_comment.js';
import * as eventUtils from '../events/utils.js';
import {Coordinate} from '../utils/coordinate.js';
import * as serializationRegistry from './registry.js';
import {Size} from '../utils/size.js';
export interface State {
id?: string;
text?: string;
x?: number;
y?: number;
width?: number;
height?: number;
collapsed?: boolean;
editable?: boolean;
movable?: boolean;
deletable?: boolean;
}
/** Serializes the state of the given comment to JSON. */
export function save(
comment: WorkspaceComment,
{
addCoordinates = false,
saveIds = true,
}: {
addCoordinates?: boolean;
saveIds?: boolean;
} = {},
): State {
const state: State = Object.create(null);
state.height = comment.getSize().height;
state.width = comment.getSize().width;
if (saveIds) state.id = comment.id;
if (addCoordinates) {
state.x = comment.getRelativeToSurfaceXY().x;
state.y = comment.getRelativeToSurfaceXY().y;
}
if (comment.getText()) state.text = comment.getText();
if (comment.isCollapsed()) state.collapsed = true;
if (!comment.isOwnEditable()) state.editable = false;
if (!comment.isOwnMovable()) state.movable = false;
if (!comment.isOwnDeletable()) state.deletable = false;
return state;
}
/** Appends the comment defined by the given state to the given workspace. */
export function append(
state: State,
workspace: Workspace,
{recordUndo = false}: {recordUndo?: boolean} = {},
): WorkspaceComment {
const prevRecordUndo = eventUtils.getRecordUndo();
eventUtils.setRecordUndo(recordUndo);
const comment =
workspace instanceof WorkspaceSvg
? new RenderedWorkspaceComment(workspace, state.id)
: new WorkspaceComment(workspace, state.id);
if (state.text !== undefined) comment.setText(state.text);
if (state.x !== undefined || state.y !== undefined) {
const defaultLoc = comment.getRelativeToSurfaceXY();
comment.moveTo(
new Coordinate(state.x ?? defaultLoc.x, state.y ?? defaultLoc.y),
);
}
if (state.width !== undefined || state.height) {
const defaultSize = comment.getSize();
comment.setSize(
new Size(
state.width ?? defaultSize.width,
state.height ?? defaultSize.height,
),
);
}
if (state.collapsed !== undefined) comment.setCollapsed(state.collapsed);
if (state.editable !== undefined) comment.setEditable(state.editable);
if (state.movable !== undefined) comment.setMovable(state.movable);
if (state.deletable !== undefined) comment.setDeletable(state.deletable);
eventUtils.setRecordUndo(prevRecordUndo);
return comment;
}
// Alias to disambiguate saving within the serializer.
const saveComment = save;
/** Serializer for saving and loading workspace comment state. */
export class WorkspaceCommentSerializer implements ISerializer {
priority = priorities.WORKSPACE_COMMENTS;
/**
* Returns the state of all workspace comments in the given workspace.
*/
save(workspace: Workspace): State[] | null {
const commentStates = [];
for (const comment of workspace.getTopComments()) {
const state = saveComment(comment as AnyDuringMigration, {
addCoordinates: true,
saveIds: true,
});
if (state) commentStates.push(state);
}
return commentStates.length ? commentStates : null;
}
/**
* Deserializes the comments defined by the given state into the given
* workspace.
*/
load(state: State[], workspace: Workspace) {
for (const commentState of state) {
append(commentState, workspace, {recordUndo: eventUtils.getRecordUndo()});
}
}
/** Disposes of any comments that exist on the given workspace. */
clear(workspace: Workspace) {
for (const comment of workspace.getTopComments()) {
comment.dispose();
}
}
}
serializationRegistry.register(
'workspaceComments',
new WorkspaceCommentSerializer(),
);

View File

@@ -894,4 +894,166 @@ suite('JSO Serialization', function () {
);
});
});
suite('Workspace comments', function () {
suite('IDs', function () {
test('IDs are saved by default', function () {
const comment = new Blockly.comments.WorkspaceComment(
this.workspace,
'testID',
);
const json = Blockly.serialization.workspaceComments.save(comment);
assertProperty(json, 'id', 'testID');
});
test('saving IDs can be disabled', function () {
const comment = new Blockly.comments.WorkspaceComment(
this.workspace,
'testID',
);
const json = Blockly.serialization.workspaceComments.save(comment, {
saveIds: false,
});
assertNoProperty(json, 'id');
});
});
suite('Coordinates', function () {
test('coordinates are not saved by default', function () {
const comment = new Blockly.comments.WorkspaceComment(this.workspace);
comment.moveTo(new Blockly.utils.Coordinate(42, 1337));
const json = Blockly.serialization.workspaceComments.save(comment);
assertNoProperty(json, 'x');
assertNoProperty(json, 'y');
});
test('saving coordinates can be enabled', function () {
const comment = new Blockly.comments.WorkspaceComment(this.workspace);
comment.moveTo(new Blockly.utils.Coordinate(42, 1337));
const json = Blockly.serialization.workspaceComments.save(comment, {
addCoordinates: true,
});
assertProperty(json, 'x', 42);
assertProperty(json, 'y', 1337);
});
});
suite('Text', function () {
test('the empty string is not saved', function () {
const comment = new Blockly.comments.WorkspaceComment(this.workspace);
comment.setText('');
const json = Blockly.serialization.workspaceComments.save(comment);
assertNoProperty(json, 'text');
});
test('text is saved', function () {
const comment = new Blockly.comments.WorkspaceComment(this.workspace);
comment.setText('test text');
const json = Blockly.serialization.workspaceComments.save(comment);
assertProperty(json, 'text', 'test text');
});
});
test('size is saved', function () {
const comment = new Blockly.comments.WorkspaceComment(this.workspace);
comment.setSize(new Blockly.utils.Size(42, 1337));
const json = Blockly.serialization.workspaceComments.save(comment);
assertProperty(json, 'width', 42);
assertProperty(json, 'height', 1337);
});
suite('Collapsed', function () {
test('collapsed is not saved if false', function () {
const comment = new Blockly.comments.WorkspaceComment(this.workspace);
comment.setCollapsed(false);
const json = Blockly.serialization.workspaceComments.save(comment);
assertNoProperty(json, 'collapsed');
});
test('collapsed is saved if true', function () {
const comment = new Blockly.comments.WorkspaceComment(this.workspace);
comment.setCollapsed(true);
const json = Blockly.serialization.workspaceComments.save(comment);
assertProperty(json, 'collapsed', true);
});
});
suite('Editable', function () {
test('editable is not saved if true', function () {
const comment = new Blockly.comments.WorkspaceComment(this.workspace);
comment.setEditable(true);
const json = Blockly.serialization.workspaceComments.save(comment);
assertNoProperty(json, 'editable');
});
test('editable is saved if false', function () {
const comment = new Blockly.comments.WorkspaceComment(this.workspace);
comment.setEditable(false);
const json = Blockly.serialization.workspaceComments.save(comment);
assertProperty(json, 'editable', false);
});
});
suite('Movable', function () {
test('movable is not saved if true', function () {
const comment = new Blockly.comments.WorkspaceComment(this.workspace);
comment.setMovable(true);
const json = Blockly.serialization.workspaceComments.save(comment);
assertNoProperty(json, 'movable');
});
test('movable is saved if false', function () {
const comment = new Blockly.comments.WorkspaceComment(this.workspace);
comment.setMovable(false);
const json = Blockly.serialization.workspaceComments.save(comment);
assertProperty(json, 'movable', false);
});
});
suite('Deletable', function () {
test('deletable is not saved if true', function () {
const comment = new Blockly.comments.WorkspaceComment(this.workspace);
comment.setDeletable(true);
const json = Blockly.serialization.workspaceComments.save(comment);
assertNoProperty(json, 'deletable');
});
test('deletable is saved if false', function () {
const comment = new Blockly.comments.WorkspaceComment(this.workspace);
comment.setDeletable(false);
const json = Blockly.serialization.workspaceComments.save(comment);
assertProperty(json, 'deletable', false);
});
});
});
});