feat: retarget to dev app 519 + krishna-style API layer
APP_ID/ORG_ID -> 519/900000103 (all other uuids already match dev). Runtime-config BASE_URL (config.js + SAME_ORIGIN, dev fallback), relative vite base + BASE_HREF placeholder for the frontgen artifact contract. API updates from krishna_sales: rdbms lookup via workflow_uuid + form_data, values[] multi-filters, preset_alias, wf-lookup/dataset-options/me endpoints, numeric org_id on login.
This commit is contained in:
parent
bd48e12c23
commit
447b2ec283
3
.env.example
Normal file
3
.env.example
Normal file
@ -0,0 +1,3 @@
|
||||
# Local dev only — a deployed placement gets its API URL from the config.js
|
||||
# the frontgen server writes next to index.html (window.__RUNTIME_CONFIG__).
|
||||
VITE_ZINO_API_URL=https://dev.getzino.in
|
||||
@ -3,11 +3,13 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/zino.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="./zino.svg" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
|
||||
<title>P2P</title>
|
||||
<!--BASE_HREF-->
|
||||
<script src="./config.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { APP_ID } from "../config";
|
||||
import { APP_ID, BASE_URL } from "../config";
|
||||
|
||||
const TOKEN_KEY = "p2p_token";
|
||||
|
||||
function baseUrl(): string {
|
||||
return (import.meta.env.VITE_ZINO_API_URL || "https://studio.getzino.in").replace(/\/+$/, "");
|
||||
return BASE_URL.replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
function headers(): Record<string, string> {
|
||||
@ -62,6 +62,25 @@ async function postFile<T>(path: string, file: File, ctx: FieldFileContext): Pro
|
||||
return parseResponse<T>(res);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Current user (user-service)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface MeResponse {
|
||||
id: string;
|
||||
org_id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
mobile?: string;
|
||||
roles?: string[];
|
||||
groups?: string[];
|
||||
}
|
||||
|
||||
/** Fresh profile for the logged-in user, app-scoped. */
|
||||
export function getMe(): Promise<MeResponse> {
|
||||
return request("GET", `/usr/app/${APP_ID}/me`);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Header
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -151,7 +170,11 @@ export interface SearchQuery {
|
||||
sort_by?: string;
|
||||
sort_dir?: "asc" | "desc";
|
||||
search?: string;
|
||||
filters?: Array<{ field_key: string; value: string; data_type?: string }>;
|
||||
// `values` (multi-value, ORed server-side) supersedes repeating the same
|
||||
// field_key with single `value` entries.
|
||||
filters?: Array<{ field_key: string; value?: string; values?: string[]; data_type?: string }>;
|
||||
// Named filter preset configured on the RV template.
|
||||
preset_alias?: string;
|
||||
}
|
||||
|
||||
export interface RecordViewField {
|
||||
@ -203,13 +226,26 @@ export function getRecordView(
|
||||
return request("POST", `/app/${APP_ID}/view/recordview`, {
|
||||
rv_template_uid: rvTemplateUid,
|
||||
rv_screen_id: String(rvScreenId),
|
||||
...(query.preset_alias ? { preset_alias: query.preset_alias } : {}),
|
||||
search_query: {
|
||||
page: query.page || 1,
|
||||
limit: query.limit || 50,
|
||||
sort_by: query.sort_by || "",
|
||||
sort_dir: query.sort_dir || "desc",
|
||||
search: query.search || "",
|
||||
filters: query.filters || [],
|
||||
filters: (query.filters || []).map((f) => {
|
||||
const item: Record<string, unknown> = {
|
||||
field_key: f.field_key,
|
||||
data_type: f.data_type ?? "character varying",
|
||||
};
|
||||
if (f.values && f.values.length > 0) {
|
||||
item.values = f.values;
|
||||
} else {
|
||||
item.value = f.value ?? "";
|
||||
item.value2 = "";
|
||||
}
|
||||
return item;
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
@ -641,21 +677,83 @@ export interface RdbmsLookupResponse {
|
||||
}
|
||||
|
||||
export interface RdbmsLookupQuery {
|
||||
workflowId: number;
|
||||
workflowUuid: string;
|
||||
activityId: string;
|
||||
fieldId: string;
|
||||
instanceId?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
search?: string;
|
||||
// Current form values, so the server can apply the activity's field_rules
|
||||
// filter_options (dependent/cascading lookups).
|
||||
formData?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// Core-service expects the workflow UUID as `workflow_uuid` — the numeric
|
||||
// `workflow_id` field was removed when the clone-portable identifier rolled out.
|
||||
export function fetchRdbmsLookupRecords(templateUuid: string, q: RdbmsLookupQuery): Promise<RdbmsLookupResponse> {
|
||||
return request("POST", `/app/${APP_ID}/rdbms-templates/${encodeURIComponent(templateUuid)}/records`, {
|
||||
workflow_id: q.workflowId,
|
||||
workflow_uuid: q.workflowUuid,
|
||||
activity_id: q.activityId,
|
||||
field_id: q.fieldId,
|
||||
instance_id: q.instanceId || undefined,
|
||||
limit: q.limit ?? 1000,
|
||||
offset: q.offset ?? 0,
|
||||
search: q.search ?? "",
|
||||
form_data: q.formData ?? {},
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// WF Lookup records (cross-workflow references) & dataset-backed options
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Fetch records for a wf_lookup field. The server resolves the field's lookup
|
||||
* config + filter rules from activityId/fieldId against formData. */
|
||||
export function wfLookupRecords(
|
||||
workflowUuid: string,
|
||||
opts: {
|
||||
activityId: string;
|
||||
fieldId: string;
|
||||
formData?: Record<string, unknown>;
|
||||
search?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
},
|
||||
): Promise<{ data?: Record<string, unknown>[]; records?: Record<string, unknown>[] } | Record<string, unknown>[]> {
|
||||
return request("POST", `/app/${APP_ID}/wf-lookup/records`, {
|
||||
workflow_uuid: workflowUuid,
|
||||
activity_id: opts.activityId,
|
||||
field_id: opts.fieldId,
|
||||
form_data: opts.formData ?? {},
|
||||
search: opts.search ?? "",
|
||||
limit: opts.limit ?? 100,
|
||||
offset: opts.offset ?? 0,
|
||||
});
|
||||
}
|
||||
|
||||
/** Fetch options for a dataset-backed select. The server applies the activity's
|
||||
* rules against formData, so e.g. City options come back scoped to the chosen
|
||||
* State. Returns {label, value} rows (plus the raw dataset row). */
|
||||
export function datasetOptions(
|
||||
workflowUuid: string,
|
||||
opts: {
|
||||
activityId: string;
|
||||
fieldId: string;
|
||||
search?: string;
|
||||
instanceId?: string;
|
||||
limit?: number;
|
||||
formData?: Record<string, unknown>;
|
||||
},
|
||||
): Promise<{ options: Array<{ label: string; value: string; _raw?: Record<string, string> }> }> {
|
||||
return request("POST", `/app/${APP_ID}/dataset-options`, {
|
||||
workflow_uuid: workflowUuid,
|
||||
activity_id: opts.activityId,
|
||||
field_id: opts.fieldId,
|
||||
instance_id: opts.instanceId || undefined,
|
||||
search: opts.search ?? "",
|
||||
limit: opts.limit ?? 200,
|
||||
offset: 0,
|
||||
form_data: opts.formData ?? {},
|
||||
});
|
||||
}
|
||||
|
||||
@ -87,14 +87,12 @@ export default function RecordViewTable({ viewId, onRowClick, toolbarAction }: P
|
||||
sort_by: params.sort || undefined,
|
||||
sort_dir: params.dir,
|
||||
search: params.q || undefined,
|
||||
// One entry per selected state. NOTE: this assumes the view-service ORs
|
||||
// repeated filters on the same field_key — verify against the live API,
|
||||
// since an AND would make any multi-select return zero rows.
|
||||
filters: params.status.map((value) => ({
|
||||
field_key: STATE_FIELD,
|
||||
value,
|
||||
data_type: "text",
|
||||
})),
|
||||
// Multi-select ships as one filter with a `values` array, which the
|
||||
// view-service ORs server-side.
|
||||
filters:
|
||||
params.status.length > 0
|
||||
? [{ field_key: STATE_FIELD, values: params.status, data_type: "text" }]
|
||||
: [],
|
||||
}),
|
||||
[params],
|
||||
);
|
||||
|
||||
@ -1,8 +1,29 @@
|
||||
// P2P Invoice System (dev) — App 169, Org 47
|
||||
// P2P Invoice System (dev) — App 519, Org 900000103
|
||||
// Mirrors the studio app 20 build but with dev's UUID-based UIDs.
|
||||
|
||||
export const ORG_ID = "47";
|
||||
export const APP_ID = "169";
|
||||
export const ORG_ID = "900000103";
|
||||
export const APP_ID = "519";
|
||||
|
||||
// Backend URL comes from the runtime config.js the frontgen server writes at
|
||||
// placement time (window.__RUNTIME_CONFIG__.VITE_ZINO_API_URL) so one built
|
||||
// artifact can be promoted across environments. The env fallback is dev-gated:
|
||||
// a production build without a config.js must not silently serve the build
|
||||
// environment's API URL.
|
||||
declare global {
|
||||
interface Window { __RUNTIME_CONFIG__?: Record<string, string> }
|
||||
}
|
||||
const runtimeApiURL =
|
||||
typeof window !== "undefined" ? window.__RUNTIME_CONFIG__?.VITE_ZINO_API_URL : undefined;
|
||||
|
||||
// SAME_ORIGIN means "call the host that served this page": on a custom domain
|
||||
// the same Ingress serves /usr and /app/, so the API is this origin. It is a
|
||||
// sentinel rather than '' because the || below treats '' as absent.
|
||||
export const BASE_URL =
|
||||
runtimeApiURL === "SAME_ORIGIN"
|
||||
? window.location.origin.replace(/\/$/, "")
|
||||
: runtimeApiURL ||
|
||||
(import.meta.env.DEV ? import.meta.env.VITE_ZINO_API_URL : undefined) ||
|
||||
"https://dev.getzino.in";
|
||||
|
||||
// Core-service expects the workflow UUID under `workflow_uuid`.
|
||||
export const WORKFLOW_ID = "0588b645-75f7-40e5-8251-e0761e4be632";
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import type { User } from "../zino-sdk/types";
|
||||
import { BASE_URL } from "../config";
|
||||
|
||||
const TOKEN_KEY = "p2p_token";
|
||||
const USER_KEY = "p2p_user";
|
||||
@ -30,9 +31,7 @@ export function useAuth() {
|
||||
return { user, token, loading: false, error: null };
|
||||
});
|
||||
|
||||
const apiUrl = (
|
||||
import.meta.env.VITE_ZINO_API_URL || "https://studio.getzino.in"
|
||||
).replace(/\/+$/, "");
|
||||
const apiUrl = BASE_URL.replace(/\/+$/, "");
|
||||
|
||||
const login = useCallback(
|
||||
async ({ email, password, orgId }: LoginParams) => {
|
||||
@ -44,7 +43,7 @@ export function useAuth() {
|
||||
body: JSON.stringify({
|
||||
email,
|
||||
password,
|
||||
...(orgId ? { org_id: orgId } : {}),
|
||||
...(orgId ? { org_id: Number(orgId) } : {}),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
12
src/main.tsx
12
src/main.tsx
@ -10,13 +10,19 @@ import { Toaster } from './components/ui/sonner'
|
||||
import './index.css'
|
||||
import App from './App'
|
||||
|
||||
const apiUrl = import.meta.env.VITE_ZINO_API_URL || 'https://studio.getzino.in'
|
||||
import { BASE_URL } from './config'
|
||||
|
||||
// Mount path comes from the document <base href> the frontgen server writes at
|
||||
// placement time, so one built artifact serves any mount without a hardcoded
|
||||
// slug. Falls back to "/" for local dev.
|
||||
const routerBasename =
|
||||
(document.querySelector('base')?.getAttribute('href') || '/').replace(/\/$/, '') || '/'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<ThemeProvider>
|
||||
<ZinoProvider baseUrl={apiUrl}>
|
||||
<BrowserRouter basename={import.meta.env.BASE_URL}>
|
||||
<ZinoProvider baseUrl={BASE_URL}>
|
||||
<BrowserRouter basename={routerBasename}>
|
||||
{/* NuqsAdapter must sit inside the router — it reads and writes the
|
||||
router's search params, so table state can live in the URL. */}
|
||||
<NuqsAdapter>
|
||||
|
||||
@ -3,7 +3,7 @@ import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
const devTarget = 'https://studio.getzino.in'
|
||||
const devTarget = 'https://dev.getzino.in'
|
||||
const proxy = (target: string) => ({ target, changeOrigin: true, secure: false })
|
||||
|
||||
export default defineConfig({
|
||||
@ -11,7 +11,9 @@ export default defineConfig({
|
||||
resolve: {
|
||||
alias: { '@': fileURLToPath(new URL('./src', import.meta.url)) },
|
||||
},
|
||||
base: process.env.VITE_BASE_URL ?? '/p2p/',
|
||||
// Relative base so the built artifact is mount-path agnostic; the target
|
||||
// frontgen writes the real <base href> into index.html at placement time.
|
||||
base: './',
|
||||
server: {
|
||||
port: 3007,
|
||||
proxy: {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user