From ee6ffd567d5cbcb51ef0533aa514b86dbda75043 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Mon, 9 Sep 2024 10:59:53 -0400 Subject: [PATCH 01/99] Define simple workflow with xstate --- .../node-server/workflow.ts | 42 ++++++ .../SIL.AppBuilder.Portal/package-lock.json | 142 +++++++++++++++++- source/SIL.AppBuilder.Portal/package.json | 5 +- 3 files changed, 187 insertions(+), 2 deletions(-) create mode 100644 source/SIL.AppBuilder.Portal/node-server/workflow.ts diff --git a/source/SIL.AppBuilder.Portal/node-server/workflow.ts b/source/SIL.AppBuilder.Portal/node-server/workflow.ts new file mode 100644 index 000000000..5e255c269 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/node-server/workflow.ts @@ -0,0 +1,42 @@ +import { createMachine } from "xstate"; + + +export const NoAdminS3 = createMachine({ + initial: 'start', + states: { + AppBuilderConfiguration: { + on: { + 'Continue': { + target: 'ProductBuild' + } + } + }, + SynchronizeData: { + on: { + 'Continue': { + target: 'ProductBuild' + } + } + }, + ProductBuild: { + on: { + 'BuildSuccessful': { + target: 'VerifyAndPublish' + } + } + }, + VerifyAndPublish: { + on: { + 'Reject': { + target: 'SynchronizeData' + }, + 'Approve': { + target: 'Published' + } + } + }, + Published: { + type: 'final' + } + } +}); diff --git a/source/SIL.AppBuilder.Portal/package-lock.json b/source/SIL.AppBuilder.Portal/package-lock.json index 0ff5596ab..0d3375964 100644 --- a/source/SIL.AppBuilder.Portal/package-lock.json +++ b/source/SIL.AppBuilder.Portal/package-lock.json @@ -11,8 +11,10 @@ "@auth/express": "^0.6.0", "@auth/sveltekit": "^1.4.2", "@bull-board/express": "^5.21.3", + "@statelyai/inspect": "^0.4.0", "express": "^4.19.2", - "sil.appbuilder.portal.common": "file:common" + "sil.appbuilder.portal.common": "file:common", + "xstate": "^5.18.1" }, "devDependencies": { "@auth/core": "^0.32.0", @@ -28,6 +30,7 @@ "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vvo/tzdb": "^6.142.0", + "@xstate/svelte": "^3.0.4", "autoprefixer": "^10.4.16", "commander": "^11.1.0", "daisyui": "^4.12.10", @@ -2367,6 +2370,22 @@ "dev": true, "optional": true }, + "node_modules/@statelyai/inspect": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@statelyai/inspect/-/inspect-0.4.0.tgz", + "integrity": "sha512-VxQldRlKYcu6rzLY83RSXVwMYexkH6hNx85B89YWYyXYWtNGaWHFCwV7a/Kz8FFPeUz8EKVAnyMOg2kNpn07wQ==", + "dependencies": { + "fast-safe-stringify": "^2.1.1", + "isomorphic-ws": "^5.0.0", + "partysocket": "^0.0.25", + "safe-stable-stringify": "^2.4.3", + "superjson": "^1.13.3", + "uuid": "^9.0.1" + }, + "peerDependencies": { + "xstate": "^5.5.1" + } + }, "node_modules/@sveltejs/adapter-node": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-node/-/adapter-node-5.2.0.tgz", @@ -3027,6 +3046,21 @@ "integrity": "sha512-k10BEEr0c3q65yPkhLp4fdMwSMJVj6jP+UDWRG8Psb6R/BLs9yKuz23pf+1rH5Naztiu1d4KM3eArvf3bjS6ZQ==", "dev": true }, + "node_modules/@xstate/svelte": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@xstate/svelte/-/svelte-3.0.4.tgz", + "integrity": "sha512-C/ZNdVEFt3algFzWk2XFZh6WLrlxW0dRjqSan92n75qOUuW0grRnPGZtVId9f9TOLoqi5T/LWzWhuPZ7BWhC7w==", + "dev": true, + "peerDependencies": { + "svelte": "^3.24.1 || ^4", + "xstate": "^5.18.1" + }, + "peerDependenciesMeta": { + "xstate": { + "optional": true + } + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -3754,6 +3788,20 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/crc-32": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", @@ -4502,6 +4550,17 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-6.0.2.tgz", + "integrity": "sha512-8q3LsZjRezbFZ2PN+uP+Q7pnHUMmAOziU2vA2OwoFaKIXxlxl38IylhSSgUorWu/rf4er67w0ikBqjBFk/pomA==", + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, "node_modules/execa": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", @@ -4625,6 +4684,11 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "dev": true }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" + }, "node_modules/fastparse": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/fastparse/-/fastparse-1.1.2.tgz", @@ -5336,12 +5400,31 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "dev": true }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "peerDependencies": { + "ws": "*" + } + }, "node_modules/jackspeak": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", @@ -6247,6 +6330,14 @@ "node": ">= 0.8" } }, + "node_modules/partysocket": { + "version": "0.0.25", + "resolved": "https://registry.npmjs.org/partysocket/-/partysocket-0.0.25.tgz", + "integrity": "sha512-1oCGA65fydX/FgdnsiBh68buOvfxuteoZVSb3Paci2kRp/7lhF0HyA8EDb5X/O6FxId1e+usPTQNRuzFEvkJbQ==", + "dependencies": { + "event-target-shim": "^6.0.2" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -7054,6 +7145,14 @@ } ] }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -7561,6 +7660,17 @@ "node": ">= 6" } }, + "node_modules/superjson": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.3.tgz", + "integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/superstruct": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/superstruct/-/superstruct-2.0.2.tgz", @@ -9069,6 +9179,36 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xstate": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.18.1.tgz", + "integrity": "sha512-m02IqcCQbaE/kBQLunwub/5i8epvkD2mFutnL17Oeg1eXTShe1sRF4D5mhv1dlaFO4vbW5gRGRhraeAD5c938g==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/xstate" + } + }, "node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", diff --git a/source/SIL.AppBuilder.Portal/package.json b/source/SIL.AppBuilder.Portal/package.json index c1c9ffe51..4e8da4fb4 100644 --- a/source/SIL.AppBuilder.Portal/package.json +++ b/source/SIL.AppBuilder.Portal/package.json @@ -34,6 +34,7 @@ "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vvo/tzdb": "^6.142.0", + "@xstate/svelte": "^3.0.4", "autoprefixer": "^10.4.16", "commander": "^11.1.0", "daisyui": "^4.12.10", @@ -66,7 +67,9 @@ "@auth/express": "^0.6.0", "@auth/sveltekit": "^1.4.2", "@bull-board/express": "^5.21.3", + "@statelyai/inspect": "^0.4.0", "express": "^4.19.2", - "sil.appbuilder.portal.common": "file:common" + "sil.appbuilder.portal.common": "file:common", + "xstate": "^5.18.1" } } From be0413d02bfada9970aa6dfd37c725330fe1dbd3 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Mon, 9 Sep 2024 12:08:46 -0400 Subject: [PATCH 02/99] Move workflow definition to common --- source/SIL.AppBuilder.Portal/common/index.ts | 2 +- .../SIL.AppBuilder.Portal/common/package-lock.json | 13 ++++++++++++- source/SIL.AppBuilder.Portal/common/package.json | 3 ++- .../{node-server => common/workflows}/workflow.ts | 0 4 files changed, 15 insertions(+), 3 deletions(-) rename source/SIL.AppBuilder.Portal/{node-server => common/workflows}/workflow.ts (100%) diff --git a/source/SIL.AppBuilder.Portal/common/index.ts b/source/SIL.AppBuilder.Portal/common/index.ts index cf226680c..73a39c0cf 100644 --- a/source/SIL.AppBuilder.Portal/common/index.ts +++ b/source/SIL.AppBuilder.Portal/common/index.ts @@ -2,4 +2,4 @@ export * as BullMQ from './BullJobTypes.js'; export { scriptoriaQueue } from './bullmq.js'; export { default as DatabaseWrites } from './databaseProxy/index.js'; export { readonlyPrisma as prisma } from './prisma.js'; - +export { NoAdminS3 } from './workflows/workflow.js'; diff --git a/source/SIL.AppBuilder.Portal/common/package-lock.json b/source/SIL.AppBuilder.Portal/common/package-lock.json index d2fcebc22..11ba350b7 100644 --- a/source/SIL.AppBuilder.Portal/common/package-lock.json +++ b/source/SIL.AppBuilder.Portal/common/package-lock.json @@ -10,7 +10,8 @@ "license": "ISC", "dependencies": { "@prisma/client": "^5.18.0", - "bullmq": "^5.12.2" + "bullmq": "^5.12.2", + "xstate": "^5.18.1" }, "devDependencies": { "prisma": "^5.18.0" @@ -382,6 +383,16 @@ "bin": { "uuid": "dist/bin/uuid" } + }, + "node_modules/xstate": { + "version": "5.18.1", + "resolved": "https://registry.npmjs.org/xstate/-/xstate-5.18.1.tgz", + "integrity": "sha512-m02IqcCQbaE/kBQLunwub/5i8epvkD2mFutnL17Oeg1eXTShe1sRF4D5mhv1dlaFO4vbW5gRGRhraeAD5c938g==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/xstate" + } } } } diff --git a/source/SIL.AppBuilder.Portal/common/package.json b/source/SIL.AppBuilder.Portal/common/package.json index f2d932056..8e9101ad1 100644 --- a/source/SIL.AppBuilder.Portal/common/package.json +++ b/source/SIL.AppBuilder.Portal/common/package.json @@ -11,7 +11,8 @@ "license": "ISC", "dependencies": { "@prisma/client": "^5.18.0", - "bullmq": "^5.12.2" + "bullmq": "^5.12.2", + "xstate": "^5.18.1" }, "devDependencies": { "prisma": "^5.18.0" diff --git a/source/SIL.AppBuilder.Portal/node-server/workflow.ts b/source/SIL.AppBuilder.Portal/common/workflows/workflow.ts similarity index 100% rename from source/SIL.AppBuilder.Portal/node-server/workflow.ts rename to source/SIL.AppBuilder.Portal/common/workflows/workflow.ts From 6214894fcba04b9da206b38508fc8514719c7ca8 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Mon, 9 Sep 2024 13:59:25 -0400 Subject: [PATCH 03/99] Move workflow to common/public --- source/SIL.AppBuilder.Portal/common/index.ts | 1 - source/SIL.AppBuilder.Portal/common/package.json | 3 ++- .../common/{workflows => public}/workflow.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename source/SIL.AppBuilder.Portal/common/{workflows => public}/workflow.ts (94%) diff --git a/source/SIL.AppBuilder.Portal/common/index.ts b/source/SIL.AppBuilder.Portal/common/index.ts index 73a39c0cf..69add072e 100644 --- a/source/SIL.AppBuilder.Portal/common/index.ts +++ b/source/SIL.AppBuilder.Portal/common/index.ts @@ -2,4 +2,3 @@ export * as BullMQ from './BullJobTypes.js'; export { scriptoriaQueue } from './bullmq.js'; export { default as DatabaseWrites } from './databaseProxy/index.js'; export { readonlyPrisma as prisma } from './prisma.js'; -export { NoAdminS3 } from './workflows/workflow.js'; diff --git a/source/SIL.AppBuilder.Portal/common/package.json b/source/SIL.AppBuilder.Portal/common/package.json index 8e9101ad1..f1b089be0 100644 --- a/source/SIL.AppBuilder.Portal/common/package.json +++ b/source/SIL.AppBuilder.Portal/common/package.json @@ -4,7 +4,8 @@ "description": "", "exports": { ".": "./index.js", - "./prisma": "./public/prisma.js" + "./prisma": "./public/prisma.js", + "./workflow": "./public/workflow.js" }, "author": "", "type": "module", diff --git a/source/SIL.AppBuilder.Portal/common/workflows/workflow.ts b/source/SIL.AppBuilder.Portal/common/public/workflow.ts similarity index 94% rename from source/SIL.AppBuilder.Portal/common/workflows/workflow.ts rename to source/SIL.AppBuilder.Portal/common/public/workflow.ts index 5e255c269..4140bc51d 100644 --- a/source/SIL.AppBuilder.Portal/common/workflows/workflow.ts +++ b/source/SIL.AppBuilder.Portal/common/public/workflow.ts @@ -2,7 +2,7 @@ import { createMachine } from "xstate"; export const NoAdminS3 = createMachine({ - initial: 'start', + initial: 'AppBuilderConfiguration', states: { AppBuilderConfiguration: { on: { From d88a345b98c9ab2d39df4ad94f3bd61a74197a08 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Mon, 9 Sep 2024 15:01:59 -0400 Subject: [PATCH 04/99] Change to full state names --- .../common/public/workflow.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/source/SIL.AppBuilder.Portal/common/public/workflow.ts b/source/SIL.AppBuilder.Portal/common/public/workflow.ts index 4140bc51d..a755053fc 100644 --- a/source/SIL.AppBuilder.Portal/common/public/workflow.ts +++ b/source/SIL.AppBuilder.Portal/common/public/workflow.ts @@ -2,33 +2,33 @@ import { createMachine } from "xstate"; export const NoAdminS3 = createMachine({ - initial: 'AppBuilderConfiguration', + initial: 'App Builder Configuration', states: { - AppBuilderConfiguration: { + 'App Builder Configuration': { on: { 'Continue': { - target: 'ProductBuild' + target: 'Product Build' } } }, - SynchronizeData: { + 'Synchronize Data': { on: { 'Continue': { - target: 'ProductBuild' + target: 'Product Build' } } }, - ProductBuild: { + 'Product Build': { on: { - 'BuildSuccessful': { - target: 'VerifyAndPublish' + 'Build Successful': { + target: 'Verify And Publish' } } }, - VerifyAndPublish: { + 'Verify And Publish': { on: { 'Reject': { - target: 'SynchronizeData' + target: 'Synchronize Data' }, 'Approve': { target: 'Published' From 782f03e0c97e96170e8bb17fba57b1c57e5760d4 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Mon, 9 Sep 2024 15:03:18 -0400 Subject: [PATCH 05/99] Use workflow in tasks/id/server Send task name and actions to corresponding page.svelte Handle form submission and redirect to /tasks --- .../tasks/[product_id]/+page.server.ts | 24 +++++++++++++++---- .../tasks/[product_id]/+page.svelte | 6 ++--- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts index c5d946be1..50061541e 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts @@ -1,5 +1,10 @@ -import type { PageServerLoad } from './$types'; +import type { PageServerLoad, Actions } from './$types'; import { prisma } from 'sil.appbuilder.portal.common'; +import { NoAdminS3 } from 'sil.appbuilder.portal.common/workflow'; +import { createActor } from 'xstate'; +import { redirect } from '@sveltejs/kit'; + +const actor = createActor(NoAdminS3); //later: retrieve snapshot from database type Fields = { ownerName?: string; //Product.Project.Owner.Name @@ -84,8 +89,8 @@ export const load = (async ({ params, url, locals }) => { }) return { - actions: [], - taskTitle: "Waiting", + actions: Object.keys(NoAdminS3.getStateNodeById(`(machine).${actor.getSnapshot().value}`).on), + taskTitle: actor.getSnapshot().value, instructions: "waiting", //filter fields/files/reviewers based on task once workflows are implemented //possibly filter in the original query to increase database efficiency @@ -104,4 +109,15 @@ export const load = (async ({ params, url, locals }) => { files: artifacts, reviewers: product?.Project.Reviewers } -}) satisfies PageServerLoad; \ No newline at end of file +}) satisfies PageServerLoad; + +export const actions = { + default: async ({ request }) => { + const data = await request.formData(); + + console.log(data.get('action')); + console.log(data.get('comment')); + + redirect(302, '/tasks'); + } +} satisfies Actions; \ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.svelte index 5b0e0d896..242a42f8a 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.svelte +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.svelte @@ -7,11 +7,11 @@
-
+ {#if data.actions?.length}
{#each data.actions as action} - + {/each}
{/if} @@ -19,7 +19,7 @@
Comment
- +

From d0ddbd7b532ff202b51251456cecdfefb2d7783d Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Mon, 9 Sep 2024 16:13:40 -0400 Subject: [PATCH 06/99] Filter form based on workflow state Based on workflow state: - Select form title - Select form instructions - Select specific fields - Conditionally return reviewers - Conditionally return productArtifacts and filter by artifact type --- .../common/public/workflow.ts | 71 ++++++++++-- .../src/lib/filterObject.ts | 14 +++ .../tasks/[product_id]/+page.server.ts | 106 ++++++++++-------- 3 files changed, 140 insertions(+), 51 deletions(-) create mode 100644 source/SIL.AppBuilder.Portal/src/lib/filterObject.ts diff --git a/source/SIL.AppBuilder.Portal/common/public/workflow.ts b/source/SIL.AppBuilder.Portal/common/public/workflow.ts index a755053fc..f88e67c1e 100644 --- a/source/SIL.AppBuilder.Portal/common/public/workflow.ts +++ b/source/SIL.AppBuilder.Portal/common/public/workflow.ts @@ -1,24 +1,60 @@ -import { createMachine } from "xstate"; +import { setup, assign } from 'xstate'; - -export const NoAdminS3 = createMachine({ +export const NoAdminS3 = setup({ + types: { + context: {} as { + //later: narrow types if necessary + instructions: string; + includeFields: string[]; + includeReviewers: boolean; + includeArtifacts: string | boolean; + } + } +}).createMachine({ initial: 'App Builder Configuration', + context: { + instructions: 'waiting', + /** projectName and projectDescription are always included */ + includeFields: [], + /** Reset to false on exit */ + includeReviewers: false, + /** Reset to false on exit */ + includeArtifacts: false + }, states: { 'App Builder Configuration': { + entry: assign({ + instructions: 'app_configuration', + includeFields: [ + 'storeDescription', + 'listingLanguageCode', + 'projectURL' + ] + }), on: { - 'Continue': { + Continue: { target: 'Product Build' } } }, 'Synchronize Data': { + entry: assign({ + instructions: 'synchronize_data', + includeFields: [ + 'storeDescription', + 'listingLanguageCode' + ] + }), on: { - 'Continue': { + Continue: { target: 'Product Build' } } }, 'Product Build': { + entry: assign({ + instructions: 'waiting' + }), on: { 'Build Successful': { target: 'Verify And Publish' @@ -26,16 +62,37 @@ export const NoAdminS3 = createMachine({ } }, 'Verify And Publish': { + entry: assign({ + instructions: 'verify_and_publish', + includeFields: [ + 'storeDescription', + 'listingLanguageCode', + 'projectURL' + ], + includeReviewers: true, + includeArtifacts: true + }), + exit: assign({ + includeReviewers: false, + includeArtifacts: false + }), on: { - 'Reject': { + Reject: { target: 'Synchronize Data' }, - 'Approve': { + Approve: { target: 'Published' } } }, Published: { + entry: assign({ + instructions: '', + includeFields: [ + 'storeDescription', + 'listingLanguageCode' + ] + }), type: 'final' } } diff --git a/source/SIL.AppBuilder.Portal/src/lib/filterObject.ts b/source/SIL.AppBuilder.Portal/src/lib/filterObject.ts new file mode 100644 index 000000000..90fee703a --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/lib/filterObject.ts @@ -0,0 +1,14 @@ +//source: https://www.steveruiz.me/posts/how-to-filter-an-object + +type Entry = { + [K in keyof T]: [K, T[K]] +}[keyof T] + +export function filterObject( + obj: T, + fn: (entry: Entry, i: number, arr: Entry[]) => boolean +) { + return Object.fromEntries( + (Object.entries(obj) as Entry[]).filter(fn) + ) as Partial +} \ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts index 50061541e..ed5fc0a2f 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts @@ -3,23 +3,26 @@ import { prisma } from 'sil.appbuilder.portal.common'; import { NoAdminS3 } from 'sil.appbuilder.portal.common/workflow'; import { createActor } from 'xstate'; import { redirect } from '@sveltejs/kit'; +import { filterObject } from '$lib/filterObject'; const actor = createActor(NoAdminS3); //later: retrieve snapshot from database type Fields = { - ownerName?: string; //Product.Project.Owner.Name - ownerEmail?: string; //Product.Project.Owner.Email - projectName: string; //Product.Project.Name - projectDescription: string; //Product.Project.Description - storeDescription?: string; //Product.Store.Description + ownerName?: string; //Product.Project.Owner.Name + ownerEmail?: string; //Product.Project.Owner.Email + projectName: string; //Product.Project.Name + projectDescription: string; //Product.Project.Description + storeDescription?: string; //Product.Store.Description listingLanguageCode?: string; //Product.StoreLanguage.Name - projectURL?: string; //Product.Project.WorkflowAppProjectURL - productDescription?: string; //Product.ProductDefinition.Description - appType?: string; //Product.ProductDefinition.ApplicationTypes.Description + projectURL?: string; //Product.Project.WorkflowAppProjectURL + productDescription?: string; //Product.ProductDefinition.Description + appType?: string; //Product.ProductDefinition.ApplicationTypes.Description projectLanguageCode?: string; //Product.Project.Language -} +}; export const load = (async ({ params, url, locals }) => { + const snap = actor.getSnapshot(); + const product = await prisma.products.findUnique({ where: { Id: params.product_id @@ -38,13 +41,14 @@ export const load = (async ({ params, url, locals }) => { Email: true } }, - Reviewers: { + //conditionally include reviewers + Reviewers: (snap.context.includeReviewers) ? { select: { Id: true, Name: true, Email: true } - } + } : undefined } }, Store: { @@ -66,49 +70,63 @@ export const load = (async ({ params, url, locals }) => { } } } - }, - } - }); - - // later: include only when workflow state needs it - const artifacts = await prisma.productArtifacts.findMany({ - where: { - ProductId: params.product_id, - ProductBuild: { - BuildId: product?.WorkflowBuildId } - // later: some forms don't need all artifacts, some just need aab - }, - select: { - ProductBuildId: true, - ContentType: true, - FileSize: true, - Url: true, - Id: true } - }) + }); + + const artifacts = snap.context.includeArtifacts + ? await prisma.productArtifacts.findMany({ + where: { + ProductId: params.product_id, + ProductBuild: { + BuildId: product?.WorkflowBuildId + }, + //filter by artifact type + ArtifactType: + typeof snap.context.includeArtifacts === 'string' + ? snap.context.includeArtifacts + : undefined //include all + }, + select: { + ProductBuildId: true, + ContentType: true, + FileSize: true, + Url: true, + Id: true + } + }) + : []; + + const fields = snap.context.includeFields; return { - actions: Object.keys(NoAdminS3.getStateNodeById(`(machine).${actor.getSnapshot().value}`).on), - taskTitle: actor.getSnapshot().value, - instructions: "waiting", + actions: Object.keys(NoAdminS3.getStateNodeById(`(machine).${snap.value}`).on), + taskTitle: snap.value, + instructions: snap.context.instructions, //filter fields/files/reviewers based on task once workflows are implemented //possibly filter in the original query to increase database efficiency fields: { - ownerName: product?.Project.Owner.Name, - ownerEmail: product?.Project.Owner.Email, + ...filterObject( + { + ownerName: product?.Project.Owner.Name, + ownerEmail: product?.Project.Owner.Email, + storeDescription: product?.Store?.Description, + listingLanguageCode: product?.StoreLanguage?.Name, + projectURL: product?.Project.WorkflowAppProjectUrl, + productDescription: product?.ProductDefinition.Name, + appType: product?.ProductDefinition.ApplicationTypes.Description, + projectLanguageCode: product?.Project.Language + }, + ([k, v]) => { + return fields.includes(k); + } + ), projectName: product?.Project.Name, - projectDescription: product?.Project.Description, - storeDescription: product?.Store?.Description, - listingLanguageCode: product?.StoreLanguage?.Name, - projectURL: product?.Project.WorkflowAppProjectUrl, - productDescription: product?.ProductDefinition.Name, - appType: product?.ProductDefinition.ApplicationTypes.Description, - projectLanguageCode: product?.Project.Language + projectDescription: product?.Project.Description } as Fields, files: artifacts, reviewers: product?.Project.Reviewers - } + }; }) satisfies PageServerLoad; export const actions = { @@ -120,4 +138,4 @@ export const actions = { redirect(302, '/tasks'); } -} satisfies Actions; \ No newline at end of file +} satisfies Actions; From 54b27ee49f686163a3ecbe874afd2209b6c6edc5 Mon Sep 17 00:00:00 2001 From: 7dev7urandom <30197373+7dev7urandom@users.noreply.github.com> Date: Mon, 9 Sep 2024 16:57:07 -0400 Subject: [PATCH 07/99] Fix task link --- source/SIL.AppBuilder.Portal/package-lock.json | 4 ++-- source/SIL.AppBuilder.Portal/package.json | 1 - .../src/routes/(authenticated)/tasks/+page.svelte | 8 +------- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/source/SIL.AppBuilder.Portal/package-lock.json b/source/SIL.AppBuilder.Portal/package-lock.json index 0d3375964..055b6072d 100644 --- a/source/SIL.AppBuilder.Portal/package-lock.json +++ b/source/SIL.AppBuilder.Portal/package-lock.json @@ -42,7 +42,6 @@ "prettier": "^2.8.0", "prettier-plugin-prisma": "^5.0.0", "prettier-plugin-svelte": "^2.10.1", - "prisma": "^5.4.2", "svelte": "^4.2.18", "svelte-check": "^3.8.4", "svelte-flatpickr": "^3.3.4", @@ -62,7 +61,8 @@ "license": "ISC", "dependencies": { "@prisma/client": "^5.18.0", - "bullmq": "^5.12.2" + "bullmq": "^5.12.2", + "xstate": "^5.18.1" }, "devDependencies": { "prisma": "^5.18.0" diff --git a/source/SIL.AppBuilder.Portal/package.json b/source/SIL.AppBuilder.Portal/package.json index 4e8da4fb4..50e1e2eb1 100644 --- a/source/SIL.AppBuilder.Portal/package.json +++ b/source/SIL.AppBuilder.Portal/package.json @@ -46,7 +46,6 @@ "prettier": "^2.8.0", "prettier-plugin-prisma": "^5.0.0", "prettier-plugin-svelte": "^2.10.1", - "prisma": "^5.4.2", "svelte": "^4.2.18", "svelte-check": "^3.8.4", "svelte-flatpickr": "^3.3.4", diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/+page.svelte index 2f5f5d589..e1b8331e2 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/+page.svelte +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/+page.svelte @@ -23,13 +23,7 @@ {#each data.tasks as task} - - goto( - `/tasks/${task.Id}` - )} - > + goto(`/tasks/${task.ProductId}`)}>
Date: Mon, 9 Sep 2024 17:22:13 -0400 Subject: [PATCH 08/99] Add table for workflow instances --- .../migration.sql | 25 +++++++++++++++++++ .../prisma/migrations/migration_lock.toml | 3 +++ .../common/prisma/schema.prisma | 2 +- 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 source/SIL.AppBuilder.Portal/common/prisma/migrations/20240909211931_1_workflow_instances/migration.sql create mode 100644 source/SIL.AppBuilder.Portal/common/prisma/migrations/migration_lock.toml diff --git a/source/SIL.AppBuilder.Portal/common/prisma/migrations/20240909211931_1_workflow_instances/migration.sql b/source/SIL.AppBuilder.Portal/common/prisma/migrations/20240909211931_1_workflow_instances/migration.sql new file mode 100644 index 000000000..a97872caf --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/prisma/migrations/20240909211931_1_workflow_instances/migration.sql @@ -0,0 +1,25 @@ +/* + Warnings: + + - You are about to drop the column `PublishAfterAutoRebuilds` on the `Projects` table. All the data in the column will be lost. + - You are about to drop the column `SoftwareUpdateRebuilds` on the `Projects` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "Projects" DROP COLUMN "PublishAfterAutoRebuilds", +DROP COLUMN "SoftwareUpdateRebuilds"; + +-- CreateTable +CREATE TABLE "WorkflowInstances" ( + "Id" SERIAL NOT NULL, + "Snapshot" TEXT NOT NULL, + "ProductId" UUID NOT NULL, + + CONSTRAINT "PK_WorkflowInstances" PRIMARY KEY ("Id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "WorkflowInstances_ProductId_key" ON "WorkflowInstances"("ProductId"); + +-- AddForeignKey +ALTER TABLE "WorkflowInstances" ADD CONSTRAINT "FK_WorkflowInstances_Products_ProductId" FOREIGN KEY ("ProductId") REFERENCES "Products"("Id") ON DELETE NO ACTION ON UPDATE NO ACTION; diff --git a/source/SIL.AppBuilder.Portal/common/prisma/migrations/migration_lock.toml b/source/SIL.AppBuilder.Portal/common/prisma/migrations/migration_lock.toml new file mode 100644 index 000000000..fbffa92c2 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "postgresql" \ No newline at end of file diff --git a/source/SIL.AppBuilder.Portal/common/prisma/schema.prisma b/source/SIL.AppBuilder.Portal/common/prisma/schema.prisma index d8634398b..3201d2d5d 100644 --- a/source/SIL.AppBuilder.Portal/common/prisma/schema.prisma +++ b/source/SIL.AppBuilder.Portal/common/prisma/schema.prisma @@ -287,7 +287,7 @@ model Products { StoreLanguage StoreLanguages? @relation(fields: [StoreLanguageId], references: [Id], onDelete: Restrict, onUpdate: NoAction, map: "FK_Products_StoreLanguages_StoreLanguageId") Store Stores? @relation(fields: [StoreId], references: [Id], onDelete: Restrict, onUpdate: NoAction, map: "FK_Products_Stores_StoreId") UserTasks UserTasks[] - WorkflowInstances WorkflowInstances? + WorkflowInstance WorkflowInstances? @@index([ProductDefinitionId], map: "IX_Products_ProductDefinitionId") @@index([ProjectId], map: "IX_Products_ProjectId") From 6601d9e0ec7c90d497bdc5cd1a8232bbfe0ee08f Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Tue, 10 Sep 2024 08:34:32 -0400 Subject: [PATCH 09/99] Add workflow instances to paraglide --- .../SIL.AppBuilder.Portal/src/lib/imported-locales/en-us.json | 3 +++ .../SIL.AppBuilder.Portal/src/lib/imported-locales/es-419.json | 3 +++ .../SIL.AppBuilder.Portal/src/lib/imported-locales/fr-FR.json | 3 +++ source/SIL.AppBuilder.Portal/src/lib/locales/en-us.json | 1 + source/SIL.AppBuilder.Portal/src/lib/locales/es-419.json | 1 + source/SIL.AppBuilder.Portal/src/lib/locales/fr-FR.json | 1 + 6 files changed, 12 insertions(+) diff --git a/source/SIL.AppBuilder.Portal/src/lib/imported-locales/en-us.json b/source/SIL.AppBuilder.Portal/src/lib/imported-locales/en-us.json index 1572bb48d..dda91953e 100644 --- a/source/SIL.AppBuilder.Portal/src/lib/imported-locales/en-us.json +++ b/source/SIL.AppBuilder.Portal/src/lib/imported-locales/en-us.json @@ -569,6 +569,9 @@ "status": "Status", "lastUpdated": "Last update" } + }, + "workflowInstances": { + "title": "Workflow Instances" } }, "organization-membership": { diff --git a/source/SIL.AppBuilder.Portal/src/lib/imported-locales/es-419.json b/source/SIL.AppBuilder.Portal/src/lib/imported-locales/es-419.json index 90434bb9f..3cbdbf4f2 100644 --- a/source/SIL.AppBuilder.Portal/src/lib/imported-locales/es-419.json +++ b/source/SIL.AppBuilder.Portal/src/lib/imported-locales/es-419.json @@ -569,6 +569,9 @@ "status": "Estado", "lastUpdated": "Last update" } + }, + "workflowInstances": { + "title": "Workflow Instances" } }, "organization-membership": { diff --git a/source/SIL.AppBuilder.Portal/src/lib/imported-locales/fr-FR.json b/source/SIL.AppBuilder.Portal/src/lib/imported-locales/fr-FR.json index 8097d557a..e5f10672e 100644 --- a/source/SIL.AppBuilder.Portal/src/lib/imported-locales/fr-FR.json +++ b/source/SIL.AppBuilder.Portal/src/lib/imported-locales/fr-FR.json @@ -569,6 +569,9 @@ "status": "Status", "lastUpdated": "Last update" } + }, + "workflowInstances": { + "title": "Workflow Instances" } }, "organization-membership": { diff --git a/source/SIL.AppBuilder.Portal/src/lib/locales/en-us.json b/source/SIL.AppBuilder.Portal/src/lib/locales/en-us.json index 6bd5b8a72..f8d482ec8 100644 --- a/source/SIL.AppBuilder.Portal/src/lib/locales/en-us.json +++ b/source/SIL.AppBuilder.Portal/src/lib/locales/en-us.json @@ -428,6 +428,7 @@ "admin_settings_buildEngines_disconnected": "Disconnected", "admin_settings_buildEngines_status": "Status", "admin_settings_buildEngines_lastUpdated": "Last update", + "admin_workflowInstances_title": "Workflow Instances", "organizationMembership_invite_error_expired": "Invitation has expired", "organizationMembership_invite_error_redeemed": "Invitation has already been redeemed", "organizationMembership_invite_error_notFound": "Invitation was not found", diff --git a/source/SIL.AppBuilder.Portal/src/lib/locales/es-419.json b/source/SIL.AppBuilder.Portal/src/lib/locales/es-419.json index 9d6f5bb35..3bd43df3a 100644 --- a/source/SIL.AppBuilder.Portal/src/lib/locales/es-419.json +++ b/source/SIL.AppBuilder.Portal/src/lib/locales/es-419.json @@ -427,6 +427,7 @@ "admin_settings_buildEngines_disconnected": "Disconnected", "admin_settings_buildEngines_status": "Estado", "admin_settings_buildEngines_lastUpdated": "Last update", + "admin_workflowInstances_title": "Workflow Instances", "organizationMembership_invite_error_expired": "La invitación ha caducado", "organizationMembership_invite_error_redeemed": "La invitación ya ha sido canjeada", "organizationMembership_invite_error_notFound": "No se encontró la invitación", diff --git a/source/SIL.AppBuilder.Portal/src/lib/locales/fr-FR.json b/source/SIL.AppBuilder.Portal/src/lib/locales/fr-FR.json index 2c9452759..21030080e 100644 --- a/source/SIL.AppBuilder.Portal/src/lib/locales/fr-FR.json +++ b/source/SIL.AppBuilder.Portal/src/lib/locales/fr-FR.json @@ -428,6 +428,7 @@ "admin_settings_buildEngines_disconnected": "Disconnected", "admin_settings_buildEngines_status": "Status", "admin_settings_buildEngines_lastUpdated": "Last update", + "admin_workflowInstances_title": "Workflow Instances", "organizationMembership_invite_error_expired": "Invitation has expired", "organizationMembership_invite_error_redeemed": "Invitation has already been redeemed", "organizationMembership_invite_error_notFound": "Invitation was not found", From 68264de1df6095d8eb09b0d49e6881d32a8c85f2 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Tue, 10 Sep 2024 10:23:35 -0400 Subject: [PATCH 10/99] Don't drop columns for upcoming feature --- .../migration.sql | 11 ----------- 1 file changed, 11 deletions(-) rename source/SIL.AppBuilder.Portal/common/prisma/migrations/{20240909211931_1_workflow_instances => 1_workflow_instances}/migration.sql (56%) diff --git a/source/SIL.AppBuilder.Portal/common/prisma/migrations/20240909211931_1_workflow_instances/migration.sql b/source/SIL.AppBuilder.Portal/common/prisma/migrations/1_workflow_instances/migration.sql similarity index 56% rename from source/SIL.AppBuilder.Portal/common/prisma/migrations/20240909211931_1_workflow_instances/migration.sql rename to source/SIL.AppBuilder.Portal/common/prisma/migrations/1_workflow_instances/migration.sql index a97872caf..647c5beb5 100644 --- a/source/SIL.AppBuilder.Portal/common/prisma/migrations/20240909211931_1_workflow_instances/migration.sql +++ b/source/SIL.AppBuilder.Portal/common/prisma/migrations/1_workflow_instances/migration.sql @@ -1,14 +1,3 @@ -/* - Warnings: - - - You are about to drop the column `PublishAfterAutoRebuilds` on the `Projects` table. All the data in the column will be lost. - - You are about to drop the column `SoftwareUpdateRebuilds` on the `Projects` table. All the data in the column will be lost. - -*/ --- AlterTable -ALTER TABLE "Projects" DROP COLUMN "PublishAfterAutoRebuilds", -DROP COLUMN "SoftwareUpdateRebuilds"; - -- CreateTable CREATE TABLE "WorkflowInstances" ( "Id" SERIAL NOT NULL, From 7f60baf38002b14d52ff758d4374fde30305b69f Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Wed, 11 Sep 2024 09:40:35 -0400 Subject: [PATCH 11/99] Basic workflow pages, open inspector --- .../workflow-definitions/+page.svelte | 4 ++ .../admin/workflows/+page.server.ts | 27 +++++++++++ .../admin/workflows/+page.svelte | 47 +++++++++++++++++++ .../workflows/[product_id]/+page.server.ts | 15 ++++++ .../admin/workflows/[product_id]/+page.svelte | 32 +++++++++++++ 5 files changed, 125 insertions(+) create mode 100644 source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/+page.server.ts create mode 100644 source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/+page.svelte create mode 100644 source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.server.ts create mode 100644 source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/+page.svelte index c5cf832b5..f890ce369 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/+page.svelte +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/settings/workflow-definitions/+page.svelte @@ -7,6 +7,10 @@ export let data: PageData; + + {m.admin_workflowInstances_title()} + + {m.admin_settings_workflowDefinitions_add()} diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/+page.server.ts new file mode 100644 index 000000000..158dbcf4c --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/+page.server.ts @@ -0,0 +1,27 @@ +import { prisma } from 'sil.appbuilder.portal.common'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async (event) => { + const instances = await prisma.workflowInstances.findMany({ + select: { + Product: { + select: { + Id: true, + ProductTransitions: { + select: { + DateTransition: true, + InitialState: true + }, + orderBy: [ + { + DateTransition: 'desc' + } + ], + take: 1 + } + } + } + } + }); + return { instances }; +}; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/+page.svelte new file mode 100644 index 000000000..561476d25 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/+page.svelte @@ -0,0 +1,47 @@ + + +
+

{m.admin_workflowInstances_title()}

+
+ {#if data.instances.length > 0} + + + + + + + + + + {#each data.instances as i} + goto(`/admin/workflows/${i.Product.Id}`)}> + + + + + {/each} + +
{m.tasks_product()}Last StateTransition Date
+ {i.Product.Id} + + {i.Product.ProductTransitions[0].InitialState} + + {i.Product.ProductTransitions[0].DateTransition} +
+ {:else} +
+

There are no active workflow instances

+ Try creating a product +
+ {/if} +
+
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.server.ts new file mode 100644 index 000000000..364c5ac12 --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.server.ts @@ -0,0 +1,15 @@ +import { prisma } from 'sil.appbuilder.portal.common'; +import type { PageServerLoad } from './$types'; + +export const load: PageServerLoad = async ({ params, url, locals }) => { + const instance = await prisma.workflowInstances.findUnique({ + where: { + ProductId: params.product_id + }, + select: { + ProductId: true, + Snapshot: true + } + }); + return { instance }; +}; diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte new file mode 100644 index 000000000..bbcd72afd --- /dev/null +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte @@ -0,0 +1,32 @@ + + + + + \ No newline at end of file From 9543e2ff52c1a771eb9bcd68bb7396194e160c17 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Wed, 11 Sep 2024 09:40:35 -0400 Subject: [PATCH 12/99] Basic workflow pages, open inspector --- .../src/routes/(authenticated)/admin/workflows/+page.svelte | 3 --- 1 file changed, 3 deletions(-) diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/+page.svelte index 561476d25..2bb3fdeae 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/+page.svelte +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/+page.svelte @@ -1,9 +1,6 @@ - + + {#each transform(NoAdminS3.toJSON()) as state, i} + + + + + {state.label} + + + {#each state.connections as conn} + + {/each} + + + {/each} + \ No newline at end of file + .nodeText { + align-items: center; + } + From f10abdfc73bc5ec6197d0b3e6ac1d2d118be6811 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Thu, 12 Sep 2024 10:58:32 -0400 Subject: [PATCH 14/99] Jump to arbitrary state from visualizer --- .../common/public/workflow.ts | 49 +++++++- .../admin/workflows/[product_id]/+page.svelte | 111 +++++++++++------- .../tasks/[product_id]/+page.server.ts | 2 +- 3 files changed, 117 insertions(+), 45 deletions(-) diff --git a/source/SIL.AppBuilder.Portal/common/public/workflow.ts b/source/SIL.AppBuilder.Portal/common/public/workflow.ts index f88e67c1e..8f0892b80 100644 --- a/source/SIL.AppBuilder.Portal/common/public/workflow.ts +++ b/source/SIL.AppBuilder.Portal/common/public/workflow.ts @@ -8,20 +8,53 @@ export const NoAdminS3 = setup({ includeFields: string[]; includeReviewers: boolean; includeArtifacts: string | boolean; + start?: string; + }, + input: {} as { + start?: string; } } }).createMachine({ - initial: 'App Builder Configuration', - context: { + initial: 'Start', + context: ({ input }) => ({ instructions: 'waiting', /** projectName and projectDescription are always included */ includeFields: [], /** Reset to false on exit */ includeReviewers: false, /** Reset to false on exit */ - includeArtifacts: false - }, + includeArtifacts: false, + start: input.start + }), states: { + Start: { + always: [ + { + guard: ({context}) => context.start === 'Synchronize Data', + target: 'Synchronize Data' + }, + { + guard: ({context}) => context.start === 'Product Build', + target: 'Product Build' + }, + { + guard: ({context}) => context.start === 'Verify And Publish', + target: 'Verify And Publish' + }, + { + guard: ({context}) => context.start === 'Published', + target: 'Published' + }, + { + target: 'App Builder Configuration' + } + ], + on: { + Default: { + target: 'App Builder Configuration' + } + } + }, 'App Builder Configuration': { entry: assign({ instructions: 'app_configuration', @@ -95,5 +128,13 @@ export const NoAdminS3 = setup({ }), type: 'final' } + }, + on: { + jump: { + actions: assign({ + start: ({ event }) => event.target + }), + target: '.Start' + } } }); diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte index e6d390fd6..47fb3f69a 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte @@ -9,61 +9,92 @@ const { snapshot, send, actorRef } = useMachine(NoAdminS3, { snapshot: data.instance?.Snapshot ? (JSON.parse(data.instance?.Snapshot || '') as Snapshot) - : undefined + : undefined, + input: {} }); - function transform( - machine: StateMachineDefinition - ): { id: number; label: string; connections: { id: number; target?: string; label?: string }[] }[] { + type StateNode = { + id: number; + label: string; + connections: { id: number; target?: string; label?: string }[]; + }; + + function transform(machine: StateMachineDefinition): StateNode[] { const id = machine.id; const lookup = Object.keys(machine.states); - const a = Object.entries(machine.states).map(([k, v]) => {return { - id: lookup.indexOf(k), - label: k, - connections: Object.values(v.on).map((o) => {return { - id: lookup.indexOf(o[0].toJSON().target?.at(0)?.replace('#'+id+'.', '') ?? ''), - target: o[0].toJSON().target?.at(0)?.replace('#'+id+'.', ''), - label: o[0].eventType - }}) - }}); + const a = Object.entries(machine.states).map(([k, v]) => { + return { + id: lookup.indexOf(k), + label: k, + connections: Object.values(v.on).map((o) => { + return { + id: lookup.indexOf( + o[0] + .toJSON() + .target?.at(0) + ?.replace('#' + id + '.', '') ?? '' + ), + target: o[0] + .toJSON() + .target?.at(0) + ?.replace('#' + id + '.', ''), + label: o[0].eventType + }; + }) + }; + }); console.log(JSON.stringify(a, null, 4)); return a; } + + function handleContextmenu(state: string) { + console.log(state); + console.log("old: "+$snapshot.value); + send({ type: 'jump', target: state}); + console.log("new: "+$snapshot.value); + } - + {#each transform(NoAdminS3.toJSON()) as state, i} - - - - - {state.label} - - - {#each state.connections as conn} - - {/each} - - + + + { + handleContextmenu(state.label); + }} + > + + + {state.label} + + + {#each state.connections as conn} + + {/each} + + {/each} - diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts index ed5fc0a2f..1beba4d38 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts @@ -5,7 +5,7 @@ import { createActor } from 'xstate'; import { redirect } from '@sveltejs/kit'; import { filterObject } from '$lib/filterObject'; -const actor = createActor(NoAdminS3); //later: retrieve snapshot from database +const actor = createActor(NoAdminS3, { input: {}}); //later: retrieve snapshot from database type Fields = { ownerName?: string; //Product.Project.Owner.Name From 9c2fb7af1daf763f525692c2ead5328d4c9d2ad8 Mon Sep 17 00:00:00 2001 From: 7dev7urandom <30197373+7dev7urandom@users.noreply.github.com> Date: Thu, 12 Sep 2024 15:26:02 -0400 Subject: [PATCH 15/99] Style and todos --- .../admin/workflows/+page.svelte | 2 + .../tasks/[product_id]/+page.server.ts | 50 ++++++++++--------- 2 files changed, 29 insertions(+), 23 deletions(-) diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/+page.svelte index 2bb3fdeae..8439ad9e6 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/+page.svelte +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/+page.svelte @@ -14,6 +14,7 @@ {m.tasks_product()} + Last State Transition Date @@ -36,6 +37,7 @@ {:else}
+

There are no active workflow instances

Try creating a product
diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts index 1beba4d38..c5cc9da4c 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts @@ -5,7 +5,7 @@ import { createActor } from 'xstate'; import { redirect } from '@sveltejs/kit'; import { filterObject } from '$lib/filterObject'; -const actor = createActor(NoAdminS3, { input: {}}); //later: retrieve snapshot from database +const actor = createActor(NoAdminS3, { input: {} }); //later: retrieve snapshot from database type Fields = { ownerName?: string; //Product.Project.Owner.Name @@ -21,6 +21,7 @@ type Fields = { }; export const load = (async ({ params, url, locals }) => { + // TODO: permission check const snap = actor.getSnapshot(); const product = await prisma.products.findUnique({ @@ -42,13 +43,15 @@ export const load = (async ({ params, url, locals }) => { } }, //conditionally include reviewers - Reviewers: (snap.context.includeReviewers) ? { - select: { - Id: true, - Name: true, - Email: true + Reviewers: snap.context.includeReviewers + ? { + select: { + Id: true, + Name: true, + Email: true + } } - } : undefined + : undefined } }, Store: { @@ -76,25 +79,25 @@ export const load = (async ({ params, url, locals }) => { const artifacts = snap.context.includeArtifacts ? await prisma.productArtifacts.findMany({ - where: { - ProductId: params.product_id, - ProductBuild: { - BuildId: product?.WorkflowBuildId - }, - //filter by artifact type - ArtifactType: + where: { + ProductId: params.product_id, + ProductBuild: { + BuildId: product?.WorkflowBuildId + }, + //filter by artifact type + ArtifactType: typeof snap.context.includeArtifacts === 'string' ? snap.context.includeArtifacts : undefined //include all - }, - select: { - ProductBuildId: true, - ContentType: true, - FileSize: true, - Url: true, - Id: true - } - }) + }, + select: { + ProductBuildId: true, + ContentType: true, + FileSize: true, + Url: true, + Id: true + } + }) : []; const fields = snap.context.includeFields; @@ -131,6 +134,7 @@ export const load = (async ({ params, url, locals }) => { export const actions = { default: async ({ request }) => { + // TODO: permission check const data = await request.formData(); console.log(data.get('action')); From 036f0b3f273ef871602d6a6c889f3af02c687a4c Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Fri, 13 Sep 2024 09:50:24 -0400 Subject: [PATCH 16/99] Improved instance management menu - Show some product information - Jump state is now a button in the menu rather than a contextmenu event --- .../workflows/[product_id]/+page.server.ts | 27 +++++++- .../admin/workflows/[product_id]/+page.svelte | 66 +++++++++++++++++-- 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.server.ts index 364c5ac12..c0ad675e1 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.server.ts +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.server.ts @@ -7,7 +7,32 @@ export const load: PageServerLoad = async ({ params, url, locals }) => { ProductId: params.product_id }, select: { - ProductId: true, + Product: { + select: { + Project: { + select: { + Name: true + } + }, + ProductDefinition: { + select: { + Name: true + } + }, + ProductTransitions: { + select: { + DateTransition: true, + InitialState: true + }, + orderBy: [ + { + DateTransition: 'desc' + } + ], + take: 1 + } + } + }, Snapshot: true } }); diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte index 47fb3f69a..daf6468c0 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte @@ -3,6 +3,8 @@ import { useMachine } from '@xstate/svelte'; import type { AnyEventObject, Snapshot, StateMachineDefinition } from 'xstate'; import { Node, Svelvet, Anchor } from 'svelvet'; + import { HamburgerIcon } from '$lib/icons/index.js'; + import { instance } from 'valibot'; export let data; @@ -13,6 +15,8 @@ input: {} }); + let selected: string = actorRef.getSnapshot().value; + type StateNode = { id: number; label: string; @@ -47,14 +51,41 @@ return a; } - function handleContextmenu(state: string) { - console.log(state); + function jumpState() { + console.log(selected); console.log("old: "+$snapshot.value); - send({ type: 'jump', target: state}); + send({ type: 'jump', target: selected}); console.log("new: "+$snapshot.value); } + {#each transform(NoAdminS3.toJSON()) as state, i} + { - handleContextmenu(state.label); + on:click={() => { + selected = state.label; }} > - + {state.label} @@ -80,6 +112,9 @@ {/each} + + + {/each} @@ -90,11 +125,28 @@ } .rect { @apply fill-primary h-full w-full; + stroke-width: 3px; } .rect text { @apply items-center fill-primary-content; } .active { - @apply fill-accent; + @apply fill-info; + } + .selected { + @apply stroke-white; + } + #menu { + position: absolute; + z-index: 5; /* position above canvas, but below drawer */ + right: 0; + } + + details > summary { + display: block; /* remove arrow */ + } + + details:not([open]) > summary strong { + display: none; } From 411cd7f65f6889943a6a0d35670e192f24d27254 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Fri, 13 Sep 2024 10:25:05 -0400 Subject: [PATCH 17/99] Retrieve snapshot from db there are no records in db currently, so can't test this yet. --- .../admin/workflows/[product_id]/+page.svelte | 2 +- .../tasks/[product_id]/+page.server.ts | 60 +++++++++++-------- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte index daf6468c0..adae51ef8 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte @@ -10,7 +10,7 @@ const { snapshot, send, actorRef } = useMachine(NoAdminS3, { snapshot: data.instance?.Snapshot - ? (JSON.parse(data.instance?.Snapshot || '') as Snapshot) + ? (JSON.parse(data.instance?.Snapshot || 'null') as Snapshot) : undefined, input: {} }); diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts index c5cc9da4c..2f6991aa7 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/tasks/[product_id]/+page.server.ts @@ -1,12 +1,10 @@ import type { PageServerLoad, Actions } from './$types'; import { prisma } from 'sil.appbuilder.portal.common'; import { NoAdminS3 } from 'sil.appbuilder.portal.common/workflow'; -import { createActor } from 'xstate'; +import { createActor, type Snapshot } from 'xstate'; import { redirect } from '@sveltejs/kit'; import { filterObject } from '$lib/filterObject'; -const actor = createActor(NoAdminS3, { input: {} }); //later: retrieve snapshot from database - type Fields = { ownerName?: string; //Product.Project.Owner.Name ownerEmail?: string; //Product.Project.Owner.Email @@ -22,6 +20,20 @@ type Fields = { export const load = (async ({ params, url, locals }) => { // TODO: permission check + const actor = createActor(NoAdminS3, { + snapshot: + JSON.parse(( + await prisma.workflowInstances.findUnique({ + where: { + ProductId: params.product_id + }, + select: { + Snapshot: true + } + }) + )?.Snapshot || 'null') as Snapshot ?? undefined, + input: {} + }); const snap = actor.getSnapshot(); const product = await prisma.products.findUnique({ @@ -45,12 +57,12 @@ export const load = (async ({ params, url, locals }) => { //conditionally include reviewers Reviewers: snap.context.includeReviewers ? { - select: { - Id: true, - Name: true, - Email: true + select: { + Id: true, + Name: true, + Email: true + } } - } : undefined } }, @@ -79,25 +91,25 @@ export const load = (async ({ params, url, locals }) => { const artifacts = snap.context.includeArtifacts ? await prisma.productArtifacts.findMany({ - where: { - ProductId: params.product_id, - ProductBuild: { - BuildId: product?.WorkflowBuildId - }, - //filter by artifact type - ArtifactType: + where: { + ProductId: params.product_id, + ProductBuild: { + BuildId: product?.WorkflowBuildId + }, + //filter by artifact type + ArtifactType: typeof snap.context.includeArtifacts === 'string' ? snap.context.includeArtifacts : undefined //include all - }, - select: { - ProductBuildId: true, - ContentType: true, - FileSize: true, - Url: true, - Id: true - } - }) + }, + select: { + ProductBuildId: true, + ContentType: true, + FileSize: true, + Url: true, + Id: true + } + }) : []; const fields = snap.context.includeFields; From 325743cf908982bea55266acbcd903ccc526af0f Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Fri, 13 Sep 2024 14:09:18 -0400 Subject: [PATCH 18/99] Add more states to flow --- .../common/public/workflow.ts | 171 ++++++++++++++---- 1 file changed, 140 insertions(+), 31 deletions(-) diff --git a/source/SIL.AppBuilder.Portal/common/public/workflow.ts b/source/SIL.AppBuilder.Portal/common/public/workflow.ts index 8f0892b80..8f6aa456a 100644 --- a/source/SIL.AppBuilder.Portal/common/public/workflow.ts +++ b/source/SIL.AppBuilder.Portal/common/public/workflow.ts @@ -1,5 +1,7 @@ import { setup, assign } from 'xstate'; +//later: update snapshot on state exits (define a function to do it), store instance id in context +//later: update UserTasks on entry? export const NoAdminS3 = setup({ types: { context: {} as { @@ -30,27 +32,66 @@ export const NoAdminS3 = setup({ Start: { always: [ { - guard: ({context}) => context.start === 'Synchronize Data', + guard: ({ context }) => context.start === 'App Builder Configuration', + target: 'App Builder Configuration' + }, + { + guard: ({ context }) => context.start === 'Author Configuration', + //later: guard project has authors + target: 'Author Configuration' + }, + { + guard: ({ context }) => context.start === 'Synchronize Data', target: 'Synchronize Data' }, { - guard: ({context}) => context.start === 'Product Build', + //later: guard project has authors + guard: ({ context }) => context.start === 'Author Download', + target: 'Author Download' + }, + { + //later: guard project has authors + //note: authors can upload at any time, this state is just to prompt an upload + guard: ({ context }) => context.start === 'Author Upload', + target: 'Author Upload' + }, + { + guard: ({ context }) => context.start === 'Product Build', target: 'Product Build' }, { - guard: ({context}) => context.start === 'Verify And Publish', + guard: ({ context }) => context.start === 'Verify And Publish', target: 'Verify And Publish' }, { - guard: ({context}) => context.start === 'Published', + guard: ({ context }) => context.start === 'Publish Product', + target: 'Publish Product' + }, + { + guard: ({ context }) => context.start === 'Published', target: 'Published' }, { - target: 'App Builder Configuration' + target: 'Product Creation' + } + ], + on: { + // this is here just so the default start transition shows up in the visualizer + 'Default.Auto': { + target: 'Product Creation' + } + } + }, + 'Product Creation': { + entry: [ + assign({ instructions: 'waiting' }), + () => { + //later: hook into build engine + console.log('Creating Product'); } ], on: { - Default: { + 'Product Created.Auto': { target: 'App Builder Configuration' } } @@ -58,50 +99,98 @@ export const NoAdminS3 = setup({ 'App Builder Configuration': { entry: assign({ instructions: 'app_configuration', - includeFields: [ - 'storeDescription', - 'listingLanguageCode', - 'projectURL' - ] + includeFields: ['storeDescription', 'listingLanguageCode', 'projectURL'] }), on: { - Continue: { + 'Continue.Owner': { target: 'Product Build' + }, + 'Send to Authors.Owner': { + //later: guard project has authors + target: 'Author Configuration' + } + } + }, + 'Author Configuration': { + entry: assign({ + instructions: 'app_configuration', + includeFields: ['storeDescription', 'listingLanguageCode', 'projectURL'] + }), + on: { + 'Continue.Author': { + target: 'App Builder Configuration' + }, + 'Take Back.Owner': { + target: 'App Builder Configuration' } } }, 'Synchronize Data': { entry: assign({ instructions: 'synchronize_data', - includeFields: [ - 'storeDescription', - 'listingLanguageCode' - ] + includeFields: ['storeDescription', 'listingLanguageCode'] }), on: { - Continue: { + 'Continue.Owner': { target: 'Product Build' + }, + 'Transfer to Authors.Owner': { + //later: guard project has authors + target: 'Author Download' } } }, - 'Product Build': { + 'Author Download': { entry: assign({ - instructions: 'waiting' + instructions: 'authors_download', + includeFields: ['storeDescription', 'listingLanguageCode', 'projectURL'] }), on: { - 'Build Successful': { + 'Continue.Author': { + target: 'Author Upload' + }, + 'Take Back.Owner': { + target: 'Synchronize Data' + } + } + }, + 'Author Upload': { + entry: assign({ + instructions: 'authors_upload', + includeFields: ['storeDescription', 'listingLanguageCode'] + }), + on: { + 'Continue.Author': { + target: 'Synchronize Data' + }, + 'Take Back.Owner': { + target: 'Synchronize Data' + } + } + }, + 'Product Build': { + entry: [ + //later: connect to backend to build product + assign({ + instructions: 'waiting' + }), + () => { + console.log('Building Product'); + } + ], + on: { + 'Build Successful.Auto': { target: 'Verify And Publish' + }, + 'Build Failed.Auto': { + target: 'Synchronize Data' } } }, 'Verify And Publish': { entry: assign({ instructions: 'verify_and_publish', - includeFields: [ - 'storeDescription', - 'listingLanguageCode', - 'projectURL' - ], + includeFields: ['storeDescription', 'listingLanguageCode', 'projectURL'], includeReviewers: true, includeArtifacts: true }), @@ -110,21 +199,41 @@ export const NoAdminS3 = setup({ includeArtifacts: false }), on: { - Reject: { + 'Reject.Owner': { target: 'Synchronize Data' }, - Approve: { + 'Approve.Owner': { + target: 'Publish Product' + }, + 'Email Reviewers.Owner': { + //later: guard project has reviewers + //later: connect to backend to email reviewers + actions: () => { + console.log('Emailing Reviewers'); + } + } + } + }, + 'Publish Product': { + entry: [ + assign({ instructions: 'waiting' }), + () => { + console.log('Publishing Product'); + } + ], + on: { + 'Publish Completed.Auto': { target: 'Published' + }, + 'Publish Failed.Auto': { + target: 'Synchronize Data' } } }, Published: { entry: assign({ instructions: '', - includeFields: [ - 'storeDescription', - 'listingLanguageCode' - ] + includeFields: ['storeDescription', 'listingLanguageCode'] }), type: 'final' } From 1748b1a91a2525ef9b741d4def6a8f578a0ef210 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Fri, 13 Sep 2024 14:32:24 -0400 Subject: [PATCH 19/99] Dynamic number of input anchors on visualization --- .../admin/workflows/[product_id]/+page.svelte | 78 ++++++++++++------- .../tasks/[product_id]/+page.server.ts | 3 +- 2 files changed, 54 insertions(+), 27 deletions(-) diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte index adae51ef8..e44703c49 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte @@ -1,10 +1,14 @@ @@ -64,7 +83,7 @@
- + Information @@ -82,7 +101,9 @@ Date: {data.instance?.Product.ProductTransitions[0].DateTransition?.toLocaleTimeString()} - +
@@ -91,19 +112,25 @@ { selected = state.label; }} > - + {state.label} @@ -111,15 +138,14 @@ {#each state.connections as conn} {/each} - - - - + {#each { length: state.inCount } as c} + + {/each} {/each} - From 737292ba54a81bbfcf4e1cdc32484c2f03bad8f1 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Tue, 17 Sep 2024 09:42:19 -0400 Subject: [PATCH 22/99] Support nodes with fixed position --- .../src/lib/springyGraph.ts | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/source/SIL.AppBuilder.Portal/src/lib/springyGraph.ts b/source/SIL.AppBuilder.Portal/src/lib/springyGraph.ts index 27f7840cc..a9fbd7c74 100644 --- a/source/SIL.AppBuilder.Portal/src/lib/springyGraph.ts +++ b/source/SIL.AppBuilder.Portal/src/lib/springyGraph.ts @@ -36,6 +36,7 @@ export namespace Springy { export type NodeData = { mass?: number; label?: string; + static?: Physics.Vector; // static position }; export type Node = { @@ -96,6 +97,10 @@ export namespace Springy { } } + addNodeData(id: string, data: NodeData) { + this.nodeSet[id].data = data; + } + addEdge(edge: Edge): Edge { let exists = false; this.edges.forEach(function (e) { @@ -163,7 +168,7 @@ export namespace Springy { * * { * "nodes": [ - * "center", + * "center", * "left", * "right", * "up", @@ -177,17 +182,19 @@ export namespace Springy { * } * **/ - loadJSON(json: string | { nodes: string[], edges: string[][]}) { - const obj = typeof json === 'string'? JSON.parse(json) : json; + loadJSON(json: string | { nodes: string[]; edges: string[][] }) { + const obj = typeof json === 'string' ? JSON.parse(json) : json; if ('nodes' in obj || 'edges' in obj) { this.addNodes(obj.nodes); - this.addEdges(obj.edges.map((e: string[]) => { - return { - source: e[0], - target: e[1] - } - })); + this.addEdges( + obj.edges.map((e: string[]) => { + return { + source: e[0], + target: e[1] + }; + }) + ); } } @@ -351,8 +358,8 @@ export namespace Springy { } translateToScreenSpace(offset: Vector, scale: number | Vector) { - const sx = typeof scale === 'number'? scale: scale.x; - const sy = typeof scale === 'number'? scale: scale.y; + const sx = typeof scale === 'number' ? scale : scale.x; + const sy = typeof scale === 'number' ? scale : scale.y; return new Vector(offset.x + this.x * sx, offset.y + this.y * sy); } } @@ -362,15 +369,18 @@ export namespace Springy { m: number; // mass v: Vector; // velocity a: Vector; // acceleration + fixed: boolean; - constructor(position: Vector, mass: number) { + constructor(position: Vector, mass: number, fixed: boolean = false) { this.p = position; // position this.m = mass; // mass this.v = new Vector(0, 0); // velocity this.a = new Vector(0, 0); // acceleration + this.fixed = fixed; } applyForce(force: Vector) { + if (this.fixed) return; // don't apply force if fixed this.a = this.a.add(force.divide(this.m)); } } @@ -420,7 +430,11 @@ export namespace Springy { point(node: Node) { if (!(node.id in this.nodePoints)) { var mass = node.data?.mass !== undefined ? node.data.mass : 1.0; - this.nodePoints[node.id] = new Physics.Point(Physics.Vector.random(), mass); + this.nodePoints[node.id] = new Physics.Point( + node.data?.static ? node.data.static : Physics.Vector.random(), + mass, + node.data?.static !== undefined + ); } return this.nodePoints[node.id]; @@ -720,7 +734,9 @@ export namespace Springy { this.onRenderStart = onRenderStart; this.onRenderFrame = onRenderFrame; - this.layout.graph.subscribe((e) => { this.graphChanged(); }); + this.layout.graph.subscribe((e) => { + this.graphChanged(); + }); } /** From 6b0e4f0364a00dc7d4bd33faa263c30c200de190 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Tue, 17 Sep 2024 09:42:41 -0400 Subject: [PATCH 23/99] Create fixed nodes, show graph before finished --- .../admin/workflows/[product_id]/+page.svelte | 143 +++++++++++------- 1 file changed, 86 insertions(+), 57 deletions(-) diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte index 8fc468ecc..9d733b06e 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte @@ -80,36 +80,58 @@ send({ type: 'jump', target: selected }); } - let positions: { [key: string]: Springy.Physics.Vector } = {}; + let positions: { [key: string]: Springy.Physics.Vector } = Object.keys(NoAdminS3.toJSON().states) + .map((s) => { + return { key: s, value: new Springy.Physics.Vector(0.0, 0.0) }; + }) + .reduce((p, c) => { + p[c.key] = c.value; + return p; + }, {} as { [key: string]: Springy.Physics.Vector }); let ready = false; onMount(() => { const graph = new Springy.Graph(); + graph.loadJSON({ + nodes: Object.keys(NoAdminS3.toJSON().states), + edges: Object.entries(NoAdminS3.toJSON().states) + .map(([k, v]) => { + return Object.values(v.on).map((o) => [k, targetStringFromEvent(o, NoAdminS3.id)]); + }) + .reduce((p, c) => p.concat(c), []) + }); + const bounds = Math.ceil(Math.sqrt(graph.nodes.length)); + graph.addNodeData('Start', { + label: 'Start', + static: new Springy.Physics.Vector(-bounds, -bounds) + }); + graph.addNodeData('Published', { + label: 'Published', + static: new Springy.Physics.Vector(bounds, bounds) + }); + + const layout = new Springy.ForceDirected(graph, 400.0, 400.0, 0.5, 0.00001); const renderer = new Springy.Renderer( - new Springy.ForceDirected(graph, 400.0, 400.0, 0.5, 0.00001), + layout, () => {}, // clear () => {}, // drawEdge (node: Springy.Node, position: Springy.Physics.Vector) => { // drawNode positions[node.id] = position; }, - () => { - // onRenderStop - ready = true; - }, + () => {}, () => {}, // onRenderStart - () => {} // onRenderFrame + () => { + // onRenderFrame + // begin showing earlier, still simulating, just less loading time + if (layout.totalEnergy() < 0.5) { + ready = true; + } + } ); - graph.loadJSON({ - nodes: Object.keys(NoAdminS3.toJSON().states), - edges: Object.entries(NoAdminS3.toJSON().states) - .map(([k, v]) => { - return Object.values(v.on).map((o) => [k, targetStringFromEvent(o, NoAdminS3.id)]); - }) - .reduce((p, c) => p.concat(c), []) - }); + renderer.start(); }); @@ -143,50 +165,57 @@ {#if ready} - - {#each transform(NoAdminS3.toJSON()) as state, i} - - - - { - selected = state.label; - }} + + {#each transform(NoAdminS3.toJSON()) as state, i} + - - - {state.label} - - - {#each state.connections as conn} - - {/each} - {#each { length: state.inCount } as c} - - {/each} - - {/each} - + + + { + selected = state.label; + }} + > + + + {state.label} + + + {#each state.connections as conn} + + {/each} + {#each { length: state.inCount } as c} + + {/each} + + {/each} + {:else} - + {/if} \ No newline at end of file + From c6e68be622d545a410ea7abf36ab84e8f6f3e3d1 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Fri, 18 Oct 2024 08:49:00 -0400 Subject: [PATCH 78/99] Write WorkflowType to ProductTransitions --- source/SIL.AppBuilder.Portal/common/workflow/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/source/SIL.AppBuilder.Portal/common/workflow/index.ts b/source/SIL.AppBuilder.Portal/common/workflow/index.ts index 56147ac07..32fc39b47 100644 --- a/source/SIL.AppBuilder.Portal/common/workflow/index.ts +++ b/source/SIL.AppBuilder.Portal/common/workflow/index.ts @@ -20,7 +20,7 @@ import { Snapshot } from '../public/workflow.js'; import prisma from '../prisma.js'; -import { RoleId, ProductTransitionType } from '../public/prisma.js'; +import { RoleId, ProductTransitionType, WorkflowType } from '../public/prisma.js'; import { allUsersByRole } from '../databaseProxy/UserRoles.js'; import { Prisma } from '@prisma/client'; @@ -364,7 +364,8 @@ export class Workflow { TransitionType: ProductTransitionType.Activity, InitialState: Workflow.stateName(state), DestinationState: Workflow.targetStringFromEvent(t), - Command: t.meta.type !== ActionType.Auto ? t.eventType : null + Command: t.meta.type !== ActionType.Auto ? t.eventType : null, + WorkflowType: WorkflowType.Default // TODO: Change this once we support more workflow types }; } From 0f61f7be782362c753e3d02dd9798e31e984736b Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Fri, 25 Oct 2024 15:00:25 -0400 Subject: [PATCH 79/99] Update in accordance with auto date --- source/SIL.AppBuilder.Portal/common/workflow/index.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/source/SIL.AppBuilder.Portal/common/workflow/index.ts b/source/SIL.AppBuilder.Portal/common/workflow/index.ts index 32fc39b47..2a8399c65 100644 --- a/source/SIL.AppBuilder.Portal/common/workflow/index.ts +++ b/source/SIL.AppBuilder.Portal/common/workflow/index.ts @@ -325,17 +325,13 @@ export class Workflow { .reduce((p, c) => p.concat(c), []) .filter((u, i, a) => a.indexOf(u) === i); - const timestamp = new Date(); - return DatabaseWrites.userTasks.createMany({ data: uids.map((u) => ({ UserId: u, ProductId: this.productId, ActivityName: Workflow.stateName(this.currentState), Status: Workflow.stateName(this.currentState), - Comment: comment ?? null, - DateCreated: timestamp, - DateUpdated: timestamp + Comment: comment ?? null })) }); } From 431c07cdfc6b4e33d9f5f02ea2074b9d4d6cfc74 Mon Sep 17 00:00:00 2001 From: Aidan Jones Date: Fri, 25 Oct 2024 16:06:03 -0400 Subject: [PATCH 80/99] Fix text visibility on workflow admin menu --- .../(authenticated)/admin/workflows/[product_id]/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte index e0fcfe4d1..ad6796056 100644 --- a/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte +++ b/source/SIL.AppBuilder.Portal/src/routes/(authenticated)/admin/workflows/[product_id]/+page.svelte @@ -88,7 +88,7 @@