diff --git a/flow-typed/flatted.js b/flow-typed/flatted.js
new file mode 100644
index 00000000..53090d48
--- /dev/null
+++ b/flow-typed/flatted.js
@@ -0,0 +1,5 @@
+// @flow
+
+declare module 'flatted' {
+ declare module.exports: any;
+}
diff --git a/licit/client/index.js b/licit/client/index.js
index 49c5133d..59884a78 100644
--- a/licit/client/index.js
+++ b/licit/client/index.js
@@ -29,7 +29,7 @@ function main(): void {
const plugins = null;
ReactDOM.render(, el);
+ runtime={null} plugins={plugins} />, el);
}
function onChangeCB(data) {
diff --git a/licit/server/collab/instance.js b/licit/server/collab/instance.js
index e6263d31..d645f8e6 100644
--- a/licit/server/collab/instance.js
+++ b/licit/server/collab/instance.js
@@ -2,7 +2,10 @@
const { readFileSync, writeFile } = require("fs")
-import EditorSchema from "../../../src/EditorSchema"
+// [FS] IRAD-1040 2020-09-02
+import { Schema } from 'prosemirror-model';
+
+let _editorSchema: Schema = null;
const MAX_STEP_HISTORY = 10000
@@ -19,10 +22,11 @@ export class Instance {
waiting = [];
collecting: any;
// end
- constructor(id: any, doc: any) {
+ constructor(id: any, doc: any, effectiveSchema: Schema) {
this.id = id
- this.doc = doc || EditorSchema.node("doc", null, [EditorSchema.node("paragraph", null, [
- EditorSchema.text(" ")
+ // [FS] IRAD-1040 2020-09-02
+ this.doc = doc || _editorSchema.node("doc", null, [_editorSchema.node("paragraph", null, [
+ _editorSchema.text(" ")
])])
// The version number of the document instance.
this.version = 0
@@ -135,7 +139,8 @@ if (process.argv.indexOf("--fresh") == -1) {
if (json) {
for (let prop in json)
- newInstance(prop, EditorSchema.nodeFromJSON(json[prop].doc))
+ // [FS] IRAD-1040 2020-09-02
+ newInstance(prop, _editorSchema.nodeFromJSON(json[prop].doc))
}
let saveTimeout = null, saveEvery = 1e4
@@ -143,6 +148,7 @@ function scheduleSave() {
if (saveTimeout != null) return
saveTimeout = setTimeout(doSave, saveEvery)
}
+
function doSave() {
saveTimeout = null
let out = {}
@@ -151,6 +157,24 @@ function doSave() {
writeFile(saveFile, JSON.stringify(out), () => { null })
}
+// [FS] IRAD-1040 2020-09-02
+function updateDocs() {
+ for (var prop in instances) {
+ instances[prop].doc = _editorSchema.nodeFromJSON(instances[prop].doc.toJSON());
+ }
+}
+
+export function setEditorSchema(effectiveSchema: Schema) {
+ _editorSchema = effectiveSchema;
+ updateDocs();
+}
+
+export function initEditorSchema(effectiveSchema: Schema) {
+ if(null == _editorSchema) {
+ _editorSchema = effectiveSchema;
+ }
+}
+
export function getInstance(id: any, ip: any) {
let inst = instances[id] || newInstance(id)
if (ip) inst.registerUser(ip)
diff --git a/licit/server/collab/server.js b/licit/server/collab/server.js
index 5a59be7c..5ffe42ed 100644
--- a/licit/server/collab/server.js
+++ b/licit/server/collab/server.js
@@ -1,28 +1,38 @@
// @flow
import { Step } from "prosemirror-transform"
+import { Schema } from 'prosemirror-model'
import Router from "./route"
import EditorSchema from "../../../src/EditorSchema"
-import { getInstance, instanceInfo } from "./instance"
+import { getInstance, instanceInfo, setEditorSchema, initEditorSchema } from "./instance"
// [FS] IRAD-899 2020-03-13
// This is for Capcomode document attribute. Shared Step, so that capcomode can be dealt collaboratively.
import SetDocAttrStep from "../../../src/SetDocAttrStep";
+// [FS] IRAD-1040 2020-09-02
+import * as Flatted from 'flatted';
const router = new Router();
+// [FS] IRAD-1040 2020-09-02
+let effectiveSchema = EditorSchema;
+let lastUpdatedSchema = null;
+
function handleCollabRequest(req: any, resp: any) {
+ // [FS] IRAD-1040 2020-09-02
+ initEditorSchema(effectiveSchema);
+ const headers = {
+ 'Access-Control-Allow-Origin': '*',
+ 'Access-Control-Allow-Methods': ';POST, GET, PUT, DELETE, OPTIONS',
+ 'Access-Control-Allow-Credential': false,
+ 'Access-Control-Max-Age': 86400, // 24hrs
+ 'Access-Control-Allow-Headers':
+ 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept'
+ };
if (!router.resolve(req, resp)) {
const method = req.method.toUpperCase();
if (method === 'OPTIONS') {
- const headers = {
- 'Access-Control-Allow-Origin': '*',
- 'Access-Control-Allow-Methods': ';POST, GET, PUT, DELETE, OPTIONS',
- 'Access-Control-Allow-Credential': false,
- 'Access-Control-Max-Age': 86400, // 24hrs
- 'Access-Control-Allow-Headers':
- 'X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept'
- };
+
resp.writeHead(200, headers);
resp.end();
} else {
@@ -84,9 +94,23 @@ function readStreamAsJSON(stream, callback) {
stream.on("error", e => callback(e))
}
+// : (stream.Readable, Function)
+// Invoke a callback with a stream's data.
+function readStreamAsFlatted(stream, callback) {
+ let data = ""
+ stream.on("data", chunk => data += chunk)
+ stream.on("end", () => {
+ let result, error
+ try { result = Flatted.parse(data) }
+ catch (e) { error = e }
+ callback(error, result)
+ })
+ stream.on("error", e => callback(e))
+}
+
// : (string, Array, Function)
// Register a server route.
-function handle(method, url, f) {
+function handle(method, url, f, readFlatted = false) {
router.add(method, url, (req, resp, ...args) => {
function finish() {
let output
@@ -99,11 +123,13 @@ function handle(method, url, f) {
if (output) output.resp(resp)
}
- if (method == "PUT" || method == "POST")
- readStreamAsJSON(req, (err, val) => {
+ if (method == "PUT" || method == "POST") {
+ const readMethod = readFlatted ? readStreamAsFlatted : readStreamAsJSON;
+ readMethod(req, (err, val) => {
if (err) new Output(500, err.toString()).resp(resp)
else { args.unshift(val); finish() }
})
+ }
else
finish()
})
@@ -209,10 +235,37 @@ function reqIP(request) {
// The event submission endpoint, which a client sends an event to.
handle("POST", ["docs", null, "events"], (data, id, req) => {
let version = nonNegInteger(data.version)
- let steps = data.steps.map(s => Step.fromJSON(EditorSchema, s))
+ let steps = data.steps.map(s => Step.fromJSON(effectiveSchema, s))
let result = getInstance(id, reqIP(req)).addEvents(version, steps, data.clientID)
if (!result)
return new Output(409, "Version not current")
else
return Output.json(result)
})
+
+// [FS] IRAD-1040 2020-09-02
+// set the effective schema from client to work the plugins collaboratively
+handle("POST", ["docs", null, "schema"], (data, id, req) => {
+ const updatedSchema = Flatted.stringify(data);
+ // Do a string comparison to see if they are same or not.
+ // if same, don't update
+ if(lastUpdatedSchema !== updatedSchema) {
+ lastUpdatedSchema = updatedSchema;
+ const spec = data['spec'];
+ updateSpec(spec, 'nodes');
+ updateSpec(spec, 'marks');
+ effectiveSchema = new Schema({ nodes: effectiveSchema.spec.nodes, marks: effectiveSchema.spec.marks });
+ setEditorSchema(effectiveSchema);
+ }
+ return Output.json({ result: 'success' });
+}, true)
+
+function updateSpec(spec, attrName) {
+ // clear current array
+ effectiveSchema.spec[attrName].content.splice(0, effectiveSchema.spec[attrName].content.length);
+ const collection = spec[attrName]['content'];
+ // update current array with the latest info
+ for (var i = 0; i < collection.length; i += 2) {
+ effectiveSchema.spec[attrName] = effectiveSchema.spec[attrName].update(collection[i], collection[i+1]);
+ }
+}
\ No newline at end of file
diff --git a/package.json b/package.json
index 5d7bc07e..9462b19d 100644
--- a/package.json
+++ b/package.json
@@ -77,6 +77,7 @@
"cors": "^2.8.5",
"exports-loader": "1.1.0",
"express": "^4.17.1",
+ "flatted": "^3.0.4",
"flow-typed": "^3.2.0",
"formidable": "^1.2.2",
"invariant": "2.2.4",
diff --git a/src/client/CollabConnector.js b/src/client/CollabConnector.js
index aace277c..278eac63 100644
--- a/src/client/CollabConnector.js
+++ b/src/client/CollabConnector.js
@@ -2,6 +2,7 @@
import { Transform } from 'prosemirror-transform';
import { EditorState } from 'prosemirror-state';
+import { Schema } from 'prosemirror-model';
import SimpleConnector from './SimpleConnector';
import type { SetStateCall } from './SimpleConnector';
import EditorConnection from './EditorConnection';
@@ -29,7 +30,7 @@ class CollabConnector extends SimpleConnector {
this._docID = docID;
// [FS][11-MAR-2020]
- // Modified the scripts to ensure not to always replace 3001 with 3002 to run both servers together,
+ // Modified the scripts to ensure not to always replace 3001 with 3002 to run both servers together,
// instead used running hostname and configured port.
const url = window.location.protocol + '\/\/' +
window.location.hostname + ':3002/docs/' +
@@ -57,6 +58,12 @@ class CollabConnector extends SimpleConnector {
this._connection.dispatch({ type: 'transaction', transaction });
});
};
+
+ // FS IRAD-1040 2020-09-02
+ // Send the modified schema to server
+ updateSchema = (schema: Schema) => {
+ this._connection.updateSchema(schema);
+ };
}
export default CollabConnector;
diff --git a/src/client/EditorConnection.js b/src/client/EditorConnection.js
index 20de953c..e17ab230 100644
--- a/src/client/EditorConnection.js
+++ b/src/client/EditorConnection.js
@@ -1,21 +1,21 @@
// @flow
-import nullthrows from 'nullthrows';
import {
collab,
getVersion,
receiveTransaction,
sendableSteps,
} from 'prosemirror-collab';
-import {EditorState} from 'prosemirror-state';
-import {Step} from 'prosemirror-transform';
-import {EditorView} from 'prosemirror-view';
-
+import { EditorState } from 'prosemirror-state';
+import { Step } from 'prosemirror-transform';
+import { EditorView } from 'prosemirror-view';
import EditorPlugins from '../EditorPlugins';
import EditorSchema from '../EditorSchema';
import uuid from '../uuid';
-import {GET, POST} from './http';
-import throttle from './throttle';
+import { GET, POST } from './http';
+// [FS] IRAD-1040 2020-09-02
+import { Schema } from 'prosemirror-model';
+import { stringify } from 'flatted';
function badVersion(err: Object) {
return err.status == 400 && /invalid version/i.test(String(err));
@@ -168,25 +168,25 @@ class EditorConnection {
if (err.status == 410 || badVersion(err)) {
// Too far behind. Revert to server state
this.report.failure(err);
- this.dispatch({type: 'restart'});
+ this.dispatch({ type: 'restart' });
} else if (err) {
- this.dispatch({type: 'recover', error: err});
+ this.dispatch({ type: 'recover', error: err });
}
}
);
}
- sendable(editState: EditorState): ?{steps: Array} {
+ sendable(editState: EditorState): ?{ steps: Array } {
const steps = sendableSteps(editState);
if (steps) {
- return {steps};
+ return { steps };
}
return null;
}
// Send the given steps to the server
send(editState: EditorState, sendable: Object) {
- const {steps} = sendable;
+ const { steps } = sendable;
const json = JSON.stringify({
version: getVersion(editState),
steps: steps ? steps.steps.map(s => s.toJSON()) : [],
@@ -198,10 +198,10 @@ class EditorConnection {
this.backOff = 0;
const tr = steps
? receiveTransaction(
- this.state.edit,
- steps.steps,
- repeat(steps.clientID, steps.steps.length)
- )
+ this.state.edit,
+ steps.steps,
+ repeat(steps.clientID, steps.steps.length)
+ )
: this.state.edit.tr;
this.dispatch({
@@ -215,17 +215,32 @@ class EditorConnection {
// The client's document conflicts with the server's version.
// Poll for changes and then try again.
this.backOff = 0;
- this.dispatch({type: 'poll'});
+ this.dispatch({ type: 'poll' });
} else if (badVersion(err)) {
this.report.failure(err);
- this.dispatch({type: 'restart'});
+ this.dispatch({ type: 'restart' });
} else {
- this.dispatch({type: 'recover', error: err});
+ this.dispatch({ type: 'recover', error: err });
}
}
);
}
+ // [FS] IRAD-1040 2020-09-02
+ // Send the modified schema to server
+ updateSchema(schema: Schema) {
+ // to avoid cyclic reference error, use flatted string.
+ const schemaFlatted = stringify(schema);
+ this.run(POST(this.url + '/schema/', schemaFlatted, 'text/plain')).then(
+ data => {
+ console.log('schema updated');
+ },
+ err => {
+ this.report.failure(err);
+ }
+ );
+ }
+
// Try to recover from an error
recover(err: Error): void {
const newBackOff = this.backOff ? Math.min(this.backOff * 2, 6e4) : 200;
@@ -235,7 +250,7 @@ class EditorConnection {
this.backOff = newBackOff;
setTimeout(() => {
if (this.state.comm == 'recover') {
- this.dispatch({type: 'poll'});
+ this.dispatch({ type: 'poll' });
}
}, this.backOff);
}
diff --git a/src/client/Licit.js b/src/client/Licit.js
index 02d0eed4..300725cc 100644
--- a/src/client/Licit.js
+++ b/src/client/Licit.js
@@ -89,6 +89,11 @@ class Licit extends React.Component {
embedded,
runtime
};
+ // FS IRAD-1040 2020-26-08
+ // Get the modified schema from editorstate and send it to collab server
+ if (this._connector.updateSchema) {
+ this._connector.updateSchema(this.state.editorState.schema);
+ }
}
setContent = (content: any = {}): void => {
diff --git a/src/client/SimpleConnector.js b/src/client/SimpleConnector.js
index a0784742..54c5f275 100644
--- a/src/client/SimpleConnector.js
+++ b/src/client/SimpleConnector.js
@@ -2,6 +2,7 @@
import {EditorState} from 'prosemirror-state';
import {Transform} from 'prosemirror-transform';
+import {Schema} from 'prosemirror-model';
import ReactDOM from 'react-dom';
export type SetStateCall = (
@@ -36,6 +37,11 @@ class SimpleConnector {
getState = (): EditorState => {
return this._editorState;
};
+
+ // FS IRAD-1040 2020-09-02
+ // Send the modified schema to server
+ updateSchema = (schema: Schema) => {
+ };
}
export default SimpleConnector;
\ No newline at end of file
diff --git a/src/convertFromJSON.js b/src/convertFromJSON.js
index e897dba5..c7f2b6e0 100644
--- a/src/convertFromJSON.js
+++ b/src/convertFromJSON.js
@@ -21,9 +21,11 @@ export default function convertFromJSON(
let effectivePlugins = EditorPlugins;
if (plugins) {
for (let p of plugins) {
- effectivePlugins.push(p);
- if (p.getEffectiveSchema) {
- editorSchema = p.getEffectiveSchema(editorSchema);
+ if (!effectivePlugins.includes(p)) {
+ effectivePlugins.push(p);
+ if (p.getEffectiveSchema) {
+ editorSchema = p.getEffectiveSchema(editorSchema);
+ }
}
}
}