diff --git a/dist/assets/assets/images/radio.svg b/dist/assets/assets/images/radio.svg deleted file mode 100644 index 4ce24fc8..00000000 --- a/dist/assets/assets/images/radio.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/dist/assets/assets/images/radio_active.svg b/dist/assets/assets/images/radio_active.svg deleted file mode 100644 index 5edab1b4..00000000 --- a/dist/assets/assets/images/radio_active.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/dist/assets/assets/images/spinner.svg b/dist/assets/assets/images/spinner.svg deleted file mode 100644 index ef958cc3..00000000 --- a/dist/assets/assets/images/spinner.svg +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/dist/assets/images/forward.svg b/dist/assets/images/forward.svg new file mode 100644 index 00000000..0b1cb8b9 --- /dev/null +++ b/dist/assets/images/forward.svg @@ -0,0 +1,3 @@ + + + diff --git a/dist/framework/assembly.d.ts b/dist/framework/assembly.d.ts index acc334ef..2cd59534 100644 --- a/dist/framework/assembly.d.ts +++ b/dist/framework/assembly.d.ts @@ -1,2 +1,8 @@ -import VisualisationEngine from './abstractions/visualisation_engine'; -export declare const Assembly: (worker: Worker) => VisualisationEngine; +import { VisualisationEngine, ProcessingEngine, System } from './types/modules'; +import CommandRouter from './command_router'; +export default class Assembly { + visualisationEngine: VisualisationEngine; + processingEngine: ProcessingEngine; + router: CommandRouter; + constructor(worker: Worker, system: System); +} diff --git a/dist/framework/assembly.js b/dist/framework/assembly.js index 4c0e58a6..c82a6ad0 100644 --- a/dist/framework/assembly.js +++ b/dist/framework/assembly.js @@ -1,9 +1,13 @@ import ReactEngine from './visualisation/react/engine'; import ReactFactory from './visualisation/react/factory'; import WorkerProcessingEngine from './processing/worker_engine'; -export var Assembly = function (worker) { - var processingEngine = new WorkerProcessingEngine(worker); - var visualisationEngine = new ReactEngine(new ReactFactory(), processingEngine); - processingEngine.eventListener = visualisationEngine.onEvent; - return visualisationEngine; -}; +import CommandRouter from './command_router'; +var Assembly = /** @class */ (function () { + function Assembly(worker, system) { + this.visualisationEngine = new ReactEngine(new ReactFactory()); + this.router = new CommandRouter(system, this.visualisationEngine); + this.processingEngine = new WorkerProcessingEngine(worker, this.router); + } + return Assembly; +}()); +export default Assembly; diff --git a/dist/framework/command_router.d.ts b/dist/framework/command_router.d.ts new file mode 100644 index 00000000..9c6f2b94 --- /dev/null +++ b/dist/framework/command_router.d.ts @@ -0,0 +1,10 @@ +import { Command, Response, CommandUI, CommandSystem } from './types/commands'; +import { CommandHandler, System, VisualisationEngine } from './types/modules'; +export default class CommandRouter implements CommandHandler { + system: System; + visualisationEngine: VisualisationEngine; + constructor(system: System, visualisationEngine: VisualisationEngine); + onCommand(command: Command): Promise; + onCommandSystem(command: CommandSystem, resolve: (response: Response) => void): void; + onCommandUI(command: CommandUI, reject: (reason?: any) => void): void; +} diff --git a/dist/framework/command_router.js b/dist/framework/command_router.js new file mode 100644 index 00000000..6ca0529d --- /dev/null +++ b/dist/framework/command_router.js @@ -0,0 +1,73 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (_) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +import { isCommandSystem, isCommandUI } from './types/commands'; +var CommandRouter = /** @class */ (function () { + function CommandRouter(system, visualisationEngine) { + this.system = system; + this.visualisationEngine = visualisationEngine; + } + CommandRouter.prototype.onCommand = function (command) { + return __awaiter(this, void 0, void 0, function () { + var _this = this; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, new Promise(function (resolve, reject) { + if (isCommandSystem(command)) { + _this.onCommandSystem(command, resolve); + } + else if (isCommandUI(command)) { + _this.onCommandUI(command, resolve); + } + else { + reject(new TypeError('Unknown command' + JSON.stringify(command))); + } + })]; + case 1: return [2 /*return*/, _a.sent()]; + } + }); + }); + }; + CommandRouter.prototype.onCommandSystem = function (command, resolve) { + this.system.send(command); + resolve({ command: command, payload: { __type__: 'PayloadVoid', value: undefined } }); + }; + CommandRouter.prototype.onCommandUI = function (command, reject) { + this.visualisationEngine.render(command).then(function (response) { reject(response); }, function () { }); + }; + return CommandRouter; +}()); +export default CommandRouter; diff --git a/dist/framework/helpers.d.ts b/dist/framework/helpers.d.ts new file mode 100644 index 00000000..86ddc25b --- /dev/null +++ b/dist/framework/helpers.d.ts @@ -0,0 +1,4 @@ +import { Omit } from 'lodash'; +export declare const childOf: (arg: any, superType: string) => boolean; +export declare const instanceOf: (arg: any, properties: (keyof T)[]) => arg is T; +export declare type Weak = Omit; diff --git a/dist/framework/helpers.js b/dist/framework/helpers.js new file mode 100644 index 00000000..9e54ae66 --- /dev/null +++ b/dist/framework/helpers.js @@ -0,0 +1,7 @@ +export var childOf = function (arg, superType) { + var _a; + return (_a = arg === null || arg === void 0 ? void 0 : arg.__type__) === null || _a === void 0 ? void 0 : _a.startsWith(superType); +}; +export var instanceOf = function (arg, properties) { + return !properties.some(function (property) { return arg[property] === undefined; }); +}; diff --git a/dist/framework/processing/python/worker.d.ts b/dist/framework/processing/python/worker.d.ts index 484861c8..be211e78 100644 --- a/dist/framework/processing/python/worker.d.ts +++ b/dist/framework/processing/python/worker.d.ts @@ -1,8 +1,8 @@ -declare function runCycle(userInput: any): void; +declare function runCycle(payload: any): void; declare function unwrap(response: any): Promise; declare function copyFileToPyFS(file: any, resolve: any): void; declare function initialise(): any; declare function loadScript(script: any): void; declare function pyWorker(): string; declare let pyScript: any; -declare const pyPortApi: "\nclass Event:\n def toDict(self):\n return setType({}, \"Event\")\n\n\nclass EndOfFlow(Event):\n __slots__ = \"result\"\n def __init__(self, result):\n self.result = result\n def translate_result(self):\n print(\"translate\")\n data_output = []\n for data in self.result:\n df = data[\"data_frame\"]\n data_output.append({\"id\": data[\"id\"], \"data_frame\": df.to_json()})\n return {\n \"title\": data[\"title\"],\n \"data\": data_output,\n }\n def toDict(self):\n print(\"toDict2\")\n dict = toDict(super(), \"EndOfFlow\") \n dict = dict | self.translate_result()\n return dict\n \n\nclass Command(Event):\n def toDict(self):\n return toDict(super(), \"Command\")\n\n\nclass Prompt(Command):\n __slots__ = \"title\", \"description\"\n def __init__(self, title, description):\n self.title = title\n self.description = description\n def toDict(self):\n dict = toDict(super(), \"Prompt\")\n dict[\"title\"] = self.title.toDict()\n dict[\"description\"] = self.description.toDict()\n return dict\n\n\nclass FileInput(Prompt):\n __slots__ = \"extensions\"\n def __init__(self, title, description, extensions):\n super().__init__(title, description)\n self.extensions = extensions\n def toDict(self):\n dict = toDict(super(), \"FileInput\")\n dict[\"extensions\"] = self.extensions\n return dict\n\n\nclass RadioInput(Prompt):\n def __init__(self, title, description, items):\n super().__init__(title, description)\n self.items = items\n def toDict(self):\n dict = toDict(super(), \"RadioInput\")\n dict[\"items\"] = self.items\n return dict\n\n\nclass Translatable:\n __slots__ = \"translations\"\n def __init__(self):\n self.translations = {}\n def add(self, locale, text):\n self.translations[locale] = text\n return self\n def toDict(self):\n return setType(self.translations, \"Translatable\")\n\n\ndef toDict(zuper, type):\n return setType(zuper.toDict(), type)\n\n\ndef setType(dict, type):\n key = \"__type__\"\n seperator = \".\"\n\n path = [type]\n if key in dict:\n path.insert(0, dict[key])\n dict[key] = seperator.join(path)\n return dict\n"; +declare const pyPortApi: "\nclass CommandUIRender:\n __slots__ = \"page\"\n def __init__(self, page):\n self.page = page\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"CommandUIRender\"\n dict[\"page\"] = self.page.toDict()\n return dict\n\nclass CommandSystemDonate:\n __slots__ = \"key\", \"data\"\n def __init__(self, key, data):\n self.key = key\n self.data = data\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"CommandSystemDonate\"\n dict[\"key\"] = self.key\n dict[\"data\"] = self.data\n return dict\n\n\nclass PropsUIHeader:\n __slots__ = \"title\"\n def __init__(self, title):\n self.title = title\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUIHeader\"\n dict[\"title\"] = self.title.toDict()\n return dict\n\n\nclass PropsUIPromptConfirm:\n __slots__ = \"text\", \"ok\", \"cancel\"\n def __init__(self, text, ok, cancel):\n self.text = text\n self.ok = ok\n self.cancel = cancel\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUIPromptConfirm\"\n dict[\"text\"] = self.text.toDict()\n dict[\"ok\"] = self.ok.toDict()\n dict[\"cancel\"] = self.cancel.toDict()\n return dict\n\n\nclass PropsUISpinner:\n __slots__ = \"text\"\n def __init__(self, text):\n self.text = text\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUISpinner\"\n dict[\"text\"] = self.text.toDict()\n return dict\n\n\nclass PropsUIPromptConsentForm:\n __slots__ = \"title\", \"description\", \"tables\"\n def __init__(self, title, description, tables):\n self.title = title\n self.description = description \n self.tables = tables\n def translate_tables(self):\n tables_output = []\n for table in self.tables:\n tables_output.append(table.toDict())\n return tables_output\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUIPromptConsentForm\"\n dict[\"title\"] = self.title.toDict()\n dict[\"description\"] = self.description.toDict()\n dict[\"tables\"] = self.translate_tables()\n return dict\n\n\nclass PropsUIPromptConsentFormTable:\n __slots__ = \"id\", \"title\", \"data_frame\"\n def __init__(self, id, title, data_frame):\n self.id = id\n self.title = title\n self.data_frame = data_frame\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUIPromptConsentFormTable\"\n dict[\"id\"] = self.id\n dict[\"title\"] = self.title\n dict[\"data_frame\"] = self.data_frame.to_json()\n return dict\n\n\nclass PropsUIPromptFileInput:\n __slots__ = \"title\", \"description\", \"extensions\"\n def __init__(self, title, description, extensions):\n self.title = title\n self.description = description\n self.extensions = extensions\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUIPromptFileInput\"\n dict[\"title\"] = self.title.toDict()\n dict[\"description\"] = self.description.toDict()\n dict[\"extensions\"] = self.extensions\n return dict\n\n\nclass PropsUIPromptRadioInput:\n __slots__ = \"title\", \"description\", \"items\"\n def __init__(self, title, description, items):\n self.title = title\n self.description = description\n self.items = items\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUIPromptRadioInput\"\n dict[\"title\"] = self.title.toDict()\n dict[\"description\"] = self.description.toDict()\n dict[\"items\"] = self.items\n return dict\n\n\nclass PropsUIPageDonation:\n __slots__ = \"header\", \"body\", \"spinner\"\n def __init__(self, header, body, spinner):\n self.header = header\n self.body = body\n self.spinner = spinner\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUIPageDonation\"\n dict[\"header\"] = self.header.toDict()\n dict[\"body\"] = self.body.toDict()\n dict[\"spinner\"] = self.spinner.toDict()\n return dict\n\n\nclass PropsUIPageStart:\n __slots__ = \"header\", \"spinner\"\n def __init__(self, header, spinner):\n self.header = header\n self.spinner = spinner\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUIPageStart\"\n dict[\"header\"] = self.header.toDict()\n dict[\"spinner\"] = self.spinner.toDict()\n return dict\n\n\nclass PropsUIPageEnd:\n __slots__ = \"header\" \n def __init__(self, header):\n self.header = header\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUIPageEnd\"\n dict[\"header\"] = self.header.toDict()\n return dict\n\n\nclass Translatable:\n __slots__ = \"translations\"\n def __init__(self, translations):\n self.translations = translations\n def toDict(self):\n dict = {}\n dict[\"translations\"] = self.translations\n return dict \n"; diff --git a/dist/framework/processing/python/worker.js b/dist/framework/processing/python/worker.js index 9937a990..a23f8638 100644 --- a/dist/framework/processing/python/worker.js +++ b/dist/framework/processing/python/worker.js @@ -25,8 +25,9 @@ onmessage = function (event) { console.log('[ProcessingWorker] Received unsupported event: ', eventType); } }; -function runCycle(userInput) { - scriptEvent = pyScript.send(userInput); +function runCycle(payload) { + console.log('[ProcessingWorker] runCycle ' + JSON.stringify(payload)); + scriptEvent = pyScript.send(payload); self.postMessage({ eventType: 'runCycleDone', scriptEvent: scriptEvent.toJs({ @@ -36,13 +37,14 @@ function runCycle(userInput) { }); } function unwrap(response) { + console.log('[ProcessingWorker] unwrap response: ' + JSON.stringify(response.payload)); return new Promise(function (resolve) { - switch (response.prompt.__type__) { - case 'Event.Command.Prompt.FileInput': - copyFileToPyFS(response.userInput, resolve); + switch (response.payload.__type__) { + case 'PayloadFile': + copyFileToPyFS(response.payload.value, resolve); break; default: - resolve(response.userInput); + resolve(response.payload); } }); } @@ -52,7 +54,7 @@ function copyFileToPyFS(file, resolve) { var writeToPyFS = function (_a) { var done = _a.done, value = _a.value; if (done) { - resolve(file.name); + resolve({ __type__: 'PayloadString', value: file.name }); } else { self.pyodide.FS.write(pyFile, value, 0, value.length); @@ -62,10 +64,13 @@ function copyFileToPyFS(file, resolve) { reader.read().then(writeToPyFS); } function initialise() { + console.log('[ProcessingWorker] initialise'); importScripts('https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js'); + console.log('[ProcessingWorker] loading Pyodide'); return loadPyodide({ indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.21.2/full/' }).then(function (pyodide) { + console.log('[ProcessingWorker] loading packages'); self.pyodide = pyodide; return self.pyodide.loadPackage(['micropip', 'numpy', 'pandas']); }); @@ -75,7 +80,7 @@ function loadScript(script) { self.pyodide.runPython(pyPortApi); self.pyodide.runPython(script); } -var pyPortApi = "\nclass Event:\n def toDict(self):\n return setType({}, \"Event\")\n\n\nclass EndOfFlow(Event):\n __slots__ = \"result\"\n def __init__(self, result):\n self.result = result\n def translate_result(self):\n print(\"translate\")\n data_output = []\n for data in self.result:\n df = data[\"data_frame\"]\n data_output.append({\"id\": data[\"id\"], \"data_frame\": df.to_json()})\n return {\n \"title\": data[\"title\"],\n \"data\": data_output,\n }\n def toDict(self):\n print(\"toDict2\")\n dict = toDict(super(), \"EndOfFlow\") \n dict = dict | self.translate_result()\n return dict\n \n\nclass Command(Event):\n def toDict(self):\n return toDict(super(), \"Command\")\n\n\nclass Prompt(Command):\n __slots__ = \"title\", \"description\"\n def __init__(self, title, description):\n self.title = title\n self.description = description\n def toDict(self):\n dict = toDict(super(), \"Prompt\")\n dict[\"title\"] = self.title.toDict()\n dict[\"description\"] = self.description.toDict()\n return dict\n\n\nclass FileInput(Prompt):\n __slots__ = \"extensions\"\n def __init__(self, title, description, extensions):\n super().__init__(title, description)\n self.extensions = extensions\n def toDict(self):\n dict = toDict(super(), \"FileInput\")\n dict[\"extensions\"] = self.extensions\n return dict\n\n\nclass RadioInput(Prompt):\n def __init__(self, title, description, items):\n super().__init__(title, description)\n self.items = items\n def toDict(self):\n dict = toDict(super(), \"RadioInput\")\n dict[\"items\"] = self.items\n return dict\n\n\nclass Translatable:\n __slots__ = \"translations\"\n def __init__(self):\n self.translations = {}\n def add(self, locale, text):\n self.translations[locale] = text\n return self\n def toDict(self):\n return setType(self.translations, \"Translatable\")\n\n\ndef toDict(zuper, type):\n return setType(zuper.toDict(), type)\n\n\ndef setType(dict, type):\n key = \"__type__\"\n seperator = \".\"\n\n path = [type]\n if key in dict:\n path.insert(0, dict[key])\n dict[key] = seperator.join(path)\n return dict\n"; +var pyPortApi = "\nclass CommandUIRender:\n __slots__ = \"page\"\n def __init__(self, page):\n self.page = page\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"CommandUIRender\"\n dict[\"page\"] = self.page.toDict()\n return dict\n\nclass CommandSystemDonate:\n __slots__ = \"key\", \"data\"\n def __init__(self, key, data):\n self.key = key\n self.data = data\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"CommandSystemDonate\"\n dict[\"key\"] = self.key\n dict[\"data\"] = self.data\n return dict\n\n\nclass PropsUIHeader:\n __slots__ = \"title\"\n def __init__(self, title):\n self.title = title\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUIHeader\"\n dict[\"title\"] = self.title.toDict()\n return dict\n\n\nclass PropsUIPromptConfirm:\n __slots__ = \"text\", \"ok\", \"cancel\"\n def __init__(self, text, ok, cancel):\n self.text = text\n self.ok = ok\n self.cancel = cancel\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUIPromptConfirm\"\n dict[\"text\"] = self.text.toDict()\n dict[\"ok\"] = self.ok.toDict()\n dict[\"cancel\"] = self.cancel.toDict()\n return dict\n\n\nclass PropsUISpinner:\n __slots__ = \"text\"\n def __init__(self, text):\n self.text = text\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUISpinner\"\n dict[\"text\"] = self.text.toDict()\n return dict\n\n\nclass PropsUIPromptConsentForm:\n __slots__ = \"title\", \"description\", \"tables\"\n def __init__(self, title, description, tables):\n self.title = title\n self.description = description \n self.tables = tables\n def translate_tables(self):\n tables_output = []\n for table in self.tables:\n tables_output.append(table.toDict())\n return tables_output\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUIPromptConsentForm\"\n dict[\"title\"] = self.title.toDict()\n dict[\"description\"] = self.description.toDict()\n dict[\"tables\"] = self.translate_tables()\n return dict\n\n\nclass PropsUIPromptConsentFormTable:\n __slots__ = \"id\", \"title\", \"data_frame\"\n def __init__(self, id, title, data_frame):\n self.id = id\n self.title = title\n self.data_frame = data_frame\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUIPromptConsentFormTable\"\n dict[\"id\"] = self.id\n dict[\"title\"] = self.title\n dict[\"data_frame\"] = self.data_frame.to_json()\n return dict\n\n\nclass PropsUIPromptFileInput:\n __slots__ = \"title\", \"description\", \"extensions\"\n def __init__(self, title, description, extensions):\n self.title = title\n self.description = description\n self.extensions = extensions\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUIPromptFileInput\"\n dict[\"title\"] = self.title.toDict()\n dict[\"description\"] = self.description.toDict()\n dict[\"extensions\"] = self.extensions\n return dict\n\n\nclass PropsUIPromptRadioInput:\n __slots__ = \"title\", \"description\", \"items\"\n def __init__(self, title, description, items):\n self.title = title\n self.description = description\n self.items = items\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUIPromptRadioInput\"\n dict[\"title\"] = self.title.toDict()\n dict[\"description\"] = self.description.toDict()\n dict[\"items\"] = self.items\n return dict\n\n\nclass PropsUIPageDonation:\n __slots__ = \"header\", \"body\", \"spinner\"\n def __init__(self, header, body, spinner):\n self.header = header\n self.body = body\n self.spinner = spinner\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUIPageDonation\"\n dict[\"header\"] = self.header.toDict()\n dict[\"body\"] = self.body.toDict()\n dict[\"spinner\"] = self.spinner.toDict()\n return dict\n\n\nclass PropsUIPageStart:\n __slots__ = \"header\", \"spinner\"\n def __init__(self, header, spinner):\n self.header = header\n self.spinner = spinner\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUIPageStart\"\n dict[\"header\"] = self.header.toDict()\n dict[\"spinner\"] = self.spinner.toDict()\n return dict\n\n\nclass PropsUIPageEnd:\n __slots__ = \"header\" \n def __init__(self, header):\n self.header = header\n def toDict(self):\n dict = {}\n dict[\"__type__\"] = \"PropsUIPageEnd\"\n dict[\"header\"] = self.header.toDict()\n return dict\n\n\nclass Translatable:\n __slots__ = \"translations\"\n def __init__(self, translations):\n self.translations = translations\n def toDict(self):\n dict = {}\n dict[\"translations\"] = self.translations\n return dict \n"; function pyWorker() { - return "\n from collections.abc import Generator\n import json\n import html\n import pandas as pd\n\n class ScriptWrapper(Generator):\n def __init__(self, script):\n self.script = script\n def send(self, data):\n print(\"toDict\")\n event = self.script.send(data)\n return event.toDict()\n def throw(self, type=None, value=None, traceback=None):\n raise StopIteration\n script = process()\n ScriptWrapper(script)\n "; + return "\n from collections.abc import Generator\n import json\n import html\n import pandas as pd\n\n\n class ScriptWrapper(Generator):\n def __init__(self, script):\n self.script = script\n def send(self, data):\n command = self.script.send(data)\n return command.toDict()\n def throw(self, type=None, value=None, traceback=None):\n raise StopIteration\n script = process()\n ScriptWrapper(script)\n "; } diff --git a/dist/framework/processing/worker_engine.d.ts b/dist/framework/processing/worker_engine.d.ts index 1fcc2ccd..92591eca 100644 --- a/dist/framework/processing/worker_engine.d.ts +++ b/dist/framework/processing/worker_engine.d.ts @@ -1,11 +1,15 @@ -import ProcessingEngine from '../abstractions/processing_engine'; +import { CommandHandler, ProcessingEngine } from '../types/modules'; +import { Response, Script } from '../types/commands'; export default class WorkerProcessingEngine implements ProcessingEngine { - eventListener: (event: any) => void; worker: Worker; - constructor(worker: Worker); - start(): void; + commandHandler: CommandHandler; + script: Script; + constructor(worker: Worker, commandHandler: CommandHandler); + handleEvent(event: any): void; + start(script: Script): void; loadScript(script: any): void; firstRunCycle(): void; - nextRunCycle(response: any): void; + nextRunCycle(response: Response): void; terminate(): void; + handleRunCycle(command: any): void; } diff --git a/dist/framework/processing/worker_engine.js b/dist/framework/processing/worker_engine.js index a645fcf0..38b1809e 100644 --- a/dist/framework/processing/worker_engine.js +++ b/dist/framework/processing/worker_engine.js @@ -1,18 +1,38 @@ +import { isCommand } from '../types/commands'; var WorkerProcessingEngine = /** @class */ (function () { - function WorkerProcessingEngine(worker) { + function WorkerProcessingEngine(worker, commandHandler) { var _this = this; - this.eventListener = function (event) { - var eventString = JSON.stringify(event); - console.log('[WorkerProcessingEngine] No event listener registered for event: ', eventString); - }; + this.commandHandler = commandHandler; this.worker = worker; this.worker.onerror = console.log; this.worker.onmessage = function (event) { console.log('[WorkerProcessingEngine] Received event from worker: ', event.data.eventType); - _this.eventListener(event); + _this.handleEvent(event); }; } - WorkerProcessingEngine.prototype.start = function () { + WorkerProcessingEngine.prototype.handleEvent = function (event) { + var eventType = event.data.eventType; + console.log('[ReactEngine] received eventType: ', eventType); + switch (eventType) { + case 'initialiseDone': + console.log('[ReactEngine] received: initialiseDone'); + this.loadScript(this.script); + break; + case 'loadScriptDone': + console.log('[ReactEngine] Received: loadScriptDone'); + this.firstRunCycle(); + break; + case 'runCycleDone': + console.log('[ReactEngine] received: event', event.data.scriptEvent); + this.handleRunCycle(event.data.scriptEvent); + break; + default: + console.log('[ReactEngine] received unsupported flow event: ', eventType); + } + }; + WorkerProcessingEngine.prototype.start = function (script) { + console.log('[WorkerProcessingEngine] started'); + this.script = script; this.worker.postMessage({ eventType: 'initialise' }); }; WorkerProcessingEngine.prototype.loadScript = function (script) { @@ -27,6 +47,12 @@ var WorkerProcessingEngine = /** @class */ (function () { WorkerProcessingEngine.prototype.terminate = function () { this.worker.terminate(); }; + WorkerProcessingEngine.prototype.handleRunCycle = function (command) { + var _this = this; + if (isCommand(command)) { + this.commandHandler.onCommand(command).then(function (response) { return _this.nextRunCycle(response); }, function () { }); + } + }; return WorkerProcessingEngine; }()); export default WorkerProcessingEngine; diff --git a/dist/framework/text_bundle.d.ts b/dist/framework/text_bundle.d.ts new file mode 100644 index 00000000..8680859e --- /dev/null +++ b/dist/framework/text_bundle.d.ts @@ -0,0 +1,10 @@ +import { Translatable } from './types/elements'; +export default class TextBundle implements Translatable { + translations: { + [key: string]: string; + }; + defaultLocale: string; + add(locale: string, text: string): TextBundle; + translate(locale: string): string; + resolve(locale: string): string; +} diff --git a/dist/framework/text_bundle.js b/dist/framework/text_bundle.js new file mode 100644 index 00000000..e1a581b4 --- /dev/null +++ b/dist/framework/text_bundle.js @@ -0,0 +1,30 @@ +import _ from 'lodash'; +var TextBundle = /** @class */ (function () { + function TextBundle() { + this.translations = {}; + this.defaultLocale = 'nl'; + } + TextBundle.prototype.add = function (locale, text) { + this.translations[locale] = text; + return this; + }; + TextBundle.prototype.translate = function (locale) { + return _.escape(this.resolve(locale)); + }; + TextBundle.prototype.resolve = function (locale) { + var text = this.translations[locale]; + if (text !== null) { + return text; + } + var defaultText = this.translations[this.defaultLocale]; + if (defaultText !== null) { + return defaultText; + } + if (Object.values(this.translations).length > 0) { + return Object.values(this.translations)[0]; + } + return '?text?'; + }; + return TextBundle; +}()); +export default TextBundle; diff --git a/dist/framework/translator.d.ts b/dist/framework/translator.d.ts new file mode 100644 index 00000000..501e19e4 --- /dev/null +++ b/dist/framework/translator.d.ts @@ -0,0 +1,4 @@ +import { Text } from './types/elements'; +export declare const Translator: { + translate: (text: Text, locale: string) => string; +}; diff --git a/dist/framework/translator.js b/dist/framework/translator.js new file mode 100644 index 00000000..7af274c0 --- /dev/null +++ b/dist/framework/translator.js @@ -0,0 +1,28 @@ +import _ from 'lodash'; +import { isTranslatable } from './types/elements'; +export var Translator = (function () { + var defaultLocale = 'nl'; + function translate(text, locale) { + if (isTranslatable(text)) { + return _.escape(resolve(text, locale)); + } + return text; + } + function resolve(translatable, locale) { + var text = translatable.translations[locale]; + if (text !== null) { + return text; + } + var defaultText = translatable.translations[defaultLocale]; + if (defaultText !== null) { + return defaultText; + } + if (Object.values(translatable.translations).length > 0) { + return Object.values(translatable.translations)[0]; + } + return '?text?'; + } + return { + translate: translate + }; +})(); diff --git a/dist/framework/types/commands.d.ts b/dist/framework/types/commands.d.ts new file mode 100644 index 00000000..594b2d93 --- /dev/null +++ b/dist/framework/types/commands.d.ts @@ -0,0 +1,60 @@ +import { PropsUIPage } from './pages'; +export declare type Script = string | File | URL; +export declare function isScript(arg: any): arg is Script; +export declare function isFile(arg: unknown): arg is File; +export declare function isURL(arg: any): arg is URL; +export interface Table { + id: string; + title: Text; + data: any; +} +export declare function isTable(arg: any): arg is Table; +export interface Response { + command: Command; + payload: Payload; +} +export declare function isResponse(arg: any): arg is Response; +export declare type Payload = PayloadResolved | PayloadRejected; +export declare type PayloadRejected = PayloadFalse | PayloadError; +export interface PayloadFalse { + __type__: 'PayloadFalse'; + value: false; +} +export interface PayloadError { + __type__: 'PayloadError'; + value: string; +} +export declare type PayloadResolved = PayloadVoid | PayloadTrue | PayloadString | PayloadFile; +export interface PayloadVoid { + __type__: 'PayloadVoid'; + value: undefined; +} +export interface PayloadTrue { + __type__: 'PayloadTrue'; + value: true; +} +export interface PayloadString { + __type__: 'PayloadString'; + value: string; +} +export interface PayloadFile { + __type__: 'PayloadFile'; + value: File; +} +export declare type Command = CommandUI | CommandSystem; +export declare function isCommand(arg: any): arg is Command; +export declare type CommandSystem = CommandSystemDonate; +export declare function isCommandSystem(arg: any): arg is CommandSystem; +export declare type CommandUI = CommandUIRender; +export declare function isCommandUI(arg: any): arg is CommandUI; +export interface CommandSystemDonate { + __type__: 'CommandSystemDonate'; + key: string; + data: string; +} +export declare function isCommandSystemDonate(arg: any): arg is CommandSystemDonate; +export interface CommandUIRender { + __type__: 'CommandUIRender'; + page: PropsUIPage; +} +export declare function isCommandUIRender(arg: any): arg is CommandUIRender; diff --git a/dist/framework/types/commands.js b/dist/framework/types/commands.js new file mode 100644 index 00000000..c106d363 --- /dev/null +++ b/dist/framework/types/commands.js @@ -0,0 +1,31 @@ +import { instanceOf, childOf } from '../helpers'; +export function isScript(arg) { + return typeof arg === 'string' || isFile(arg) || isURL(arg); +} +export function isFile(arg) { + return instanceOf(arg, ['arrayBuffer', 'lastModified', 'name', 'size', 'slice', 'stream', 'text', 'type', 'webkitRelativePath']); +} +export function isURL(arg) { + return instanceOf(arg, ['hash', 'host', 'hostname', 'href', 'origin', 'toString', 'password', 'pathname', 'port', 'protocol', 'search', 'searchParams', 'username', 'toJSON']); +} +export function isTable(arg) { + return instanceOf(arg, ['id', 'title', 'data']); +} +export function isResponse(arg) { + return instanceOf(arg, ['command', 'payload']); +} +export function isCommand(arg) { + return childOf(arg, 'Command'); +} +export function isCommandSystem(arg) { + return childOf(arg, 'CommandSystem'); +} +export function isCommandUI(arg) { + return childOf(arg, 'CommandUI'); +} +export function isCommandSystemDonate(arg) { + return instanceOf(arg, ['__type__', 'key', 'data']) && arg.__type__ === 'CommandSystemDonate'; +} +export function isCommandUIRender(arg) { + return instanceOf(arg, ['__type__', 'page']) && arg.__type__ === 'CommandUIRender'; +} diff --git a/dist/framework/types/elements.d.ts b/dist/framework/types/elements.d.ts new file mode 100644 index 00000000..771438a2 --- /dev/null +++ b/dist/framework/types/elements.d.ts @@ -0,0 +1,108 @@ +import { Weak } from '../helpers'; +import { PropsUIPage } from './pages'; +import { PropsUIPrompt } from './prompts'; +export declare type PropsUI = PropsUIText | PropsUIButton | PropsUISpinner | PropsUIHeader | PropsUITable | PropsUIPage | PropsUIPrompt; +export declare type PropsUIText = PropsUITextTitle0 | PropsUITextTitle1 | PropsUITextTitle2; +export declare type PropsUIButton = PropsUIButtonPrimary | PropsUIButtonSecundary | PropsUIButtonForward | PropsUIButtonLabel; +export declare function isPropsUI(arg: any): arg is PropsUI; +export declare function isPropsUIText(arg: any): arg is PropsUIText; +export interface PropsUITextBody { + __type__: 'PropsUITextBody'; + text: string; + color?: string; + margin?: string; +} +export declare function isPropsUITextBody(arg: any): arg is PropsUITextBody; +export interface PropsUITextTitle0 { + __type__: 'PropsUITextTitle0'; + text: string; + color?: string; + margin?: string; +} +export declare function isPropsUITextTitle0(arg: any): arg is PropsUITextTitle0; +export interface PropsUITextTitle1 { + __type__: 'PropsUITextTitle1'; + text: string; + color?: string; + margin?: string; +} +export declare function isPropsUITextTitle1(arg: any): arg is PropsUITextTitle1; +export interface PropsUITextTitle2 { + __type__: 'PropsUITextTitle2'; + text: string; + color?: string; + margin?: string; +} +export declare function isPropsUITextTitle2(arg: any): arg is PropsUITextTitle2; +export declare function isPropsUIButton(arg: any): arg is PropsUIButton; +export interface PropsUIButtonPrimary { + __type__: 'PropsUIButtonPrimary'; + label: string; + color?: string; + onClick: () => void; +} +export declare function isPropsUIButtonPrimary(arg: any): arg is PropsUIButtonPrimary; +export interface PropsUIButtonSecundary { + __type__: 'PropsUIButtonSecundary'; + label: string; + color?: string; + onClick: () => void; +} +export declare function isPropsUIButtonSecundary(arg: any): arg is PropsUIButtonSecundary; +export interface PropsUIButtonForward { + __type__: 'PropsUIButtonForward'; + label: string; + onClick: () => void; +} +export declare function isPropsUIButtonForward(arg: any): arg is PropsUIButtonForward; +export interface PropsUIButtonLabel { + __type__: 'PropsUIButtonLabel'; + label: string; + onClick: () => void; +} +export declare function isPropsUIButtonLabel(arg: any): arg is PropsUIButtonLabel; +export interface PropsUISpinner { + __type__: 'PropsUISpinner'; + text: Text; +} +export declare function isPropsUISpinner(arg: any): arg is PropsUISpinner; +export interface PropsUIHeader { + __type__: 'PropsUIHeader'; + title: Text; +} +export declare function isPropsUIHeader(arg: any): arg is PropsUIHeader; +export interface PropsUITable { + __type__: 'PropsUITable'; + id: string; + head: Weak; + body: Weak; +} +export declare function isPropsUITable(arg: any): arg is PropsUITable; +export interface PropsUITableHead { + __type__: 'PropsUITableHead'; + cells: PropsUITableCell[]; +} +export declare function isPropsUITableHeader(arg: any): arg is PropsUITableHead; +export interface PropsUITableBody { + __type__: 'PropsUITableBody'; + rows: Weak; +} +export declare function isPropsUITableBody(arg: any): arg is PropsUITableBody; +export interface PropsUITableRow { + __type__: 'PropsUITableRow'; + cells: PropsUITableCell[]; +} +export declare function isPropsUITableRow(arg: any): arg is PropsUITableRow; +export interface PropsUITableCell { + __type__: 'PropsUITableCell'; + text: string; +} +export declare function isPropsUITableCell(arg: any): arg is PropsUITableCell; +export declare type Text = Translatable | string; +export declare function isText(arg: any): arg is Text; +export interface Translatable { + translations: { + [locale: string]: string; + }; +} +export declare function isTranslatable(arg: any): arg is Translatable; diff --git a/dist/framework/types/elements.js b/dist/framework/types/elements.js new file mode 100644 index 00000000..16e8eae4 --- /dev/null +++ b/dist/framework/types/elements.js @@ -0,0 +1,64 @@ +import { instanceOf, childOf } from '../helpers'; +// UI +export function isPropsUI(arg) { + return childOf(arg, 'PropsUI'); +} +// TEXTS +export function isPropsUIText(arg) { + return childOf(arg, 'PropsUIText'); +} +export function isPropsUITextBody(arg) { + return instanceOf(arg, ['__type__', 'text', 'color', 'margin']) && arg.__type__ === 'PropsUITextBody'; +} +export function isPropsUITextTitle0(arg) { + return instanceOf(arg, ['__type__', 'text', 'color', 'margin']) && arg.__type__ === 'PropsUITextTitle0'; +} +export function isPropsUITextTitle1(arg) { + return instanceOf(arg, ['__type__', 'text', 'color', 'margin']) && arg.__type__ === 'PropsUITextTitle1'; +} +export function isPropsUITextTitle2(arg) { + return instanceOf(arg, ['__type__', 'text', 'color', 'margin']) && arg.__type__ === 'PropsUITextTitle2'; +} +// BUTTONS +export function isPropsUIButton(arg) { + return childOf(arg, 'PropsUIButton'); +} +export function isPropsUIButtonPrimary(arg) { + return instanceOf(arg, ['__type__', 'label', 'color', 'onClick']) && arg.__type__ === 'PropsUIButtonPrimary'; +} +export function isPropsUIButtonSecundary(arg) { + return instanceOf(arg, ['__type__', 'label', 'color', 'onClick']) && arg.__type__ === 'PropsUIButtonSecundary'; +} +export function isPropsUIButtonForward(arg) { + return instanceOf(arg, ['__type__', 'label', 'onClick']) && arg.__type__ === 'PropsUIButtonForward'; +} +export function isPropsUIButtonLabel(arg) { + return instanceOf(arg, ['__type__', 'label', 'onClick']) && arg.__type__ === 'PropsUIButtonLabel'; +} +export function isPropsUISpinner(arg) { + return instanceOf(arg, ['__type__', 'text']) && arg.__type__ === 'PropsUISpinner'; +} +export function isPropsUIHeader(arg) { + return instanceOf(arg, ['__type__', 'title']) && arg.__type__ === 'PropsUIHeader'; +} +export function isPropsUITable(arg) { + return instanceOf(arg, ['__type__', 'id', 'head', 'body']) && arg.__type__ === 'PropsUITable'; +} +export function isPropsUITableHeader(arg) { + return instanceOf(arg, ['__type__', 'cells']) && arg.__type__ === 'PropsUITableHead'; +} +export function isPropsUITableBody(arg) { + return instanceOf(arg, ['__type__', 'rows']) && arg.__type__ === 'PropsUITableBody'; +} +export function isPropsUITableRow(arg) { + return instanceOf(arg, ['__type__', 'cells']) && arg.__type__ === 'PropsUITableRow'; +} +export function isPropsUITableCell(arg) { + return instanceOf(arg, ['__type__', 'text']) && arg.__type__ === 'PropsUITableCell'; +} +export function isText(arg) { + return typeof arg === 'string' || isTranslatable(arg); +} +export function isTranslatable(arg) { + return instanceOf(arg, ['translations']); +} diff --git a/dist/framework/types/modules.d.ts b/dist/framework/types/modules.d.ts new file mode 100644 index 00000000..7691712f --- /dev/null +++ b/dist/framework/types/modules.d.ts @@ -0,0 +1,17 @@ +import { Script, Command, Response, CommandSystem, CommandUI } from './commands'; +export interface ProcessingEngine { + start: (script: Script) => void; + commandHandler: CommandHandler; + terminate: () => void; +} +export interface VisualisationEngine { + start: (rootElement: HTMLElement, locale: string) => void; + render: (command: CommandUI) => Promise; + terminate: () => void; +} +export interface System { + send: (command: CommandSystem) => void; +} +export interface CommandHandler { + onCommand: (command: Command) => Promise; +} diff --git a/dist/framework/types/modules.js b/dist/framework/types/modules.js new file mode 100644 index 00000000..cb0ff5c3 --- /dev/null +++ b/dist/framework/types/modules.js @@ -0,0 +1 @@ +export {}; diff --git a/dist/framework/types/pages.d.ts b/dist/framework/types/pages.d.ts new file mode 100644 index 00000000..42a0647d --- /dev/null +++ b/dist/framework/types/pages.d.ts @@ -0,0 +1,24 @@ +import { PropsUIHeader, PropsUISpinner } from './elements'; +import { PropsUIPromptFileInput, PropsUIPromptConfirm, PropsUIPromptConsentForm } from './prompts'; +export declare type PropsUIPage = PropsUIPageSplashScreen | PropsUIPageDonation | PropsUIPageStart | PropsUIPageEnd; +export declare function isPropsUIPage(arg: any): arg is PropsUIPage; +export interface PropsUIPageSplashScreen { + __type__: 'PropsUIPageSplashScreen'; +} +export declare function isPropsUIPageSplashScreen(arg: any): arg is PropsUIPageSplashScreen; +export interface PropsUIPageStart { + __type__: 'PropsUIPageStart'; +} +export declare function isPropsUIPageStart(arg: any): arg is PropsUIPageStart; +export interface PropsUIPageDonation { + __type__: 'PropsUIPageDonation'; + header: PropsUIHeader; + body: PropsUIPromptFileInput | PropsUIPromptConfirm | PropsUIPromptConsentForm; + spinner: PropsUISpinner; +} +export declare function isPropsUIPageDonation(arg: any): arg is PropsUIPageDonation; +export interface PropsUIPageEnd { + __type__: 'PropsUIPageEnd'; + header: PropsUIHeader; +} +export declare function isPropsUIPageEnd(arg: any): arg is PropsUIPageEnd; diff --git a/dist/framework/types/pages.js b/dist/framework/types/pages.js new file mode 100644 index 00000000..f1fab0e6 --- /dev/null +++ b/dist/framework/types/pages.js @@ -0,0 +1,16 @@ +import { childOf, instanceOf } from '../helpers'; +export function isPropsUIPage(arg) { + return childOf(arg, 'PropsUIPage'); +} +export function isPropsUIPageSplashScreen(arg) { + return instanceOf(arg, ['__type__']) && arg.__type__ === 'PropsUIPageSplashScreen'; +} +export function isPropsUIPageStart(arg) { + return instanceOf(arg, ['__type__']) && arg.__type__ === 'PropsUIPageStart'; +} +export function isPropsUIPageDonation(arg) { + return instanceOf(arg, ['__type__', 'header', 'body']) && arg.__type__ === 'PropsUIPageDonation'; +} +export function isPropsUIPageEnd(arg) { + return instanceOf(arg, ['__type__', 'header']) && arg.__type__ === 'PropsUIPageEnd'; +} diff --git a/dist/framework/types/prompts.d.ts b/dist/framework/types/prompts.d.ts new file mode 100644 index 00000000..8afadf1d --- /dev/null +++ b/dist/framework/types/prompts.d.ts @@ -0,0 +1,39 @@ +import { Text } from './elements'; +export declare type PropsUIPrompt = PropsUIPromptFileInput | PropsUIPromptRadioInput | PropsUIPromptConsentForm; +export declare function isPropsUIPrompt(arg: any): arg is PropsUIPrompt; +export interface PropsUIPromptConfirm { + __type__: 'PropsUIPromptConfirm'; + text: Text; + ok: Text; + cancel: Text; +} +export declare function isPropsUIPromptConfirm(arg: any): arg is PropsUIPromptConfirm; +export interface PropsUIPromptFileInput { + __type__: 'PropsUIPromptFileInput'; + title: Text; + description: Text; + extensions: string; +} +export declare function isPropsUIPromptFileInput(arg: any): arg is PropsUIPromptFileInput; +export interface PropsUIPromptRadioInput { + __type__: 'PropsUIPromptRadioInput'; + title: Text; + description: Text; + items: string[]; +} +export declare function isPropsUIPromptRadioInput(arg: any): arg is PropsUIPromptRadioInput; +export interface PropsUIPromptConsentForm { + __type__: 'PropsUIPromptConsentForm'; + title: Text; + description: Text; + tables: PropsUIPromptConsentFormTable[]; +} +export declare function isPropsUIPromptConsentForm(arg: any): arg is PropsUIPromptConsentForm; +export interface PropsUIPromptConsentFormTable { + __type__: 'PropsUIPromptConsentFormTable'; + id: string; + title: Text; + description: Text; + data_frame: any; +} +export declare function isPropsUIPromptConsentFormTable(arg: any): arg is PropsUIPromptConsentFormTable; diff --git a/dist/framework/types/prompts.js b/dist/framework/types/prompts.js new file mode 100644 index 00000000..39ca48d7 --- /dev/null +++ b/dist/framework/types/prompts.js @@ -0,0 +1,19 @@ +import { childOf, instanceOf } from '../helpers'; +export function isPropsUIPrompt(arg) { + return childOf(arg, 'PropsUIPrompt'); +} +export function isPropsUIPromptConfirm(arg) { + return instanceOf(arg, ['__type__', 'text', 'ok', 'cancel']) && arg.__type__ === 'PropsUIPromptConfirm'; +} +export function isPropsUIPromptFileInput(arg) { + return instanceOf(arg, ['__type__', 'title', 'description', 'extensions']) && arg.__type__ === 'PropsUIPromptFileInput'; +} +export function isPropsUIPromptRadioInput(arg) { + return instanceOf(arg, ['__type__', 'title', 'description', 'items']) && arg.__type__ === 'PropsUIPromptRadioInput'; +} +export function isPropsUIPromptConsentForm(arg) { + return instanceOf(arg, ['__type__', 'title', 'description', 'tables']) && arg.__type__ === 'PropsUIPromptConsentForm'; +} +export function isPropsUIPromptConsentFormTable(arg) { + return instanceOf(arg, ['__type__', 'id', 'title', 'description', 'data_frame']) && arg.__type__ === 'PropsUIPromptConsentFormTable'; +} diff --git a/dist/framework/visualisation/react/engine.d.ts b/dist/framework/visualisation/react/engine.d.ts index ea128c77..18009fcc 100644 --- a/dist/framework/visualisation/react/engine.d.ts +++ b/dist/framework/visualisation/react/engine.d.ts @@ -1,25 +1,18 @@ /// import * as ReactDOM from 'react-dom/client'; -import VisualisationEngine from '../../abstractions/visualisation_engine'; -import ProcessingEngine from '../../abstractions/processing_engine'; +import { VisualisationEngine } from '../../types/modules'; +import { Response, CommandUIRender } from '../../types/commands'; +import { PropsUIPage } from '../../types/pages'; import VisualisationFactory from './factory'; export default class ReactEngine implements VisualisationEngine { factory: VisualisationFactory; - processingEngine: ProcessingEngine; - onEvent: (event: any) => void; locale: string; - script: string; root: ReactDOM.Root; - finishFlow: (value: unknown) => void; - constructor(factory: VisualisationFactory, processingEngine: ProcessingEngine); - start(script: string, rootElement: HTMLElement, locale: string): Promise; + constructor(factory: VisualisationFactory); + start(rootElement: HTMLElement, locale: string): void; + render(command: CommandUIRender): Promise; + renderSplashScreen(): void; + renderPage(props: PropsUIPage): Promise; terminate(): void; - renderPage(elements: JSX.Element[]): void; - showSpinner(): void; - showStartPage(): void; - showFinalPage(): void; - create(type: string, props?: any): JSX.Element; - handleEvent(event: any): void; - handleRunCycle(scriptEvent: any): void; - renderComponent(data: any): Promise; + renderElements(elements: JSX.Element[]): void; } diff --git a/dist/framework/visualisation/react/engine.js b/dist/framework/visualisation/react/engine.js index 01c999a5..9d049d1d 100644 --- a/dist/framework/visualisation/react/engine.js +++ b/dist/framework/visualisation/react/engine.js @@ -1,14 +1,3 @@ -var __assign = (this && this.__assign) || function () { - __assign = Object.assign || function(t) { - for (var s, i = 1, n = arguments.length; i < n; i++) { - s = arguments[i]; - for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) - t[p] = s[p]; - } - return t; - }; - return __assign.apply(this, arguments); -}; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { @@ -49,117 +38,56 @@ import { jsx as _jsx } from "react/jsx-runtime"; import * as ReactDOM from 'react-dom/client'; import { Main } from './main'; var ReactEngine = /** @class */ (function () { - function ReactEngine(factory, processingEngine) { - var _this = this; + function ReactEngine(factory) { this.factory = factory; - this.processingEngine = processingEngine; - this.onEvent = function (event) { - _this.handleEvent(event); - }; } - ReactEngine.prototype.start = function (script, rootElement, locale) { + ReactEngine.prototype.start = function (rootElement, locale) { + console.log('[ReactEngine] started'); + this.root = ReactDOM.createRoot(rootElement); + this.locale = locale; + this.renderSplashScreen(); + }; + ReactEngine.prototype.render = function (command) { return __awaiter(this, void 0, void 0, function () { var _this = this; return __generator(this, function (_a) { switch (_a.label) { - case 0: - console.log('[ReactEngine] started'); - this.script = script; - this.root = ReactDOM.createRoot(rootElement); - this.locale = locale; - this.showStartPage(); - this.processingEngine.start(); - return [4 /*yield*/, new Promise(function (resolve) { - _this.finishFlow = resolve; - })]; + case 0: return [4 /*yield*/, new Promise(function (resolve) { + _this.renderPage(command.page).then(function (payload) { + resolve({ command: command, payload: payload }); + }, function () { }); + })]; case 1: return [2 /*return*/, _a.sent()]; } }); }); }; - ReactEngine.prototype.terminate = function () { - this.processingEngine.terminate(); - }; - ReactEngine.prototype.renderPage = function (elements) { - this.root.render(_jsx(Main, { elements: elements })); - }; - ReactEngine.prototype.showSpinner = function () { - var spinner = this.create('Spinner'); - this.renderPage([spinner]); - }; - ReactEngine.prototype.showStartPage = function () { - var welcome = this.create('Title0', { text: 'Welcome' }); - var spinner = this.create('Spinner'); - this.renderPage([welcome, spinner]); - }; - ReactEngine.prototype.showFinalPage = function () { - var thanks = this.create('Title0', { text: 'Thank you' }); - this.renderPage([thanks]); - }; - ReactEngine.prototype.create = function (type, props) { - if (props === void 0) { props = {}; } - return this.factory.createComponent(__assign({ __type__: type }, props), this.locale, function () { }); + ReactEngine.prototype.renderSplashScreen = function () { + var context = { locale: this.locale, resolve: function () { } }; + var page = this.factory.createPage({ __type__: 'PropsUIPageSplashScreen' }, context); + this.renderElements([page]); }; - ReactEngine.prototype.handleEvent = function (event) { - var eventType = event.data.eventType; - console.log('[ReactEngine] received eventType: ', eventType); - switch (eventType) { - case 'initialiseDone': - console.log('[ReactEngine] received: initialiseDone'); - this.processingEngine.loadScript(this.script); - break; - case 'loadScriptDone': - console.log('[ReactEngine] Received: loadScriptDone'); - this.processingEngine.firstRunCycle(); - break; - case 'runCycleDone': - console.log('[ReactEngine] received: event', event.data.scriptEvent); - this.handleRunCycle(event.data.scriptEvent); - break; - default: - console.log('[ReactEngine] received unsupported flow event: ', eventType); - } - }; - ReactEngine.prototype.handleRunCycle = function (scriptEvent) { - var _this = this; - var type = scriptEvent.__type__; - if (type.startsWith('Event.EndOfFlow')) { - this.renderComponent(scriptEvent).then(function (result) { - var _a; - _this.showFinalPage(); - (_a = _this.finishFlow) === null || _a === void 0 ? void 0 : _a.call(_this, result); - }, null); - return; - } - if (type.startsWith('Event.Command.Prompt')) { - this.renderComponent(scriptEvent).then(function (userInput) { - _this.showSpinner(); - _this.processingEngine.nextRunCycle({ - prompt: scriptEvent, - userInput: userInput - }); - }, null); - return; - } - console.log('[ReactEngine] Received unsupported script event: ', type); - }; - ReactEngine.prototype.renderComponent = function (data) { + ReactEngine.prototype.renderPage = function (props) { return __awaiter(this, void 0, void 0, function () { - var locale; var _this = this; return __generator(this, function (_a) { switch (_a.label) { case 0: - locale = this.locale; + console.log('[ReactEngine] render page: ' + JSON.stringify(props)); return [4 /*yield*/, new Promise(function (resolve) { - var component = _this.factory.createComponent(data, locale, resolve); - _this.renderPage([component]); + var context = { locale: _this.locale, resolve: resolve }; + var page = _this.factory.createPage(props, context); + _this.renderElements([page]); })]; case 1: return [2 /*return*/, _a.sent()]; } }); }); }; + ReactEngine.prototype.terminate = function () { }; + ReactEngine.prototype.renderElements = function (elements) { + this.root.render(_jsx(Main, { elements: elements })); + }; return ReactEngine; }()); export default ReactEngine; diff --git a/dist/framework/visualisation/react/factory.d.ts b/dist/framework/visualisation/react/factory.d.ts index e95b6e46..43f93412 100644 --- a/dist/framework/visualisation/react/factory.d.ts +++ b/dist/framework/visualisation/react/factory.d.ts @@ -1,9 +1,10 @@ /// +import { PropsUIPage } from '../../types/pages'; +import { Payload } from '../../types/commands'; +export interface ReactFactoryContext { + locale: string; + resolve?: (payload: Payload) => void; +} export default class ReactFactory { - mapping: { - [name: string]: (props: any) => JSX.Element; - }; - constructor(); - add(factory: (props: any) => JSX.Element, name: string): void; - createComponent(data: any, locale: string, resolve: (value: any) => void): JSX.Element; + createPage(page: PropsUIPage, context: ReactFactoryContext): JSX.Element; } diff --git a/dist/framework/visualisation/react/factory.js b/dist/framework/visualisation/react/factory.js index 0944e568..2a45a3a6 100644 --- a/dist/framework/visualisation/react/factory.js +++ b/dist/framework/visualisation/react/factory.js @@ -9,37 +9,29 @@ var __assign = (this && this.__assign) || function () { }; return __assign.apply(this, arguments); }; -import { TableFactory } from './components/table'; -import { SpinnerFactory } from './components/spinner'; -import { FileInputFactory } from './components/file_input'; -import { RadioInputFactory } from './components/radio_input'; -import { EndOfFlowFactory } from './components/end_of_flow'; -import { Title0Factory, Title1Factory, Title2Factory } from './components/text'; +import { jsx as _jsx } from "react/jsx-runtime"; +import { EndPage } from './ui/pages/end_page'; +import { StartPage } from './ui/pages/start_page'; +import { isPropsUIPageEnd, isPropsUIPageDonation, isPropsUIPageStart, isPropsUIPageSplashScreen } from '../../types/pages'; +import { DonationPage } from './ui/pages/donation_page'; +import { SplashScreen } from './ui/pages/splash_screen'; var ReactFactory = /** @class */ (function () { function ReactFactory() { - this.mapping = {}; - this.mapping.Table = TableFactory; - this.mapping.Spinner = SpinnerFactory; - this.mapping.FileInput = FileInputFactory; - this.mapping.RadioInput = RadioInputFactory; - this.mapping.EndOfFlow = EndOfFlowFactory; - this.mapping.Title0 = Title0Factory; - this.mapping.Title1 = Title1Factory; - this.mapping.Title2 = Title2Factory; } - ReactFactory.prototype.add = function (factory, name) { - this.mapping[name] = factory; - }; - ReactFactory.prototype.createComponent = function (data, locale, resolve) { - var type = data.__type__.split('.').pop(); - var props = __assign(__assign({}, data), { locale: locale, resolve: resolve }); - if (this.mapping[type] !== null) { - var factoryMethod = this.mapping[type]; - return factoryMethod(props); + ReactFactory.prototype.createPage = function (page, context) { + if (isPropsUIPageSplashScreen(page)) { + return _jsx(SplashScreen, __assign({}, page, context)); + } + if (isPropsUIPageStart(page)) { + return _jsx(StartPage, __assign({}, page, context)); + } + if (isPropsUIPageEnd(page)) { + return _jsx(EndPage, __assign({}, page, context)); } - else { - throw new Error("[ReactFactory] Received unsupported prompt: ".concat(type)); + if (isPropsUIPageDonation(page)) { + return _jsx(DonationPage, __assign({}, page, context)); } + throw TypeError('Unknown page: ' + JSON.stringify(page)); }; return ReactFactory; }()); diff --git a/dist/framework/visualisation/react/ui/elements/button.d.ts b/dist/framework/visualisation/react/ui/elements/button.d.ts new file mode 100644 index 00000000..b752bd94 --- /dev/null +++ b/dist/framework/visualisation/react/ui/elements/button.d.ts @@ -0,0 +1,7 @@ +/// +import { Weak } from '../../../../helpers'; +import { PropsUIButtonForward, PropsUIButtonLabel, PropsUIButtonPrimary, PropsUIButtonSecundary } from '../../../../types/elements'; +export declare const PrimaryButton: ({ label, color, onClick }: Weak) => JSX.Element; +export declare const SecondaryButton: ({ label, color, onClick }: Weak) => JSX.Element; +export declare const ForwardButton: ({ label, onClick }: Weak) => JSX.Element; +export declare const LabelButton: ({ label, onClick }: Weak) => JSX.Element; diff --git a/dist/framework/visualisation/react/ui/elements/button.js b/dist/framework/visualisation/react/ui/elements/button.js new file mode 100644 index 00000000..4371cca4 --- /dev/null +++ b/dist/framework/visualisation/react/ui/elements/button.js @@ -0,0 +1,29 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import ForwardSvg from '../../../../../assets/images/forward.svg'; +export var PrimaryButton = function (_a) { + var label = _a.label, _b = _a.color, color = _b === void 0 ? 'bg-primary text-white' : _b, onClick = _a.onClick; + return (_jsx("div", __assign({ className: "pt-15px pb-15px active:shadow-top4px active:pt-4 active:pb-14px leading-none font-button text-button rounded pr-4 pl-4 cursor-pointer ".concat(color), onClick: onClick }, { children: _jsx("div", __assign({ id: 'confirm-button', className: 'flex-wrap' }, { children: label })) }))); +}; +export var SecondaryButton = function (_a) { + var label = _a.label, _b = _a.color, color = _b === void 0 ? 'bg-delete text-delete' : _b, onClick = _a.onClick; + return (_jsx("div", __assign({ className: "pt-13px pb-13px active:pt-14px active:pb-3 active:shadow-top2px border-2 font-button text-button rounded bg-opacity-0 pr-4 pl-4 ".concat(color), onClick: onClick }, { children: _jsx("div", __assign({ className: 'flex-wrap' }, { children: label })) }))); +}; +export var ForwardButton = function (_a) { + var label = _a.label, onClick = _a.onClick; + return (_jsx("div", __assign({ className: 'pt-1 pb-1 active:pt-5px active:pb-3px rounded bg-opacity-0 focus:outline-none', onClick: onClick }, { children: _jsxs("div", __assign({ className: 'flex items-center' }, { children: [_jsx("div", __assign({ className: 'focus:outline-none' }, { children: _jsx("div", __assign({ className: 'flex flex-col justify-center h-full items-center' }, { children: _jsx("div", __assign({ className: 'flex-wrap text-button font-button text-grey1' }, { children: label })) })) })), _jsx("div", { children: _jsx("img", { className: 'ml-4 -mt-2px', src: ForwardSvg, alt: label }) })] })) }))); +}; +export var LabelButton = function (_a) { + var label = _a.label, onClick = _a.onClick; + return (_jsx("div", __assign({ className: 'pt-15px pb-15px active:shadow-top4px active:pt-4 active:pb-14px leading-none font-button text-button rounded pr-4 pl-4 cursor-pointer bg-opacity-0', onClick: onClick }, { children: _jsx("div", __assign({ id: 'confirm-button', className: 'flex-wrap' }, { children: label })) }))); +}; diff --git a/dist/framework/visualisation/react/ui/elements/header.d.ts b/dist/framework/visualisation/react/ui/elements/header.d.ts new file mode 100644 index 00000000..a99d97e1 --- /dev/null +++ b/dist/framework/visualisation/react/ui/elements/header.d.ts @@ -0,0 +1,7 @@ +/// +import { Weak } from '../../../../helpers'; +import { PropsUIHeader } from '../../../../types/elements'; +import { ReactFactoryContext } from '../../factory'; +declare type Props = Weak & ReactFactoryContext; +export declare const Header: (props: Props) => JSX.Element; +export {}; diff --git a/dist/framework/visualisation/react/ui/elements/header.js b/dist/framework/visualisation/react/ui/elements/header.js new file mode 100644 index 00000000..0252b769 --- /dev/null +++ b/dist/framework/visualisation/react/ui/elements/header.js @@ -0,0 +1,13 @@ +import { jsx as _jsx } from "react/jsx-runtime"; +import { Translator } from '../../../../translator'; +import { Title0 } from './text'; +function prepareCopy(_a) { + var title = _a.title, locale = _a.locale; + return { + title: Translator.translate(title, locale) + }; +} +export var Header = function (props) { + var title = prepareCopy(props).title; + return (_jsx(Title0, { text: title })); +}; diff --git a/dist/framework/visualisation/react/ui/elements/spinner.d.ts b/dist/framework/visualisation/react/ui/elements/spinner.d.ts new file mode 100644 index 00000000..79402e33 --- /dev/null +++ b/dist/framework/visualisation/react/ui/elements/spinner.d.ts @@ -0,0 +1,7 @@ +/// +import { Weak } from '../../../../helpers'; +import { PropsUISpinner } from '../../../../types/elements'; +import { ReactFactoryContext } from '../../factory'; +declare type Props = Weak & ReactFactoryContext; +export declare const Spinner: (props: Props) => JSX.Element; +export {}; diff --git a/dist/framework/visualisation/react/ui/elements/spinner.js b/dist/framework/visualisation/react/ui/elements/spinner.js new file mode 100644 index 00000000..93d7e93f --- /dev/null +++ b/dist/framework/visualisation/react/ui/elements/spinner.js @@ -0,0 +1,24 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +import SpinnerSvg from '../../../../../assets/images/spinner.svg'; +import { Translator } from '../../../../translator'; +function prepareCopy(_a) { + var text = _a.text, locale = _a.locale; + return { + text: Translator.translate(text, locale) + }; +} +export var Spinner = function (props) { + var text = prepareCopy(props).text; + return (_jsxs("div", __assign({ id: 'spinner', className: 'flex flex-row items-center gap-4' }, { children: [_jsx("div", __assign({ className: 'font-body text-bodymedium text-grey1' }, { children: text })), _jsx("div", __assign({ className: 'w-10 h-10' }, { children: _jsx("img", { src: SpinnerSvg }) }))] }))); +}; diff --git a/dist/framework/visualisation/react/ui/elements/table.d.ts b/dist/framework/visualisation/react/ui/elements/table.d.ts new file mode 100644 index 00000000..852f692e --- /dev/null +++ b/dist/framework/visualisation/react/ui/elements/table.d.ts @@ -0,0 +1,4 @@ +/// +import { Weak } from '../../../../helpers'; +import { PropsUITable } from '../../../../types/elements'; +export declare const Table: ({ id, head, body }: Weak) => JSX.Element; diff --git a/dist/framework/visualisation/react/ui/elements/table.js b/dist/framework/visualisation/react/ui/elements/table.js new file mode 100644 index 00000000..96f89882 --- /dev/null +++ b/dist/framework/visualisation/react/ui/elements/table.js @@ -0,0 +1,31 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; +export var Table = function (_a) { + var id = _a.id, head = _a.head, body = _a.body; + function renderHead(props) { + return _jsx("tr", { children: props.cells.map(function (cell, index) { return renderHeadCell(cell, index); }) }); + } + function renderHeadCell(props, index) { + return _jsx("th", __assign({ className: 'px-2 pb-3 font-button text-button text-left' }, { children: props.text }), "".concat(index)); + } + function renderRows(props) { + return props.rows.map(function (row, index) { return renderRow(row, index); }); + } + function renderRow(row, rowIndex) { + return _jsx("tr", { children: row.cells.map(function (cell, cellIndex) { return renderRowCell(cell, cellIndex); }) }, "".concat(rowIndex)); + } + function renderRowCell(props, cellIndex) { + return _jsx("td", __assign({ className: 'px-2 font-body text-body' }, { children: props.text }), "".concat(cellIndex)); + } + return (_jsxs("table", __assign({ className: 'text-grey1 table-auto' }, { children: [_jsx("thead", { children: renderHead(head) }), _jsx("tbody", { children: renderRows(body) })] }))); +}; diff --git a/dist/framework/visualisation/react/ui/elements/text.d.ts b/dist/framework/visualisation/react/ui/elements/text.d.ts new file mode 100644 index 00000000..83e4cbb7 --- /dev/null +++ b/dist/framework/visualisation/react/ui/elements/text.d.ts @@ -0,0 +1,7 @@ +/// +import { Weak } from '../../../../helpers'; +import { PropsUITextBody, PropsUITextTitle0, PropsUITextTitle1, PropsUITextTitle2 } from '../../../../types/elements'; +export declare const BodyLarge: ({ text, color, margin }: Weak) => JSX.Element; +export declare const Title0: ({ text, color, margin }: Weak) => JSX.Element; +export declare const Title1: ({ text, color, margin }: Weak) => JSX.Element; +export declare const Title2: ({ text, color, margin }: Weak) => JSX.Element; diff --git a/dist/framework/visualisation/react/ui/elements/text.js b/dist/framework/visualisation/react/ui/elements/text.js new file mode 100644 index 00000000..10f65f98 --- /dev/null +++ b/dist/framework/visualisation/react/ui/elements/text.js @@ -0,0 +1,28 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +export var BodyLarge = function (_a) { + var text = _a.text, _b = _a.color, color = _b === void 0 ? 'text-grey1' : _b, _c = _a.margin, margin = _c === void 0 ? 'mb-6 md:mb-8 lg:mb-10' : _c; + return (_jsx("div", __assign({ className: "text-bodylarge font-body ".concat(color, " ").concat(margin) }, { children: text }))); +}; +export var Title0 = function (_a) { + var text = _a.text, _b = _a.color, color = _b === void 0 ? 'text-grey1' : _b, _c = _a.margin, margin = _c === void 0 ? 'mb-6 md:mb-8 lg:mb-10' : _c; + return (_jsx("div", __assign({ className: "text-title4 font-title4 sm:text-title2 sm:font-title2 lg:text-title0 lg:font-title0 ".concat(color, " ").concat(margin) }, { children: text }))); +}; +export var Title1 = function (_a) { + var text = _a.text, _b = _a.color, color = _b === void 0 ? 'text-grey1' : _b, _c = _a.margin, margin = _c === void 0 ? 'mb-6 md:mb-8 lg:mb-10' : _c; + return (_jsx("div", __assign({ className: "text-title3 font-title3 sm:text-title2 lg:text-title1 lg:font-title1 ".concat(color, " ").concat(margin) }, { children: text }))); +}; +export var Title2 = function (_a) { + var text = _a.text, _b = _a.color, color = _b === void 0 ? 'text-grey1' : _b, _c = _a.margin, margin = _c === void 0 ? 'mb-6 md:mb-8 lg:mb-10' : _c; + return (_jsx("div", __assign({ className: "text-title4 font-title4 sm:text-title3 sm:font-title3 lg:text-title2 lg:font-title2 ".concat(color, " ").concat(margin) }, { children: text }))); +}; diff --git a/dist/framework/visualisation/react/ui/pages/donation_page.d.ts b/dist/framework/visualisation/react/ui/pages/donation_page.d.ts new file mode 100644 index 00000000..6213bcf2 --- /dev/null +++ b/dist/framework/visualisation/react/ui/pages/donation_page.d.ts @@ -0,0 +1,7 @@ +/// +import { Weak } from '../../../../helpers'; +import { PropsUIPageDonation } from '../../../../types/pages'; +import { ReactFactoryContext } from '../../factory'; +declare type Props = Weak & ReactFactoryContext; +export declare const DonationPage: (props: Props) => JSX.Element; +export {}; diff --git a/dist/framework/visualisation/react/ui/pages/donation_page.js b/dist/framework/visualisation/react/ui/pages/donation_page.js new file mode 100644 index 00000000..c4cc7af3 --- /dev/null +++ b/dist/framework/visualisation/react/ui/pages/donation_page.js @@ -0,0 +1,45 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import React from 'react'; +import { Translator } from '../../../../translator'; +import { isPropsUIPromptConfirm, isPropsUIPromptConsentForm, isPropsUIPromptFileInput } from '../../../../types/prompts'; +import { Spinner } from '../elements/spinner'; +import { Title0 } from '../elements/text'; +import { Confirm } from '../prompts/confirm'; +import { ConsentForm } from '../prompts/consent_form'; +import { FileInput } from '../prompts/file_input'; +function prepareCopy(_a) { + var title = _a.header.title, locale = _a.locale; + return { + title: Translator.translate(title, locale) + }; +} +export var DonationPage = function (props) { + var spinnerHidden = React.useState(true)[0]; + var title = prepareCopy(props).title; + function renderBody(props) { + var context = { locale: props.locale, resolve: props.resolve }; + var body = props.body; + if (isPropsUIPromptFileInput(body)) { + return _jsx(FileInput, __assign({}, body, context)); + } + if (isPropsUIPromptConfirm(body)) { + return _jsx(Confirm, __assign({}, body, context)); + } + if (isPropsUIPromptConsentForm(body)) { + return _jsx(ConsentForm, __assign({}, body, context)); + } + throw new TypeError('Unknown body type'); + } + return (_jsxs(_Fragment, { children: [_jsx(Title0, { text: title }), renderBody(props), _jsx("div", __assign({ className: spinnerHidden ? 'hidden' : '' }, { children: _jsx(Spinner, __assign({}, props.spinner, { locale: props.locale })) }))] })); +}; diff --git a/dist/framework/visualisation/react/ui/pages/end_page.d.ts b/dist/framework/visualisation/react/ui/pages/end_page.d.ts new file mode 100644 index 00000000..7e0b4d75 --- /dev/null +++ b/dist/framework/visualisation/react/ui/pages/end_page.d.ts @@ -0,0 +1,7 @@ +/// +import { Weak } from '../../../../helpers'; +import { PropsUIPageEnd } from '../../../../types/pages'; +import { ReactFactoryContext } from '../../factory'; +declare type Props = Weak & ReactFactoryContext; +export declare const EndPage: (props: Props) => JSX.Element; +export {}; diff --git a/dist/framework/visualisation/react/ui/pages/end_page.js b/dist/framework/visualisation/react/ui/pages/end_page.js new file mode 100644 index 00000000..b5aba48a --- /dev/null +++ b/dist/framework/visualisation/react/ui/pages/end_page.js @@ -0,0 +1,16 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx } from "react/jsx-runtime"; +import { Header } from '../elements/header'; +export var EndPage = function (props) { + return (_jsx(Header, __assign({}, props.header, { locale: props.locale }))); +}; diff --git a/dist/framework/visualisation/react/ui/pages/splash_screen.d.ts b/dist/framework/visualisation/react/ui/pages/splash_screen.d.ts new file mode 100644 index 00000000..2f9206a7 --- /dev/null +++ b/dist/framework/visualisation/react/ui/pages/splash_screen.d.ts @@ -0,0 +1,7 @@ +/// +import { Weak } from '../../../../helpers'; +import { PropsUIPageSplashScreen } from '../../../../types/pages'; +import { ReactFactoryContext } from '../../factory'; +declare type Props = Weak & ReactFactoryContext; +export declare const SplashScreen: (props: Props) => JSX.Element; +export {}; diff --git a/dist/framework/visualisation/react/ui/pages/splash_screen.js b/dist/framework/visualisation/react/ui/pages/splash_screen.js new file mode 100644 index 00000000..474ee7fa --- /dev/null +++ b/dist/framework/visualisation/react/ui/pages/splash_screen.js @@ -0,0 +1,36 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import TextBundle from '../../../../text_bundle'; +import { Translator } from '../../../../translator'; +import { Spinner } from '../elements/spinner'; +import { BodyLarge, Title0 } from '../elements/text'; +function prepareCopy(_a) { + var locale = _a.locale; + return { + title: Translator.translate(title, locale), + description: Translator.translate(description, locale) + }; +} +export var SplashScreen = function (props) { + var _a = prepareCopy(props), title = _a.title, description = _a.description; + return (_jsxs(_Fragment, { children: [_jsx(Title0, { text: title }), _jsx(BodyLarge, { text: description }), _jsx(Spinner, __assign({ text: spinnerText }, props))] })); +}; +var title = new TextBundle() + .add('en', 'Welcome') + .add('nl', 'Welkom'); +var description = new TextBundle() + .add('en', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.') + .add('nl', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'); +var spinnerText = new TextBundle() + .add('en', 'One moment please..') + .add('nl', 'Een moment geduld..'); diff --git a/dist/framework/visualisation/react/ui/pages/start_page.d.ts b/dist/framework/visualisation/react/ui/pages/start_page.d.ts new file mode 100644 index 00000000..7406f007 --- /dev/null +++ b/dist/framework/visualisation/react/ui/pages/start_page.d.ts @@ -0,0 +1,7 @@ +/// +import { Weak } from '../../../../helpers'; +import { PropsUIPageStart } from '../../../../types/pages'; +import { ReactFactoryContext } from '../../factory'; +declare type Props = Weak & ReactFactoryContext; +export declare const StartPage: (props: Props) => JSX.Element; +export {}; diff --git a/dist/framework/visualisation/react/ui/pages/start_page.js b/dist/framework/visualisation/react/ui/pages/start_page.js new file mode 100644 index 00000000..042cb85f --- /dev/null +++ b/dist/framework/visualisation/react/ui/pages/start_page.js @@ -0,0 +1,41 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; +import TextBundle from '../../../../text_bundle'; +import { Translator } from '../../../../translator'; +import { PrimaryButton } from '../elements/button'; +import { BodyLarge, Title0 } from '../elements/text'; +export var StartPage = function (props) { + var resolve = props.resolve; + var _a = prepareCopy(props), title = _a.title, description = _a.description, startButton = _a.startButton; + function handleStart() { + resolve === null || resolve === void 0 ? void 0 : resolve({ __type__: 'PayloadVoid', value: undefined }); + } + return (_jsxs(_Fragment, { children: [_jsx(Title0, { text: title }), _jsx(BodyLarge, { text: description }), _jsx("div", __assign({ className: 'flex flex-row gap-4' }, { children: _jsx(PrimaryButton, { label: startButton, onClick: handleStart }) }))] })); +}; +function prepareCopy(_a) { + var locale = _a.locale; + return { + title: Translator.translate(title, locale), + description: Translator.translate(description, locale), + startButton: Translator.translate(startButtonLabel, locale) + }; +} +var title = new TextBundle() + .add('en', 'Instructions') + .add('nl', 'Instructies'); +var startButtonLabel = new TextBundle() + .add('en', 'Start') + .add('nl', 'Start'); +var description = new TextBundle() + .add('en', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.') + .add('nl', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'); diff --git a/dist/framework/visualisation/react/ui/prompts/confirm.d.ts b/dist/framework/visualisation/react/ui/prompts/confirm.d.ts new file mode 100644 index 00000000..6dc7ad12 --- /dev/null +++ b/dist/framework/visualisation/react/ui/prompts/confirm.d.ts @@ -0,0 +1,7 @@ +/// +import { Weak } from '../../../../helpers'; +import { ReactFactoryContext } from '../../factory'; +import { PropsUIPromptConfirm } from '../../../../types/prompts'; +declare type Props = Weak & ReactFactoryContext; +export declare const Confirm: (props: Props) => JSX.Element; +export {}; diff --git a/dist/framework/visualisation/react/ui/prompts/confirm.js b/dist/framework/visualisation/react/ui/prompts/confirm.js new file mode 100644 index 00000000..32ecad99 --- /dev/null +++ b/dist/framework/visualisation/react/ui/prompts/confirm.js @@ -0,0 +1,34 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { Translator } from '../../../../translator'; +import { BodyLarge } from '../elements/text'; +import { LabelButton, PrimaryButton } from '../elements/button'; +export var Confirm = function (props) { + var resolve = props.resolve; + var _a = prepareCopy(props), text = _a.text, ok = _a.ok, cancel = _a.cancel; + function handleOk() { + resolve === null || resolve === void 0 ? void 0 : resolve({ __type__: 'PayloadTrue', value: true }); + } + function handleCancel() { + resolve === null || resolve === void 0 ? void 0 : resolve({ __type__: 'PayloadFalse', value: false }); + } + return (_jsxs(_Fragment, { children: [_jsx(BodyLarge, { text: text, margin: 'mb-4' }), _jsxs("div", __assign({ className: 'flex flex-row gap-4' }, { children: [_jsx(PrimaryButton, { label: ok, onClick: handleOk }), _jsx(LabelButton, { label: cancel, onClick: handleCancel })] }))] })); +}; +function prepareCopy(_a) { + var text = _a.text, ok = _a.ok, cancel = _a.cancel, locale = _a.locale; + return { + text: Translator.translate(text, locale), + ok: Translator.translate(ok, locale), + cancel: Translator.translate(cancel, locale) + }; +} diff --git a/dist/framework/visualisation/react/ui/prompts/consent_form.d.ts b/dist/framework/visualisation/react/ui/prompts/consent_form.d.ts new file mode 100644 index 00000000..85f55b3c --- /dev/null +++ b/dist/framework/visualisation/react/ui/prompts/consent_form.d.ts @@ -0,0 +1,7 @@ +/// +import { Weak } from '../../../../helpers'; +import { PropsUIPromptConsentForm } from '../../../../types/prompts'; +import { ReactFactoryContext } from '../../factory'; +declare type Props = Weak & ReactFactoryContext; +export declare const ConsentForm: (props: Props) => JSX.Element; +export {}; diff --git a/dist/framework/visualisation/react/ui/prompts/consent_form.js b/dist/framework/visualisation/react/ui/prompts/consent_form.js new file mode 100644 index 00000000..4edd8f78 --- /dev/null +++ b/dist/framework/visualisation/react/ui/prompts/consent_form.js @@ -0,0 +1,92 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import { Table } from '../elements/table'; +import { PrimaryButton, SecondaryButton } from '../elements/button'; +import { BodyLarge, Title1, Title2 } from '../elements/text'; +import TextBundle from '../../../../text_bundle'; +import { Translator } from '../../../../translator'; +export var ConsentForm = function (props) { + var tables = props.tables, resolve = props.resolve; + var _a = prepareCopy(props), title = _a.title, description = _a.description, donateButton = _a.donateButton, declineButton = _a.declineButton; + function handleDonate() { + resolve === null || resolve === void 0 ? void 0 : resolve({ __type__: 'PayloadString', value: JSON.stringify(tables) }); + } + function handleDecline() { + resolve === null || resolve === void 0 ? void 0 : resolve({ __type__: 'PayloadFalse', value: false }); + } + function rowCell(dataFrame, column, row) { + var text = dataFrame[column]["".concat(row)]; + return { __type__: 'PropsUITableCell', text: text }; + } + function headCell(dataFrame, column) { + return { __type__: 'PropsUITableCell', text: column }; + } + function columnNames(dataFrame) { + return Object.keys(dataFrame); + } + function columnCount(dataFrame) { + return columnNames(dataFrame).length; + } + function rowCount(dataFrame) { + if (columnCount(dataFrame) === 0) { + return 0; + } + else { + var firstColumn = dataFrame[columnNames(dataFrame)[0]]; + return Object.keys(firstColumn).length; + } + } + function rows(data) { + var result = []; + var _loop_1 = function (row) { + var cells = columnNames(data).map(function (column) { return rowCell(data, column, row); }); + result.push({ __type__: 'PropsUITableRow', cells: cells }); + }; + for (var row = 0; row <= rowCount(data); row++) { + _loop_1(row); + } + return result; + } + function parse(tableData) { + var id = tableData.id; + var dataFrame = JSON.parse(tableData.data_frame); + var head = { cells: columnNames(dataFrame).map(function (column) { return headCell(dataFrame, column); }) }; + var body = { rows: rows(dataFrame) }; + return { __type__: 'PropsUITable', id: id, head: head, body: body }; + } + function renderTable(tableData) { + var title = tableData.title; + var tableProps = parse(tableData); + return (_jsxs("div", __assign({ className: 'flex flex-col gap-2' }, { children: [_jsx(Title2, { text: title }), _jsx(Table, __assign({}, tableProps))] }), tableProps.id)); + } + return (_jsxs(_Fragment, { children: [_jsx(Title1, { text: title }), _jsx(BodyLarge, { text: description }), _jsxs("div", __assign({ className: 'flex flex-col gap-8' }, { children: [tables.map(function (table) { return renderTable(table); }), _jsxs("div", __assign({ className: 'flex flex-row gap-4 mt-2' }, { children: [_jsx(PrimaryButton, { label: donateButton, onClick: handleDonate }), _jsx(SecondaryButton, { label: declineButton, onClick: handleDecline })] }))] }))] })); +}; +function prepareCopy(_a) { + var title = _a.title, description = _a.description, locale = _a.locale; + return { + title: Translator.translate(title, locale), + description: Translator.translate(description, locale), + donateButton: Translator.translate(donateButtonLabel(), locale), + declineButton: Translator.translate(declineButtonLabel(), locale) + }; +} +var donateButtonLabel = function () { + return new TextBundle() + .add('en', 'Yes, donate') + .add('nl', 'Ja, doneer'); +}; +var declineButtonLabel = function () { + return new TextBundle() + .add('en', 'No') + .add('nl', 'Nee'); +}; diff --git a/dist/framework/visualisation/react/ui/prompts/file_input.d.ts b/dist/framework/visualisation/react/ui/prompts/file_input.d.ts new file mode 100644 index 00000000..bb60eedf --- /dev/null +++ b/dist/framework/visualisation/react/ui/prompts/file_input.d.ts @@ -0,0 +1,7 @@ +/// +import { Weak } from '../../../../helpers'; +import { ReactFactoryContext } from '../../factory'; +import { PropsUIPromptFileInput } from '../../../../types/prompts'; +declare type Props = Weak & ReactFactoryContext; +export declare const FileInput: (props: Props) => JSX.Element; +export {}; diff --git a/dist/framework/visualisation/react/ui/prompts/file_input.js b/dist/framework/visualisation/react/ui/prompts/file_input.js new file mode 100644 index 00000000..a9abc371 --- /dev/null +++ b/dist/framework/visualisation/react/ui/prompts/file_input.js @@ -0,0 +1,72 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import * as React from 'react'; +import TextBundle from '../../../../text_bundle'; +import { Translator } from '../../../../translator'; +import { ForwardButton, PrimaryButton } from '../elements/button'; +export var FileInput = function (props) { + var _a = React.useState(), selectedFile = _a[0], setSelectedFile = _a[1]; + var _b = React.useState(true), confirmHidden = _b[0], setConfirmHidden = _b[1]; + var input = React.useRef(null); + var resolve = props.resolve; + var _c = prepareCopy(props), title = _c.title, description = _c.description, extensions = _c.extensions, selectButton = _c.selectButton, continueButton = _c.continueButton, forwardButton = _c.forwardButton; + function handleClick() { + var _a; + (_a = input.current) === null || _a === void 0 ? void 0 : _a.click(); + } + function handleSkip() { + resolve === null || resolve === void 0 ? void 0 : resolve({ __type__: 'PayloadFalse', value: false }); + } + function handleSelect(event) { + var files = event.target.files; + if (files != null && files.length > 0) { + setSelectedFile(files[0]); + setConfirmHidden(false); + } + else { + console.log('Error selecting file: ' + JSON.stringify(files)); + } + } + function handleConfirm() { + if (selectedFile !== undefined) { + resolve === null || resolve === void 0 ? void 0 : resolve({ __type__: 'PayloadFile', value: selectedFile }); + } + } + return (_jsxs(_Fragment, { children: [_jsx("div", __assign({ className: 'text-title5 font-title5 sm:text-title4 sm:font-title4 lg:text-title3 lg:font-title3 text-grey1' }, { children: title })), _jsx("div", { className: 'mt-8' }), _jsxs("div", __assign({ id: 'select-panel' }, { children: [_jsx("div", __assign({ className: 'flex-wrap text-bodylarge font-body text-grey1 text-left' }, { children: description })), _jsx("div", { className: 'mt-4' }), _jsxs("div", __assign({ className: 'flex flex-row items-center gap-4' }, { children: [_jsx("div", __assign({ className: 'flex-wrap cursor-pointer' }, { children: _jsx("div", __assign({ id: 'select-button', className: 'pt-15px pb-15px active:shadow-top4px active:pt-4 active:pb-14px leading-none font-button text-button rounded pr-4 pl-4 bg-primary text-white', onClick: handleClick }, { children: selectButton })) })), _jsx("div", __assign({ className: 'flex-wrap' }, { children: _jsx("div", __assign({ id: 'selected-filename', className: 'flex-wrap text-subhead font-subhead text-grey1' }, { children: selectedFile === null || selectedFile === void 0 ? void 0 : selectedFile.name })) }))] })), _jsx("input", { ref: input, id: 'input', type: 'file', className: 'hidden', accept: extensions, onChange: handleSelect })] })), _jsx("div", { className: 'mt-10' }), _jsxs("div", __assign({ className: 'flex flex-row gap-4 items-center' }, { children: [_jsx("div", __assign({ className: confirmHidden ? 'hidden' : '' }, { children: _jsx(PrimaryButton, { label: continueButton, onClick: handleConfirm, color: 'bg-tertiary text-grey1' }) })), _jsx(ForwardButton, { label: forwardButton, onClick: handleSkip })] }))] })); +}; +function prepareCopy(_a) { + var title = _a.title, description = _a.description, extensions = _a.extensions, locale = _a.locale; + return { + title: Translator.translate(title, locale), + description: Translator.translate(description, locale), + extensions: extensions, + selectButton: Translator.translate(selectButtonLabel(), locale), + continueButton: Translator.translate(continueButtonLabel(), locale), + forwardButton: Translator.translate(forwardButtonLabel(), locale) + }; +} +var continueButtonLabel = function () { + return new TextBundle() + .add('en', 'Continue') + .add('nl', 'Doorgaan'); +}; +var selectButtonLabel = function () { + return new TextBundle() + .add('en', 'Choose file') + .add('nl', 'Kies bestand'); +}; +var forwardButtonLabel = function () { + return new TextBundle() + .add('en', 'Skip this step') + .add('nl', 'Sla deze stap over'); +}; diff --git a/dist/framework/visualisation/react/ui/prompts/radio_input.d.ts b/dist/framework/visualisation/react/ui/prompts/radio_input.d.ts new file mode 100644 index 00000000..a2552bd3 --- /dev/null +++ b/dist/framework/visualisation/react/ui/prompts/radio_input.d.ts @@ -0,0 +1,15 @@ +/// +import { Weak } from '../../../../helpers'; +import { ReactFactoryContext } from '../../factory'; +import { PropsUIPromptRadioInput } from '../../../../types/prompts'; +declare type Props = Weak & ReactFactoryContext; +export declare const RadioInputFactory: (props: Props) => JSX.Element; +export declare const RadioInput: (props: Props) => JSX.Element; +export interface RadioItemProps { + id: number; + value: string; + selected: boolean; + onSelect: (id: number) => void; +} +export declare const RadioItem: ({ id, value, selected, onSelect }: RadioItemProps) => JSX.Element; +export {}; diff --git a/dist/framework/visualisation/react/ui/prompts/radio_input.js b/dist/framework/visualisation/react/ui/prompts/radio_input.js new file mode 100644 index 00000000..22eda0cc --- /dev/null +++ b/dist/framework/visualisation/react/ui/prompts/radio_input.js @@ -0,0 +1,52 @@ +var __assign = (this && this.__assign) || function () { + __assign = Object.assign || function(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) + t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); +}; +import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; +import * as React from 'react'; +import TextBundle from '../../../../text_bundle'; +import RadioSvg from '../../../../../assets/images/spinner.svg'; +import RadioActiveSvg from '../../../../../assets/images/radio_active.svg'; +import { Translator } from '../../../../translator'; +function prepareCopy(_a) { + var title = _a.title, description = _a.description, locale = _a.locale; + return { + title: Translator.translate(title, locale), + description: Translator.translate(description, locale), + continueButton: Translator.translate(continueButtonLabel(), locale) + }; +} +export var RadioInputFactory = function (props) { return _jsx(RadioInput, __assign({}, props)); }; +export var RadioInput = function (props) { + var _a = React.useState(-1), selectedId = _a[0], setSelectedId = _a[1]; + var _b = React.useState(true), confirmHidden = _b[0], setConfirmHidden = _b[1]; + var items = props.items, resolve = props.resolve; + var _c = prepareCopy(props), title = _c.title, description = _c.description, continueButton = _c.continueButton; + function handleSelect(id) { + setSelectedId(id); + setConfirmHidden(false); + } + function handleConfirm() { + var item = items.at(selectedId); + if (item !== undefined) { + resolve === null || resolve === void 0 ? void 0 : resolve({ __type__: 'PayloadString', value: item }); + } + } + return (_jsxs(_Fragment, { children: [_jsx("div", __assign({ className: 'text-title5 font-title5 sm:text-title4 sm:font-title4 lg:text-title3 lg:font-title3 text-grey1' }, { children: title })), _jsx("div", { className: 'mt-8' }), _jsxs("div", __assign({ id: 'select-panel' }, { children: [_jsx("div", __assign({ className: 'flex-wrap text-bodylarge font-body text-grey1 text-left' }, { children: description })), _jsx("div", { className: 'mt-4' }), _jsx("div", { children: _jsx("div", __assign({ id: 'radio-group', className: 'flex flex-col gap-3' }, { children: items.map(function (value, index) { return _jsx(RadioItem, { onSelect: handleSelect, id: index, value: value, selected: selectedId === index }, index); }) })) })] })), _jsx("div", { className: 'mt-8' }), _jsx("div", __assign({ className: "flex flex-row ".concat(confirmHidden ? 'hidden' : '') }, { children: _jsx("div", __assign({ className: 'pt-15px pb-15px active:shadow-top4px active:pt-4 active:pb-14px leading-none font-button text-button rounded pr-4 pl-4 bg-primary text-white cursor-pointer', onClick: handleConfirm }, { children: _jsx("div", __assign({ id: 'confirm-button', className: 'flex-wrap' }, { children: continueButton })) })) }))] })); +}; +var continueButtonLabel = function () { + return new TextBundle() + .add('en', 'Continue') + .add('nl', 'Doorgaan'); +}; +export var RadioItem = function (_a) { + var id = _a.id, value = _a.value, selected = _a.selected, onSelect = _a.onSelect; + return (_jsxs("div", __assign({ id: "".concat(id), className: 'radio-item flex flex-row gap-3 items-center cursor-pointer', onClick: function () { return onSelect(id); } }, { children: [_jsxs("div", { children: [_jsx("img", { src: RadioSvg, id: "".concat(id, "-off"), className: selected ? 'hidden' : '' }), _jsx("img", { src: RadioActiveSvg, id: "".concat(id, "-on"), className: selected ? '' : 'hidden' })] }), _jsx("div", __assign({ className: 'text-grey1 text-label font-label select-none mt-1' }, { children: value }))] }))); +}; diff --git a/dist/py_script.js b/dist/py_script.js index 78ed6646..a6b740ac 100644 --- a/dist/py_script.js +++ b/dist/py_script.js @@ -1 +1 @@ -export var PyScript = "\nimport pandas as pd\n\n\ndef process():\n chat_file_name = yield prompt_file()\n usernames = extract_usernames(chat_file_name)\n username = yield prompt_radio(usernames)\n yield result(usernames, username)\n\n\ndef prompt_file():\n title = Translatable()\n title.add(\"en\", \"Step 1: Select the chat file\")\n title.add(\"nl\", \"Stap 1: Selecteer het chat file\")\n\n description = Translatable()\n description.add(\"en\", \"We previously asked you to export a chat file from Whatsapp. Please select this file so we can extract relevant information for our research.\")\n description.add(\"nl\", \"We hebben je gevraagd een chat bestand te exporteren uit Whatsapp. Je kan deze file nu selecteren zodat wij er relevante informatie uit kunnen halen voor ons onderzoek.\")\n\n extensions = \"application/zip, text/plain\"\n\n return FileInput(title, description, extensions)\n\n\ndef prompt_radio(usernames):\n title = Translatable()\n title.add(\"en\", \"Step 2: Select your username\")\n title.add(\"nl\", \"Stap 2: Selecteer je gebruikersnaam\")\n\n description = Translatable()\n description.add(\"en\", \"The following users are extracted from the chat file. Which one are you?\")\n description.add(\"nl\", \"De volgende gebruikers hebben we uit de chat file gehaald. Welke ben jij?\")\n\n return RadioInput(title, description, usernames)\n\n\ndef extract_usernames(chat_file_name):\n print(f\"filename: {chat_file_name}\")\n\n with open(chat_file_name) as chat_file:\n while (line := chat_file.readline().rstrip()):\n print(line)\n\n return [\"emielvdveen\", \"a.m.mendrik\", \"9bitcat\"]\n\n\ndef result(usernames, selected_username):\n data = []\n for username in usernames:\n description = \"you\" if username == selected_username else \"-\"\n data.append((username, description))\n\n data_frame = pd.DataFrame(data, columns=[\"username\", \"description\"])\n\n print(data_frame)\n\n result = [{\n \"id\": \"overview\",\n \"title\": \"The following usernames where extracted:\",\n \"data_frame\": data_frame\n }]\n return EndOfFlow(result)\n"; +export var PyScript = "\nimport pandas as pd\nimport zipfile\n\ndef process():\n yield render_start_page()\n\n platforms = [\"Twitter\", \"Instagram\", \"Youtube\"]\n for index, platform in enumerate(platforms):\n data = None\n while True:\n promptFile = prompt_file(platform, \"application/zip, text/plain\")\n fileResult = yield render_donation_page(index+1, platform, promptFile)\n if fileResult.__type__ == 'PayloadString':\n extractionResult = doSomethingWithTheFile(platform, fileResult.value)\n if extractionResult != 'invalid':\n data = extractionResult\n break\n else:\n retry_result = yield render_donation_page(index+1, platform, retry_confirmation())\n if retry_result.__type__ == 'PayloadTrue':\n continue\n else: \n break\n else:\n break\n\n if data is not None:\n prompt = prompt_consent(platform, data)\n consent_result = yield render_donation_page(index+1, platform, prompt)\n if consent_result.__type__ == \"PayloadString\":\n yield donate(platform, consent_result.value)\n\n yield render_end_page()\n\n\ndef render_start_page():\n header = PropsUIHeader(Translatable({\n \"en\": \"Welcome\",\n \"nl\": \"Welkom\"\n }))\n page = PropsUIPageStart(header, spinner())\n return CommandUIRender(page)\n\n\ndef render_end_page():\n header = PropsUIHeader(Translatable({\n \"en\": \"Thank you\",\n \"nl\": \"Dank je wel\"\n }))\n page = PropsUIPageEnd(header)\n return CommandUIRender(page)\n\n\ndef render_donation_page(index, platform, body):\n header = PropsUIHeader(Translatable({\n \"en\": f\"Step {index}: {platform}\",\n \"nl\": f\"Stap {index}: {platform}\"\n }))\n page = PropsUIPageDonation(header, body, spinner())\n return CommandUIRender(page)\n\n\ndef retry_confirmation():\n text = Translatable({\n \"en\": \"The selected file is invalid. Do you want to select a different file?\",\n \"nl\": \"Het geselecteerde bestaand is ongeldig. Wil je een ander bestand selecteren ?\"\n })\n ok = Translatable({\n \"en\": \"Different file\",\n \"nl\": \"Ander bestand\"\n })\n cancel = Translatable({\n \"en\": \"Cancel\",\n \"nl\": \"Annuleren\"\n })\n return PropsUIPromptConfirm(text, ok, cancel)\n\n\ndef spinner():\n return PropsUISpinner(Translatable({\n \"en\": \"One moment please\",\n \"nl\": \"Een moment geduld\"\n }))\n\n\ndef prompt_file(platform, extensions):\n title = Translatable({\n \"en\": f\"Select {platform} file\",\n \"nl\": f\"Selecteer {platform} bestand\"\n })\n\n description = Translatable({\n \"en\": \"Please select this file so we can extract relevant information for our research.\",\n \"nl\": \"Je kan deze file nu selecteren zodat wij er relevante informatie uit kunnen halen voor ons onderzoek.\"\n })\n\n return PropsUIPromptFileInput(title, description, extensions)\n\n\ndef doSomethingWithTheFile(platform, filename):\n return extract_zip_contents(filename)\n\n\ndef extract_zip_contents(filename):\n names = []\n try:\n file = zipfile.ZipFile(filename)\n data = []\n for name in file.namelist():\n names.append(name)\n info = file.getinfo(name)\n data.append((name, info.compress_size, info.file_size))\n return data\n except:\n return \"invalid\" \n\n\ndef prompt_consent(id, data):\n title = Translatable({\n \"en\": \"Extracted data\",\n \"nl\": \"Gevonden gegevens\"\n })\n\n description = Translatable({\n \"en\": \"Please have a good look at the extracted data before giving consent to use this data.\",\n \"nl\": \"Bekijk de gegevens goed voordat je consent geeft om deze te gebruiken.\"\n })\n\n data_frame = pd.DataFrame(data, columns=[\"filename\", \"compressed size\", \"size\"])\n table = PropsUIPromptConsentFormTable(id, \"The zip contains the following files:\", data_frame)\n return PropsUIPromptConsentForm(title, description, [table])\n\n\ndef donate(key, consent_data):\n return CommandSystemDonate(key, consent_data)\n"; diff --git a/dist/styles.css b/dist/styles.css index 76de01a4..7baaea4e 100644 --- a/dist/styles.css +++ b/dist/styles.css @@ -1 +1 @@ -/*! tailwindcss v3.1.8 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::-webkit-backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.m-6{margin:1.5rem}.mb-4{margin-bottom:1rem}.mt-8{margin-top:2rem}.mt-4{margin-top:1rem}.mt-1{margin-top:.25rem}.mb-6{margin-bottom:1.5rem}.flex{display:flex}.table{display:table}.hidden{display:none}.h-14{height:3.5rem}.h-10{height:2.5rem}.w-full{width:100%}.w-10{width:2.5rem}.max-w-sheet{max-width:760px}.flex-grow{flex-grow:1}.table-auto{table-layout:auto}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;user-select:none}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.gap-4{gap:1rem}.gap-3{gap:.75rem}.rounded{border-radius:.25rem}.border-2{border-width:2px}.bg-primary{--tw-bg-opacity:1;background-color:rgb(66 114 239/var(--tw-bg-opacity))}.bg-delete{--tw-bg-opacity:1;background-color:rgb(219 30 30/var(--tw-bg-opacity))}.bg-grey5{--tw-bg-opacity:1;background-color:rgb(246 246 246/var(--tw-bg-opacity))}.bg-opacity-0{--tw-bg-opacity:0}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.pt-15px{padding-top:15px}.pb-15px{padding-bottom:15px}.pr-4{padding-right:1rem}.pl-4{padding-left:1rem}.pt-13px{padding-top:13px}.pb-13px{padding-bottom:13px}.pb-3{padding-bottom:.75rem}.text-left{text-align:left}.font-button,.font-title5{font-family:Finador-Bold,sans-serif}.font-body{font-family:Finador-Light,sans-serif}.font-subhead{font-family:Finador-Medium,sans-serif}.font-label{font-family:Finador-Bold,sans-serif}.font-title3,.font-title4{font-family:Finador-Black,sans-serif}.text-button{font-size:18px;line-height:18px}.text-title5{font-size:24px;line-height:26px}.text-bodylarge{font-size:24px;line-height:36px}.text-subhead{font-size:20px;line-height:20px}.text-label{font-size:16px;line-height:16px}.text-bodymedium{font-size:20px;line-height:30px}.text-title4{font-size:28px;line-height:32px}.text-title3{font-size:32px;line-height:38px}.leading-none{line-height:1}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-delete{--tw-text-opacity:1;color:rgb(219 30 30/var(--tw-text-opacity))}.text-grey1{--tw-text-opacity:1;color:rgb(34 34 34/var(--tw-text-opacity))}.active\:pt-4:active{padding-top:1rem}.active\:pb-14px:active{padding-bottom:14px}.active\:pt-14px:active{padding-top:14px}.active\:pb-3:active{padding-bottom:.75rem}.active\:shadow-top4px:active{--tw-shadow:inset 0 4px 0 0 rgba(0,0,0,.15);--tw-shadow-colored:inset 0 4px 0 0 var(--tw-shadow-color)}.active\:shadow-top2px:active,.active\:shadow-top4px:active{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.active\:shadow-top2px:active{--tw-shadow:inset 0 2px 0 0 rgba(0,0,0,.15);--tw-shadow-colored:inset 0 2px 0 0 var(--tw-shadow-color)}@media (min-width:640px){.sm\:font-title2,.sm\:font-title3,.sm\:font-title4{font-family:Finador-Black,sans-serif}.sm\:text-title4{font-size:28px;line-height:32px}.sm\:text-title2{font-size:40px;line-height:44px}.sm\:text-title3{font-size:32px;line-height:38px}}@media (min-width:768px){.md\:mb-8{margin-bottom:2rem}}@media (min-width:1024px){.lg\:m-14{margin:3.5rem}.lg\:mb-10{margin-bottom:2.5rem}.lg\:font-title0,.lg\:font-title3{font-family:Finador-Black,sans-serif}.lg\:font-title1{font-family:Arial,sans-serif}.lg\:font-title2{font-family:Finador-Black,sans-serif}.lg\:text-title3{font-size:32px;line-height:38px}.lg\:text-title0{font-size:64px;line-height:68px}.lg\:text-title1{font-size:50px;line-height:50px}.lg\:text-title2{font-size:40px;line-height:44px}} \ No newline at end of file +/*! tailwindcss v3.1.8 | MIT License | https://tailwindcss.com*/*,:after,:before{border:0 solid;box-sizing:border-box}:after,:before{--tw-content:""}html{-webkit-text-size-adjust:100%;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;line-height:1.5;tab-size:4}body{line-height:inherit;margin:0}hr{border-top-width:1px;color:inherit;height:0}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{border-collapse:collapse;border-color:inherit;text-indent:0}button,input,optgroup,select,textarea{color:inherit;font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-webkit-input-placeholder,textarea::-webkit-input-placeholder{color:#9ca3af;opacity:1}input::placeholder,textarea::placeholder{color:#9ca3af;opacity:1}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{height:auto;max-width:100%}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::-webkit-backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.m-6{margin:1.5rem}.ml-4{margin-left:1rem}.-mt-2px{margin-top:-2px}.mb-6{margin-bottom:1.5rem}.mb-4{margin-bottom:1rem}.mt-2{margin-top:.5rem}.mt-8{margin-top:2rem}.mt-4{margin-top:1rem}.mt-10{margin-top:2.5rem}.mt-1{margin-top:.25rem}.flex{display:flex}.table{display:table}.hidden{display:none}.h-full{height:100%}.h-10{height:2.5rem}.w-full{width:100%}.w-10{width:2.5rem}.max-w-sheet{max-width:760px}.flex-grow{flex-grow:1}.table-auto{table-layout:auto}.cursor-pointer{cursor:pointer}.select-none{-webkit-user-select:none;user-select:none}.flex-row{flex-direction:row}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.justify-center{justify-content:center}.gap-4{gap:1rem}.gap-2{gap:.5rem}.gap-8{gap:2rem}.gap-3{gap:.75rem}.rounded{border-radius:.25rem}.border-2{border-width:2px}.bg-primary{--tw-bg-opacity:1;background-color:rgb(66 114 239/var(--tw-bg-opacity))}.bg-delete{--tw-bg-opacity:1;background-color:rgb(219 30 30/var(--tw-bg-opacity))}.bg-tertiary{--tw-bg-opacity:1;background-color:rgb(255 207 96/var(--tw-bg-opacity))}.bg-opacity-0{--tw-bg-opacity:0}.px-2{padding-left:.5rem;padding-right:.5rem}.pt-15px{padding-top:15px}.pb-15px{padding-bottom:15px}.pr-4{padding-right:1rem}.pl-4{padding-left:1rem}.pt-13px{padding-top:13px}.pb-13px{padding-bottom:13px}.pt-1{padding-top:.25rem}.pb-1{padding-bottom:.25rem}.pb-3{padding-bottom:.75rem}.text-left{text-align:left}.font-button{font-family:Finador-Bold,sans-serif}.font-body{font-family:Finador-Light,sans-serif}.font-title3,.font-title4{font-family:Finador-Black,sans-serif}.font-title5{font-family:Finador-Bold,sans-serif}.font-subhead{font-family:Finador-Medium,sans-serif}.font-label{font-family:Finador-Bold,sans-serif}.text-button{font-size:18px;line-height:18px}.text-bodymedium{font-size:20px;line-height:30px}.text-bodylarge{font-size:24px;line-height:36px}.text-title4{font-size:28px;line-height:32px}.text-title3{font-size:32px;line-height:38px}.text-title5{font-size:24px;line-height:26px}.text-subhead{font-size:20px;line-height:20px}.text-label{font-size:16px;line-height:16px}.leading-none{line-height:1}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-delete{--tw-text-opacity:1;color:rgb(219 30 30/var(--tw-text-opacity))}.text-grey1{--tw-text-opacity:1;color:rgb(34 34 34/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.active\:pt-4:active{padding-top:1rem}.active\:pb-14px:active{padding-bottom:14px}.active\:pt-14px:active{padding-top:14px}.active\:pb-3:active{padding-bottom:.75rem}.active\:pt-5px:active{padding-top:5px}.active\:pb-3px:active{padding-bottom:3px}.active\:shadow-top4px:active{--tw-shadow:inset 0 4px 0 0 rgba(0,0,0,.15);--tw-shadow-colored:inset 0 4px 0 0 var(--tw-shadow-color)}.active\:shadow-top2px:active,.active\:shadow-top4px:active{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.active\:shadow-top2px:active{--tw-shadow:inset 0 2px 0 0 rgba(0,0,0,.15);--tw-shadow-colored:inset 0 2px 0 0 var(--tw-shadow-color)}@media (min-width:640px){.sm\:font-title2,.sm\:font-title3,.sm\:font-title4{font-family:Finador-Black,sans-serif}.sm\:text-title2{font-size:40px;line-height:44px}.sm\:text-title3{font-size:32px;line-height:38px}.sm\:text-title4{font-size:28px;line-height:32px}}@media (min-width:768px){.md\:mb-8{margin-bottom:2rem}}@media (min-width:1024px){.lg\:m-14{margin:3.5rem}.lg\:mb-10{margin-bottom:2.5rem}.lg\:font-title0{font-family:Finador-Black,sans-serif}.lg\:font-title1{font-family:Arial,sans-serif}.lg\:font-title2,.lg\:font-title3{font-family:Finador-Black,sans-serif}.lg\:text-title0{font-size:64px;line-height:68px}.lg\:text-title1{font-size:50px;line-height:50px}.lg\:text-title2{font-size:40px;line-height:44px}.lg\:text-title3{font-size:32px;line-height:38px}} \ No newline at end of file diff --git a/src/assets/images/forward.svg b/src/assets/images/forward.svg new file mode 100644 index 00000000..0b1cb8b9 --- /dev/null +++ b/src/assets/images/forward.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/framework/abstractions/processing_engine.ts b/src/framework/abstractions/processing_engine.ts deleted file mode 100644 index 47f22fb5..00000000 --- a/src/framework/abstractions/processing_engine.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default interface ProcessingEngine { - start: () => void - loadScript: (script: any) => void - firstRunCycle: () => void - nextRunCycle: (response: any) => void - terminate: () => void -} diff --git a/src/framework/abstractions/visualisation_engine.ts b/src/framework/abstractions/visualisation_engine.ts deleted file mode 100644 index 61e1344a..00000000 --- a/src/framework/abstractions/visualisation_engine.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default interface VisualisationEngine { - start: (script: string, rootElement: HTMLElement, locale: string) => Promise - terminate: () => void -} diff --git a/src/framework/assembly.ts b/src/framework/assembly.ts index 4c80bf82..1a7adb7e 100644 --- a/src/framework/assembly.ts +++ b/src/framework/assembly.ts @@ -1,14 +1,17 @@ import ReactEngine from './visualisation/react/engine' import ReactFactory from './visualisation/react/factory' import WorkerProcessingEngine from './processing/worker_engine' -import VisualisationEngine from './abstractions/visualisation_engine' +import { VisualisationEngine, ProcessingEngine, System } from './types/modules' +import CommandRouter from './command_router' -export const Assembly = (worker: Worker): VisualisationEngine => { - const processingEngine = new WorkerProcessingEngine(worker) - const visualisationEngine = new ReactEngine( - new ReactFactory(), - processingEngine - ) - processingEngine.eventListener = visualisationEngine.onEvent - return visualisationEngine +export default class Assembly { + visualisationEngine: VisualisationEngine + processingEngine: ProcessingEngine + router: CommandRouter + + constructor (worker: Worker, system: System) { + this.visualisationEngine = new ReactEngine(new ReactFactory()) + this.router = new CommandRouter(system, this.visualisationEngine) + this.processingEngine = new WorkerProcessingEngine(worker, this.router) + } } diff --git a/src/framework/command_router.ts b/src/framework/command_router.ts new file mode 100644 index 00000000..19b2d2bd --- /dev/null +++ b/src/framework/command_router.ts @@ -0,0 +1,36 @@ +import { Command, Response, isCommandSystem, isCommandUI, CommandUI, CommandSystem } from './types/commands' +import { CommandHandler, System, VisualisationEngine } from './types/modules' + +export default class CommandRouter implements CommandHandler { + system: System + visualisationEngine: VisualisationEngine + + constructor (system: System, visualisationEngine: VisualisationEngine) { + this.system = system + this.visualisationEngine = visualisationEngine + } + + async onCommand (command: Command): Promise { + return await new Promise((resolve, reject) => { + if (isCommandSystem(command)) { + this.onCommandSystem(command, resolve) + } else if (isCommandUI(command)) { + this.onCommandUI(command, resolve) + } else { + reject(new TypeError('Unknown command' + JSON.stringify(command))) + } + }) + } + + onCommandSystem (command: CommandSystem, resolve: (response: Response) => void): void { + this.system.send(command) + resolve({ command, payload: { __type__: 'PayloadVoid', value: undefined } }) + } + + onCommandUI (command: CommandUI, reject: (reason?: any) => void): void { + this.visualisationEngine.render(command).then( + (response) => { reject(response) }, + () => {} + ) + } +} diff --git a/src/framework/helpers.ts b/src/framework/helpers.ts new file mode 100644 index 00000000..12918373 --- /dev/null +++ b/src/framework/helpers.ts @@ -0,0 +1,14 @@ +import { Omit } from 'lodash' + +export const childOf = (arg: any, superType: string): boolean => { + return arg?.__type__?.startsWith(superType) +} + +export const instanceOf = ( + arg: any, + properties: Array +): arg is T => { + return !properties.some((property) => (arg as T)[property] === undefined) +} + +export type Weak = Omit diff --git a/src/framework/processing/python/worker.js b/src/framework/processing/python/worker.js index 9f73baca..ba5b4bde 100755 --- a/src/framework/processing/python/worker.js +++ b/src/framework/processing/python/worker.js @@ -31,8 +31,9 @@ onmessage = (event) => { } } -function runCycle (userInput) { - scriptEvent = pyScript.send(userInput) +function runCycle (payload) { + console.log('[ProcessingWorker] runCycle ' + JSON.stringify(payload)) + scriptEvent = pyScript.send(payload) self.postMessage({ eventType: 'runCycleDone', scriptEvent: scriptEvent.toJs({ @@ -43,14 +44,15 @@ function runCycle (userInput) { } function unwrap (response) { + console.log('[ProcessingWorker] unwrap response: ' + JSON.stringify(response.payload)) return new Promise((resolve) => { - switch (response.prompt.__type__) { - case 'Event.Command.Prompt.FileInput': - copyFileToPyFS(response.userInput, resolve) + switch (response.payload.__type__) { + case 'PayloadFile': + copyFileToPyFS(response.payload.value, resolve) break default: - resolve(response.userInput) + resolve(response.payload) } }) } @@ -61,7 +63,7 @@ function copyFileToPyFS (file, resolve) { const writeToPyFS = ({ done, value }) => { if (done) { - resolve(file.name) + resolve({ __type__: 'PayloadString', value: file.name }) } else { self.pyodide.FS.write(pyFile, value, 0, value.length) reader.read().then(writeToPyFS) @@ -71,11 +73,14 @@ function copyFileToPyFS (file, resolve) { } function initialise () { + console.log('[ProcessingWorker] initialise') importScripts('https://cdn.jsdelivr.net/pyodide/v0.21.2/full/pyodide.js') + console.log('[ProcessingWorker] loading Pyodide') return loadPyodide({ indexURL: 'https://cdn.jsdelivr.net/pyodide/v0.21.2/full/' }).then((pyodide) => { + console.log('[ProcessingWorker] loading packages') self.pyodide = pyodide return self.pyodide.loadPackage(['micropip', 'numpy', 'pandas']) }) @@ -88,94 +93,178 @@ function loadScript (script) { } const pyPortApi = ` -class Event: +class CommandUIRender: + __slots__ = "page" + def __init__(self, page): + self.page = page def toDict(self): - return setType({}, "Event") - - -class EndOfFlow(Event): - __slots__ = "result" - def __init__(self, result): - self.result = result - def translate_result(self): - print("translate") - data_output = [] - for data in self.result: - df = data["data_frame"] - data_output.append({"id": data["id"], "data_frame": df.to_json()}) - return { - "title": data["title"], - "data": data_output, - } + dict = {} + dict["__type__"] = "CommandUIRender" + dict["page"] = self.page.toDict() + return dict + +class CommandSystemDonate: + __slots__ = "key", "data" + def __init__(self, key, data): + self.key = key + self.data = data def toDict(self): - print("toDict2") - dict = toDict(super(), "EndOfFlow") - dict = dict | self.translate_result() + dict = {} + dict["__type__"] = "CommandSystemDonate" + dict["key"] = self.key + dict["data"] = self.data return dict - -class Command(Event): + +class PropsUIHeader: + __slots__ = "title" + def __init__(self, title): + self.title = title def toDict(self): - return toDict(super(), "Command") + dict = {} + dict["__type__"] = "PropsUIHeader" + dict["title"] = self.title.toDict() + return dict + + +class PropsUIPromptConfirm: + __slots__ = "text", "ok", "cancel" + def __init__(self, text, ok, cancel): + self.text = text + self.ok = ok + self.cancel = cancel + def toDict(self): + dict = {} + dict["__type__"] = "PropsUIPromptConfirm" + dict["text"] = self.text.toDict() + dict["ok"] = self.ok.toDict() + dict["cancel"] = self.cancel.toDict() + return dict -class Prompt(Command): - __slots__ = "title", "description" - def __init__(self, title, description): +class PropsUISpinner: + __slots__ = "text" + def __init__(self, text): + self.text = text + def toDict(self): + dict = {} + dict["__type__"] = "PropsUISpinner" + dict["text"] = self.text.toDict() + return dict + + +class PropsUIPromptConsentForm: + __slots__ = "title", "description", "tables" + def __init__(self, title, description, tables): self.title = title - self.description = description + self.description = description + self.tables = tables + def translate_tables(self): + tables_output = [] + for table in self.tables: + tables_output.append(table.toDict()) + return tables_output def toDict(self): - dict = toDict(super(), "Prompt") + dict = {} + dict["__type__"] = "PropsUIPromptConsentForm" dict["title"] = self.title.toDict() dict["description"] = self.description.toDict() + dict["tables"] = self.translate_tables() + return dict + + +class PropsUIPromptConsentFormTable: + __slots__ = "id", "title", "data_frame" + def __init__(self, id, title, data_frame): + self.id = id + self.title = title + self.data_frame = data_frame + def toDict(self): + dict = {} + dict["__type__"] = "PropsUIPromptConsentFormTable" + dict["id"] = self.id + dict["title"] = self.title + dict["data_frame"] = self.data_frame.to_json() return dict -class FileInput(Prompt): - __slots__ = "extensions" +class PropsUIPromptFileInput: + __slots__ = "title", "description", "extensions" def __init__(self, title, description, extensions): - super().__init__(title, description) + self.title = title + self.description = description self.extensions = extensions def toDict(self): - dict = toDict(super(), "FileInput") + dict = {} + dict["__type__"] = "PropsUIPromptFileInput" + dict["title"] = self.title.toDict() + dict["description"] = self.description.toDict() dict["extensions"] = self.extensions return dict -class RadioInput(Prompt): +class PropsUIPromptRadioInput: + __slots__ = "title", "description", "items" def __init__(self, title, description, items): - super().__init__(title, description) + self.title = title + self.description = description self.items = items def toDict(self): - dict = toDict(super(), "RadioInput") + dict = {} + dict["__type__"] = "PropsUIPromptRadioInput" + dict["title"] = self.title.toDict() + dict["description"] = self.description.toDict() dict["items"] = self.items return dict -class Translatable: - __slots__ = "translations" - def __init__(self): - self.translations = {} - def add(self, locale, text): - self.translations[locale] = text - return self +class PropsUIPageDonation: + __slots__ = "header", "body", "spinner" + def __init__(self, header, body, spinner): + self.header = header + self.body = body + self.spinner = spinner def toDict(self): - return setType(self.translations, "Translatable") + dict = {} + dict["__type__"] = "PropsUIPageDonation" + dict["header"] = self.header.toDict() + dict["body"] = self.body.toDict() + dict["spinner"] = self.spinner.toDict() + return dict -def toDict(zuper, type): - return setType(zuper.toDict(), type) +class PropsUIPageStart: + __slots__ = "header", "spinner" + def __init__(self, header, spinner): + self.header = header + self.spinner = spinner + def toDict(self): + dict = {} + dict["__type__"] = "PropsUIPageStart" + dict["header"] = self.header.toDict() + dict["spinner"] = self.spinner.toDict() + return dict -def setType(dict, type): - key = "__type__" - seperator = "." +class PropsUIPageEnd: + __slots__ = "header" + def __init__(self, header): + self.header = header + def toDict(self): + dict = {} + dict["__type__"] = "PropsUIPageEnd" + dict["header"] = self.header.toDict() + return dict + - path = [type] - if key in dict: - path.insert(0, dict[key]) - dict[key] = seperator.join(path) - return dict +class Translatable: + __slots__ = "translations" + def __init__(self, translations): + self.translations = translations + def toDict(self): + dict = {} + dict["translations"] = self.translations + return dict ` function pyWorker () { @@ -185,13 +274,13 @@ function pyWorker () { import html import pandas as pd + class ScriptWrapper(Generator): def __init__(self, script): self.script = script def send(self, data): - print("toDict") - event = self.script.send(data) - return event.toDict() + command = self.script.send(data) + return command.toDict() def throw(self, type=None, value=None, traceback=None): raise StopIteration script = process() diff --git a/src/framework/processing/worker_engine.ts b/src/framework/processing/worker_engine.ts index c0d9a9b6..df16c528 100755 --- a/src/framework/processing/worker_engine.ts +++ b/src/framework/processing/worker_engine.ts @@ -1,18 +1,14 @@ -import ProcessingEngine from '../abstractions/processing_engine' +import { CommandHandler, ProcessingEngine } from '../types/modules' +import { isCommand, Response, Script } from '../types/commands' export default class WorkerProcessingEngine implements ProcessingEngine { - eventListener: (event: any) => void worker: Worker + commandHandler: CommandHandler - constructor (worker: Worker) { - this.eventListener = (event) => { - const eventString = JSON.stringify(event) - console.log( - '[WorkerProcessingEngine] No event listener registered for event: ', - eventString - ) - } + script!: Script + constructor (worker: Worker, commandHandler: CommandHandler) { + this.commandHandler = commandHandler this.worker = worker this.worker.onerror = console.log this.worker.onmessage = (event) => { @@ -20,11 +16,39 @@ export default class WorkerProcessingEngine implements ProcessingEngine { '[WorkerProcessingEngine] Received event from worker: ', event.data.eventType ) - this.eventListener(event) + this.handleEvent(event) + } + } + + handleEvent (event: any): void { + const { eventType } = event.data + console.log('[ReactEngine] received eventType: ', eventType) + switch (eventType) { + case 'initialiseDone': + console.log('[ReactEngine] received: initialiseDone') + this.loadScript(this.script) + break + + case 'loadScriptDone': + console.log('[ReactEngine] Received: loadScriptDone') + this.firstRunCycle() + break + + case 'runCycleDone': + console.log('[ReactEngine] received: event', event.data.scriptEvent) + this.handleRunCycle(event.data.scriptEvent) + break + default: + console.log( + '[ReactEngine] received unsupported flow event: ', + eventType + ) } } - start (): void { + start (script: Script): void { + console.log('[WorkerProcessingEngine] started') + this.script = script this.worker.postMessage({ eventType: 'initialise' }) } @@ -36,11 +60,20 @@ export default class WorkerProcessingEngine implements ProcessingEngine { this.worker.postMessage({ eventType: 'firstRunCycle' }) } - nextRunCycle (response: any): void { + nextRunCycle (response: Response): void { this.worker.postMessage({ eventType: 'nextRunCycle', response }) } terminate (): void { this.worker.terminate() } + + handleRunCycle (command: any): void { + if (isCommand(command)) { + this.commandHandler.onCommand(command).then( + (response) => this.nextRunCycle(response), + () => {} + ) + } + } } diff --git a/src/framework/translatable.ts b/src/framework/text_bundle.ts similarity index 75% rename from src/framework/translatable.ts rename to src/framework/text_bundle.ts index 6cb51906..ae889875 100644 --- a/src/framework/translatable.ts +++ b/src/framework/text_bundle.ts @@ -1,15 +1,16 @@ import _ from 'lodash' +import { Translatable } from './types/elements' -export default class Translatable { +export default class TextBundle implements Translatable { translations: { [key: string]: string } = {} defaultLocale: string = 'nl' - add (locale: string, text: string): Translatable { + add (locale: string, text: string): TextBundle { this.translations[locale] = text return this } - text (locale: string): string { + translate (locale: string): string { return _.escape(this.resolve(locale)) } diff --git a/src/framework/translator.ts b/src/framework/translator.ts new file mode 100644 index 00000000..e8b8a246 --- /dev/null +++ b/src/framework/translator.ts @@ -0,0 +1,35 @@ +import _ from 'lodash' +import { isTranslatable, Text, Translatable } from './types/elements' + +export const Translator = (function () { + const defaultLocale: string = 'nl' + + function translate (text: Text, locale: string): string { + if (isTranslatable(text)) { + return _.escape(resolve(text, locale)) + } + return text + } + + function resolve (translatable: Translatable, locale: string): string { + const text = translatable.translations[locale] + if (text !== null) { + return text + } + + const defaultText = translatable.translations[defaultLocale] + if (defaultText !== null) { + return defaultText + } + + if (Object.values(translatable.translations).length > 0) { + return Object.values(translatable.translations)[0] + } + + return '?text?' + } + + return { + translate + } +})() diff --git a/src/framework/types/commands.ts b/src/framework/types/commands.ts new file mode 100644 index 00000000..a38ba79c --- /dev/null +++ b/src/framework/types/commands.ts @@ -0,0 +1,113 @@ +import { instanceOf, childOf } from '../helpers' +import { PropsUIPage } from './pages' + +export type Script = string | File | URL +export function isScript (arg: any): arg is Script { + return typeof arg === 'string' || isFile(arg) || isURL(arg) +} +export function isFile (arg: unknown): arg is File { + return instanceOf(arg, ['arrayBuffer', 'lastModified', 'name', 'size', 'slice', 'stream', 'text', 'type', 'webkitRelativePath']) +} +export function isURL (arg: any): arg is URL { + return instanceOf(arg, ['hash', 'host', 'hostname', 'href', 'origin', 'toString', 'password', 'pathname', 'port', 'protocol', 'search', 'searchParams', 'username', 'toJSON']) +} + +export interface Table { + id: string + title: Text + data: any +} +export function isTable (arg: any): arg is Table { + return instanceOf(arg, ['id', 'title', 'data']) +} + +export interface Response { + command: Command + payload: Payload +} +export function isResponse (arg: any): arg is Response { + return instanceOf(arg, ['command', 'payload']) +} + +export type Payload = + PayloadResolved | + PayloadRejected + +export type PayloadRejected = + PayloadFalse | + PayloadError + +export interface PayloadFalse { + __type__: 'PayloadFalse' + value: false +} + +export interface PayloadError { + __type__: 'PayloadError' + value: string +} + +export type PayloadResolved = + PayloadVoid | + PayloadTrue | + PayloadString | + PayloadFile + +export interface PayloadVoid { + __type__: 'PayloadVoid' + value: undefined +} + +export interface PayloadTrue { + __type__: 'PayloadTrue' + value: true +} + +export interface PayloadString { + __type__: 'PayloadString' + value: string +} + +export interface PayloadFile { + __type__: 'PayloadFile' + value: File +} + +export type Command = + CommandUI | + CommandSystem + +export function isCommand (arg: any): arg is Command { + return childOf(arg, 'Command') +} + +export type CommandSystem = + CommandSystemDonate + +export function isCommandSystem (arg: any): arg is CommandSystem { + return childOf(arg, 'CommandSystem') +} + +export type CommandUI = + CommandUIRender + +export function isCommandUI (arg: any): arg is CommandUI { + return childOf(arg, 'CommandUI') +} + +export interface CommandSystemDonate { + __type__: 'CommandSystemDonate' + key: string + data: string +} +export function isCommandSystemDonate (arg: any): arg is CommandSystemDonate { + return instanceOf(arg, ['__type__', 'key', 'data']) && arg.__type__ === 'CommandSystemDonate' +} + +export interface CommandUIRender { + __type__: 'CommandUIRender' + page: PropsUIPage +} +export function isCommandUIRender (arg: any): arg is CommandUIRender { + return instanceOf(arg, ['__type__', 'page']) && arg.__type__ === 'CommandUIRender' +} diff --git a/src/framework/types/elements.ts b/src/framework/types/elements.ts new file mode 100644 index 00000000..18bf965f --- /dev/null +++ b/src/framework/types/elements.ts @@ -0,0 +1,199 @@ +import { instanceOf, childOf, Weak } from '../helpers' +import { } from './commands' +import { PropsUIPage } from './pages' +import { PropsUIPrompt } from './prompts' + +export type PropsUI = + PropsUIText | + PropsUIButton | + PropsUISpinner | + PropsUIHeader | + PropsUITable | + PropsUIPage | + PropsUIPrompt + +export type PropsUIText = + PropsUITextTitle0 | + PropsUITextTitle1 | + PropsUITextTitle2 + +export type PropsUIButton = + PropsUIButtonPrimary | + PropsUIButtonSecundary | + PropsUIButtonForward | + PropsUIButtonLabel + +// UI + +export function isPropsUI (arg: any): arg is PropsUI { + return childOf(arg, 'PropsUI') +} + +// TEXTS + +export function isPropsUIText (arg: any): arg is PropsUIText { + return childOf(arg, 'PropsUIText') +} + +export interface PropsUITextBody { + __type__: 'PropsUITextBody' + text: string + color?: string + margin?: string +} +export function isPropsUITextBody (arg: any): arg is PropsUITextBody { + return instanceOf(arg, ['__type__', 'text', 'color', 'margin']) && arg.__type__ === 'PropsUITextBody' +} + +export interface PropsUITextTitle0 { + __type__: 'PropsUITextTitle0' + text: string + color?: string + margin?: string +} +export function isPropsUITextTitle0 (arg: any): arg is PropsUITextTitle0 { + return instanceOf(arg, ['__type__', 'text', 'color', 'margin']) && arg.__type__ === 'PropsUITextTitle0' +} + +export interface PropsUITextTitle1 { + __type__: 'PropsUITextTitle1' + text: string + color?: string + margin?: string +} +export function isPropsUITextTitle1 (arg: any): arg is PropsUITextTitle1 { + return instanceOf(arg, ['__type__', 'text', 'color', 'margin']) && arg.__type__ === 'PropsUITextTitle1' +} + +export interface PropsUITextTitle2 { + __type__: 'PropsUITextTitle2' + text: string + color?: string + margin?: string +} +export function isPropsUITextTitle2 (arg: any): arg is PropsUITextTitle2 { + return instanceOf(arg, ['__type__', 'text', 'color', 'margin']) && arg.__type__ === 'PropsUITextTitle2' +} + +// BUTTONS + +export function isPropsUIButton (arg: any): arg is PropsUIButton { + return childOf(arg, 'PropsUIButton') +} + +export interface PropsUIButtonPrimary { + __type__: 'PropsUIButtonPrimary' + label: string + color?: string + onClick: () => void +} +export function isPropsUIButtonPrimary (arg: any): arg is PropsUIButtonPrimary { + return instanceOf(arg, ['__type__', 'label', 'color', 'onClick']) && arg.__type__ === 'PropsUIButtonPrimary' +} + +export interface PropsUIButtonSecundary { + __type__: 'PropsUIButtonSecundary' + label: string + color?: string + onClick: () => void +} +export function isPropsUIButtonSecundary (arg: any): arg is PropsUIButtonSecundary { + return instanceOf(arg, ['__type__', 'label', 'color', 'onClick']) && arg.__type__ === 'PropsUIButtonSecundary' +} + +export interface PropsUIButtonForward { + __type__: 'PropsUIButtonForward' + label: string + onClick: () => void +} +export function isPropsUIButtonForward (arg: any): arg is PropsUIButtonForward { + return instanceOf(arg, ['__type__', 'label', 'onClick']) && arg.__type__ === 'PropsUIButtonForward' +} + +export interface PropsUIButtonLabel { + __type__: 'PropsUIButtonLabel' + label: string + onClick: () => void +} +export function isPropsUIButtonLabel (arg: any): arg is PropsUIButtonLabel { + return instanceOf(arg, ['__type__', 'label', 'onClick']) && arg.__type__ === 'PropsUIButtonLabel' +} + +// SPINNER + +export interface PropsUISpinner { + __type__: 'PropsUISpinner' + text: Text +} +export function isPropsUISpinner (arg: any): arg is PropsUISpinner { + return instanceOf(arg, ['__type__', 'text']) && arg.__type__ === 'PropsUISpinner' +} + +// Header + +export interface PropsUIHeader { + __type__: 'PropsUIHeader' + title: Text +} +export function isPropsUIHeader (arg: any): arg is PropsUIHeader { + return instanceOf(arg, ['__type__', 'title']) && arg.__type__ === 'PropsUIHeader' +} + +// TABLE + +export interface PropsUITable { + __type__: 'PropsUITable' + id: string + head: Weak + body: Weak +} +export function isPropsUITable (arg: any): arg is PropsUITable { + return instanceOf(arg, ['__type__', 'id', 'head', 'body']) && arg.__type__ === 'PropsUITable' +} + +export interface PropsUITableHead { + __type__: 'PropsUITableHead' + cells: PropsUITableCell[] +} +export function isPropsUITableHeader (arg: any): arg is PropsUITableHead { + return instanceOf(arg, ['__type__', 'cells']) && arg.__type__ === 'PropsUITableHead' +} + +export interface PropsUITableBody { + __type__: 'PropsUITableBody' + rows: Weak +} +export function isPropsUITableBody (arg: any): arg is PropsUITableBody { + return instanceOf(arg, ['__type__', 'rows']) && arg.__type__ === 'PropsUITableBody' +} + +export interface PropsUITableRow { + __type__: 'PropsUITableRow' + cells: PropsUITableCell[] +} +export function isPropsUITableRow (arg: any): arg is PropsUITableRow { + return instanceOf(arg, ['__type__', 'cells']) && arg.__type__ === 'PropsUITableRow' +} + +export interface PropsUITableCell { + __type__: 'PropsUITableCell' + text: string +} +export function isPropsUITableCell (arg: any): arg is PropsUITableCell { + return instanceOf(arg, ['__type__', 'text']) && arg.__type__ === 'PropsUITableCell' +} + +// BASE TYPES + +export type Text = Translatable | string + +export function isText (arg: any): arg is Text { + return typeof arg === 'string' || isTranslatable(arg) +} + +export interface Translatable { + translations: { [locale: string]: string } +} +export function isTranslatable (arg: any): arg is Translatable { + return instanceOf(arg, ['translations']) +} diff --git a/src/framework/types/modules.ts b/src/framework/types/modules.ts new file mode 100644 index 00000000..f9d48812 --- /dev/null +++ b/src/framework/types/modules.ts @@ -0,0 +1,21 @@ +import { Script, Command, Response, CommandSystem, CommandUI } from './commands' + +export interface ProcessingEngine { + start: (script: Script) => void + commandHandler: CommandHandler + terminate: () => void +} + +export interface VisualisationEngine { + start: (rootElement: HTMLElement, locale: string) => void + render: (command: CommandUI) => Promise + terminate: () => void +} + +export interface System { + send: (command: CommandSystem) => void +} + +export interface CommandHandler { + onCommand: (command: Command) => Promise +} diff --git a/src/framework/types/pages.ts b/src/framework/types/pages.ts new file mode 100644 index 00000000..a445331d --- /dev/null +++ b/src/framework/types/pages.ts @@ -0,0 +1,45 @@ +import { childOf, instanceOf } from '../helpers' +import { PropsUIHeader, PropsUISpinner } from './elements' +import { PropsUIPromptFileInput, PropsUIPromptConfirm, PropsUIPromptConsentForm } from './prompts' + +export type PropsUIPage = + PropsUIPageSplashScreen | + PropsUIPageDonation | + PropsUIPageStart | + PropsUIPageEnd + +export function isPropsUIPage (arg: any): arg is PropsUIPage { + return childOf(arg, 'PropsUIPage') +} + +export interface PropsUIPageSplashScreen { + __type__: 'PropsUIPageSplashScreen' +} +export function isPropsUIPageSplashScreen (arg: any): arg is PropsUIPageSplashScreen { + return instanceOf(arg, ['__type__']) && arg.__type__ === 'PropsUIPageSplashScreen' +} + +export interface PropsUIPageStart { + __type__: 'PropsUIPageStart' +} +export function isPropsUIPageStart (arg: any): arg is PropsUIPageStart { + return instanceOf(arg, ['__type__']) && arg.__type__ === 'PropsUIPageStart' +} + +export interface PropsUIPageDonation { + __type__: 'PropsUIPageDonation' + header: PropsUIHeader + body: PropsUIPromptFileInput | PropsUIPromptConfirm | PropsUIPromptConsentForm + spinner: PropsUISpinner +} +export function isPropsUIPageDonation (arg: any): arg is PropsUIPageDonation { + return instanceOf(arg, ['__type__', 'header', 'body']) && arg.__type__ === 'PropsUIPageDonation' +} + +export interface PropsUIPageEnd { + __type__: 'PropsUIPageEnd' + header: PropsUIHeader +} +export function isPropsUIPageEnd (arg: any): arg is PropsUIPageEnd { + return instanceOf(arg, ['__type__', 'header']) && arg.__type__ === 'PropsUIPageEnd' +} diff --git a/src/framework/types/prompts.ts b/src/framework/types/prompts.ts new file mode 100644 index 00000000..025bac8d --- /dev/null +++ b/src/framework/types/prompts.ts @@ -0,0 +1,61 @@ +import { childOf, instanceOf } from '../helpers' +import { Text } from './elements' + +export type PropsUIPrompt = + PropsUIPromptFileInput | + PropsUIPromptRadioInput | + PropsUIPromptConsentForm + +export function isPropsUIPrompt (arg: any): arg is PropsUIPrompt { + return childOf(arg, 'PropsUIPrompt') +} + +export interface PropsUIPromptConfirm { + __type__: 'PropsUIPromptConfirm' + text: Text + ok: Text + cancel: Text +} +export function isPropsUIPromptConfirm (arg: any): arg is PropsUIPromptConfirm { + return instanceOf(arg, ['__type__', 'text', 'ok', 'cancel']) && arg.__type__ === 'PropsUIPromptConfirm' +} + +export interface PropsUIPromptFileInput { + __type__: 'PropsUIPromptFileInput' + title: Text + description: Text + extensions: string +} +export function isPropsUIPromptFileInput (arg: any): arg is PropsUIPromptFileInput { + return instanceOf(arg, ['__type__', 'title', 'description', 'extensions']) && arg.__type__ === 'PropsUIPromptFileInput' +} + +export interface PropsUIPromptRadioInput { + __type__: 'PropsUIPromptRadioInput' + title: Text + description: Text + items: string[] +} +export function isPropsUIPromptRadioInput (arg: any): arg is PropsUIPromptRadioInput { + return instanceOf(arg, ['__type__', 'title', 'description', 'items']) && arg.__type__ === 'PropsUIPromptRadioInput' +} +export interface PropsUIPromptConsentForm { + __type__: 'PropsUIPromptConsentForm' + title: Text + description: Text + tables: PropsUIPromptConsentFormTable[] +} +export function isPropsUIPromptConsentForm (arg: any): arg is PropsUIPromptConsentForm { + return instanceOf(arg, ['__type__', 'title', 'description', 'tables']) && arg.__type__ === 'PropsUIPromptConsentForm' +} + +export interface PropsUIPromptConsentFormTable { + __type__: 'PropsUIPromptConsentFormTable' + id: string + title: Text + description: Text + data_frame: any +} +export function isPropsUIPromptConsentFormTable (arg: any): arg is PropsUIPromptConsentFormTable { + return instanceOf(arg, ['__type__', 'id', 'title', 'description', 'data_frame']) && arg.__type__ === 'PropsUIPromptConsentFormTable' +} diff --git a/src/framework/visualisation/react/components/button.tsx b/src/framework/visualisation/react/components/button.tsx deleted file mode 100644 index f06bd52d..00000000 --- a/src/framework/visualisation/react/components/button.tsx +++ /dev/null @@ -1,35 +0,0 @@ -interface ButtonProps { - label: string - color?: string - onClick: () => void -} - -export const PrimaryButtonFactory = (props: ButtonProps): JSX.Element => - -export const PrimaryButton = ({ label, color = 'bg-primary text-white', onClick }: ButtonProps): JSX.Element => { - return ( -
-
- {label} -
-
- ) -} - -interface ButtonProps { - label: string - color?: string - onClick: () => void -} - -export const SecondaryButtonFactory = (props: ButtonProps): JSX.Element => - -export const SecondaryButton = ({ label, color = 'bg-delete text-delete', onClick }: ButtonProps): JSX.Element => { - return ( -
-
- {label} -
-
- ) -} diff --git a/src/framework/visualisation/react/components/end_of_flow.tsx b/src/framework/visualisation/react/components/end_of_flow.tsx deleted file mode 100644 index 816e738f..00000000 --- a/src/framework/visualisation/react/components/end_of_flow.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import Translatable from '../../../translatable' -import { Table } from './table' -import { PrimaryButton, SecondaryButton } from './button' -import { Title2 } from './text' - -export interface EndOfFlowProps { - title: string - data: any[] - locale: string - resolve: (value: any) => void -} - -interface Copy { - donateButton: string - declineButton: string -} - -function prepareCopy ({ locale }: EndOfFlowProps): Copy { - return { - donateButton: donateButtonLabel().text(locale), - declineButton: declineButtonLabel().text(locale) - } -} - -const donateButtonLabel = (): Translatable => { - return new Translatable() - .add('en', 'Yes, donate') - .add('nl', 'Ja, doneer') -} - -const declineButtonLabel = (): Translatable => { - return new Translatable() - .add('en', 'No') - .add('nl', 'Nee') -} - -export const EndOfFlowFactory = (props: EndOfFlowProps): JSX.Element => - -export const EndOfFlow = (props: EndOfFlowProps): JSX.Element => { - const { title, data, resolve } = props - const { donateButton, declineButton } = prepareCopy(props) - - function handleDonate (): void { - resolve(JSON.stringify(data)) - } - - function handleDecline (): void { - resolve(false) - } - - function renderTable (table: any): JSX.Element { - const id = table.id as string - const dataFrame = JSON.parse(table.data_frame) - const rowCount = Object.keys(dataFrame).length - - const header = { cells: Object.keys(dataFrame) } - const rows = [] - for (let i = 0; i <= rowCount; i++) { - const cells = Object.keys(dataFrame).map((column: any) => dataFrame[column][`${i}`]) - rows.push({ cells: cells }) - } - const body = { rows: rows } - - return ( -
- ) - } - - const tables = data.map((table) => renderTable(table)) - - return ( - <> - -
-
- {tables} -
-
- - -
-
- - ) -} diff --git a/src/framework/visualisation/react/components/file_input.tsx b/src/framework/visualisation/react/components/file_input.tsx deleted file mode 100644 index 886d5b7b..00000000 --- a/src/framework/visualisation/react/components/file_input.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import * as React from 'react' -import Translatable from '../../../translatable' - -export interface FileInputProps { - title: any - description: any - extensions: string - locale: string - resolve: (value: any) => void -} - -interface Copy { - title: string - description: string - extensions: string - selectButton: string - continueButton: string - resetButton: string -} - -function prepareCopy ({ title, description, extensions, locale }: any): Copy { - return { - title: title.en, - description: description.en, - extensions: extensions, - selectButton: selectButtonLabel().text(locale), - continueButton: continueButtonLabel().text(locale), - resetButton: resetButtonLabel().text(locale) - } -} - -export const FileInputFactory = (props: FileInputProps): JSX.Element => - -const FileInput = (props: FileInputProps): JSX.Element => { - const [selectedFile, setSelectedFile] = React.useState() - const [confirmHidden, setConfirmHidden] = React.useState(true) - const input = React.useRef(null) - - const { resolve } = props - const { title, description, extensions, selectButton, continueButton, resetButton } = prepareCopy(props) - - function handleClick (): void { - input.current?.click() - } - - function handleReset (): void { - handleClick() - } - - function handleSelect (event: React.ChangeEvent): void { - const files = event.target.files - if (files != null && files.length > 0) { - setSelectedFile(files[0]) - setConfirmHidden(false) - } - } - - function handleConfirm (): void { - resolve(selectedFile) - } - - return ( - <> -
- {title} -
-
- -
-
- {description} -
-
-
-
-
- {selectButton} -
-
-
- -
-
- -
-
-
-
-
{selectedFile?.name}
-
-
-
-
-
- Continue with the selected file, or select again? -
-
-
-
-
- {continueButton} -
-
-
-
- {resetButton} -
-
-
-
- - ) -} - -const continueButtonLabel = (): Translatable => { - return new Translatable() - .add('en', 'Continue') - .add('nl', 'Doorgaan') -} - -const selectButtonLabel = (): Translatable => { - return new Translatable() - .add('en', 'Select file') - .add('nl', 'Selecteer bestand') -} - -const resetButtonLabel = (): Translatable => { - return new Translatable() - .add('en', 'Select again') - .add('nl', 'Opnieuw') -} diff --git a/src/framework/visualisation/react/components/spinner.tsx b/src/framework/visualisation/react/components/spinner.tsx deleted file mode 100644 index 43e9f7a0..00000000 --- a/src/framework/visualisation/react/components/spinner.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import Translatable from '../../../translatable' -import SpinnerSvg from '../../../../assets/images/spinner.svg' - -export interface SpinnerProps { - locale: string -} - -interface Copy { - text: string -} - -function prepareCopy ({ texts, locale }: any): Copy { - return { - text: texts.text(locale) - } -} - -const texts = (): Translatable => { - return new Translatable() - .add('en', 'One moment please') - .add('nl', 'Een moment geduld') -} - -export const SpinnerFactory = (props: SpinnerProps): JSX.Element => - -export const Spinner = (props: SpinnerProps): JSX.Element => { - const { text } = prepareCopy({ texts: texts(), ...props }) - - return ( -
-
{text}
-
- -
-
- ) -} diff --git a/src/framework/visualisation/react/components/table.tsx b/src/framework/visualisation/react/components/table.tsx deleted file mode 100644 index 8e963c1a..00000000 --- a/src/framework/visualisation/react/components/table.tsx +++ /dev/null @@ -1,62 +0,0 @@ -interface TableProps { - id: string - header: TableHeadProps - body: TableBodyProps -} - -export const TableFactory = (props: TableProps): JSX.Element =>
- -export const Table = ({ id, header, body }: TableProps): JSX.Element => { - return ( -
- - -
- ) -} - -interface TableHeadProps { - cells: string[] -} - -export const TableHead = ({ cells }: TableHeadProps): JSX.Element => { - return ( - {cells.map((cell, index) => )} - ) -} - -interface TableBodyProps { - rows: TableRowProps[] -} - -export const TableBody = ({ rows }: TableBodyProps): JSX.Element => { - return ( - - {rows.map((row, index) => { return () })} - - ) -} - -interface TableRowProps { - header?: boolean - cells: string[] -} - -export const TableRow = ({ header = false, cells }: TableRowProps): JSX.Element => { - return ( - {cells.map((cell, index) => )} - ) -} - -interface TableCellProps { - header?: boolean - cell: string -} - -export const TableCell = ({ header = false, cell }: TableCellProps): JSX.Element => { - return ( - header - ? {cell} - : {cell} - ) -} diff --git a/src/framework/visualisation/react/engine.tsx b/src/framework/visualisation/react/engine.tsx index e9f94c85..0241cef0 100644 --- a/src/framework/visualisation/react/engine.tsx +++ b/src/framework/visualisation/react/engine.tsx @@ -1,130 +1,56 @@ import * as ReactDOM from 'react-dom/client' -import VisualisationEngine from '../../abstractions/visualisation_engine' -import ProcessingEngine from '../../abstractions/processing_engine' +import { VisualisationEngine } from '../../types/modules' +import { Response, Payload, CommandUIRender } from '../../types/commands' +import { PropsUIPage } from '../../types/pages' import VisualisationFactory from './factory' import { Main } from './main' export default class ReactEngine implements VisualisationEngine { factory: VisualisationFactory - processingEngine: ProcessingEngine - onEvent: (event: any) => void locale!: string - script!: string root!: ReactDOM.Root - finishFlow!: (value: unknown) => void - - constructor (factory: VisualisationFactory, processingEngine: ProcessingEngine) { + constructor (factory: VisualisationFactory) { this.factory = factory - this.processingEngine = processingEngine - this.onEvent = (event) => { - this.handleEvent(event) - } } - async start (script: string, rootElement: HTMLElement, locale: string): Promise { + start (rootElement: HTMLElement, locale: string): void { console.log('[ReactEngine] started') - this.script = script this.root = ReactDOM.createRoot(rootElement) this.locale = locale - this.showStartPage() - this.processingEngine.start() - return await new Promise((resolve) => { - this.finishFlow = resolve - }) - } - - terminate (): void { - this.processingEngine.terminate() - } - - renderPage (elements: JSX.Element[]): void { - this.root.render(
) - } - - showSpinner (): void { - const spinner = this.create('Spinner') - this.renderPage([spinner]) + this.renderSplashScreen() } - showStartPage (): void { - const welcome = this.create('Title0', { text: 'Welcome' }) - const spinner = this.create('Spinner') - this.renderPage([welcome, spinner]) - } - - showFinalPage (): void { - const thanks = this.create('Title0', { text: 'Thank you' }) - this.renderPage([thanks]) + async render (command: CommandUIRender): Promise { + return await new Promise((resolve) => { + this.renderPage(command.page).then( + (payload: Payload) => { + resolve({ command, payload }) + }, + () => {} + ) + }) } - create (type: string, props: any = {}): JSX.Element { - return this.factory.createComponent({ __type__: type, ...props }, this.locale, () => {}) + renderSplashScreen (): void { + const context = { locale: this.locale, resolve: () => {} } + const page = this.factory.createPage({ __type__: 'PropsUIPageSplashScreen' }, context) + this.renderElements([page]) } - handleEvent (event: any): void { - const { eventType } = event.data - console.log('[ReactEngine] received eventType: ', eventType) - switch (eventType) { - case 'initialiseDone': - console.log('[ReactEngine] received: initialiseDone') - this.processingEngine.loadScript(this.script) - break - - case 'loadScriptDone': - console.log('[ReactEngine] Received: loadScriptDone') - this.processingEngine.firstRunCycle() - break - - case 'runCycleDone': - console.log('[ReactEngine] received: event', event.data.scriptEvent) - this.handleRunCycle(event.data.scriptEvent) - break - default: - console.log( - '[ReactEngine] received unsupported flow event: ', - eventType - ) - } + async renderPage (props: PropsUIPage): Promise { + console.log('[ReactEngine] render page: ' + JSON.stringify(props)) + return await new Promise((resolve) => { + const context = { locale: this.locale, resolve } + const page = this.factory.createPage(props, context) + this.renderElements([page]) + }) } - handleRunCycle (scriptEvent: any): void { - const type = scriptEvent.__type__ as string - if (type.startsWith('Event.EndOfFlow')) { - this.renderComponent(scriptEvent).then( - (result) => { - this.showFinalPage() - this.finishFlow?.(result) - }, - null) - return - } + terminate (): void {} - if (type.startsWith('Event.Command.Prompt')) { - this.renderComponent(scriptEvent).then( - (userInput) => { - this.showSpinner() - this.processingEngine.nextRunCycle({ - prompt: scriptEvent, - userInput: userInput - }) - }, - null) - return - } - - console.log( - '[ReactEngine] Received unsupported script event: ', - type - ) - } - - async renderComponent (data: any): Promise { - const locale = this.locale - return await new Promise((resolve) => { - const component = this.factory.createComponent(data, locale, resolve) - this.renderPage([component]) - }) + renderElements (elements: JSX.Element[]): void { + this.root.render(
) } } diff --git a/src/framework/visualisation/react/factory.tsx b/src/framework/visualisation/react/factory.tsx index 9dab13a7..2d261b01 100644 --- a/src/framework/visualisation/react/factory.tsx +++ b/src/framework/visualisation/react/factory.tsx @@ -1,38 +1,30 @@ -import { TableFactory } from './components/table' -import { SpinnerFactory } from './components/spinner' -import { FileInputFactory } from './components/file_input' -import { RadioInputFactory } from './components/radio_input' -import { EndOfFlowFactory } from './components/end_of_flow' -import { Title0Factory, Title1Factory, Title2Factory } from './components/text' +import { EndPage } from './ui/pages/end_page' +import { StartPage } from './ui/pages/start_page' +import { isPropsUIPageEnd, isPropsUIPageDonation, isPropsUIPageStart, PropsUIPage, isPropsUIPageSplashScreen } from '../../types/pages' +import { DonationPage } from './ui/pages/donation_page' +import { Payload } from '../../types/commands' +import { SplashScreen } from './ui/pages/splash_screen' -export default class ReactFactory { - mapping: { [name: string]: (props: any) => JSX.Element} = {} - - constructor () { - this.mapping.Table = TableFactory - this.mapping.Spinner = SpinnerFactory - this.mapping.FileInput = FileInputFactory - this.mapping.RadioInput = RadioInputFactory - this.mapping.EndOfFlow = EndOfFlowFactory - this.mapping.Title0 = Title0Factory - this.mapping.Title1 = Title1Factory - this.mapping.Title2 = Title2Factory - } - - add (factory: (props: any) => JSX.Element, name: string): void { - this.mapping[name] = factory - } - - createComponent (data: any, locale: string, resolve: (value: any) => void): JSX.Element { - const type = data.__type__.split('.').pop() as string - const props = { ...data, locale, resolve } +export interface ReactFactoryContext { + locale: string + resolve?: (payload: Payload) => void +} - if (this.mapping[type] !== null) { - const factoryMethod = this.mapping[type] - return factoryMethod(props) - } else { - throw new Error(`[ReactFactory] Received unsupported prompt: ${type}`) +export default class ReactFactory { + createPage (page: PropsUIPage, context: ReactFactoryContext): JSX.Element { + if (isPropsUIPageSplashScreen(page)) { + return + } + if (isPropsUIPageStart(page)) { + return + } + if (isPropsUIPageEnd(page)) { + return + } + if (isPropsUIPageDonation(page)) { + return } + throw TypeError('Unknown page: ' + JSON.stringify(page)) } } diff --git a/src/framework/visualisation/react/ui/elements/button.tsx b/src/framework/visualisation/react/ui/elements/button.tsx new file mode 100644 index 00000000..72c78216 --- /dev/null +++ b/src/framework/visualisation/react/ui/elements/button.tsx @@ -0,0 +1,53 @@ +import { Weak } from '../../../../helpers' +import { PropsUIButtonForward, PropsUIButtonLabel, PropsUIButtonPrimary, PropsUIButtonSecundary } from '../../../../types/elements' + +import ForwardSvg from '../../../../../assets/images/forward.svg' + +export const PrimaryButton = ({ label, color = 'bg-primary text-white', onClick }: Weak): JSX.Element => { + return ( +
+
+ {label} +
+
+ ) +} + +export const SecondaryButton = ({ label, color = 'bg-delete text-delete', onClick }: Weak): JSX.Element => { + return ( +
+
+ {label} +
+
+ ) +} + +export const ForwardButton = ({ label, onClick }: Weak): JSX.Element => { + return ( +
+
+
+
+
+ {label} +
+
+
+
+ {label} +
+
+
+ ) +} + +export const LabelButton = ({ label, onClick }: Weak): JSX.Element => { + return ( +
+
+ {label} +
+
+ ) +} diff --git a/src/framework/visualisation/react/ui/elements/header.tsx b/src/framework/visualisation/react/ui/elements/header.tsx new file mode 100644 index 00000000..9d7183e2 --- /dev/null +++ b/src/framework/visualisation/react/ui/elements/header.tsx @@ -0,0 +1,25 @@ +import { Weak } from '../../../../helpers' +import { Translator } from '../../../../translator' +import { PropsUIHeader } from '../../../../types/elements' +import { ReactFactoryContext } from '../../factory' +import { Title0 } from './text' + +interface Copy { + title: string +} + +type Props = Weak & ReactFactoryContext + +function prepareCopy ({ title, locale }: Props): Copy { + return { + title: Translator.translate(title, locale) + } +} + +export const Header = (props: Props): JSX.Element => { + const { title } = prepareCopy(props) + + return ( + + ) +} diff --git a/src/framework/visualisation/react/ui/elements/spinner.tsx b/src/framework/visualisation/react/ui/elements/spinner.tsx new file mode 100644 index 00000000..2ca1f751 --- /dev/null +++ b/src/framework/visualisation/react/ui/elements/spinner.tsx @@ -0,0 +1,30 @@ +import SpinnerSvg from '../../../../../assets/images/spinner.svg' +import { Weak } from '../../../../helpers' +import { Translator } from '../../../../translator' +import { PropsUISpinner } from '../../../../types/elements' +import { ReactFactoryContext } from '../../factory' + +interface Copy { + text: string +} + +function prepareCopy ({ text, locale }: Props): Copy { + return { + text: Translator.translate(text, locale) + } +} + +type Props = Weak & ReactFactoryContext + +export const Spinner = (props: Props): JSX.Element => { + const { text } = prepareCopy(props) + + return ( +
+
{text}
+
+ +
+
+ ) +} diff --git a/src/framework/visualisation/react/ui/elements/table.tsx b/src/framework/visualisation/react/ui/elements/table.tsx new file mode 100644 index 00000000..7e00c95f --- /dev/null +++ b/src/framework/visualisation/react/ui/elements/table.tsx @@ -0,0 +1,35 @@ +import { Weak } from '../../../../helpers' +import { PropsUITable, PropsUITableBody, PropsUITableCell, PropsUITableHead, PropsUITableRow } from '../../../../types/elements' + +export const Table = ({ id, head, body }: Weak): JSX.Element => { + function renderHead (props: Weak): JSX.Element { + return {props.cells.map((cell, index) => renderHeadCell(cell, index))} + } + + function renderHeadCell (props: Weak, index: number): JSX.Element { + return {props.text} + } + + function renderRows (props: Weak): JSX.Element[] { + return props.rows.map((row, index) => renderRow(row, index)) + } + + function renderRow (row: Weak, rowIndex: number): JSX.Element { + return {row.cells.map((cell, cellIndex) => renderRowCell(cell, cellIndex))} + } + + function renderRowCell (props: Weak, cellIndex: number): JSX.Element { + return {props.text} + } + + return ( + + + {renderHead(head)} + + + {renderRows(body)} + +
+ ) +} diff --git a/src/framework/visualisation/react/components/text.tsx b/src/framework/visualisation/react/ui/elements/text.tsx similarity index 53% rename from src/framework/visualisation/react/components/text.tsx rename to src/framework/visualisation/react/ui/elements/text.tsx index d88dcc46..8039d558 100644 --- a/src/framework/visualisation/react/components/text.tsx +++ b/src/framework/visualisation/react/ui/elements/text.tsx @@ -1,12 +1,15 @@ +import { Weak } from '../../../../helpers' +import { PropsUITextBody, PropsUITextTitle0, PropsUITextTitle1, PropsUITextTitle2 } from '../../../../types/elements' -interface TextProps { - text: string - color?: string - margin?: string +export const BodyLarge = ({ text, color = 'text-grey1', margin = 'mb-6 md:mb-8 lg:mb-10' }: Weak): JSX.Element => { + return ( +
+ {text} +
+ ) } -export const Title0Factory = (props: TextProps): JSX.Element => -export const Title0 = ({ text, color = 'text-grey1', margin = 'mb-6 md:mb-8 lg:mb-10' }: TextProps): JSX.Element => { +export const Title0 = ({ text, color = 'text-grey1', margin = 'mb-6 md:mb-8 lg:mb-10' }: Weak): JSX.Element => { return (
{text} @@ -14,8 +17,7 @@ export const Title0 = ({ text, color = 'text-grey1', margin = 'mb-6 md:mb-8 lg:m ) } -export const Title1Factory = (props: TextProps): JSX.Element => -export const Title1 = ({ text, color = 'text-grey1', margin = 'mb-6 md:mb-8 lg:mb-10' }: TextProps): JSX.Element => { +export const Title1 = ({ text, color = 'text-grey1', margin = 'mb-6 md:mb-8 lg:mb-10' }: Weak): JSX.Element => { return (
{text} @@ -23,8 +25,7 @@ export const Title1 = ({ text, color = 'text-grey1', margin = 'mb-6 md:mb-8 lg:m ) } -export const Title2Factory = (props: TextProps): JSX.Element => -export const Title2 = ({ text, color = 'text-grey1', margin = 'mb-6 md:mb-8 lg:mb-10' }: TextProps): JSX.Element => { +export const Title2 = ({ text, color = 'text-grey1', margin = 'mb-6 md:mb-8 lg:mb-10' }: Weak): JSX.Element => { return (
{text} diff --git a/src/framework/visualisation/react/ui/pages/donation_page.tsx b/src/framework/visualisation/react/ui/pages/donation_page.tsx new file mode 100644 index 00000000..0d7ef253 --- /dev/null +++ b/src/framework/visualisation/react/ui/pages/donation_page.tsx @@ -0,0 +1,53 @@ +import React from 'react' +import { Weak } from '../../../../helpers' +import { Translator } from '../../../../translator' +import { PropsUIPageDonation } from '../../../../types/pages' +import { isPropsUIPromptConfirm, isPropsUIPromptConsentForm, isPropsUIPromptFileInput } from '../../../../types/prompts' +import { ReactFactoryContext } from '../../factory' +import { Spinner } from '../elements/spinner' +import { Title0 } from '../elements/text' +import { Confirm } from '../prompts/confirm' +import { ConsentForm } from '../prompts/consent_form' +import { FileInput } from '../prompts/file_input' + +interface Copy { + title: string +} + +type Props = Weak & ReactFactoryContext + +function prepareCopy ({ header: { title }, locale }: Props): Copy { + return { + title: Translator.translate(title, locale) + } +} + +export const DonationPage = (props: Props): JSX.Element => { + const [spinnerHidden] = React.useState(true) + const { title } = prepareCopy(props) + + function renderBody (props: Props): JSX.Element { + const context = { locale: props.locale, resolve: props.resolve } + const body = props.body + if (isPropsUIPromptFileInput(body)) { + return + } + if (isPropsUIPromptConfirm(body)) { + return + } + if (isPropsUIPromptConsentForm(body)) { + return + } + throw new TypeError('Unknown body type') + } + + return ( + <> + + {renderBody(props)} +
+ +
+ + ) +} diff --git a/src/framework/visualisation/react/ui/pages/end_page.tsx b/src/framework/visualisation/react/ui/pages/end_page.tsx new file mode 100644 index 00000000..dabbde05 --- /dev/null +++ b/src/framework/visualisation/react/ui/pages/end_page.tsx @@ -0,0 +1,12 @@ +import { Weak } from '../../../../helpers' +import { PropsUIPageEnd } from '../../../../types/pages' +import { ReactFactoryContext } from '../../factory' +import { Header } from '../elements/header' + +type Props = Weak & ReactFactoryContext + +export const EndPage = (props: Props): JSX.Element => { + return ( +
+ ) +} diff --git a/src/framework/visualisation/react/ui/pages/splash_screen.tsx b/src/framework/visualisation/react/ui/pages/splash_screen.tsx new file mode 100644 index 00000000..9444841d --- /dev/null +++ b/src/framework/visualisation/react/ui/pages/splash_screen.tsx @@ -0,0 +1,44 @@ +import { Weak } from '../../../../helpers' +import TextBundle from '../../../../text_bundle' +import { Translator } from '../../../../translator' +import { PropsUIPageSplashScreen } from '../../../../types/pages' +import { ReactFactoryContext } from '../../factory' +import { Spinner } from '../elements/spinner' +import { BodyLarge, Title0 } from '../elements/text' + +interface Copy { + title: string + description: string +} + +type Props = Weak & ReactFactoryContext + +function prepareCopy ({ locale }: Props): Copy { + return { + title: Translator.translate(title, locale), + description: Translator.translate(description, locale) + } +} + +export const SplashScreen = (props: Props): JSX.Element => { + const { title, description } = prepareCopy(props) + + return ( + <> + + + + + ) +} +const title = new TextBundle() + .add('en', 'Welcome') + .add('nl', 'Welkom') + +const description = new TextBundle() + .add('en', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.') + .add('nl', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.') + +const spinnerText = new TextBundle() + .add('en', 'One moment please..') + .add('nl', 'Een moment geduld..') diff --git a/src/framework/visualisation/react/ui/pages/start_page.tsx b/src/framework/visualisation/react/ui/pages/start_page.tsx new file mode 100644 index 00000000..d3a023e9 --- /dev/null +++ b/src/framework/visualisation/react/ui/pages/start_page.tsx @@ -0,0 +1,54 @@ +import { Weak } from '../../../../helpers' +import TextBundle from '../../../../text_bundle' +import { Translator } from '../../../../translator' +import { PropsUIPageStart } from '../../../../types/pages' +import { ReactFactoryContext } from '../../factory' +import { PrimaryButton } from '../elements/button' +import { BodyLarge, Title0 } from '../elements/text' + +type Props = Weak & ReactFactoryContext + +export const StartPage = (props: Props): JSX.Element => { + const { resolve } = props + const { title, description, startButton } = prepareCopy(props) + + function handleStart (): void { + resolve?.({ __type__: 'PayloadVoid', value: undefined }) + } + + return ( + <> + + +
+ +
+ + ) +} + +interface Copy { + title: string + description: string + startButton: string +} + +function prepareCopy ({ locale }: Props): Copy { + return { + title: Translator.translate(title, locale), + description: Translator.translate(description, locale), + startButton: Translator.translate(startButtonLabel, locale) + } +} + +const title = new TextBundle() + .add('en', 'Instructions') + .add('nl', 'Instructies') + +const startButtonLabel = new TextBundle() + .add('en', 'Start') + .add('nl', 'Start') + +const description = new TextBundle() + .add('en', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.') + .add('nl', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.') diff --git a/src/framework/visualisation/react/ui/prompts/confirm.tsx b/src/framework/visualisation/react/ui/prompts/confirm.tsx new file mode 100644 index 00000000..989c5d4c --- /dev/null +++ b/src/framework/visualisation/react/ui/prompts/confirm.tsx @@ -0,0 +1,45 @@ +import { Weak } from '../../../../helpers' +import { ReactFactoryContext } from '../../factory' +import { PropsUIPromptConfirm } from '../../../../types/prompts' +import { Translator } from '../../../../translator' +import { BodyLarge } from '../elements/text' +import { LabelButton, PrimaryButton } from '../elements/button' + +type Props = Weak & ReactFactoryContext + +export const Confirm = (props: Props): JSX.Element => { + const { resolve } = props + const { text, ok, cancel } = prepareCopy(props) + + function handleOk (): void { + resolve?.({ __type__: 'PayloadTrue', value: true }) + } + + function handleCancel (): void { + resolve?.({ __type__: 'PayloadFalse', value: false }) + } + + return ( + <> + +
+ + +
+ + ) +} + +interface Copy { + text: string + ok: string + cancel: string +} + +function prepareCopy ({ text, ok, cancel, locale }: Props): Copy { + return { + text: Translator.translate(text, locale), + ok: Translator.translate(ok, locale), + cancel: Translator.translate(cancel, locale) + } +} diff --git a/src/framework/visualisation/react/ui/prompts/consent_form.tsx b/src/framework/visualisation/react/ui/prompts/consent_form.tsx new file mode 100644 index 00000000..c4688a0d --- /dev/null +++ b/src/framework/visualisation/react/ui/prompts/consent_form.tsx @@ -0,0 +1,122 @@ +import { Weak } from '../../../../helpers' +import { PropsUITable, PropsUITableCell, PropsUITableRow, Translatable } from '../../../../types/elements' +import { PropsUIPromptConsentForm } from '../../../../types/prompts' +import { Table } from '../elements/table' +import { PrimaryButton, SecondaryButton } from '../elements/button' +import { BodyLarge, Title1, Title2 } from '../elements/text' +import TextBundle from '../../../../text_bundle' +import { Translator } from '../../../../translator' +import { ReactFactoryContext } from '../../factory' + +type Props = Weak & ReactFactoryContext + +export const ConsentForm = (props: Props): JSX.Element => { + const { tables, resolve } = props + const { title, description, donateButton, declineButton } = prepareCopy(props) + + function handleDonate (): void { + resolve?.({ __type__: 'PayloadString', value: JSON.stringify(tables) }) + } + + function handleDecline (): void { + resolve?.({ __type__: 'PayloadFalse', value: false }) + } + + function rowCell (dataFrame: any, column: string, row: number): PropsUITableCell { + const text = dataFrame[column][`${row}`] as string + return { __type__: 'PropsUITableCell', text: text } + } + + function headCell (dataFrame: any, column: string): PropsUITableCell { + return { __type__: 'PropsUITableCell', text: column } + } + + function columnNames (dataFrame: any): string[] { + return Object.keys(dataFrame) + } + + function columnCount (dataFrame: any): number { + return columnNames(dataFrame).length + } + + function rowCount (dataFrame: any): number { + if (columnCount(dataFrame) === 0) { + return 0 + } else { + const firstColumn = dataFrame[columnNames(dataFrame)[0]] + return Object.keys(firstColumn).length + } + } + + function rows (data: any): PropsUITableRow[] { + const result: PropsUITableRow[] = [] + for (let row = 0; row <= rowCount(data); row++) { + const cells = columnNames(data).map((column: string) => rowCell(data, column, row)) + result.push({ __type__: 'PropsUITableRow', cells: cells }) + } + return result + } + + function parse (tableData: any): PropsUITable { + const id = tableData.id as string + const dataFrame = JSON.parse(tableData.data_frame) + const head = { cells: columnNames(dataFrame).map((column: string) => headCell(dataFrame, column)) } + const body = { rows: rows(dataFrame) } + + return { __type__: 'PropsUITable', id, head, body } + } + + function renderTable (tableData: any): JSX.Element { + const title = tableData.title as string + const tableProps = parse(tableData) + + return ( +
+ + + + ) + } + + return ( + <> + + +
+ {tables.map((table) => renderTable(table))} +
+ + +
+
+ + ) +} + +interface Copy { + title: string + description: string + donateButton: string + declineButton: string +} + +function prepareCopy ({ title, description, locale }: Props): Copy { + return { + title: Translator.translate(title, locale), + description: Translator.translate(description, locale), + donateButton: Translator.translate(donateButtonLabel(), locale), + declineButton: Translator.translate(declineButtonLabel(), locale) + } +} + +const donateButtonLabel = (): Translatable => { + return new TextBundle() + .add('en', 'Yes, donate') + .add('nl', 'Ja, doneer') +} + +const declineButtonLabel = (): Translatable => { + return new TextBundle() + .add('en', 'No') + .add('nl', 'Nee') +} diff --git a/src/framework/visualisation/react/ui/prompts/file_input.tsx b/src/framework/visualisation/react/ui/prompts/file_input.tsx new file mode 100644 index 00000000..8c00b7dd --- /dev/null +++ b/src/framework/visualisation/react/ui/prompts/file_input.tsx @@ -0,0 +1,116 @@ +import { Weak } from '../../../../helpers' +import * as React from 'react' +import { Translatable } from '../../../../types/elements' +import TextBundle from '../../../../text_bundle' +import { Translator } from '../../../../translator' +import { ReactFactoryContext } from '../../factory' +import { PropsUIPromptFileInput } from '../../../../types/prompts' +import { ForwardButton, PrimaryButton } from '../elements/button' + +type Props = Weak & ReactFactoryContext + +export const FileInput = (props: Props): JSX.Element => { + const [selectedFile, setSelectedFile] = React.useState() + const [confirmHidden, setConfirmHidden] = React.useState(true) + const input = React.useRef(null) + + const { resolve } = props + const { title, description, extensions, selectButton, continueButton, forwardButton } = prepareCopy(props) + + function handleClick (): void { + input.current?.click() + } + + function handleSkip (): void { + resolve?.({ __type__: 'PayloadFalse', value: false }) + } + + function handleSelect (event: React.ChangeEvent): void { + const files = event.target.files + if (files != null && files.length > 0) { + setSelectedFile(files[0]) + setConfirmHidden(false) + } else { + console.log('Error selecting file: ' + JSON.stringify(files)) + } + } + + function handleConfirm (): void { + if (selectedFile !== undefined) { + resolve?.({ __type__: 'PayloadFile', value: selectedFile }) + } + } + + return ( + <> +
+ {title} +
+
+ +
+
+ {description} +
+
+
+
+
+ {selectButton} +
+
+
+
{selectedFile?.name}
+
+
+ +
+
+ +
+
+ +
+ +
+ + ) +} + +interface Copy { + title: string + description: string + extensions: string + selectButton: string + continueButton: string + forwardButton: string +} + +function prepareCopy ({ title, description, extensions, locale }: Props): Copy { + return { + title: Translator.translate(title, locale), + description: Translator.translate(description, locale), + extensions: extensions, + selectButton: Translator.translate(selectButtonLabel(), locale), + continueButton: Translator.translate(continueButtonLabel(), locale), + forwardButton: Translator.translate(forwardButtonLabel(), locale) + } +} + +const continueButtonLabel = (): Translatable => { + return new TextBundle() + .add('en', 'Continue') + .add('nl', 'Doorgaan') +} + +const selectButtonLabel = (): Translatable => { + return new TextBundle() + .add('en', 'Choose file') + .add('nl', 'Kies bestand') +} + +const forwardButtonLabel = (): Translatable => { + return new TextBundle() + .add('en', 'Skip this step') + .add('nl', 'Sla deze stap over') +} diff --git a/src/framework/visualisation/react/components/radio_input.tsx b/src/framework/visualisation/react/ui/prompts/radio_input.tsx similarity index 68% rename from src/framework/visualisation/react/components/radio_input.tsx rename to src/framework/visualisation/react/ui/prompts/radio_input.tsx index 682930ed..e23c5223 100644 --- a/src/framework/visualisation/react/components/radio_input.tsx +++ b/src/framework/visualisation/react/ui/prompts/radio_input.tsx @@ -1,15 +1,12 @@ import * as React from 'react' -import Translatable from '../../../translatable' -import RadioSvg from '../../../../assets/images/radio.svg' -import RadioActiveSvg from '../../../../assets/images/radio_active.svg' - -export interface RadioInputProps { - title: any - description: any - items: string[] - locale: string - resolve: (value: any) => void -} +import { Weak } from '../../../../helpers' +import { Translatable } from '../../../../types/elements' +import TextBundle from '../../../../text_bundle' +import RadioSvg from '../../../../../assets/images/spinner.svg' +import RadioActiveSvg from '../../../../../assets/images/radio_active.svg' +import { Translator } from '../../../../translator' +import { ReactFactoryContext } from '../../factory' +import { PropsUIPromptRadioInput } from '../../../../types/prompts' interface Copy { title: string @@ -17,17 +14,19 @@ interface Copy { continueButton: string } -function prepareCopy ({ title, description, locale }: RadioInputProps): Copy { +type Props = Weak & ReactFactoryContext + +function prepareCopy ({ title, description, locale }: Props): Copy { return { - title: title.en, - description: description.en, - continueButton: continueButtonLabel().text(locale) + title: Translator.translate(title, locale), + description: Translator.translate(description, locale), + continueButton: Translator.translate(continueButtonLabel(), locale) } } -export const RadioInputFactory = (props: RadioInputProps): JSX.Element => +export const RadioInputFactory = (props: Props): JSX.Element => -export const RadioInput = (props: RadioInputProps): JSX.Element => { +export const RadioInput = (props: Props): JSX.Element => { const [selectedId, setSelectedId] = React.useState(-1) const [confirmHidden, setConfirmHidden] = React.useState(true) @@ -40,8 +39,10 @@ export const RadioInput = (props: RadioInputProps): JSX.Element => { } function handleConfirm (): void { - const value = items.at(selectedId) - resolve(value) + const item = items.at(selectedId) + if (item !== undefined) { + resolve?.({ __type__: 'PayloadString', value: item }) + } } return ( @@ -74,7 +75,7 @@ export const RadioInput = (props: RadioInputProps): JSX.Element => { } const continueButtonLabel = (): Translatable => { - return new Translatable() + return new TextBundle() .add('en', 'Continue') .add('nl', 'Doorgaan') } diff --git a/src/index.tsx b/src/index.tsx index 7ba408b5..470ba56c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,14 +1,16 @@ import './fonts.css' import './framework/styles.css' -import { Assembly } from './framework/assembly' +import Assembly from './framework/assembly' import { PyScript } from './py_script' +import LocalSystem from './local_system' +const rootElement = document.getElementById('root') as HTMLElement + +const locale = 'en' +const system = new LocalSystem() const workerFile = new URL('./framework/processing/python/worker.js', import.meta.url) const worker = new Worker(workerFile) -const rootElement = document.getElementById('root') as HTMLElement -const visualisationEngine = Assembly(worker) -visualisationEngine.start(PyScript, rootElement, 'en').then( - () => {}, - () => {} -) +const assembly = new Assembly(worker, system) +assembly.visualisationEngine.start(rootElement, locale) +assembly.processingEngine.start(PyScript) diff --git a/src/local_system.ts b/src/local_system.ts new file mode 100644 index 00000000..e5fecd01 --- /dev/null +++ b/src/local_system.ts @@ -0,0 +1,16 @@ +import { CommandSystem, CommandSystemDonate, isCommandSystemDonate } from './framework/types/commands' +import { System } from './framework/types/modules' + +export default class LocalSystem implements System { + send (command: CommandSystem): void { + if (isCommandSystemDonate(command)) { + this.handleDonation(command) + } else { + console.log('[LocalSystem] received unknown command: ' + JSON.stringify(command)) + } + } + + handleDonation (command: CommandSystemDonate): void { + console.log('[LocalSystem] received donation: ' + JSON.stringify(command)) + } +} diff --git a/src/py_script.ts b/src/py_script.ts index a2bc957a..21b1d0a3 100644 --- a/src/py_script.ts +++ b/src/py_script.ts @@ -1,64 +1,137 @@ export const PyScript: string = ` import pandas as pd - +import zipfile def process(): - chat_file_name = yield prompt_file() - usernames = extract_usernames(chat_file_name) - username = yield prompt_radio(usernames) - yield result(usernames, username) - - -def prompt_file(): - title = Translatable() - title.add("en", "Step 1: Select the chat file") - title.add("nl", "Stap 1: Selecteer het chat file") - - description = Translatable() - description.add("en", "We previously asked you to export a chat file from Whatsapp. Please select this file so we can extract relevant information for our research.") - description.add("nl", "We hebben je gevraagd een chat bestand te exporteren uit Whatsapp. Je kan deze file nu selecteren zodat wij er relevante informatie uit kunnen halen voor ons onderzoek.") - - extensions = "application/zip, text/plain" - - return FileInput(title, description, extensions) - - -def prompt_radio(usernames): - title = Translatable() - title.add("en", "Step 2: Select your username") - title.add("nl", "Stap 2: Selecteer je gebruikersnaam") - - description = Translatable() - description.add("en", "The following users are extracted from the chat file. Which one are you?") - description.add("nl", "De volgende gebruikers hebben we uit de chat file gehaald. Welke ben jij?") - - return RadioInput(title, description, usernames) - - -def extract_usernames(chat_file_name): - print(f"filename: {chat_file_name}") - - with open(chat_file_name) as chat_file: - while (line := chat_file.readline().rstrip()): - print(line) - - return ["emielvdveen", "a.m.mendrik", "9bitcat"] - - -def result(usernames, selected_username): - data = [] - for username in usernames: - description = "you" if username == selected_username else "-" - data.append((username, description)) - - data_frame = pd.DataFrame(data, columns=["username", "description"]) - - print(data_frame) - - result = [{ - "id": "overview", - "title": "The following usernames where extracted:", - "data_frame": data_frame - }] - return EndOfFlow(result) + yield render_start_page() + + platforms = ["Twitter", "Instagram", "Youtube"] + for index, platform in enumerate(platforms): + data = None + while True: + promptFile = prompt_file(platform, "application/zip, text/plain") + fileResult = yield render_donation_page(index+1, platform, promptFile) + if fileResult.__type__ == 'PayloadString': + extractionResult = doSomethingWithTheFile(platform, fileResult.value) + if extractionResult != 'invalid': + data = extractionResult + break + else: + retry_result = yield render_donation_page(index+1, platform, retry_confirmation()) + if retry_result.__type__ == 'PayloadTrue': + continue + else: + break + else: + break + + if data is not None: + prompt = prompt_consent(platform, data) + consent_result = yield render_donation_page(index+1, platform, prompt) + if consent_result.__type__ == "PayloadString": + yield donate(platform, consent_result.value) + + yield render_end_page() + + +def render_start_page(): + header = PropsUIHeader(Translatable({ + "en": "Welcome", + "nl": "Welkom" + })) + page = PropsUIPageStart(header, spinner()) + return CommandUIRender(page) + + +def render_end_page(): + header = PropsUIHeader(Translatable({ + "en": "Thank you", + "nl": "Dank je wel" + })) + page = PropsUIPageEnd(header) + return CommandUIRender(page) + + +def render_donation_page(index, platform, body): + header = PropsUIHeader(Translatable({ + "en": f"Step {index}: {platform}", + "nl": f"Stap {index}: {platform}" + })) + page = PropsUIPageDonation(header, body, spinner()) + return CommandUIRender(page) + + +def retry_confirmation(): + text = Translatable({ + "en": "The selected file is invalid. Do you want to select a different file?", + "nl": "Het geselecteerde bestaand is ongeldig. Wil je een ander bestand selecteren ?" + }) + ok = Translatable({ + "en": "Different file", + "nl": "Ander bestand" + }) + cancel = Translatable({ + "en": "Cancel", + "nl": "Annuleren" + }) + return PropsUIPromptConfirm(text, ok, cancel) + + +def spinner(): + return PropsUISpinner(Translatable({ + "en": "One moment please", + "nl": "Een moment geduld" + })) + + +def prompt_file(platform, extensions): + title = Translatable({ + "en": f"Select {platform} file", + "nl": f"Selecteer {platform} bestand" + }) + + description = Translatable({ + "en": "Please select this file so we can extract relevant information for our research.", + "nl": "Je kan deze file nu selecteren zodat wij er relevante informatie uit kunnen halen voor ons onderzoek." + }) + + return PropsUIPromptFileInput(title, description, extensions) + + +def doSomethingWithTheFile(platform, filename): + return extract_zip_contents(filename) + + +def extract_zip_contents(filename): + names = [] + try: + file = zipfile.ZipFile(filename) + data = [] + for name in file.namelist(): + names.append(name) + info = file.getinfo(name) + data.append((name, info.compress_size, info.file_size)) + return data + except: + return "invalid" + + +def prompt_consent(id, data): + title = Translatable({ + "en": "Extracted data", + "nl": "Gevonden gegevens" + }) + + description = Translatable({ + "en": "Please have a good look at the extracted data before giving consent to use this data.", + "nl": "Bekijk de gegevens goed voordat je consent geeft om deze te gebruiken." + }) + + data_frame = pd.DataFrame(data, columns=["filename", "compressed size", "size"]) + table = PropsUIPromptConsentFormTable(id, "The zip contains the following files:", data_frame) + return PropsUIPromptConsentForm(title, description, [table]) + + +def donate(key, consent_data): + return CommandSystemDonate(key, consent_data) `