diff --git a/source/SIL.AppBuilder.Portal/common/index.ts b/source/SIL.AppBuilder.Portal/common/index.ts index f13e19115..0e5ced185 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, getSnapshot } from './workflow/index.js'; +export { DefaultWorkflow, getSnapshot } from './workflow/index.js'; diff --git a/source/SIL.AppBuilder.Portal/common/public/workflow.ts b/source/SIL.AppBuilder.Portal/common/public/workflow.ts index c67e1746f..995502853 100644 --- a/source/SIL.AppBuilder.Portal/common/public/workflow.ts +++ b/source/SIL.AppBuilder.Portal/common/public/workflow.ts @@ -1,24 +1,128 @@ import type { AnyEventObject, StateMachineDefinition, + StateMachine, TransitionDefinition, StateNode as XStateNode } from 'xstate'; +import type { RoleId } from './prisma.js'; + +export enum ActionType { + Auto = 0, + User +} + +export enum AdminLevel { + /** NoAdmin/OwnerAdmin */ + None = 0, + /** LowAdmin */ + Low, + /** Approval required */ + High +} + +export enum ProductType { + Android_GooglePlay = 0, + Android_S3, + AssetPackage, + Web +} + +export type StateName = + | 'Readiness Check' + | 'Approval' + | 'Approval Pending' + | 'Terminated' + | 'App Builder Configuration' + | 'Author Configuration' + | 'Synchronize Data' + | 'Author Download' + | 'Author Upload' + | 'Product Build' + | 'Set Google Play Existing' + | 'App Store Preview' + | 'Create App Store Entry' + | 'Set Google Play Uploaded' + | 'Verify and Publish' + | 'Email Reviewers' + | 'Product Publish' + | 'Make It Live' + | 'Published' + | 'Product Creation'; export type WorkflowContext = { - //later: narrow types if necessary - instructions: string; - includeFields: string[]; + instructions: + | 'asset_package_verify_and_publish' + | 'app_configuration' + | 'create_app_entry' + | 'authors_download' + | 'googleplay_configuration' + | 'googleplay_verify_and_publish' + | 'make_it_live' + | 'approval_pending' + | 'readiness_check' + | 'synchronize_data' + | 'authors_upload' + | 'verify_and_publish' + | 'waiting' + | 'web_verify' + | null; + includeFields: ( + | 'ownerName' + | 'ownerEmail' + | 'storeDescription' + | 'listingLanguageCode' + | 'projectURL' + | 'productDescription' + | 'appType' + | 'projectLanguageCode' + )[]; includeReviewers: boolean; - includeArtifacts: string | boolean; - start?: string; + includeArtifacts: 'apk' | 'aab' | boolean; + start?: StateName; productId: string; + adminLevel: AdminLevel; + environment: { [key: string]: any }; + productType: ProductType; + currentState?: StateName; }; export type WorkflowInput = { productId?: string; + adminLevel?: AdminLevel; + environment?: { [key: string]: any }; + productType?: ProductType; +}; + +export type WorkflowStateMeta = { + level?: AdminLevel | AdminLevel[]; + product?: ProductType | ProductType[]; }; +export type WorkflowTransitionMeta = { + type: ActionType; + user?: RoleId; + level?: AdminLevel | AdminLevel[]; + product?: ProductType | ProductType[]; +}; + +export type WorkflowMachine = StateMachine< + WorkflowContext, + any, + any, + any, + any, + any, + any, + any, + any, + WorkflowInput, + any, + any, + WorkflowStateMeta | WorkflowTransitionMeta, + any +>; + export type StateNode = { id: number; label: string; @@ -33,18 +137,41 @@ export function stateName(s: XStateNode, machineId: string) { } export function targetStringFromEvent( - e: TransitionDefinition[], + e: TransitionDefinition, machineId: string ): string { return ( - e[0] + e .toJSON() .target?.at(0) - ?.replace('#' + machineId + '.', '') ?? '' + ?.replace('#' + machineId + '.', '') || '' + ); +} + +export function filterMachine(machine: WorkflowMachine) {} + +export function filterMeta( + meta: WorkflowStateMeta | WorkflowTransitionMeta | undefined, + ctx: WorkflowContext +) { + return ( + meta === undefined || + ((meta.level + ? Array.isArray(meta.level) + ? meta.level.includes(ctx.adminLevel) + : meta.level === ctx.adminLevel + : true) && + (meta.product + ? Array.isArray(meta.product) + ? meta.product.includes(ctx.productType) + : meta.product === ctx.productType + : true)) ); } -export function transform(machine: StateMachineDefinition): StateNode[] { +export function transform( + machine: StateMachineDefinition +): StateNode[] { const id = machine.id; const lookup = Object.keys(machine.states); const a = Object.entries(machine.states).map(([k, v]) => { @@ -53,8 +180,9 @@ export function transform(machine: StateMachineDefinition): label: k, connections: Object.values(v.on).map((o) => { return { - id: lookup.indexOf(targetStringFromEvent(o, id)), - target: targetStringFromEvent(o, id), + // treat no target on transition as self target + id: lookup.indexOf(targetStringFromEvent(o[0], id) || k), + target: targetStringFromEvent(o[0], id) || k, label: o[0].eventType }; }), @@ -62,7 +190,7 @@ export function transform(machine: StateMachineDefinition): .map(([k, v]) => { return Object.values(v.on).map((e) => { // treat no target on transition as self target - return { from: k, to: targetStringFromEvent(e, id) || k }; + return { from: k, to: targetStringFromEvent(e[0], id) || k }; }); }) .reduce((p, c) => { diff --git a/source/SIL.AppBuilder.Portal/common/workflow/db.ts b/source/SIL.AppBuilder.Portal/common/workflow/db.ts index 83c88fb92..8c4fde4dc 100644 --- a/source/SIL.AppBuilder.Portal/common/workflow/db.ts +++ b/source/SIL.AppBuilder.Portal/common/workflow/db.ts @@ -1,7 +1,15 @@ import DatabaseWrites from '../databaseProxy/index.js'; import { prisma } from '../index.js'; -import { WorkflowContext, targetStringFromEvent, stateName } from '../public/workflow.js'; -import { AnyStateMachine, StateNode } from 'xstate'; +import { + WorkflowContext, + targetStringFromEvent, + stateName, + WorkflowMachine, + filterMeta, + ActionType, + StateName +} from '../public/workflow.js'; +import { StateNode } from 'xstate'; import { RoleId, ProductTransitionType } from '../public/prisma.js'; export type Snapshot = { @@ -9,7 +17,7 @@ export type Snapshot = { context: WorkflowContext; }; -export async function createSnapshot(state: string, context: WorkflowContext) { +export async function createSnapshot(state: StateName, context: WorkflowContext) { return DatabaseWrites.workflowInstances.update({ where: { ProductId: context.productId @@ -20,7 +28,7 @@ export async function createSnapshot(state: string, context: WorkflowContext) { }); } -export async function getSnapshot(productId: string, machine: AnyStateMachine) { +export async function getSnapshot(productId: string, machine: WorkflowMachine) { const snap = JSON.parse( ( await prisma.workflowInstances.findUnique({ @@ -50,7 +58,7 @@ export async function getSnapshot(productId: string, machine: AnyStateMachine) { export async function updateUserTasks( productId: string, roles: RoleId[], - state: string, + state: StateName, comment?: string ) { // Delete all tasks for a product @@ -120,25 +128,28 @@ export async function updateUserTasks( }); } -function transitionFromState(state: StateNode, machineId: string, productId: string) { - console.log(state); +function transitionFromState( + state: StateNode, + machineId: string, + context: WorkflowContext +) { + const t = Object.values(state.on) + .map((v) => v.filter((t) => filterMeta(t.meta, context))) + .filter((v) => v.length > 0 && filterMeta(v[0].meta, context))[0][0]; return { - ProductId: productId, + ProductId: context.productId, TransitionType: ProductTransitionType.Activity, InitialState: stateName(state, machineId), - DestinationState: targetStringFromEvent(Object.values(state.on)[0], machineId), - Command: - Object.keys(state.on)[0].split(':')[1] !== 'Auto' - ? Object.keys(state.on)[0].split(':')[0] - : null + DestinationState: targetStringFromEvent(t, machineId), + Command: t.meta.type !== ActionType.Auto ? t.eventType : null }; } -async function populateTransitions(machine: AnyStateMachine, productId: string) { +async function populateTransitions(machine: WorkflowMachine, context: WorkflowContext) { return DatabaseWrites.productTransitions.createManyAndReturn({ data: [ { - ProductId: productId, + ProductId: context.productId, DateTransition: new Date(), TransitionType: ProductTransitionType.StartWorkflow } @@ -146,8 +157,9 @@ async function populateTransitions(machine: AnyStateMachine, productId: string) Object.entries(machine.states).reduce( (p, [k, v], i) => p.concat( - i === 1 || (i > 1 && p[p.length - 1]?.DestinationState === k && v.type !== 'final') - ? [transitionFromState(v, machine.id, productId)] + filterMeta(v.meta, context) && + (i === 1 || (i > 1 && p[p.length - 1]?.DestinationState === k && v.type !== 'final')) + ? [transitionFromState(v, machine.id, context)] : [] ), [] @@ -163,25 +175,25 @@ async function populateTransitions(machine: AnyStateMachine, productId: string) * Otherwise, create. */ export async function updateProductTransitions( - machine: AnyStateMachine, - productId: string, + machine: WorkflowMachine, + context: WorkflowContext, userId: number | null, - initialState: string, - destinationState: string, + initialState: StateName, + destinationState: StateName, command?: string, comment?: string ) { const transitions = await prisma.productTransitions.count({ where: { - ProductId: productId + ProductId: context.productId } }); if (transitions <= 0) { - await populateTransitions(machine, productId); + await populateTransitions(machine, context); } const transition = await prisma.productTransitions.findFirst({ where: { - ProductId: productId, + ProductId: context.productId, InitialState: initialState, DestinationState: destinationState, DateTransition: null @@ -219,7 +231,7 @@ export async function updateProductTransitions( } else { await DatabaseWrites.productTransitions.create({ data: { - ProductId: productId, + ProductId: context.productId, WorkflowUserId: user?.WorkflowUserId ?? null, AllowedUserNames: user?.Name ?? null, InitialState: initialState, diff --git a/source/SIL.AppBuilder.Portal/common/workflow/default-workflow.ts b/source/SIL.AppBuilder.Portal/common/workflow/default-workflow.ts new file mode 100644 index 000000000..32ee72e4a --- /dev/null +++ b/source/SIL.AppBuilder.Portal/common/workflow/default-workflow.ts @@ -0,0 +1,1010 @@ +import { setup, assign } from 'xstate'; +import DatabaseWrites from '../databaseProxy/index.js'; +import { + WorkflowContext, + WorkflowInput, + WorkflowStateMeta, + WorkflowTransitionMeta, + AdminLevel, + ProductType, + ActionType, + StateName +} from '../public/workflow.js'; +import { createSnapshot, updateUserTasks, updateProductTransitions } from './db.js'; +import { RoleId } from '../public/prisma.js'; + +/** + * IMPORTANT: READ THIS BEFORE EDITING A STATE MACHINE! + * + * Conventions: + * - Each state must have a `meta` with the appropriate properties if it should be included only under certain conditions + * - Each state must have an `entry` in which: + * - All relevant context variables are `assign`ed (e.g. `instructions`, `includeFields`) + * - If `includeReviewers` or `includeFields` is set in `entry`, there must be a corresponding `exit` that `assign`s them to `false` + * - the `snapAndTasks` action is called with the appropriate parameters + * - Each transition must use the `transit` action with the appropriate parameters + * - Each transition must have a `meta` that specifies who can initiate the transition + * - The `meta` may also specify filtering criteria like in a state meta + * - The first transition in a state will be the "happy" path, assuming the state is in the "happy" path + */ +export const DefaultWorkflow = setup({ + types: { + context: {} as WorkflowContext, + input: {} as WorkflowInput, + meta: {} as WorkflowStateMeta | WorkflowTransitionMeta, + events: {} as { + type: any; + comment?: string; + target?: StateName; + userId: number | null; + } + }, + actions: { + snapAndTasks: ( + { context, event }, + params?: { role?: RoleId | RoleId[] | ((context: WorkflowContext) => RoleId | RoleId[]) } + ) => { + const roles = typeof params.role === 'function' ? params.role(context) : params.role; + createSnapshot(context.currentState, context); + updateUserTasks( + context.productId, + roles ? (Array.isArray(roles) ? roles : [roles]) : [], + context.currentState, + event.comment + ); + }, + transit: ({ context, event }, params?: { target?: StateName }) => { + const command = event.type.split(':'); + updateProductTransitions( + DefaultWorkflow, + context, + event.userId, + context.currentState, + params?.target ?? event.target, + command[1] === 'Auto' ? null : command[0], + event.comment + ); + } + }, + guards: { + canJump: ( + { context }, + params: { target: StateName; product?: ProductType; level?: AdminLevel } + ) => { + return ( + context.start === params.target && + (params.product ? params.product === context.productType : true) && + (params.level ? params.level === context.adminLevel : true) + ); + } + } +}).createMachine({ + id: 'DefaultWorkflow', + initial: 'Start', + context: ({ input }) => ({ + instructions: null, + /** projectName and projectDescription are always included */ + includeFields: [], + /** Reset to false on exit */ + includeReviewers: false, + /** Reset to false on exit */ + includeArtifacts: false, + productId: input.productId, + adminLevel: input.adminLevel ?? AdminLevel.None, + environment: input.environment ?? {}, + productType: input.productType ?? ProductType.Android_GooglePlay + }), + states: { + Start: { + entry: ({ context }) => { + DatabaseWrites.workflowInstances.upsert({ + where: { + ProductId: context.productId + }, + update: {}, + create: { + Snapshot: '', + ProductId: context.productId + } + }); + }, + always: [ + { + guard: { type: 'canJump', params: { target: 'Readiness Check', level: AdminLevel.High } }, + target: 'Readiness Check' + }, + { + guard: { type: 'canJump', params: { target: 'Approval', level: AdminLevel.High } }, + target: 'Approval' + }, + { + guard: { + type: 'canJump', + params: { target: 'Approval Pending', level: AdminLevel.High } + }, + target: 'Approval Pending' + }, + { + guard: { type: 'canJump', params: { target: 'Terminated', level: AdminLevel.High } }, + target: 'Terminated' + }, + { + guard: { type: 'canJump', params: { target: 'App Builder Configuration' } }, + target: 'App Builder Configuration' + }, + { + guard: { type: 'canJump', params: { target: 'Author Configuration' } }, + //later: guard project has authors + target: 'Author Configuration' + }, + { + guard: { type: 'canJump', params: { target: 'Synchronize Data' } }, + target: 'Synchronize Data' + }, + { + //later: guard project has authors + guard: { type: 'canJump', params: { target: '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: { type: 'canJump', params: { target: 'Author Upload' } }, + target: 'Author Upload' + }, + { + guard: { type: 'canJump', params: { target: 'Product Build' } }, + target: 'Product Build' + }, + { + guard: { + type: 'canJump', + params: { target: 'Set Google Play Existing', product: ProductType.Android_GooglePlay } + }, + target: 'Set Google Play Existing' + }, + { + guard: { + type: 'canJump', + params: { target: 'App Store Preview', product: ProductType.Android_GooglePlay } + }, + target: 'App Store Preview' + }, + { + guard: { + type: 'canJump', + params: { target: 'Create App Store Entry', product: ProductType.Android_GooglePlay } + }, + target: 'Create App Store Entry' + }, + { + guard: { + type: 'canJump', + params: { target: 'Set Google Play Uploaded', product: ProductType.Android_GooglePlay } + }, + target: 'Set Google Play Uploaded' + }, + { + guard: { type: 'canJump', params: { target: 'Verify and Publish' } }, + target: 'Verify and Publish' + }, + { + guard: { type: 'canJump', params: { target: 'Email Reviewers' } }, + target: 'Email Reviewers' + }, + { + guard: { type: 'canJump', params: { target: 'Product Publish' } }, + target: 'Product Publish' + }, + { + guard: { + type: 'canJump', + params: { target: 'Make It Live', product: ProductType.Android_GooglePlay } + }, + target: 'Make It Live' + }, + { + guard: { type: 'canJump', params: { target: 'Published' } }, + target: 'Published' + }, + { + target: 'Product Creation' + } + ], + on: { + // this is here just so the default start transition shows up in the visualization + // don't actually use this transition + Default: [ + { + meta: { + type: ActionType.Auto, + level: AdminLevel.High + }, + target: 'Readiness Check' + }, + { + meta: { type: ActionType.Auto }, + target: 'Product Creation' + } + ] + } + }, + 'Readiness Check': { + meta: { + level: AdminLevel.High + }, + entry: [ + assign({ + instructions: 'readiness_check', + includeFields: ['storeDescription', 'listingLanguageCode'], + currentState: 'Readiness Check' + }), + { type: 'snapAndTasks', params: { role: RoleId.AppBuilder } } + ], + on: { + Continue: { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder + }, + actions: { type: 'transit', params: { target: 'Approval' } }, + target: 'Approval' + } + } + }, + Approval: { + meta: { + level: AdminLevel.High + }, + entry: [ + assign({ + instructions: null, + includeFields: [ + 'ownerName', + 'ownerEmail', + 'storeDescription', + 'listingLanguageCode', + 'productDescription', + 'appType', + 'projectLanguageCode' + ], + currentState: 'Approval' + }), + { type: 'snapAndTasks', params: { role: RoleId.OrgAdmin } } + ], + on: { + Approve: { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin + }, + actions: { type: 'transit', params: { target: 'Product Creation' } }, + target: 'Product Creation' + }, + Hold: { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin + }, + actions: { type: 'transit', params: { target: 'Approval Pending' } }, + target: 'Approval Pending' + }, + Reject: { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin + }, + actions: { type: 'transit', params: { target: 'Terminated' } }, + target: 'Terminated' + } + } + }, + 'Approval Pending': { + meta: { + level: AdminLevel.High + }, + entry: [ + assign({ + instructions: 'approval_pending', + includeFields: ['storeDescription', 'listingLanguageCode'], + currentState: 'Approval Pending' + }), + { type: 'snapAndTasks', params: { role: RoleId.OrgAdmin } } + ], + on: { + Reject: { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin + }, + actions: { type: 'transit', params: { target: 'Terminated' } }, + target: 'Terminated' + }, + Hold: { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin + }, + actions: { type: 'transit', params: { target: 'Approval Pending' } } + }, + Approve: { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin + }, + actions: { type: 'transit', params: { target: 'Product Creation' } }, + target: 'Product Creation' + } + } + }, + Terminated: { + meta: { + level: AdminLevel.High + }, + entry: [ + assign({ + instructions: null, + includeFields: [], + currentState: 'Terminated' + }), + { type: 'snapAndTasks' } + ], + type: 'final' + }, + 'Product Creation': { + entry: [ + assign({ instructions: 'waiting', currentState: 'Product Creation' }), + { type: 'snapAndTasks' }, + () => { + //later: hook into build engine + console.log('Creating Product'); + } + ], + on: { + 'Product Created': { + meta: { type: ActionType.Auto }, + actions: { type: 'transit', params: { target: 'App Builder Configuration' } }, + target: 'App Builder Configuration' + } + } + }, + 'App Builder Configuration': { + entry: [ + assign({ + instructions: ({ context }) => + context.productType === ProductType.Android_GooglePlay + ? 'googleplay_configuration' + : 'app_configuration', + includeFields: ['storeDescription', 'listingLanguageCode', 'projectURL'], + currentState: 'App Builder Configuration' + }), + { + type: 'snapAndTasks', + params: { role: RoleId.AppBuilder } + } + ], + on: { + 'New App': { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + product: ProductType.Android_GooglePlay + }, + actions: { type: 'transit', params: { target: 'Product Build' } }, + target: 'Product Build' + }, + Continue: { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + product: [ProductType.Android_S3, ProductType.AssetPackage, ProductType.Web] + }, + actions: { type: 'transit', params: { target: 'Product Build' } }, + target: 'Product Build' + }, + 'Existing App': { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + product: ProductType.Android_GooglePlay + }, + actions: { type: 'transit', params: { target: 'Set Google Play Existing' } }, + target: 'Set Google Play Existing' + }, + 'Transfer to Authors': { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder + }, + actions: [ + { type: 'transit', params: { target: 'Author Configuration' } }, + () => { + console.log('Transferring to Authors'); + } + ], + //later: guard project has authors + target: 'Author Configuration' + } + } + }, + 'Set Google Play Existing': { + meta: { + product: ProductType.Android_GooglePlay + }, + entry: [ + assign({ + environment: ({ context }) => { + context.environment['google_play_existing'] = true; + return context.environment; + }, + currentState: 'Set Google Play Existing' + }), + { type: 'snapAndTasks' } + ], + always: [ + { + actions: { type: 'transit', params: { target: 'Product Build' } }, + target: 'Product Build' + } + ], + on: { + // this is here just so the default transition shows up in the visualization + Default: { + meta: { type: ActionType.Auto }, + target: 'Product Build' + } + } + }, + 'Author Configuration': { + entry: [ + assign({ + instructions: 'app_configuration', + includeFields: ['storeDescription', 'listingLanguageCode', 'projectURL'], + currentState: 'Author Configuration' + }), + { + type: 'snapAndTasks', + params: { role: [RoleId.AppBuilder, RoleId.Author] } + } + ], + on: { + Continue: { + meta: { + type: ActionType.User, + user: RoleId.Author + }, + actions: { type: 'transit', params: { target: 'App Builder Configuration' } }, + target: 'App Builder Configuration' + }, + 'Take Back': { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder + }, + actions: { type: 'transit', params: { target: 'App Builder Configuration' } }, + target: 'App Builder Configuration' + } + } + }, + 'Synchronize Data': { + entry: [ + assign({ + instructions: 'synchronize_data', + includeFields: ['storeDescription', 'listingLanguageCode'], + currentState: 'Synchronize Data' + }), + { type: 'snapAndTasks', params: { role: RoleId.AppBuilder } } + ], + on: { + Continue: { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder + }, + actions: { type: 'transit', params: { target: 'Product Build' } }, + target: 'Product Build' + }, + 'Transfer to Authors': { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder + }, + //later: guard project has authors + actions: { type: 'transit', params: { target: 'Author Download' } }, + target: 'Author Download' + } + } + }, + 'Author Download': { + entry: [ + assign({ + instructions: 'authors_download', + includeFields: ['storeDescription', 'listingLanguageCode', 'projectURL'], + currentState: 'Author Download' + }), + { + type: 'snapAndTasks', + params: { role: [RoleId.AppBuilder, RoleId.Author] } + } + ], + on: { + Continue: { + meta: { + type: ActionType.User, + user: RoleId.Author + }, + actions: { type: 'transit', params: { target: 'Author Upload' } }, + target: 'Author Upload' + }, + 'Take Back': { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder + }, + actions: { type: 'transit', params: { target: 'Synchronize Data' } }, + target: 'Synchronize Data' + } + } + }, + 'Author Upload': { + entry: [ + assign({ + instructions: 'authors_upload', + includeFields: ['storeDescription', 'listingLanguageCode'], + currentState: 'Author Upload' + }), + { + type: 'snapAndTasks', + params: { role: [RoleId.AppBuilder, RoleId.Author] } + } + ], + on: { + Continue: { + meta: { + type: ActionType.User, + user: RoleId.Author + }, + actions: { type: 'transit', params: { target: 'Synchronize Data' } }, + target: 'Synchronize Data' + }, + 'Take Back': { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder + }, + actions: { type: 'transit', params: { target: 'Synchronize Data' } }, + target: 'Synchronize Data' + } + } + }, + 'Product Build': { + entry: [ + //later: connect to backend to build product + assign({ + instructions: 'waiting', + currentState: 'Product Build' + }), + { type: 'snapAndTasks' }, + () => { + console.log('Building Product'); + } + ], + on: { + 'Build Successful': [ + { + meta: { + type: ActionType.Auto, + level: [AdminLevel.High, AdminLevel.Low], + product: ProductType.Android_GooglePlay + }, + guard: ({ context }) => + context.productType === ProductType.Android_GooglePlay && + context.adminLevel !== AdminLevel.None && + !context.environment['google_play_existing'], + actions: { type: 'transit', params: { target: 'App Store Preview' } }, + target: 'App Store Preview' + }, + { + meta: { + type: ActionType.Auto, + level: AdminLevel.None, + product: ProductType.Android_GooglePlay + }, + guard: ({ context }) => + context.productType === ProductType.Android_GooglePlay && + context.adminLevel === AdminLevel.None && + !context.environment['google_play_existing'], + actions: { type: 'transit', params: { target: 'App Store Preview' } }, + target: 'App Store Preview' + }, + { + meta: { type: ActionType.Auto }, + guard: ({ context }) => context.productType !== ProductType.Android_GooglePlay, // || other conditions + actions: { type: 'transit', params: { target: 'Verify and Publish' } }, + target: 'Verify and Publish' + } + ], + 'Build Failed': { + meta: { type: ActionType.Auto }, + actions: { type: 'transit', params: { target: 'Synchronize Data' } }, + target: 'Synchronize Data' + } + } + }, + 'App Store Preview': { + meta: { + product: ProductType.Android_GooglePlay + }, + entry: [ + assign({ + instructions: null, + includeFields: [ + 'ownerName', + 'ownerEmail', + 'storeDescription', + 'listingLanguageCode', + 'productDescription', + 'appType', + 'projectLanguageCode' + ], + includeArtifacts: true, + currentState: 'App Store Preview' + }), + { + type: 'snapAndTasks', + params: { + role: (context) => + context.adminLevel === AdminLevel.None ? RoleId.AppBuilder : RoleId.OrgAdmin + } + } + ], + exit: assign({ includeArtifacts: false }), + on: { + Approve: [ + { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin, + level: [AdminLevel.High, AdminLevel.Low] + }, + actions: { type: 'transit', params: { target: 'Create App Store Entry' } }, + target: 'Create App Store Entry' + }, + { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + level: AdminLevel.None + }, + actions: { type: 'transit', params: { target: 'Create App Store Entry' } }, + target: 'Create App Store Entry' + } + ], + Reject: [ + { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin, + level: [AdminLevel.High, AdminLevel.Low] + }, + actions: { type: 'transit', params: { target: 'Synchronize Data' } }, + target: 'Synchronize Data' + }, + { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + level: AdminLevel.None + }, + actions: { type: 'transit', params: { target: 'Synchronize Data' } }, + target: 'Synchronize Data' + } + ] + } + }, + 'Create App Store Entry': { + meta: { + product: ProductType.Android_GooglePlay + }, + entry: [ + assign({ + instructions: 'create_app_entry', + includeFields: ['storeDescription', 'listingLanguageCode'], + includeArtifacts: true, + environment: ({ context }) => { + context.environment['google_play_draft'] = true; + return context.environment; + }, + currentState: 'Create App Store Entry' + }), + { + type: 'snapAndTasks', + params: { + role: (context) => + context.adminLevel === AdminLevel.None ? RoleId.AppBuilder : RoleId.OrgAdmin + } + } + ], + exit: assign({ includeArtifacts: false }), + on: { + Continue: [ + { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin, + level: [AdminLevel.High, AdminLevel.Low] + }, + actions: { type: 'transit', params: { target: 'Set Google Play Uploaded' } }, + target: 'Set Google Play Uploaded' + }, + { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + level: AdminLevel.None + }, + actions: { type: 'transit', params: { target: 'Set Google Play Uploaded' } }, + target: 'Set Google Play Uploaded' + } + ], + Reject: [ + { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin, + level: [AdminLevel.High, AdminLevel.Low] + }, + actions: { type: 'transit', params: { target: 'Synchronize Data' } }, + target: 'Synchronize Data' + }, + { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + level: AdminLevel.None + }, + actions: { type: 'transit', params: { target: 'Synchronize Data' } }, + target: 'Synchronize Data' + } + ] + } + }, + 'Set Google Play Uploaded': { + meta: { + product: ProductType.Android_GooglePlay + }, + entry: [ + assign({ + environment: ({ context }) => { + context.environment['google_play_uploaded'] = true; + return context.environment; + }, + currentState: 'Set Google Play Uploaded' + }), + { type: 'snapAndTasks' } + ], + always: [ + { + actions: { type: 'transit', params: { target: 'Verify and Publish' } }, + target: 'Verify and Publish' + } + ], + on: { + // this is here just so the default transition shows up in the visualization + Default: { + meta: { type: ActionType.Auto }, + target: 'Verify and Publish' + } + } + }, + 'Verify and Publish': { + entry: [ + assign({ + instructions: ({ context }) => { + switch (context.productType) { + case ProductType.Android_GooglePlay: + return 'googleplay_verify_and_publish'; + case ProductType.Android_S3: + return 'verify_and_publish'; + case ProductType.AssetPackage: + return 'asset_package_verify_and_publish'; + case ProductType.Web: + return 'web_verify'; + } + }, + includeFields: ({ context }) => { + switch (context.productType) { + case ProductType.Android_GooglePlay: + case ProductType.Android_S3: + return ['storeDescription', 'listingLanguageCode']; + case ProductType.AssetPackage: + case ProductType.Web: + return ['storeDescription']; + } + }, + includeReviewers: true, + includeArtifacts: true, + currentState: 'Verify and Publish' + }), + { + type: 'snapAndTasks', + params: { role: RoleId.AppBuilder } + } + ], + exit: assign({ + includeReviewers: false, + includeArtifacts: false + }), + on: { + Approve: { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder + }, + actions: { type: 'transit', params: { target: 'Product Publish' } }, + target: 'Product Publish' + }, + Reject: { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder + }, + actions: { type: 'transit', params: { target: 'Synchronize Data' } }, + target: 'Synchronize Data' + }, + 'Email Reviewers': { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder + }, + //later: guard project has reviewers + actions: { type: 'transit', params: { target: 'Email Reviewers' } }, + target: 'Email Reviewers' + } + } + }, + 'Email Reviewers': { + //later: connect to backend to email reviewers + entry: [ + assign({ + currentState: 'Email Reviewers' + }), + { type: 'snapAndTasks' }, + () => { + console.log('Emailing Reviewers'); + } + ], + on: { + Default: { + meta: { type: ActionType.Auto }, + actions: { type: 'transit', params: { target: 'Verify and Publish' } }, + target: 'Verify and Publish' + } + } + }, + 'Product Publish': { + entry: [ + assign({ instructions: 'waiting', currentState: 'Product Publish' }), + { type: 'snapAndTasks' }, + () => { + console.log('Publishing Product'); + } + ], + on: { + 'Publish Completed': [ + { + meta: { + type: ActionType.Auto, + product: ProductType.Android_GooglePlay + }, + guard: ({ context }) => + context.productType === ProductType.Android_GooglePlay && + !context.environment['google_play_existing'], + actions: { type: 'transit', params: { target: 'Make It Live' } }, + target: 'Make It Live' + }, + { + meta: { type: ActionType.Auto }, + guard: ({ context }) => context.productType !== ProductType.Android_GooglePlay, // || other conditions + actions: { type: 'transit', params: { target: 'Published' } }, + target: 'Published' + } + ], + 'Publish Failed': { + meta: { type: ActionType.Auto }, + actions: { type: 'transit', params: { target: 'Synchronize Data' } }, + target: 'Synchronize Data' + } + } + }, + 'Make It Live': { + meta: { + product: ProductType.Android_GooglePlay + }, + entry: [ + assign({ + instructions: 'make_it_live', + includeFields: ['storeDescription', 'listingLanguageCode'], + currentState: 'Make It Live' + }), + { + type: 'snapAndTasks', + params: { + role: (context) => + context.adminLevel === AdminLevel.None ? RoleId.AppBuilder : RoleId.OrgAdmin + } + } + ], + on: { + Continue: [ + { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin, + level: [AdminLevel.High, AdminLevel.Low] + }, + actions: { type: 'transit', params: { target: 'Published' } }, + target: 'Published' + }, + { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + level: AdminLevel.None + }, + actions: { type: 'transit', params: { target: 'Published' } }, + target: 'Published' + } + ], + Reject: [ + { + meta: { + type: ActionType.User, + user: RoleId.OrgAdmin, + level: [AdminLevel.High, AdminLevel.Low] + }, + actions: { type: 'transit', params: { target: 'Synchronize Data' } }, + target: 'Synchronize Data' + }, + { + meta: { + type: ActionType.User, + user: RoleId.AppBuilder, + level: AdminLevel.None + }, + actions: { type: 'transit', params: { target: 'Synchronize Data' } }, + target: 'Synchronize Data' + } + ] + } + }, + Published: { + entry: [ + assign({ + instructions: null, + includeFields: ['storeDescription', 'listingLanguageCode'], + currentState: 'Published' + }), + { type: 'snapAndTasks' } + ], + type: 'final' + } + }, + on: { + Jump: { + actions: [ + assign({ + start: ({ context, event }) => { + console.log(context.start + ' => ' + event.target); + return event.target; + } + }), + { type: 'transit' }, + ({ event }) => { + console.log('Jumping to: ' + event.target); + } + ], + target: '.Start' + } + } +}); diff --git a/source/SIL.AppBuilder.Portal/common/workflow/index.ts b/source/SIL.AppBuilder.Portal/common/workflow/index.ts index 645436f90..533a90add 100644 --- a/source/SIL.AppBuilder.Portal/common/workflow/index.ts +++ b/source/SIL.AppBuilder.Portal/common/workflow/index.ts @@ -1,2 +1,2 @@ -export { NoAdminS3 } from './no-admin-s3.js'; +export { DefaultWorkflow } from './default-workflow.js'; export { getSnapshot } from './db.js'; diff --git a/source/SIL.AppBuilder.Portal/common/workflow/no-admin-s3.ts b/source/SIL.AppBuilder.Portal/common/workflow/no-admin-s3.ts deleted file mode 100644 index 4c7876941..000000000 --- a/source/SIL.AppBuilder.Portal/common/workflow/no-admin-s3.ts +++ /dev/null @@ -1,575 +0,0 @@ -import { setup, assign } from 'xstate'; -import DatabaseWrites from '../databaseProxy/index.js'; -import { WorkflowContext, WorkflowInput } from '../public/workflow.js'; -import { createSnapshot, updateUserTasks, updateProductTransitions } from './db.js'; -import { RoleId } from '../public/prisma.js'; - -//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 WorkflowContext, - input: {} as WorkflowInput - } -}).createMachine({ - 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, - productId: input.productId - }), - states: { - Start: { - entry: ({ context }) => { - DatabaseWrites.workflowInstances.upsert({ - where: { - ProductId: context.productId - }, - update: {}, - create: { - Snapshot: '', - ProductId: context.productId - } - }); - }, - always: [ - { - 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' - }, - { - //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', - target: 'Verify and Publish' - }, - { - guard: ({ context }) => context.start === 'Product Publish', - target: 'Product Publish' - }, - { - guard: ({ context }) => context.start === 'Published', - target: 'Published' - }, - { - 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' }), - ({ context }) => { - createSnapshot('Product Creation', context); - //later: hook into build engine - console.log('Creating Product'); - updateUserTasks(context.productId, [], ''); - } - ], - on: { - 'Product Created:Auto': { - actions: ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'Product Creation', - 'App Builder Configuration', - null, - event.comment - ); - }, - target: 'App Builder Configuration' - } - } - }, - 'App Builder Configuration': { - entry: [ - assign({ - instructions: 'app_configuration', - includeFields: ['storeDescription', 'listingLanguageCode', 'projectURL'] - }), - ({ context, event }) => { - createSnapshot('App Builder Configuration', context); - updateUserTasks( - context.productId, - [RoleId.AppBuilder], - 'App Builder Configuration', - event.comment - ); - } - ], - on: { - 'Continue:Owner': { - actions: ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'App Builder Configuration', - 'Product Build', - 'Continue', - event.comment - ); - }, - target: 'Product Build' - }, - 'Transfer to Authors:Owner': { - actions: ({ context, event }) => { - console.log('Transferring to Authors'); - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'App Builder Configuration', - 'Author Configuration', - 'Transfer to Authors', - event.comment - ); - }, - //later: guard project has authors - target: 'Author Configuration' - } - } - }, - 'Author Configuration': { - entry: [ - assign({ - instructions: 'app_configuration', - includeFields: ['storeDescription', 'listingLanguageCode', 'projectURL'] - }), - ({ context, event }) => { - createSnapshot('Author Configuration', context); - updateUserTasks( - context.productId, - [RoleId.AppBuilder, RoleId.Author], - 'Author Configuration', - event.comment - ); - } - ], - on: { - 'Continue:Author': { - actions: ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'Author Configuration', - 'App Builder Configuration', - 'Continue', - event.comment - ); - }, - target: 'App Builder Configuration' - }, - 'Take Back:Owner': { - actions: ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'Author Configuration', - 'App Builder Configuration', - 'Take Back', - event.comment - ); - }, - target: 'App Builder Configuration' - } - } - }, - 'Synchronize Data': { - entry: [ - assign({ - instructions: 'synchronize_data', - includeFields: ['storeDescription', 'listingLanguageCode'] - }), - ({ context, event }) => { - createSnapshot('Synchronize Data', context); - updateUserTasks( - context.productId, - [RoleId.AppBuilder], - 'Synchronize Data', - event.comment - ); - } - ], - on: { - 'Continue:Owner': { - actions: ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'Synchronize Data', - 'Product Build', - 'Continue', - event.comment - ); - }, - target: 'Product Build' - }, - 'Transfer to Authors:Owner': { - //later: guard project has authors - actions: ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'Synchronize Data', - 'Author Download', - 'Transfer to Authors', - event.comment - ); - }, - target: 'Author Download' - } - } - }, - 'Author Download': { - entry: [ - assign({ - instructions: 'authors_download', - includeFields: ['storeDescription', 'listingLanguageCode', 'projectURL'] - }), - ({ context, event }) => { - createSnapshot('Author Download', context); - updateUserTasks( - context.productId, - [RoleId.AppBuilder, RoleId.Author], - 'Author Download', - event.comment - ); - } - ], - on: { - 'Continue:Author': { - actions: ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'Author Download', - 'Author Upload', - 'Continue', - event.comment - ); - }, - target: 'Author Upload' - }, - 'Take Back:Owner': { - actions: ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'Author Download', - 'Synchronize Data', - 'Take Back', - event.comment - ); - }, - target: 'Synchronize Data' - } - } - }, - 'Author Upload': { - entry: [ - assign({ - instructions: 'authors_upload', - includeFields: ['storeDescription', 'listingLanguageCode'] - }), - ({ context, event }) => { - createSnapshot('Author Upload', context); - updateUserTasks( - context.productId, - [RoleId.AppBuilder, RoleId.Author], - 'Author Upload', - event.comment - ); - } - ], - on: { - 'Continue:Author': { - actions: ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'Author Upload', - 'Synchronize Data', - 'Continue', - event.comment - ); - }, - target: 'Synchronize Data' - }, - 'Take Back:Owner': { - actions: ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'Author Upload', - 'Synchronize Data', - 'Take Back', - event.comment - ); - }, - target: 'Synchronize Data' - } - } - }, - 'Product Build': { - entry: [ - //later: connect to backend to build product - assign({ - instructions: 'waiting' - }), - ({ context }) => { - createSnapshot('Product Build', context); - updateUserTasks(context.productId, [], ''); - }, - () => { - console.log('Building Product'); - } - ], - on: { - 'Build Successful:Auto': { - actions: ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'Product Build', - 'Verify and Publish', - null, - event.comment - ); - }, - target: 'Verify and Publish' - }, - 'Build Failed:Auto': { - actions: ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'Product Build', - 'Synchronize Data', - null, - event.comment - ); - }, - target: 'Synchronize Data' - } - } - }, - 'Verify and Publish': { - entry: [ - assign({ - instructions: 'verify_and_publish', - includeFields: ['storeDescription', 'listingLanguageCode', 'projectURL'], - includeReviewers: true, - includeArtifacts: true - }), - ({ context, event }) => { - createSnapshot('Verify and Publish', context); - updateUserTasks( - context.productId, - [RoleId.AppBuilder], - 'Verify and Publish', - event.comment - ); - } - ], - exit: assign({ - includeReviewers: false, - includeArtifacts: false - }), - on: { - 'Approve:Owner': { - actions: ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'Verify and Publish', - 'Publish Product', - 'Approve', - event.comment - ); - }, - target: 'Product Publish' - }, - 'Reject:Owner': { - actions: ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'Verify and Publish', - 'Synchronize Data', - 'Reject', - event.comment - ); - }, - target: 'Synchronize Data' - }, - 'Email Reviewers:Owner': { - //later: guard project has reviewers - actions: ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'Verify and Publish', - 'Email Reviewers', - 'Email Reviewers', - event.comment - ); - }, - target: 'Email Reviewers' - } - } - }, - 'Email Reviewers': { - //later: connect to backend to email reviewers - entry: [ - () => { - console.log('Emailing Reviewers'); - }, - ({ context, event }) => { - createSnapshot('Email Reviewers', context); - updateUserTasks(context.productId, [], ''); - } - ], - on: { - 'Default:Auto': { - actions: ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'Email Reviewers', - 'Verify and Publish', - null, - event.comment - ); - }, - target: 'Verify and Publish' - } - } - }, - 'Product Publish': { - entry: [ - assign({ instructions: 'waiting' }), - ({ context }) => { - createSnapshot('Publish Product', context); - updateUserTasks(context.productId, [], ''); - }, - () => { - console.log('Publishing Product'); - } - ], - on: { - 'Publish Completed:Auto': { - actions: ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'Product Publish', - 'Published', - null, - event.comment - ); - }, - target: 'Published' - }, - 'Publish Failed:Auto': { - actions: ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - event.userId, - 'Product Publish', - 'Synchronize Data', - null, - event.comment - ); - }, - target: 'Synchronize Data' - } - } - }, - Published: { - entry: [ - assign({ - instructions: '', - includeFields: ['storeDescription', 'listingLanguageCode'] - }), - ({ context }) => { - createSnapshot('Published', context); - updateUserTasks(context.productId, [], ''); - } - ], - type: 'final' - } - }, - on: { - 'Jump To': { - actions: [ - assign({ - start: ({ event }) => event.target - }), - ({ context, event }) => { - updateProductTransitions( - NoAdminS3, - context.productId, - null, - event.previous, - event.target, - null, - event.comment - ); - } - ], - target: '.Start' - } - } -}); 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 236607591..8ee6f4dfd 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 @@ -1,5 +1,5 @@ -import { prisma, NoAdminS3, getSnapshot } from 'sil.appbuilder.portal.common'; -import { transform } from 'sil.appbuilder.portal.common/workflow'; +import { prisma, DefaultWorkflow, getSnapshot } from 'sil.appbuilder.portal.common'; +import { transform, type StateName } from 'sil.appbuilder.portal.common/workflow'; import { createActor, type Snapshot } from 'xstate'; import type { PageServerLoad, Actions } from './$types'; import { fail, superValidate } from 'sveltekit-superforms'; @@ -11,8 +11,8 @@ const jumpStateSchema = v.object({ }); export const load: PageServerLoad = async ({ params, url, locals }) => { - const actor = createActor(NoAdminS3, { - snapshot: await getSnapshot(params.product_id, NoAdminS3), + const actor = createActor(DefaultWorkflow, { + snapshot: await getSnapshot(params.product_id, DefaultWorkflow), input: {} }); @@ -56,7 +56,7 @@ export const load: PageServerLoad = async ({ params, url, locals }) => { return { instance: instance, snapshot: { value: snap.value }, - machine: transform(NoAdminS3.toJSON()) + machine: transform(DefaultWorkflow.toJSON()) }; }; @@ -66,16 +66,20 @@ export const actions = { const form = await superValidate(request, valibot(jumpStateSchema)); if (!form.valid) return fail(400, { form, ok: false }); - const snap = await getSnapshot(params.product_id, NoAdminS3); + const snap = await getSnapshot(params.product_id, DefaultWorkflow); - const actor = createActor(NoAdminS3, { + const actor = createActor(DefaultWorkflow, { snapshot: snap, input: {} }); actor.start(); - actor.send({ type: 'Jump To', target: form.data.state, previous: actor.getSnapshot().value }); + actor.send({ + type: 'Jump', + target: form.data.state as StateName, + userId: null + }); return { form, ok: true }; } 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 677b8c39c..b1512c728 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,6 +1,6 @@ import type { PageServerLoad, Actions } from './$types'; import { getSnapshot, prisma } from 'sil.appbuilder.portal.common'; -import { NoAdminS3 } from 'sil.appbuilder.portal.common'; +import { DefaultWorkflow } from 'sil.appbuilder.portal.common'; import { createActor, type Snapshot } from 'xstate'; import { redirect } from '@sveltejs/kit'; import { filterObject } from '$lib/filterObject'; @@ -31,8 +31,8 @@ type Fields = { export const load = (async ({ params, url, locals }) => { const session = await locals.auth(); // TODO: permission check - const actor = createActor(NoAdminS3, { - snapshot: await getSnapshot(params.product_id, NoAdminS3), + const actor = createActor(DefaultWorkflow, { + snapshot: await getSnapshot(params.product_id, DefaultWorkflow), input: {} }); const snap = actor.getSnapshot(); @@ -131,7 +131,7 @@ export const load = (async ({ params, url, locals }) => { const fields = snap.context.includeFields; return { - actions: Object.keys(NoAdminS3.getStateNodeById(`${NoAdminS3.id}.${snap.value}`).on) + actions: Object.keys(DefaultWorkflow.getStateNodeById(`${DefaultWorkflow.id}.${snap.value}`).on) .filter((a) => { if (session?.user.userId === undefined) return false; switch (a.split(':')[1]) { @@ -183,9 +183,9 @@ export const actions = { const form = await superValidate(request, valibot(sendActionSchema)); if (!form.valid) return fail(400, { form, ok: false }); - const snap = await getSnapshot(params.product_id, NoAdminS3); + const snap = await getSnapshot(params.product_id, DefaultWorkflow); - const actor = createActor(NoAdminS3, { + const actor = createActor(DefaultWorkflow, { snapshot: snap, input: {} }); @@ -195,7 +195,7 @@ export const actions = { //double check that state matches current snapshot if (form.data.state === actor.getSnapshot().value) { const action = Object.keys( - NoAdminS3.getStateNodeById(`${NoAdminS3.id}.${actor.getSnapshot().value}`).on + DefaultWorkflow.getStateNodeById(`${DefaultWorkflow.id}.${actor.getSnapshot().value}`).on ).filter((a) => a.split(':')[0] === form.data.action); actor.send({ type: action[0], comment: form.data.comment, userId: session?.user.userId }); }