// SPDX-FileCopyrightText: 2023 David Mosbach // // SPDX-License-Identifier: AGPL-3.0-or-later import { LinkObject, NodeObject } from "force-graph"; export type WF = { states : NodeFormat[], actions : EdgeFormat[] } export type NodeFormat = { stateData: SDFormat, x: number, y: number, fx: number, fy: number, id: string, name: string, val: number } export type GhostNodeFormat = { x: number, y: number, fx: number, fy: number, id: string, val: number } export type SDFormat = { abbreviation: string, final: boolean | string, messages?: MessageFormat[], viewers?: RolesFormat, payload?: string[] } export type EdgeFormat = { actionData: ADFormat, source: WFNode | WFGhostNode | string, target: WFNode | WFGhostNode | string, id: string, name: string, nodePairId : string } export type ActionMode = 'initial' | 'manual' | 'automatic'; export type ADFormat = { messages?: MessageFormat[], viewers?: RolesFormat, actors?: RolesFormat, ['actor Viewers']?: RolesFormat, mode?: ActionMode, form?: string[] } export type RoleFormat = { tag : string | null, authorized : JSON | null, user : string | null, 'payload-label': string | null } export type RoleName = 'actors' | 'viewers'; export type RolesFormat = { [roleName: string]: any, anchor: AnchorFormat, comment: string[] } export type AnchorFormat = { name: string, type: AnchorType } | 'NoAnchor' | null //TODO either NoAnchor or null in parser export type AnchorType = 'anchor' | 'alias' | 'none' export type MessageFormat = { content: { fallback: string, 'fallback-lang': string, translations: JSON }, status: string, viewers: RolesFormat } export class Workflow { states: (WFNode | WFGhostNode)[]; actions: WFEdge[]; constructor(json: WF) { const stateMap: Map = new Map(); this.states = []; for (const state of json.states) { var node = new WFNode(state); this.states.push(node); stateMap.set(node.id, node); } // Resolve ID refs to nodes var actions = json.actions.map(action => { function transformRef(ref: string | WFNode | WFGhostNode) { if (ref instanceof String) stateMap.has(ref) && (ref = stateMap.get(ref)) || console.error('Ref to unknown state: ' + ref); return ref; } action.source = transformRef(action.source); action.target = transformRef(action.target); return action; }); this.actions = []; for (const action of actions) this.actions.push(new WFEdge(action)); } } export class WFNode implements NodeObject { stateData: StateData; x: number; y: number; fx: number; fy: number; id: string; name: string; val: number; constructor(json: NodeFormat) { this.stateData = new StateData(json.stateData); this.x = json.x; this.y = json.y; this.fx = json.fx; this.fy = json.fy; this.id = json.id; this.name = json.name; this.val = json.val; } } export class StateData { abbreviation: string; messages: Message[]; viewers: Viewers; payload: Payload; viewerNames: string[]; final: boolean | string; constructor(json: SDFormat) { this.abbreviation = json.abbreviation; this.messages = json.messages ? json.messages.map(message => { return new Message(message) }) : []; this.viewers = json.viewers ? new Viewers(json.viewers) : Viewers.empty(); this.payload = new Payload(json.payload); this.viewerNames = []; this.final = json.final; } } export class WFGhostNode implements NodeObject { text: string; x: number; y: number; fx: number; fy: number; id: string; val: number; constructor(json: GhostNodeFormat) { this.text = '?'; this.x = json.x; this.y = json.y; this.fx = json.fx; this.fy = json.fy; this.id = json.id; this.val = json.val; } } export class WFEdge implements LinkObject { actionData: ActionData; source: WFNode | WFGhostNode; target: WFNode | WFGhostNode; id: string; name: string; nodePairId : string; curvature: number; __controlPoints?: any; constructor(json: EdgeFormat) { this.actionData = new ActionData(json.actionData); this.source = json.source; this.target = json.target; this.id = json.id; this.name = json.name; this.nodePairId = json.nodePairId; this.curvature = 0; } } export class ActionData { messages: Message[]; viewers: Viewers; actors: Actors; 'actor Viewers': Viewers; form: Payload; viewerNames: string[]; actorNames: string[]; mode: ActionMode | undefined; constructor(json: ADFormat) { this.messages = json.messages ? json.messages.map(message => { return new Message(message) }) : []; this.viewers = json.viewers ? new Viewers(json.viewers) : Viewers.empty(); this.actors = json.actors ? new Actors(json.actors) : Actors.empty(); this['actor Viewers'] = json['actor Viewers'] ? new Viewers(json['actor Viewers']) : Viewers.empty(); this.form = new Payload(json.form); this.viewerNames = []; this.actorNames = []; this.mode = json.mode ?? undefined; } } export class Role { json: RoleFormat; name: string; constructor(json: RoleFormat) { this.json = json; if (json.tag == 'payload-reference') { this.name = json['payload-label' as keyof RoleFormat]; } else if (json.authorized) { this.name = (json.authorized['dnf-terms' as keyof JSON])[0][0].var + ' (auth)'; //TODO ugly } else if (json.user) { this.name = json.user; } else if (json.tag) { this.name = json.tag + ' (tag)'; } else { this.name = JSON.stringify(json); } } format() { return [document.createTextNode(this.name)]; } } export class Roles { roleName: RoleName; anchor: Anchor; comment: string[]; roles: Role[]; constructor(json: RolesFormat, roleName: RoleName) { this.roleName = roleName this.anchor = json.anchor ? new Anchor(json.anchor) : new Anchor('NoAnchor'); this.roles = []; for (const role of json[roleName as keyof RolesFormat]) this.roles.push(new Role(role)); this.comment = json.comment; } length() { return this.roles.length; } format() { var r = document.createElement('h4'); var roles = document.createTextNode('Roles'); r.appendChild(roles); var rolesList = document.createElement('ul'); this.roles.forEach(r => { var role = document.createElement('li'); role.appendChild(document.createTextNode(r.name)); rolesList.appendChild(role); }); var result: HTMLElement[] = []; if (this.comment.length > 0) { var c = document.createElement('h4'); c.innerText = 'Comment'; var comment = document.createElement('p'); comment.innerText = this.comment.join(' '); result.push(c, comment); } if (this.anchor) { var a = document.createElement('h4'); a.appendChild(this.anchor.format()); result.push(a); } else result.push(r) result.push(rolesList); return result; } } export class Viewers extends Roles { static empty() { return new Viewers({ viewers: [], anchor: 'NoAnchor', comment: [] }) } constructor(json: RolesFormat) { super(json, 'viewers'); } } export class Actors extends Roles { static empty() { return new Actors({ actors: [], anchor: 'NoAnchor', comment: [] }) } constructor(json: RolesFormat) { super(json, 'actors'); } } export class Anchor { name: string | undefined; type: AnchorType; constructor(json: AnchorFormat) { if (!json || json === 'NoAnchor') { this.name = undefined; this.type = 'none'; } else { this.name = json.name; this.type = json.type; } } format() { return document.createTextNode(`${this.type == 'alias' ? '*' : '&'}${this.name}`); } } export class Message { fallback: string; fallbackLang: string; translations: JSON; status: string; viewers: Viewers; constructor(json: MessageFormat) { var content = json.content; this.fallback = content.fallback; this.fallbackLang = content['fallback-lang']; this.translations = content.translations; this.status = json.status; this.viewers = new Viewers(json.viewers); } format() { var v = document.createElement('h3'); var viewers = document.createTextNode('Viewers'); v.appendChild(viewers); var viewerList = this.viewers.format(); var h = document.createElement('h3'); var heading = document.createTextNode('Status'); h.appendChild(heading); var p: HTMLElement = document.createElement('p'); var text = document.createTextNode(this.status); p.appendChild(text); var result: HTMLElement[] = [v]; result = result.concat(viewerList); result.push(h, p); h = document.createElement('h3'); heading = document.createTextNode(this.fallbackLang); h.appendChild(heading); p = document.createElement('html'); p.setAttribute('lang', this.fallbackLang); p.innerHTML = this.fallback; result.push(h, p); for (var t in this.translations) { h = document.createElement('h3'); heading = document.createTextNode(t); h.appendChild(heading); p = document.createElement('html'); p.setAttribute('lang', this.translations[t as keyof JSON]); p.innerHTML = this.translations[t as keyof JSON]; result.push(h, p); } return result; } } export class Payload { fields: string[] constructor(json: string[] | undefined) { this.fields = []; if (json === undefined) return; for (var f in json) { this.fields.push(f); } } format() { var fieldList = document.createElement('ul'); this.fields.forEach(f => { var field = document.createElement('li'); field.appendChild(document.createTextNode(f)); fieldList.appendChild(field); }); return [fieldList]; } }