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:
Bhanu Prakash Sai Potteri 2026-08-19 12:56:46 +05:30
parent bd48e12c23
commit 447b2ec283
8 changed files with 156 additions and 27 deletions

3
.env.example Normal file
View 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

View File

@ -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>

View File

@ -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 ?? {},
});
}

View File

@ -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],
);

View File

@ -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";

View File

@ -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) } : {}),
}),
});

View File

@ -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>

View File

@ -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: {