chore: initial scaffold
This commit is contained in:
parent
ebcc0486df
commit
d2d3a12e92
8
.env.example
Normal file
8
.env.example
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Local development only (`npm run dev`). Reference for the values
|
||||||
|
# `npm run dev` reads from .env; nothing here is used by a deployed build.
|
||||||
|
#
|
||||||
|
# In a deployed environment these values come from config.js, which the server
|
||||||
|
# writes when it places the build. Do not commit environment-specific values —
|
||||||
|
# one build is promoted across environments unchanged.
|
||||||
|
VITE_ZINO_API_URL=http://localhost:8085
|
||||||
|
VITE_ZINO_MOCK=false
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
*.local
|
||||||
@ -1,3 +1,3 @@
|
|||||||
# asset-management
|
# asset-management
|
||||||
|
|
||||||
asset-management — generated by frontgen
|
Frontgen project for org 1 / app 531.
|
||||||
|
|||||||
14
index.html
Normal file
14
index.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<!--BASE_HREF-->
|
||||||
|
<title>asset-management</title>
|
||||||
|
<script src="./config.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
29
package.json
Normal file
29
package.json
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"name": "asset-management",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"typecheck": "tsc -b",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/react-query": "^5.60.5",
|
||||||
|
"lucide-react": "^0.441.0",
|
||||||
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
|
"react-router-dom": "^6.26.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^18.3.5",
|
||||||
|
"@types/react-dom": "^18.3.0",
|
||||||
|
"@vitejs/plugin-react": "^4.3.1",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"postcss": "^8.4.41",
|
||||||
|
"tailwindcss": "^3.4.10",
|
||||||
|
"typescript": "~5.5.4",
|
||||||
|
"vite": "^5.4.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
postcss.config.js
Normal file
6
postcss.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
7
src/App.tsx
Normal file
7
src/App.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
3
src/index.css
Normal file
3
src/index.css
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
17
src/main.tsx
Normal file
17
src/main.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
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>,
|
||||||
|
)
|
||||||
35
src/runtimeConfig.ts
Normal file
35
src/runtimeConfig.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/// <reference types="vite/client" />
|
||||||
175
src/zino-sdk/DynamicForm.tsx
Normal file
175
src/zino-sdk/DynamicForm.tsx
Normal file
@ -0,0 +1,175 @@
|
|||||||
|
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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
64
src/zino-sdk/auth.ts
Normal file
64
src/zino-sdk/auth.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
86
src/zino-sdk/client.ts
Normal file
86
src/zino-sdk/client.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
61
src/zino-sdk/index.ts
Normal file
61
src/zino-sdk/index.ts
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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";
|
||||||
243
src/zino-sdk/mock.ts
Normal file
243
src/zino-sdk/mock.ts
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
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),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
143
src/zino-sdk/provider.tsx
Normal file
143
src/zino-sdk/provider.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
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 };
|
||||||
285
src/zino-sdk/types.ts
Normal file
285
src/zino-sdk/types.ts
Normal file
@ -0,0 +1,285 @@
|
|||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
197
src/zino-sdk/views.ts
Normal file
197
src/zino-sdk/views.ts
Normal file
@ -0,0 +1,197 @@
|
|||||||
|
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)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
223
src/zino-sdk/workflow.ts
Normal file
223
src/zino-sdk/workflow.ts
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
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 } : {}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
6
tailwind.config.js
Normal file
6
tailwind.config.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
||||||
|
theme: { extend: {} },
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
19
tsconfig.app.json
Normal file
19
tsconfig.app.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
7
tsconfig.json
Normal file
7
tsconfig.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
15
tsconfig.node.json
Normal file
15
tsconfig.node.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"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"]
|
||||||
|
}
|
||||||
8
vite.config.ts
Normal file
8
vite.config.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
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