Replace starter with office asset management app
This commit is contained in:
parent
b67b6783cd
commit
e9478838d4
@ -1,6 +0,0 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
export default function App() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<h1 className="text-2xl font-semibold">Asset Management</h1>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
17
src/main.tsx
17
src/main.tsx
@ -1,17 +0,0 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { ZinoProvider } from './zino-sdk'
|
||||
import { requireConfigValue, basePath } from './runtimeConfig'
|
||||
import './index.css'
|
||||
import App from './App'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<ZinoProvider baseUrl={requireConfigValue('VITE_ZINO_API_URL')}>
|
||||
<BrowserRouter basename={basePath()}>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</ZinoProvider>
|
||||
</StrictMode>,
|
||||
)
|
||||
@ -1,35 +0,0 @@
|
||||
declare global {
|
||||
interface Window {
|
||||
__RUNTIME_CONFIG__?: Record<string, string>
|
||||
}
|
||||
}
|
||||
|
||||
// SAME_ORIGIN resolves to the serving origin, for a frontend mounted on a custom
|
||||
// domain where the platform is proxied under the same host. It exists because an
|
||||
// empty string is falsy and so can never survive the || chain below, which is
|
||||
// exactly what a "use the current origin" value has to do.
|
||||
export function resolveConfigValue(key: string, fallback = ''): string {
|
||||
const runtime = (window.__RUNTIME_CONFIG__ || {})[key]
|
||||
const devFallback = import.meta.env.DEV ? (import.meta.env[key] as string) : ''
|
||||
const value = runtime || devFallback || fallback
|
||||
return value === 'SAME_ORIGIN' ? window.location.origin.replace(/\/$/, '') : value
|
||||
}
|
||||
|
||||
// Throws rather than silently running against the wrong backend.
|
||||
export function requireConfigValue(key: string): string {
|
||||
const value = resolveConfigValue(key)
|
||||
if (!value) {
|
||||
throw new Error(
|
||||
'Missing runtime config "' + key + '". The server writes config.js when it ' +
|
||||
'places the build; for local dev set ' + key + ' in .env.',
|
||||
)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
// The mount path this app is served under, taken from the <base href> the server
|
||||
// writes into index.html. Drives the router basename, so one build serves any
|
||||
// mount without being rebuilt.
|
||||
export function basePath(): string {
|
||||
return new URL(document.baseURI).pathname
|
||||
}
|
||||
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@ -1,175 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
import { useZino } from "./provider";
|
||||
import type { FormField } from "./types";
|
||||
|
||||
export interface DynamicFormProps {
|
||||
/** Workflow definition ID. */
|
||||
workflowId: string;
|
||||
/** Activity UID — the form schema is fetched for this activity. */
|
||||
activityId: string;
|
||||
/** Instance ID — omit for INIT activities (start workflow). */
|
||||
instanceId?: string;
|
||||
/** Called after successful submission with the result. */
|
||||
onSuccess?: (result: unknown) => void;
|
||||
/** Called on submission error. */
|
||||
onError?: (error: unknown) => void;
|
||||
/** Optional CSS class for the form wrapper. */
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders a dynamic form based on the activity's form schema.
|
||||
*
|
||||
* Fetches the form config from the core-service, renders fields by type,
|
||||
* and submits via performActivity or startWorkflow.
|
||||
*
|
||||
* ```tsx
|
||||
* <DynamicForm
|
||||
* workflowId={WORKFLOW_ID}
|
||||
* activityId={ACTIVITY_IDS.ASSIGN_TICKET}
|
||||
* instanceId={instanceId}
|
||||
* onSuccess={() => { closeModal(); }}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
export function DynamicForm({
|
||||
workflowId,
|
||||
activityId,
|
||||
instanceId,
|
||||
onSuccess,
|
||||
onError,
|
||||
className,
|
||||
}: DynamicFormProps) {
|
||||
const { workflows } = useZino();
|
||||
const queryClient = useQueryClient();
|
||||
const [formData, setFormData] = useState<Record<string, string>>({});
|
||||
|
||||
// Fetch form schema
|
||||
const { data: form, isLoading: formLoading, error: formError } = useQuery({
|
||||
queryKey: ["form", workflowId, activityId, instanceId],
|
||||
queryFn: () => workflows.getForm(workflowId, activityId, "desktop", instanceId),
|
||||
});
|
||||
|
||||
// Submit mutation
|
||||
const submit = useMutation({
|
||||
mutationFn: async (data: Record<string, string>) => {
|
||||
if (instanceId) {
|
||||
return workflows.performActivity(workflowId, instanceId, activityId, data);
|
||||
} else {
|
||||
return workflows.startWorkflow(workflowId, activityId, data);
|
||||
}
|
||||
},
|
||||
onSuccess: (result) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["recordview"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["instance"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["detailview"] });
|
||||
queryClient.invalidateQueries({ queryKey: ["audit"] });
|
||||
onSuccess?.(result);
|
||||
},
|
||||
onError: (err) => {
|
||||
onError?.(err);
|
||||
},
|
||||
});
|
||||
|
||||
const handleChange = (fieldId: string, value: string) => {
|
||||
setFormData((prev) => ({ ...prev, [fieldId]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
// Map form field IDs to their mapped_activity_field_id
|
||||
const mapped: Record<string, string> = {};
|
||||
for (const field of form?.form_json?.fields ?? []) {
|
||||
const key = field.mapped_activity_field_id || field.id;
|
||||
mapped[key] = formData[field.id] ?? "";
|
||||
}
|
||||
submit.mutate(mapped);
|
||||
};
|
||||
|
||||
if (formLoading) {
|
||||
return <div className="flex justify-center py-8"><div className="animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full" /></div>;
|
||||
}
|
||||
|
||||
if (formError) {
|
||||
return <div className="text-red-600 text-sm py-4">Failed to load form: {(formError as any)?.message ?? "Unknown error"}</div>;
|
||||
}
|
||||
|
||||
const fields: FormField[] = form?.form_json?.fields ?? [];
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit} className={className}>
|
||||
{form?.form_json?.title && (
|
||||
<h3 className="text-lg font-semibold mb-4">{form.form_json.title}</h3>
|
||||
)}
|
||||
|
||||
<div className="space-y-4">
|
||||
{fields.map((field) => (
|
||||
<div key={field.id}>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">
|
||||
{field.label}
|
||||
</label>
|
||||
{renderField(field, formData[field.id] ?? "", (v) => handleChange(field.id, v))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{submit.error && (
|
||||
<div className="mt-3 text-sm text-red-600">
|
||||
{(submit.error as any)?.message ?? "Submission failed"}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={submit.isPending}
|
||||
className="mt-6 w-full bg-blue-600 hover:bg-blue-700 disabled:opacity-50 text-white font-medium py-2.5 px-4 rounded-lg transition-colors"
|
||||
>
|
||||
{submit.isPending ? "Submitting..." : "Submit"}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function renderField(
|
||||
field: FormField,
|
||||
value: string,
|
||||
onChange: (value: string) => void,
|
||||
) {
|
||||
const baseClass =
|
||||
"w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent";
|
||||
|
||||
switch (field.type) {
|
||||
case "paragraph":
|
||||
return (
|
||||
<textarea
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
rows={4}
|
||||
className={baseClass}
|
||||
placeholder={field.label}
|
||||
/>
|
||||
);
|
||||
case "number":
|
||||
return (
|
||||
<input
|
||||
type="number"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className={baseClass}
|
||||
placeholder={field.label}
|
||||
/>
|
||||
);
|
||||
case "text":
|
||||
default:
|
||||
return (
|
||||
<input
|
||||
type="text"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
className={baseClass}
|
||||
placeholder={field.label}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,64 +0,0 @@
|
||||
import type { ZinoClient } from "./client";
|
||||
import type { LoginResponse, User } from "./types";
|
||||
import { mockDelay, MOCK_USER } from "./mock";
|
||||
|
||||
/**
|
||||
* Authentication methods — login, logout, and JWT decoding.
|
||||
* Maps to user-service endpoints.
|
||||
*/
|
||||
export class AuthService {
|
||||
private client: ZinoClient;
|
||||
constructor(client: ZinoClient) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
async login(
|
||||
email: string,
|
||||
password: string,
|
||||
orgId?: string,
|
||||
): Promise<LoginResponse> {
|
||||
if (this.client.isMock) {
|
||||
await mockDelay();
|
||||
const res: LoginResponse = {
|
||||
token: "mock-jwt-token-" + Date.now(),
|
||||
user: { ...MOCK_USER },
|
||||
};
|
||||
this.client.setToken(res.token);
|
||||
return res;
|
||||
}
|
||||
|
||||
const res = await this.client.request<LoginResponse>("POST", "/usr/login", {
|
||||
email,
|
||||
password,
|
||||
...(orgId ? { org_id: orgId } : {}),
|
||||
});
|
||||
this.client.setToken(res.token);
|
||||
return res;
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.client.setToken(null);
|
||||
}
|
||||
|
||||
getCurrentUser(): User | null {
|
||||
if (this.client.isMock) return { ...MOCK_USER };
|
||||
|
||||
const token = this.client.getToken();
|
||||
if (!token) return null;
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split(".")[1]));
|
||||
return {
|
||||
id: payload.user_id ?? payload.sub,
|
||||
org_id: payload.org_id ?? "",
|
||||
name: payload.name ?? "",
|
||||
email: payload.email ?? "",
|
||||
roles: payload.roles ?? [],
|
||||
groups: payload.groups ?? [],
|
||||
role_assignments: payload.role_assignments ?? [],
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,86 +0,0 @@
|
||||
import type { ApiError, ZinoClientConfig } from "./types";
|
||||
|
||||
const TOKEN_KEY = "zino_token";
|
||||
|
||||
/**
|
||||
* Core HTTP client for the Zino API.
|
||||
*
|
||||
* Persists JWT in localStorage so sessions survive page refresh.
|
||||
*/
|
||||
export class ZinoClient {
|
||||
readonly baseUrl: string;
|
||||
private token: string | null = null;
|
||||
private onAuthError?: () => void;
|
||||
|
||||
constructor(config: ZinoClientConfig) {
|
||||
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
||||
this.onAuthError = config.onAuthError;
|
||||
// Restore token from localStorage on init
|
||||
if (typeof window !== "undefined") {
|
||||
this.token = localStorage.getItem(TOKEN_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
get isMock(): boolean {
|
||||
return this.baseUrl === "mock";
|
||||
}
|
||||
|
||||
setToken(token: string | null): void {
|
||||
this.token = token;
|
||||
if (typeof window !== "undefined") {
|
||||
if (token) {
|
||||
localStorage.setItem(TOKEN_KEY, token);
|
||||
} else {
|
||||
localStorage.removeItem(TOKEN_KEY);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getToken(): string | null {
|
||||
return this.token;
|
||||
}
|
||||
|
||||
async request<T>(
|
||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||
path: string,
|
||||
body?: unknown,
|
||||
): Promise<T> {
|
||||
const url = `${this.baseUrl}${path}`;
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
};
|
||||
|
||||
if (this.token) {
|
||||
headers["Authorization"] = `Bearer ${this.token}`;
|
||||
}
|
||||
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
|
||||
if (res.status === 401) {
|
||||
this.token = null;
|
||||
this.onAuthError?.();
|
||||
const err: ApiError = { status: 401, message: "Unauthorized" };
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
let message = res.statusText;
|
||||
try {
|
||||
const errBody = (await res.json()) as { error?: string };
|
||||
if (errBody.error) message = errBody.error;
|
||||
} catch {
|
||||
// body wasn't JSON
|
||||
}
|
||||
const err: ApiError = { status: res.status, message };
|
||||
throw err;
|
||||
}
|
||||
|
||||
if (res.status === 204) return undefined as T;
|
||||
|
||||
return (await res.json()) as T;
|
||||
}
|
||||
}
|
||||
@ -1,61 +0,0 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// Zino Service SDK — Public API
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Core client
|
||||
export { ZinoClient } from "./client";
|
||||
|
||||
// Service modules
|
||||
export { AuthService } from "./auth";
|
||||
export { WorkflowService } from "./workflow";
|
||||
export { ViewService } from "./views";
|
||||
|
||||
// React bindings
|
||||
export { ZinoProvider, useZino, queryClient } from "./provider";
|
||||
|
||||
// Components
|
||||
export { DynamicForm } from "./DynamicForm";
|
||||
|
||||
// Re-export TanStack Query hooks for convenience
|
||||
export { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
// Types
|
||||
export type {
|
||||
User,
|
||||
RoleAssignment,
|
||||
LoginRequest,
|
||||
LoginResponse,
|
||||
App,
|
||||
WorkflowDefinition,
|
||||
State,
|
||||
Transition,
|
||||
Activity,
|
||||
ActivityDataField,
|
||||
AllowedRole,
|
||||
WorkflowDataField,
|
||||
GridColumn,
|
||||
DataType,
|
||||
StageGate,
|
||||
StageGateRuleSet,
|
||||
SGRuleOutcome,
|
||||
WorkflowInstance,
|
||||
ActivityLogEntry,
|
||||
FormFieldType,
|
||||
FormField,
|
||||
FormConfig,
|
||||
ActivityForm,
|
||||
ViewField,
|
||||
ViewFieldType,
|
||||
ActionConfig,
|
||||
Column,
|
||||
SortDir,
|
||||
TabularViewParams,
|
||||
TabularViewResponse,
|
||||
ActivityEntry,
|
||||
InstanceReport,
|
||||
DetailViewResponse,
|
||||
ReportSection,
|
||||
StateTransition,
|
||||
ZinoClientConfig,
|
||||
ApiError,
|
||||
} from "./types";
|
||||
@ -1,243 +0,0 @@
|
||||
import type {
|
||||
ActivityForm,
|
||||
ActivityLogEntry,
|
||||
Column,
|
||||
InstanceReport,
|
||||
ReportSection,
|
||||
StateTransition,
|
||||
TabularViewParams,
|
||||
TabularViewResponse,
|
||||
User,
|
||||
WorkflowDefinition,
|
||||
WorkflowInstance,
|
||||
} from "./types";
|
||||
|
||||
export function mockDelay(): Promise<void> {
|
||||
const ms = 200 + Math.random() * 200;
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
|
||||
function randomId(): string {
|
||||
return Math.random().toString(36).slice(2, 10);
|
||||
}
|
||||
|
||||
function isoNow(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function daysAgo(n: number): string {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - n);
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
export const MOCK_USER: User = {
|
||||
id: "usr-001",
|
||||
org_id: "org-acme",
|
||||
name: "Jane Doe",
|
||||
email: "jane@acme.corp",
|
||||
roles: ["admin", "reviewer"],
|
||||
groups: ["engineering"],
|
||||
role_assignments: [
|
||||
{ role_id: "admin", positions: ["manager"] },
|
||||
{ role_id: "reviewer" },
|
||||
],
|
||||
};
|
||||
|
||||
export const MOCK_WORKFLOW_DEF: WorkflowDefinition = {
|
||||
workflow_id: "wf-support-ticket",
|
||||
version: 1,
|
||||
name: "Support Ticket",
|
||||
states: [
|
||||
{ uid: "s-new", name: "New", type: "initial", allowed_activities: ["a-init"] },
|
||||
{ uid: "s-open", name: "Open", allowed_activities: ["a-assign", "a-close"] },
|
||||
{ uid: "s-in-progress", name: "In Progress", allowed_activities: ["a-resolve", "a-escalate"] },
|
||||
{ uid: "s-resolved", name: "Resolved", allowed_activities: ["a-reopen", "a-close"] },
|
||||
{ uid: "s-closed", name: "Closed", type: "terminal", allowed_activities: [] },
|
||||
],
|
||||
activities: [
|
||||
{ uid: "a-init", name: "Create Ticket", type: "INIT", data_fields: [
|
||||
{ id: "f-title", name: "title", type: "local", data_type: "text", mandatory: true },
|
||||
{ id: "f-desc", name: "description", type: "local", data_type: "longtext", mandatory: false },
|
||||
{ id: "f-priority", name: "priority", type: "local", data_type: "text", mandatory: true },
|
||||
] },
|
||||
{ uid: "a-assign", name: "Assign Agent", type: "USER", data_fields: [
|
||||
{ id: "f-agent", name: "assigned_to", type: "local", data_type: "text", mandatory: true },
|
||||
] },
|
||||
{ uid: "a-resolve", name: "Resolve", type: "USER", data_fields: [
|
||||
{ id: "f-resolution", name: "resolution_notes", type: "local", data_type: "longtext", mandatory: true },
|
||||
] },
|
||||
{ uid: "a-escalate", name: "Escalate", type: "USER" },
|
||||
{ uid: "a-reopen", name: "Reopen", type: "USER" },
|
||||
{ uid: "a-close", name: "Close", type: "USER" },
|
||||
],
|
||||
transitions: [
|
||||
{ from_state_id: "s-new", by_activity_id: "a-init", transition_type: "direct", to_state_id: "s-open" },
|
||||
{ from_state_id: "s-open", by_activity_id: "a-assign", transition_type: "direct", to_state_id: "s-in-progress" },
|
||||
{ from_state_id: "s-in-progress", by_activity_id: "a-resolve", transition_type: "direct", to_state_id: "s-resolved" },
|
||||
{ from_state_id: "s-in-progress", by_activity_id: "a-escalate", transition_type: "direct", to_state_id: "s-open" },
|
||||
{ from_state_id: "s-resolved", by_activity_id: "a-reopen", transition_type: "direct", to_state_id: "s-open" },
|
||||
{ from_state_id: "s-resolved", by_activity_id: "a-close", transition_type: "direct", to_state_id: "s-closed" },
|
||||
{ from_state_id: "s-open", by_activity_id: "a-close", transition_type: "direct", to_state_id: "s-closed" },
|
||||
],
|
||||
workflow_data_fields: [
|
||||
{ id: "wdf-title", uid: "wdf-title", name: "title", data_type: "text" },
|
||||
{ id: "wdf-desc", uid: "wdf-desc", name: "description", data_type: "longtext" },
|
||||
{ id: "wdf-priority", uid: "wdf-priority", name: "priority", data_type: "text" },
|
||||
],
|
||||
};
|
||||
|
||||
const NAMES = ["Alice Johnson", "Bob Ramirez", "Chandra Patel", "Dina Sokolov", "Emeka Obi"];
|
||||
const PRIORITIES = ["High", "Medium", "Low"];
|
||||
const STATUSES = ["Open", "In Progress", "Resolved", "Closed"];
|
||||
const TITLES = [
|
||||
"Login page not loading",
|
||||
"Payment gateway timeout",
|
||||
"Dashboard chart misaligned",
|
||||
"Export CSV returns empty file",
|
||||
"Password reset email delayed",
|
||||
"Mobile sidebar overlaps content",
|
||||
"Search returns stale results",
|
||||
"Role permissions not syncing",
|
||||
];
|
||||
|
||||
export function mockWorkflowInstance(workflowId: string, instanceId?: string): WorkflowInstance {
|
||||
return {
|
||||
instance_id: instanceId ?? `inst-${randomId()}`,
|
||||
workflow_id: workflowId,
|
||||
workflow_version: 1,
|
||||
current_state_id: "s-open",
|
||||
data: {
|
||||
title: TITLES[Math.floor(Math.random() * TITLES.length)],
|
||||
priority: PRIORITIES[Math.floor(Math.random() * PRIORITIES.length)],
|
||||
description: "Detailed description of the reported issue.",
|
||||
},
|
||||
created_at: daysAgo(3),
|
||||
updated_at: isoNow(),
|
||||
};
|
||||
}
|
||||
|
||||
export function mockActivityForm(workflowId: string, activityId: string): ActivityForm {
|
||||
return {
|
||||
id: `form-${randomId()}`,
|
||||
workflow_id: workflowId,
|
||||
activity_id: activityId,
|
||||
form_id: `form-${activityId}`,
|
||||
device_type: "desktop",
|
||||
form_json: {
|
||||
title: activityId === "a-init" ? "Create Ticket" : "Update Ticket",
|
||||
fields: [
|
||||
{ id: "f1", label: "Title", type: "text", mapped_activity_field_id: "f-title" },
|
||||
{ id: "f2", label: "Description", type: "paragraph", mapped_activity_field_id: "f-desc" },
|
||||
{ id: "f3", label: "Priority", type: "text", mapped_activity_field_id: "f-priority" },
|
||||
],
|
||||
},
|
||||
created_at: daysAgo(30),
|
||||
};
|
||||
}
|
||||
|
||||
export function mockTabularResponse(params: TabularViewParams): TabularViewResponse {
|
||||
const pageSize = params.pageSize ?? 10;
|
||||
const page = params.page ?? 1;
|
||||
const total = 47;
|
||||
|
||||
const columns: Column[] = [
|
||||
{ key: "instance_id", label: "ID", data_type: "text", sortable: true, filterable: false, searchable: true },
|
||||
{ key: "title", label: "Title", data_type: "text", sortable: true, filterable: false, searchable: true },
|
||||
{ key: "priority", label: "Priority", data_type: "text", sortable: true, filterable: true, searchable: false },
|
||||
{ key: "status", label: "Status", data_type: "text", sortable: true, filterable: true, searchable: false },
|
||||
{ key: "assigned_to", label: "Assigned To", data_type: "text", sortable: true, filterable: true, searchable: true },
|
||||
{ key: "created_at", label: "Created", data_type: "text", sortable: true, filterable: false, searchable: false },
|
||||
];
|
||||
|
||||
const rows: Record<string, unknown>[] = [];
|
||||
const count = Math.min(pageSize, total - (page - 1) * pageSize);
|
||||
for (let i = 0; i < count; i++) {
|
||||
const idx = (page - 1) * pageSize + i;
|
||||
rows.push({
|
||||
instance_id: `TICKET-${1000 + idx}`,
|
||||
title: TITLES[idx % TITLES.length],
|
||||
priority: PRIORITIES[idx % PRIORITIES.length],
|
||||
status: STATUSES[idx % STATUSES.length],
|
||||
assigned_to: NAMES[idx % NAMES.length],
|
||||
created_at: daysAgo(total - idx),
|
||||
});
|
||||
}
|
||||
|
||||
return { rows, total, page, columns };
|
||||
}
|
||||
|
||||
export function mockInstanceReport(instanceId: string): {
|
||||
report: InstanceReport;
|
||||
sections: ReportSection[];
|
||||
history: StateTransition[];
|
||||
} {
|
||||
const report: InstanceReport = {
|
||||
workflow_id: "wf-support-ticket",
|
||||
instance_id: instanceId,
|
||||
workflow_version: 1,
|
||||
current_state_id: "s-in-progress",
|
||||
current_state_name: "In Progress",
|
||||
global_data: {
|
||||
title: "Payment gateway timeout",
|
||||
description: "Users report intermittent 504 errors during checkout.",
|
||||
priority: "High",
|
||||
assigned_to: "Bob Ramirez",
|
||||
},
|
||||
activities: [
|
||||
{ activity_id: "a-init", data: { title: "Payment gateway timeout" }, activity_performed_at: daysAgo(5), performed_by_id: "usr-001" },
|
||||
{ activity_id: "a-assign", data: { assigned_to: "Bob Ramirez" }, activity_performed_at: daysAgo(4), performed_by_id: "usr-001" },
|
||||
],
|
||||
created_at: daysAgo(5),
|
||||
updated_at: daysAgo(4),
|
||||
};
|
||||
|
||||
const sections: ReportSection[] = [
|
||||
{
|
||||
title: "Instance Details",
|
||||
fields: [
|
||||
{ label: "Instance ID", value: instanceId },
|
||||
{ label: "Workflow", value: "Support Ticket" },
|
||||
{ label: "Current State", value: "In Progress" },
|
||||
{ label: "Created", value: report.created_at },
|
||||
{ label: "Updated", value: report.updated_at },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Data",
|
||||
fields: Object.entries(report.global_data).map(([k, v]) => ({ label: k, value: v })),
|
||||
},
|
||||
];
|
||||
|
||||
const history: StateTransition[] = [
|
||||
{ from_state: "New", to_state: "Open", activity_name: "Create Ticket", performed_by: "Jane Doe", timestamp: daysAgo(5), data: {} },
|
||||
{ from_state: "Open", to_state: "In Progress", activity_name: "Assign Agent", performed_by: "Jane Doe", timestamp: daysAgo(4), data: { assigned_to: "Bob Ramirez" } },
|
||||
];
|
||||
|
||||
return { report, sections, history };
|
||||
}
|
||||
|
||||
export function mockAuditLog(): ActivityLogEntry[] {
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
user_id: "usr-001",
|
||||
user_roles: ["admin"],
|
||||
user_groups: ["engineering"],
|
||||
activity_id: "a-init",
|
||||
data: { title: "Payment gateway timeout", priority: "High" },
|
||||
execution_state: "s-open",
|
||||
created_at: daysAgo(5),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
user_id: "usr-001",
|
||||
user_roles: ["admin"],
|
||||
user_groups: ["engineering"],
|
||||
activity_id: "a-assign",
|
||||
data: { assigned_to: "Bob Ramirez" },
|
||||
execution_state: "s-in-progress",
|
||||
created_at: daysAgo(4),
|
||||
},
|
||||
];
|
||||
}
|
||||
@ -1,143 +0,0 @@
|
||||
import React, { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { ZinoClient } from "./client";
|
||||
import { AuthService } from "./auth";
|
||||
import { WorkflowService } from "./workflow";
|
||||
import { ViewService } from "./views";
|
||||
import type { User } from "./types";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Context
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ZinoContextValue {
|
||||
client: ZinoClient;
|
||||
auth: AuthService;
|
||||
workflows: WorkflowService;
|
||||
views: ViewService;
|
||||
user: User | null;
|
||||
setUser: (user: User | null) => void;
|
||||
}
|
||||
|
||||
const ZinoContext = createContext<ZinoContextValue | null>(null);
|
||||
|
||||
const MOCK_SESSION_KEY = "zino_mock_session";
|
||||
|
||||
// Shared QueryClient — sensible defaults for generated apps.
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 30_000, // 30s before refetch
|
||||
retry: 1,
|
||||
refetchOnWindowFocus: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Provider
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ZinoProviderProps {
|
||||
baseUrl: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap your app with `<ZinoProvider>` to get access to Zino services
|
||||
* and TanStack Query throughout the component tree.
|
||||
*
|
||||
* ```tsx
|
||||
* <ZinoProvider baseUrl="http://localhost:8091">
|
||||
* <App />
|
||||
* </ZinoProvider>
|
||||
* ```
|
||||
*/
|
||||
export function ZinoProvider({ baseUrl, children }: ZinoProviderProps) {
|
||||
const [user, setUserState] = useState<User | null>(null);
|
||||
|
||||
const viteMock = import.meta.env.VITE_ZINO_MOCK === "true";
|
||||
const resolvedUrl = baseUrl === "mock" || viteMock ? "mock" : baseUrl;
|
||||
const isMock = resolvedUrl === "mock";
|
||||
|
||||
const setUser = useMemo(() => (u: User | null) => {
|
||||
setUserState(u);
|
||||
if (isMock) {
|
||||
if (u) {
|
||||
sessionStorage.setItem(MOCK_SESSION_KEY, "1");
|
||||
} else {
|
||||
sessionStorage.removeItem(MOCK_SESSION_KEY);
|
||||
}
|
||||
}
|
||||
}, [isMock]);
|
||||
|
||||
// Restore session on mount — from localStorage token or mock sessionStorage.
|
||||
useEffect(() => {
|
||||
if (isMock && sessionStorage.getItem(MOCK_SESSION_KEY)) {
|
||||
const authSvc = new AuthService(new ZinoClient({ baseUrl: "mock" }));
|
||||
const mockUser = authSvc.getCurrentUser();
|
||||
if (mockUser) setUserState(mockUser);
|
||||
} else {
|
||||
// Real mode — check if a token exists in localStorage
|
||||
const client = new ZinoClient({ baseUrl: resolvedUrl });
|
||||
if (client.getToken()) {
|
||||
const authSvc = new AuthService(client);
|
||||
const restored = authSvc.getCurrentUser();
|
||||
if (restored) setUserState(restored);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const value = useMemo<ZinoContextValue>(() => {
|
||||
const client = new ZinoClient({
|
||||
baseUrl: resolvedUrl,
|
||||
onAuthError: () => setUser(null),
|
||||
});
|
||||
|
||||
return {
|
||||
client,
|
||||
auth: new AuthService(client),
|
||||
workflows: new WorkflowService(client),
|
||||
views: new ViewService(client),
|
||||
user,
|
||||
setUser,
|
||||
};
|
||||
}, [resolvedUrl, setUser]);
|
||||
|
||||
const ctx = useMemo(
|
||||
() => ({ ...value, user, setUser }),
|
||||
[value, user, setUser],
|
||||
);
|
||||
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ZinoContext.Provider value={ctx}>{children}</ZinoContext.Provider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Hook
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Access Zino services. Use with TanStack Query:
|
||||
*
|
||||
* ```tsx
|
||||
* const { views } = useZino();
|
||||
* const { data } = useQuery({
|
||||
* queryKey: ['recordview', rvId],
|
||||
* queryFn: () => views.getTabularView(rvId, { page: 1 }),
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
export function useZino(): ZinoContextValue {
|
||||
const ctx = useContext(ZinoContext);
|
||||
if (!ctx) {
|
||||
throw new Error("useZino must be used within a <ZinoProvider>");
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/** Re-export queryClient for direct invalidation in mutations. */
|
||||
export { queryClient };
|
||||
@ -1,285 +0,0 @@
|
||||
// ---------------------------------------------------------------------------
|
||||
// Zino Service SDK — TypeScript Interfaces
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// ---- Auth & User ----
|
||||
|
||||
export interface RoleAssignment {
|
||||
role_id: string;
|
||||
positions?: string[];
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
org_id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
roles: string[];
|
||||
groups: string[];
|
||||
role_assignments: RoleAssignment[];
|
||||
}
|
||||
|
||||
export interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
org_id?: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
token: string;
|
||||
user: User;
|
||||
}
|
||||
|
||||
export interface App {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
org_id: string;
|
||||
}
|
||||
|
||||
// ---- Workflow Definition ----
|
||||
|
||||
export interface AllowedRole {
|
||||
role: string;
|
||||
positions?: string;
|
||||
}
|
||||
|
||||
export interface State {
|
||||
uid: string;
|
||||
name: string;
|
||||
type?: string;
|
||||
allowed_activities: string[];
|
||||
}
|
||||
|
||||
export interface Transition {
|
||||
from_state_id: string;
|
||||
by_activity_id?: string;
|
||||
by_outcome?: string;
|
||||
transition_type: string;
|
||||
to_state_id?: string;
|
||||
stage_gate_id?: string;
|
||||
}
|
||||
|
||||
export type DataType = "text" | "longtext" | "number" | "grid";
|
||||
|
||||
export interface GridColumn {
|
||||
id: string;
|
||||
name: string;
|
||||
data_type: DataType;
|
||||
}
|
||||
|
||||
export interface WorkflowDataField {
|
||||
id: string;
|
||||
uid: string;
|
||||
name: string;
|
||||
data_type: DataType;
|
||||
columns?: GridColumn[];
|
||||
}
|
||||
|
||||
export interface ActivityDataField {
|
||||
id: string;
|
||||
name: string;
|
||||
type: "local" | "mapped";
|
||||
data_type?: DataType;
|
||||
mandatory: boolean;
|
||||
mapped_workflow_field?: string;
|
||||
columns?: GridColumn[];
|
||||
}
|
||||
|
||||
export interface Activity {
|
||||
uid: string;
|
||||
name: string;
|
||||
type: string;
|
||||
allowed_roles?: AllowedRole[];
|
||||
allowed_groups?: string[];
|
||||
dynamic_rbac_allowed?: boolean;
|
||||
only_roles_positions?: boolean;
|
||||
data_fields?: ActivityDataField[];
|
||||
trigger_uid?: string;
|
||||
}
|
||||
|
||||
export interface StageGate {
|
||||
id: string;
|
||||
rules_id: string;
|
||||
}
|
||||
|
||||
export interface SGRuleOutcome {
|
||||
state_id: string;
|
||||
sg_rule_config?: {
|
||||
condition_field?: string;
|
||||
condition_operator?: string;
|
||||
condition_value?: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
export interface StageGateRuleSet {
|
||||
id: string;
|
||||
default_state: string;
|
||||
other_states: SGRuleOutcome[];
|
||||
}
|
||||
|
||||
export interface WorkflowDefinition {
|
||||
workflow_id: string;
|
||||
version: number;
|
||||
name: string;
|
||||
states: State[];
|
||||
activities: Activity[];
|
||||
transitions: Transition[];
|
||||
workflow_data_fields: WorkflowDataField[];
|
||||
stage_gates?: StageGate[];
|
||||
sg_rules?: StageGateRuleSet[];
|
||||
}
|
||||
|
||||
// ---- Workflow Instances ----
|
||||
|
||||
export interface WorkflowInstance {
|
||||
instance_id: string;
|
||||
workflow_id: string;
|
||||
workflow_version?: number;
|
||||
current_state_id: string;
|
||||
current_state_name?: string;
|
||||
data: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface ActivityLogEntry {
|
||||
id: number;
|
||||
user_id: string;
|
||||
user_roles: string[];
|
||||
user_groups: string[];
|
||||
activity_id: string;
|
||||
data: Record<string, unknown>;
|
||||
execution_state: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// ---- Forms ----
|
||||
|
||||
export type FormFieldType = "text" | "paragraph" | "number";
|
||||
|
||||
export interface FormField {
|
||||
id: string;
|
||||
label: string;
|
||||
type: FormFieldType;
|
||||
mapped_activity_field_id: string;
|
||||
}
|
||||
|
||||
export interface FormConfig {
|
||||
title: string;
|
||||
fields: FormField[];
|
||||
}
|
||||
|
||||
export interface ActivityForm {
|
||||
id: string;
|
||||
workflow_id: string;
|
||||
activity_id: string;
|
||||
form_id: string;
|
||||
device_type: string;
|
||||
form_json: FormConfig;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// ---- Views ----
|
||||
|
||||
export interface ActionConfig {
|
||||
label: string;
|
||||
activity_uid: string;
|
||||
workflow_id?: string;
|
||||
}
|
||||
|
||||
export type ViewFieldType =
|
||||
| "global"
|
||||
| "local"
|
||||
| "system-global"
|
||||
| "system-local"
|
||||
| "action";
|
||||
|
||||
export interface ViewField {
|
||||
type: ViewFieldType;
|
||||
field_key: string;
|
||||
output_label: string;
|
||||
activity_id?: string;
|
||||
action?: ActionConfig;
|
||||
data_type: string;
|
||||
is_filter: boolean;
|
||||
is_search: boolean;
|
||||
}
|
||||
|
||||
export interface Column {
|
||||
key: string;
|
||||
label: string;
|
||||
data_type: string;
|
||||
sortable: boolean;
|
||||
filterable: boolean;
|
||||
searchable: boolean;
|
||||
}
|
||||
|
||||
export type SortDir = "asc" | "desc";
|
||||
|
||||
export interface TabularViewParams {
|
||||
page?: number;
|
||||
pageSize?: number;
|
||||
sortBy?: string;
|
||||
sortDir?: SortDir;
|
||||
filters?: Record<string, string>;
|
||||
search?: string;
|
||||
}
|
||||
|
||||
export interface TabularViewResponse {
|
||||
rows: Record<string, unknown>[];
|
||||
total: number;
|
||||
totalPages?: number;
|
||||
page: number;
|
||||
columns: Column[];
|
||||
}
|
||||
|
||||
export interface ActivityEntry {
|
||||
activity_id: string;
|
||||
data: Record<string, unknown>;
|
||||
activity_performed_at: string;
|
||||
performed_by_id: string;
|
||||
}
|
||||
|
||||
export interface InstanceReport {
|
||||
workflow_id: string;
|
||||
instance_id: string;
|
||||
workflow_version: number;
|
||||
current_state_id: string;
|
||||
current_state_name: string;
|
||||
global_data: Record<string, unknown>;
|
||||
activities: ActivityEntry[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
export interface StateTransition {
|
||||
from_state: string;
|
||||
to_state: string;
|
||||
activity_name: string;
|
||||
performed_by: string;
|
||||
timestamp: string;
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface ReportSection {
|
||||
title: string;
|
||||
fields: { label: string; value: unknown }[];
|
||||
}
|
||||
|
||||
export interface DetailViewResponse {
|
||||
data: Record<string, unknown>;
|
||||
sections: ReportSection[];
|
||||
}
|
||||
|
||||
// ---- SDK-internal ----
|
||||
|
||||
export interface ZinoClientConfig {
|
||||
baseUrl: string;
|
||||
onAuthError?: () => void;
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
status: number;
|
||||
message: string;
|
||||
}
|
||||
@ -1,197 +0,0 @@
|
||||
import type { ZinoClient } from "./client";
|
||||
import type {
|
||||
ActivityLogEntry,
|
||||
Column,
|
||||
DetailViewResponse,
|
||||
InstanceReport,
|
||||
ReportSection,
|
||||
StateTransition,
|
||||
TabularViewParams,
|
||||
TabularViewResponse,
|
||||
} from "./types";
|
||||
import {
|
||||
mockDelay,
|
||||
mockTabularResponse,
|
||||
mockInstanceReport,
|
||||
mockAuditLog,
|
||||
} from "./mock";
|
||||
|
||||
/**
|
||||
* View and reporting methods — tabular record views, detail views,
|
||||
* and audit trails. Maps to view-service endpoints.
|
||||
*/
|
||||
export class ViewService {
|
||||
private client: ZinoClient;
|
||||
constructor(client: ZinoClient) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a paginated, sortable, filterable record view.
|
||||
* Maps to `GET /view/recordview?rv_id=…`.
|
||||
*/
|
||||
async getTabularView(
|
||||
viewId: string,
|
||||
params: TabularViewParams = {},
|
||||
): Promise<TabularViewResponse> {
|
||||
if (this.client.isMock) {
|
||||
await mockDelay();
|
||||
return mockTabularResponse(params);
|
||||
}
|
||||
|
||||
const qs = new URLSearchParams();
|
||||
qs.set("rv_id", viewId);
|
||||
if (params.page) qs.set("page", String(params.page));
|
||||
if (params.pageSize) qs.set("limit", String(params.pageSize));
|
||||
if (params.sortBy) qs.set("sort_by", params.sortBy);
|
||||
if (params.sortDir) qs.set("sort_dir", params.sortDir);
|
||||
if (params.search) qs.set("search", params.search);
|
||||
if (params.filters) {
|
||||
for (const [k, v] of Object.entries(params.filters)) {
|
||||
qs.set(`filter.${k}`, v);
|
||||
}
|
||||
}
|
||||
|
||||
const raw = await this.client.request<{
|
||||
config: { fields: Array<{ field_key: string; output_label: string; data_type: string; is_filter: boolean; is_search: boolean }> };
|
||||
data: Record<string, unknown>[];
|
||||
pagination?: { page: number; limit: number; total_count: number; total_pages: number };
|
||||
}>("GET", `/view/recordview?${qs.toString()}`);
|
||||
|
||||
const columns: Column[] = raw.config.fields
|
||||
.filter((f) => f.field_key !== "")
|
||||
.map((f) => ({
|
||||
key: f.field_key,
|
||||
label: f.output_label,
|
||||
data_type: f.data_type,
|
||||
sortable: true,
|
||||
filterable: f.is_filter,
|
||||
searchable: f.is_search,
|
||||
}));
|
||||
|
||||
const rows = Array.isArray(raw.data) ? raw.data : [];
|
||||
|
||||
return {
|
||||
rows,
|
||||
total: raw.pagination?.total_count ?? rows.length,
|
||||
totalPages: raw.pagination?.total_pages,
|
||||
page: raw.pagination?.page ?? params.page ?? 1,
|
||||
columns,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a detail view for a specific workflow instance.
|
||||
* Maps to `GET /view/detailview?dv_id=…&instance_id=…`.
|
||||
*/
|
||||
async getDetailView(viewId: string, instanceId: string): Promise<DetailViewResponse> {
|
||||
if (this.client.isMock) {
|
||||
await mockDelay();
|
||||
const data: Record<string, unknown> = {
|
||||
instance_id: instanceId,
|
||||
view_id: viewId,
|
||||
status: "Open",
|
||||
created_at: new Date().toISOString(),
|
||||
};
|
||||
return {
|
||||
data,
|
||||
sections: [
|
||||
{
|
||||
title: "Details",
|
||||
fields: Object.entries(data).map(([k, v]) => ({ label: k, value: v })),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const raw = await this.client.request<{
|
||||
config: { fields: Array<{ field_key: string; output_label: string; data_type: string }> };
|
||||
data: Record<string, unknown>;
|
||||
}>("GET", `/view/detailview?dv_id=${encodeURIComponent(viewId)}&instance_id=${encodeURIComponent(instanceId)}`);
|
||||
|
||||
const data = raw.data ?? {};
|
||||
const fields = (raw.config?.fields ?? []).filter((f) => f.field_key !== "");
|
||||
|
||||
const sections: ReportSection[] = [
|
||||
{
|
||||
title: "Details",
|
||||
fields: fields.length > 0
|
||||
? fields.map((f) => ({ label: f.output_label, value: data[f.field_key] }))
|
||||
: Object.entries(data).map(([k, v]) => ({ label: k, value: v })),
|
||||
},
|
||||
];
|
||||
|
||||
return { data, sections };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a full instance report — global data, activity history,
|
||||
* and state transitions.
|
||||
*/
|
||||
async getInstanceReport(
|
||||
workflowId: string,
|
||||
instanceId: string,
|
||||
): Promise<{ report: InstanceReport; sections: ReportSection[]; history: StateTransition[] }> {
|
||||
if (this.client.isMock) {
|
||||
await mockDelay();
|
||||
return mockInstanceReport(instanceId);
|
||||
}
|
||||
|
||||
const [instance, audit] = await Promise.all([
|
||||
this.client.request<InstanceReport>("POST", "/view/instance", {
|
||||
workflow_id: workflowId,
|
||||
instance_id: instanceId,
|
||||
}),
|
||||
this.client.request<ActivityLogEntry[]>(
|
||||
"GET",
|
||||
`/view/audit?instance_id=${encodeURIComponent(instanceId)}`,
|
||||
),
|
||||
]);
|
||||
|
||||
const sections: ReportSection[] = [
|
||||
{
|
||||
title: "Instance Details",
|
||||
fields: [
|
||||
{ label: "Instance ID", value: instance.instance_id },
|
||||
{ label: "Workflow", value: instance.workflow_id },
|
||||
{ label: "Current State", value: instance.current_state_name },
|
||||
{ label: "Created", value: instance.created_at },
|
||||
{ label: "Updated", value: instance.updated_at },
|
||||
],
|
||||
},
|
||||
{
|
||||
title: "Data",
|
||||
fields: Object.entries((instance as any).data ?? instance.global_data ?? {}).map(([k, v]) => ({
|
||||
label: k,
|
||||
value: v,
|
||||
})),
|
||||
},
|
||||
];
|
||||
|
||||
const history: StateTransition[] = audit.map((entry) => ({
|
||||
from_state: "",
|
||||
to_state: entry.execution_state,
|
||||
activity_name: entry.activity_id,
|
||||
performed_by: entry.user_id,
|
||||
timestamp: entry.created_at,
|
||||
data: entry.data,
|
||||
}));
|
||||
|
||||
return { report: instance, sections, history };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the raw audit log for an instance.
|
||||
*/
|
||||
async getAuditLog(instanceId: string): Promise<ActivityLogEntry[]> {
|
||||
if (this.client.isMock) {
|
||||
await mockDelay();
|
||||
return mockAuditLog();
|
||||
}
|
||||
|
||||
return this.client.request<ActivityLogEntry[]>(
|
||||
"GET",
|
||||
`/audit?instance_id=${encodeURIComponent(instanceId)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,223 +0,0 @@
|
||||
import type { ZinoClient } from "./client";
|
||||
import type {
|
||||
Activity,
|
||||
ActivityForm,
|
||||
WorkflowDefinition,
|
||||
WorkflowInstance,
|
||||
} from "./types";
|
||||
import {
|
||||
mockDelay,
|
||||
MOCK_WORKFLOW_DEF,
|
||||
mockWorkflowInstance,
|
||||
mockActivityForm,
|
||||
} from "./mock";
|
||||
|
||||
/**
|
||||
* Workflow execution methods — start instances, perform activities,
|
||||
* fetch definitions, forms, and instance state.
|
||||
* Maps to core-service and view-service endpoints.
|
||||
*/
|
||||
export class WorkflowService {
|
||||
private client: ZinoClient;
|
||||
constructor(client: ZinoClient) {
|
||||
this.client = client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a new workflow instance.
|
||||
* Core-service returns a wrapped response; we extract instance_id.
|
||||
*/
|
||||
async startWorkflow(
|
||||
workflowId: string,
|
||||
activityId: string,
|
||||
data?: Record<string, unknown>,
|
||||
): Promise<WorkflowInstance> {
|
||||
if (this.client.isMock) {
|
||||
await mockDelay();
|
||||
return mockWorkflowInstance(workflowId);
|
||||
}
|
||||
|
||||
const raw = await this.client.request<{
|
||||
success: boolean;
|
||||
instance_id?: string;
|
||||
data?: Record<string, unknown>;
|
||||
}>("POST", "/start", {
|
||||
workflow_id: workflowId,
|
||||
activity_id: activityId,
|
||||
data,
|
||||
});
|
||||
|
||||
return {
|
||||
instance_id: raw.instance_id ?? "",
|
||||
workflow_id: workflowId,
|
||||
current_state_id: "",
|
||||
current_state_name: "",
|
||||
data: raw.data ?? {},
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
} as WorkflowInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an activity on a running workflow instance.
|
||||
* Core-service returns a wrapped response; we extract instance_id.
|
||||
*/
|
||||
async performActivity(
|
||||
workflowId: string,
|
||||
instanceId: string,
|
||||
activityId: string,
|
||||
data?: Record<string, unknown>,
|
||||
): Promise<WorkflowInstance> {
|
||||
if (this.client.isMock) {
|
||||
await mockDelay();
|
||||
return mockWorkflowInstance(workflowId, instanceId);
|
||||
}
|
||||
|
||||
const raw = await this.client.request<{
|
||||
success: boolean;
|
||||
instance_id?: string;
|
||||
data?: Record<string, unknown>;
|
||||
}>("POST", "/activity", {
|
||||
workflow_id: workflowId,
|
||||
instance_id: instanceId,
|
||||
activity_id: activityId,
|
||||
data,
|
||||
});
|
||||
|
||||
return {
|
||||
instance_id: raw.instance_id ?? instanceId,
|
||||
workflow_id: workflowId,
|
||||
current_state_id: "",
|
||||
current_state_name: "",
|
||||
data: raw.data ?? {},
|
||||
created_at: "",
|
||||
updated_at: new Date().toISOString(),
|
||||
} as WorkflowInstance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch instance state and compute available activities.
|
||||
* Uses /api-docs to get the activity list (no states endpoint exists),
|
||||
* so all non-INIT activities are returned — the server enforces real constraints.
|
||||
*/
|
||||
async getInstanceState(
|
||||
workflowId: string,
|
||||
instanceId: string,
|
||||
): Promise<{
|
||||
instance: WorkflowInstance;
|
||||
availableActivities: Activity[];
|
||||
}> {
|
||||
if (this.client.isMock) {
|
||||
await mockDelay();
|
||||
return {
|
||||
instance: mockWorkflowInstance(workflowId, instanceId),
|
||||
availableActivities: MOCK_WORKFLOW_DEF.activities.filter(
|
||||
(a) => a.type !== "INIT",
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const [apiDocs, instance] = await Promise.all([
|
||||
this.client.request<Array<{
|
||||
activity_uid: string;
|
||||
activity_name: string;
|
||||
type: string;
|
||||
}>>("GET", `/view/api-docs?workflow_id=${encodeURIComponent(workflowId)}`),
|
||||
this.client.request<WorkflowInstance>("POST", "/view/instance", {
|
||||
workflow_id: workflowId,
|
||||
instance_id: instanceId,
|
||||
}),
|
||||
]);
|
||||
|
||||
// api-docs returns activity documentation; filter out INIT activities
|
||||
const availableActivities: Activity[] = (apiDocs ?? [])
|
||||
.filter((a) => a.type !== "INIT")
|
||||
.map((a) => ({
|
||||
uid: a.activity_uid,
|
||||
name: a.activity_name,
|
||||
type: a.type,
|
||||
}));
|
||||
|
||||
return { instance, availableActivities };
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the workflow definition.
|
||||
* Note: /api-docs returns activity documentation (not full definition with states).
|
||||
* We build a partial WorkflowDefinition from it — states/transitions will be empty.
|
||||
*/
|
||||
async getWorkflowDefinition(
|
||||
workflowId: string,
|
||||
): Promise<WorkflowDefinition> {
|
||||
if (this.client.isMock) {
|
||||
await mockDelay();
|
||||
return { ...MOCK_WORKFLOW_DEF, workflow_id: workflowId };
|
||||
}
|
||||
|
||||
const apiDocs = await this.client.request<Array<{
|
||||
activity_uid: string;
|
||||
activity_name: string;
|
||||
type: string;
|
||||
}>>("GET", `/view/api-docs?workflow_id=${encodeURIComponent(workflowId)}`);
|
||||
|
||||
return {
|
||||
workflow_id: workflowId,
|
||||
version: 1,
|
||||
name: workflowId,
|
||||
states: [],
|
||||
activities: (apiDocs ?? []).map((a) => ({
|
||||
uid: a.activity_uid,
|
||||
name: a.activity_name,
|
||||
type: a.type,
|
||||
})),
|
||||
transitions: [],
|
||||
workflow_data_fields: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the form schema for a specific activity.
|
||||
*/
|
||||
async getForm(
|
||||
workflowId: string,
|
||||
activityId: string,
|
||||
deviceType: string = "desktop",
|
||||
instanceId?: string,
|
||||
): Promise<ActivityForm> {
|
||||
if (this.client.isMock) {
|
||||
await mockDelay();
|
||||
return mockActivityForm(workflowId, activityId);
|
||||
}
|
||||
|
||||
return this.client.request<ActivityForm>("POST", "/form", {
|
||||
workflow_uuid: workflowId,
|
||||
activity_id: activityId,
|
||||
device_type: deviceType,
|
||||
...(instanceId ? { instance_id: instanceId } : {}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit form data for an activity.
|
||||
*/
|
||||
async submitForm(
|
||||
workflowId: string,
|
||||
activityId: string,
|
||||
formData: Record<string, unknown>,
|
||||
deviceType: string = "desktop",
|
||||
instanceId?: string,
|
||||
): Promise<WorkflowInstance> {
|
||||
if (this.client.isMock) {
|
||||
await mockDelay();
|
||||
return mockWorkflowInstance(workflowId, instanceId);
|
||||
}
|
||||
|
||||
return this.client.request<WorkflowInstance>("POST", "/form/submit", {
|
||||
workflow_uuid: workflowId,
|
||||
activity_id: activityId,
|
||||
device_type: deviceType,
|
||||
form_data: formData,
|
||||
...(instanceId ? { instance_id: instanceId } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||
theme: { extend: {} },
|
||||
plugins: [],
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": false,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"strict": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
base: './',
|
||||
server: { port: 5173 },
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user