create-procode 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/README.md +266 -2
  2. package/bin/create-procode.mjs +9 -0
  3. package/package.json +30 -4
  4. package/src/args.mjs +145 -0
  5. package/src/cli.mjs +514 -0
  6. package/src/deps.mjs +208 -0
  7. package/src/log.mjs +23 -0
  8. package/src/multiLanguage.mjs +409 -0
  9. package/src/prompt.mjs +60 -0
  10. package/src/scaffold.mjs +104 -0
  11. package/templates/base/README.md.tmpl +185 -0
  12. package/templates/base/_dotenv.local.example.tmpl +8 -0
  13. package/templates/base/_dotenv.tmpl +41 -0
  14. package/templates/base/_doteslintrc.cjs.tmpl +125 -0
  15. package/templates/base/_dotgitignore.tmpl +32 -0
  16. package/templates/base/_dotnvmrc.tmpl +1 -0
  17. package/templates/base/_dotvscode/extensions.json +3 -0
  18. package/templates/base/index.html.tmpl +18 -0
  19. package/templates/base/package.json.tmpl +15 -0
  20. package/templates/base/public/favicon.svg +4 -0
  21. package/templates/base/src/AdapterProvider/AdapterFactory.ts.tmpl +21 -0
  22. package/templates/base/src/AdapterProvider/SampleContacts.ts.tmpl +136 -0
  23. package/templates/base/src/CustomAction.ts.tmpl +47 -0
  24. package/templates/base/src/CustomAuthorizationProvider.ts.tmpl +77 -0
  25. package/templates/base/src/Factory/Actions/myAction.ts.tmpl +51 -0
  26. package/templates/base/src/Factory/Actions/sampleContacts.ts.tmpl +244 -0
  27. package/templates/base/src/Factory/Actions/setField.ts.tmpl +20 -0
  28. package/templates/base/src/Factory/CustomRuleEffectFactory.ts.tmpl +30 -0
  29. package/templates/base/src/Factory/CustomValidationFactory.tsx.tmpl +24 -0
  30. package/templates/base/src/Factory/CustomWidgetFactory.tsx.tmpl +43 -0
  31. package/templates/base/src/Factory/RuleEffects/readOnlyEffect.ts.tmpl +27 -0
  32. package/templates/base/src/Factory/Validations/myValidation.ts.tmpl +52 -0
  33. package/templates/base/src/Factory/Widgets/MyWidget/index.tsx.tmpl +101 -0
  34. package/templates/base/src/Factory/Widgets/Notification/index.tsx.tmpl +53 -0
  35. package/templates/base/src/Factory/Widgets/Notification/notifiFunc.ts.tmpl +9 -0
  36. package/templates/base/src/Factory/Widgets/SectionNav/index.tsx +193 -0
  37. package/templates/base/src/Factory/Widgets/SectionNav/style.scss +139 -0
  38. package/templates/base/src/Module/Sample/Contacts.json.tmpl +965 -0
  39. package/templates/base/src/Module/index.ts.tmpl +26 -0
  40. package/templates/base/src/Screens/index.tsx.tmpl +45 -0
  41. package/templates/base/src/Utils/errorMessages.ts.tmpl +35 -0
  42. package/templates/base/src/Utils/fieldErrorMapper.ts.tmpl +46 -0
  43. package/templates/base/src/Utils/logger.ts.tmpl +21 -0
  44. package/templates/base/src/Utils/responseFormatter.ts.tmpl +39 -0
  45. package/templates/base/src/Utils/storages.ts.tmpl +35 -0
  46. package/templates/base/src/assets/styles/index.scss.tmpl +149 -0
  47. package/templates/base/src/constant.ts.tmpl +53 -0
  48. package/templates/base/src/icons.ts.tmpl +17 -0
  49. package/templates/base/src/localizationResolver.ts.tmpl +48 -0
  50. package/templates/base/src/main.tsx.tmpl +136 -0
  51. package/templates/base/src/routes.ts.tmpl +20 -0
  52. package/templates/base/src/vite-env.d.ts.tmpl +20 -0
  53. package/templates/base/tsconfig.json.tmpl +30 -0
  54. package/templates/base/tsconfig.node.json.tmpl +11 -0
  55. package/templates/base/vite.config.ts.tmpl +90 -0
  56. package/templates/multi-language/locales/fr/core.json +76 -0
  57. package/templates/multi-language/locales/fr/kendo.json +181 -0
  58. package/templates/multi-language/locales/fr/template.json +169 -0
  59. package/templates/multi-language/locales/fr/widget.json +65 -0
  60. package/templates/multi-language/locales/hi/core.json +76 -0
  61. package/templates/multi-language/locales/hi/kendo.json +181 -0
  62. package/templates/multi-language/locales/hi/template.json +169 -0
  63. package/templates/multi-language/locales/hi/widget.json +65 -0
  64. package/templates/multi-language/sample/fr.json +55 -0
  65. package/templates/multi-language/sample/hi.json +55 -0
  66. package/templates/tests/src/Module/schemas.test.ts +50 -0
  67. package/templates/tests-multi-language/src/multiLanguage/enText.ts +68 -0
  68. package/templates/tests-multi-language/src/multiLanguage/translations.test.ts +92 -0
@@ -0,0 +1,26 @@
1
+ import SampleContacts from "./Sample/Contacts.json";
2
+
3
+ /**
4
+ * Extension point 1 of 5: slug -> schema.
5
+ *
6
+ * The slug is the URL segment after the Vite base, so this screen is at
7
+ * __BASE_PATH__/sample. A slug with no branch here is "page not found" — no
8
+ * error, no log.
9
+ *
10
+ * A screen has FOUR coordinates that must agree:
11
+ * 1. the slug "sample" — here, and in the URL
12
+ * 2. the schema file src/Module/Sample/Contacts.json
13
+ * 3. the screenId "sample-contacts" — INSIDE that JSON
14
+ * 4. the adapter registered against that screenId in AdapterFactory.ts
15
+ *
16
+ * Miss one and the screen renders blank or empty. Read the screenId out of the
17
+ * JSON; never guess it.
18
+ */
19
+ export const getSchemaBySlugService = (slug: string, _appCode: string): any => {
20
+ switch (slug) {
21
+ case "sample":
22
+ return SampleContacts;
23
+ default:
24
+ return null;
25
+ }
26
+ };
@@ -0,0 +1,45 @@
1
+ /**
2
+ * The three app-level screens <Core> renders on its own. Each is passed as an
3
+ * ELEMENT (`LoaderElement={AppLoader}`), not a component — passing a component
4
+ * renders nothing. Styles live in src/assets/styles/index.scss.
5
+ */
6
+
7
+ /**
8
+ * LoaderElement — shown while any request is pending. The core's default nests
9
+ * its overlay inside itself (a double dim) and hardcodes its colour; this one
10
+ * reuses the same spinner classes once, recoloured by the theme.
11
+ */
12
+ export const AppLoader = (
13
+ <div className="prc-loading-container" role="status" aria-label="Loading">
14
+ <div className="prc-spinner-triple">
15
+ <div className="prc-spinner-arc prc-spinner-arc1" />
16
+ <div className="prc-spinner-arc prc-spinner-arc2" />
17
+ <div className="prc-spinner-arc prc-spinner-arc3" />
18
+ </div>
19
+ </div>
20
+ );
21
+
22
+ /**
23
+ * ErrorElement — the router error boundary for every screen route. The core
24
+ * already logs the error, so this stays static. It does NOT cover a throw while
25
+ * the header or footer schema loads; React Router's own page shows for that.
26
+ */
27
+ export const AppError = (
28
+ <div className="prc-404-container app-screen-message">
29
+ <p className="prc-oops">Oops!</p>
30
+ <p className="prc-title">Something went wrong.</p>
31
+ <p className="prc-desc">Reload the page. If it keeps happening, contact support.</p>
32
+ </div>
33
+ );
34
+
35
+ /**
36
+ * PageNotFoundElement — shown when getSchemaBySlugService returns null for the
37
+ * slug. The miss is cached, so a slug that later becomes valid needs a reload.
38
+ */
39
+ export const AppNotFound = (
40
+ <div className="prc-404-container app-screen-message">
41
+ <p className="prc-oops">404</p>
42
+ <p className="prc-title">Page not found</p>
43
+ <p className="prc-desc">Check the address, or go back to the previous page.</p>
44
+ </div>
45
+ );
@@ -0,0 +1,35 @@
1
+ import type { CoreProps } from "procode-lowcode-core";
2
+
3
+ /**
4
+ * Fallback toast text per HTTP verb and status. Used only when the verb's
5
+ * responseConfig[status] has `enableMessage: true` and neither the schema nor
6
+ * the response supplies a message.
7
+ *
8
+ * The merge with the core's table is ONE level deep: listing a verb here
9
+ * replaces that verb's whole table, so keep every status you still want. Verbs
10
+ * left out keep the core's defaults. The tables below keep every status the
11
+ * core lists for POST and PUT, reworded for users.
12
+ */
13
+ export const errorMessageMapper = {
14
+ POST: {
15
+ 201: "Successfully created",
16
+ 204: "No content",
17
+ 400: "Bad request",
18
+ 401: "Your session has expired. Please sign in again.",
19
+ 409: "This record already exists.",
20
+ 415: "Unsupported media type",
21
+ 422: "Please fix the highlighted fields.",
22
+ 500: "Something went wrong. Please try again.",
23
+ },
24
+ PUT: {
25
+ 201: "Successfully updated",
26
+ 204: "No content",
27
+ 400: "Bad request",
28
+ 401: "Your session has expired. Please sign in again.",
29
+ 404: "This record no longer exists.",
30
+ 409: "This record already exists.",
31
+ 415: "Unsupported media type",
32
+ 422: "Please fix the highlighted fields.",
33
+ 500: "Something went wrong. Please try again.",
34
+ },
35
+ } as unknown as NonNullable<CoreProps["errorMessageMapper"]>;
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Turns a 422 body into { [screenDataField]: messages[] } so each message lands
3
+ * under its field. Called only when the verb's responseConfig[status] has
4
+ * `"alertType": "fieldvalidation"`, with the formatted response's `message`.
5
+ * Returning {} falls back to a toast.
6
+ *
7
+ * Keys MUST equal the widget's screenDataField ("screenData.name"), or the
8
+ * message never renders. Accepted shapes:
9
+ *
10
+ * { propertyName, message } or an array of them (the core's default)
11
+ * { errors: { field: ["msg"] } } (ASP.NET ProblemDetails)
12
+ * { field: ["msg"] } / { field: "msg" } (a plain dictionary)
13
+ *
14
+ * A key without a "screenData." prefix gets one. Adjust FIELD_PREFIX, or map
15
+ * names here, if the backend's field names differ from the schema's.
16
+ */
17
+ const FIELD_PREFIX = "screenData.";
18
+
19
+ const toField = (name: string) => (name.startsWith(FIELD_PREFIX) ? name : FIELD_PREFIX + name);
20
+
21
+ export const fieldErrorMapper = (raw: unknown): Record<string, string[]> => {
22
+ const errors: Record<string, string[]> = {};
23
+ const add = (name: unknown, message: unknown) => {
24
+ if (typeof name !== "string" || !name) return;
25
+ const key = toField(name);
26
+ (errors[key] ??= []).push(String(message ?? ""));
27
+ };
28
+
29
+ if (Array.isArray(raw)) {
30
+ raw.forEach((item) => add(item?.propertyName, item?.message));
31
+ return errors;
32
+ }
33
+ if (!raw || typeof raw !== "object") return errors;
34
+
35
+ const body = raw as Record<string, unknown>;
36
+ if ("propertyName" in body) {
37
+ add(body.propertyName, body.message);
38
+ return errors;
39
+ }
40
+
41
+ const dictionary = (body.errors && typeof body.errors === "object" ? body.errors : body) as Record<string, unknown>;
42
+ for (const [name, value] of Object.entries(dictionary)) {
43
+ (Array.isArray(value) ? value : [value]).forEach((message) => add(name, message));
44
+ }
45
+ return errors;
46
+ };
@@ -0,0 +1,21 @@
1
+ import type { LogLevel } from "procode-lowcode-core";
2
+
3
+ /**
4
+ * One log sink for both libraries — main.tsx registers it with the core's
5
+ * registerLogger and the widget library's registerWidgetLogger.
6
+ *
7
+ * Forward to an error reporter here. Keep `warn`: most silent failures (an
8
+ * unregistered action, effect or handler, an unresolved URL token) surface
9
+ * only as a warning. The core's error screen logs the same error object on
10
+ * every render, so errors are de-duplicated by identity.
11
+ */
12
+ const reported = new WeakSet<object>();
13
+
14
+ export const logSink = (level: LogLevel, ...args: unknown[]) => {
15
+ const first = args[0];
16
+ if (level === "error" && first && typeof first === "object") {
17
+ if (reported.has(first)) return;
18
+ reported.add(first);
19
+ }
20
+ (console[level] ?? console.log)(...args);
21
+ };
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Runs on the body of EVERY response, and its return value REPLACES
3
+ * response.data — every adapter sees this output, never the raw body.
4
+ *
5
+ * The core then reads `status` and `message` from what this returns, not from
6
+ * HTTP: the 401 path, success/failure, the responseConfig[status] lookup, the
7
+ * toast text and the 422 field errors all depend on them. So:
8
+ *
9
+ * - always return a `status`;
10
+ * - never spread an array, null or a primitive — wrap it under `data`, or an
11
+ * endpoint that answers with a bare array reads as an empty result;
12
+ * - leave field errors in `message`, where fieldErrorMapper reads them.
13
+ *
14
+ * Beyond the core's own formatter this unwraps a JSON-encoded `message`
15
+ * ("{\"message\":\"...\"}") and folds a `validationErrors: string[]` array into
16
+ * `message`, so toasts show the server's text instead of a generic one.
17
+ */
18
+ export const responseFormatter = (data: any, status: number) => {
19
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
20
+ return { status, data } as any;
21
+ }
22
+
23
+ let message = data.message;
24
+
25
+ if (!message && Array.isArray(data.validationErrors) && data.validationErrors.length) {
26
+ message = data.validationErrors.filter(Boolean).join("\n");
27
+ }
28
+
29
+ if (typeof message === "string") {
30
+ try {
31
+ const parsed = JSON.parse(message);
32
+ if (parsed && typeof parsed.message === "string") message = parsed.message;
33
+ } catch {
34
+ // not JSON — keep the string as it is
35
+ }
36
+ }
37
+
38
+ return { ...data, status: data.status ?? status, message };
39
+ };
@@ -0,0 +1,35 @@
1
+ import type { IStorage } from "procode-lowcode-core";
2
+
3
+ /**
4
+ * Extra storages for the STORAGE value-replace policy, alongside the built-in
5
+ * LOCAL and SESSION. A schema reads one as
6
+ *
7
+ * { "valueReplacePolicy": "STORAGE::MEMORY", "value": "<key>" }
8
+ *
9
+ * An unknown storage key resolves to "" silently. Never register "LOCAL" or
10
+ * "SESSION" here — that replaces the built-in for the whole app.
11
+ */
12
+
13
+ /** Per-tab, in-memory: cleared on reload. Values are kept JSON-encoded. */
14
+ class MemoryStorage implements IStorage {
15
+ private map = new Map<string, string>();
16
+
17
+ read<T>(key: string): T | null {
18
+ const raw = this.map.get(key);
19
+ return raw === undefined ? null : (JSON.parse(raw) as T);
20
+ }
21
+
22
+ write(key: string, value: any) {
23
+ this.map.set(key, JSON.stringify(value));
24
+ }
25
+
26
+ clear(key: string) {
27
+ if (key === "*") this.map.clear();
28
+ else this.map.delete(key);
29
+ }
30
+ }
31
+
32
+ /** Module scope on purpose: <Core> props must keep one identity for the app's life. */
33
+ export const customStorages: Record<string, IStorage> = {
34
+ MEMORY: new MemoryStorage(),
35
+ };
@@ -0,0 +1,149 @@
1
+ // ---------------------------------------------------------------------------
2
+ // The import order below IS the cascade. Each layer expects the ones above it
3
+ // to have been declared, and the whole stack is one Sass compilation:
4
+ //
5
+ // 1. vs-theme design tokens ($space-*, $text-*, colour ramps) — nothing
6
+ // above it can reference a token, so it goes first.
7
+ // 2. kendo-widget the _kendo-bridge that binds Kendo's variables to those
8
+ // tokens; must be declared before Kendo's own theme reads
9
+ // them.
10
+ // 3. kendo-theme-default Kendo's compiled theme. NOT declared as a peer by
11
+ // anything, but the widget library imports it BY NAME — so
12
+ // it is a hard build failure when missing, not an extra.
13
+ // 4. core runtime layout (prc-cl-N columns, skeletons).
14
+ // 5. vs-template toolbar, grid chrome, saved views.
15
+ //
16
+ // Reordering these produces "Undefined variable" at build time, or a screen
17
+ // that renders with the wrong spacing and no error at all.
18
+ // ---------------------------------------------------------------------------
19
+ @import "procode-vs-theme/scss";
20
+ @import "procode-lowcode-kendo-widget/styles";
21
+ @import "@progress/kendo-theme-default/dist/all.scss";
22
+ @import "procode-lowcode-core/styles";
23
+ @import "procode-vs-template/styles";
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // App styles go BELOW this line.
27
+ //
28
+ // Reach for a theme token or a utility class before writing a rule, and never a
29
+ // literal hex value — the theme ships 200+ utilities that already exist.
30
+ // Do not redeclare a `.prc-*` or `.qo-*` class: those belong to the
31
+ // libraries, and newer versions guard them with a doubled class, so a host rule
32
+ // silently loses anyway.
33
+ // ---------------------------------------------------------------------------
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // THE FONT FAMILY IS SET ONCE, HERE.
37
+ //
38
+ // The theme's reset sets no `font-family` on `body`, and several of its own
39
+ // component classes (`.qo-page-title`, `.qo-page-subtitle`) style size, weight
40
+ // and colour only. Anything not carrying `qo-font-family-primary` therefore
41
+ // inherits the browser default, which is a SERIF. One line here is the single
42
+ // point that makes every screen agree; do not repeat the class per widget.
43
+ // ---------------------------------------------------------------------------
44
+ body {
45
+ font-family: var(--qo-font-family-primary);
46
+ }
47
+
48
+ // ---------------------------------------------------------------------------
49
+ // LOADING PLACEHOLDERS — one height, one sweep, visible on a white card.
50
+ //
51
+ // Core ships `--skeleton-field-height: 32px`; every control on this stack is
52
+ // 40px (`--qo-control-height`), so the form re-flowed 8px a row as data landed.
53
+ // Core's `#eee` base with a sheen whose brightest stop is LIGHTER than the base
54
+ // reads as "a white background animating" on a `qo-card`; both are restated as
55
+ // theme tokens. The two `.prc-loading-container` rules: core nests
56
+ // that element inside itself, so two rgba(0,0,0,.1) backdrops stacked, and the
57
+ // dim landed on the skeleton base but not its z-index:2000 shimmer.
58
+ // ---------------------------------------------------------------------------
59
+ :root {
60
+ --skeleton-field-height: var(--qo-control-height);
61
+ --skeleton-background-color: var(--qo-bg-disabled);
62
+ --skeleton-background-linear-gradient: linear-gradient(
63
+ 110deg,
64
+ var(--qo-bg-disabled) 8%,
65
+ var(--qo-bg-light) 18%,
66
+ var(--qo-bg-disabled) 33%
67
+ );
68
+ }
69
+
70
+ .prc-loading-container .prc-loading-container {
71
+ background-color: transparent;
72
+ }
73
+
74
+ body:has(.prc-skeleton) .prc-loading-container {
75
+ background-color: transparent;
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Kendo controls that a schema put on the 40px ramp — make the ramp stick.
80
+ //
81
+ // `qo-h-control` is (0,1,0). kendo-widget's `.k-dropdownlist, .k-picker`
82
+ // `{ height: 32px }` ties it and loads later, so DROPDOWNs drew 8px shorter than
83
+ // the TEXTBOXes beside them. Doubling the class is (0,2,0) and re-states the
84
+ // theme's own token — no `!important`.
85
+ // ---------------------------------------------------------------------------
86
+ // KendoReact 13+ emits no fill-mode class (`k-picker-solid`, `k-input-solid`) when
87
+ // the theme decides the fill, so the solid default is the bare base class; the
88
+ // `:where()` keeps "not flat / not outline" at the old (0,2,0) specificity.
89
+ .qo-h-control.k-picker:where(:not(.k-picker-flat, .k-picker-outline)),
90
+ .qo-h-control.k-input:where(:not(.k-input-flat, .k-input-outline)) {
91
+ height: var(--qo-control-height);
92
+ }
93
+
94
+ // A MultiSelect grows with its chips, so it takes a floor and not a height.
95
+ .qo-min-h-control.k-input:where(:not(.k-input-flat, .k-input-outline)),
96
+ .qo-min-h-control.k-picker:where(:not(.k-picker-flat, .k-picker-outline)) {
97
+ height: auto;
98
+ min-height: var(--qo-control-height);
99
+ }
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Control text — NO RULE HERE, on purpose. 12px label / 13px value.
103
+ //
104
+ // Add, Edit and View read in one pair of sizes, and all of it comes from the
105
+ // libraries: a field label is vs-theme's `qo-label` (12px / 600), and every
106
+ // control's text is 13px from kendo-widget 2.8.1 — as are a BLOCK's value and
107
+ // the grid toolbar's search from vs-template 2.12.1. Do not put a size on
108
+ // `.k-input-inner`: a declaration on the element beats every class on the
109
+ // wrapper, and globally it would also pin the toolbar search and filter cells.
110
+ // (Apps generated before 1.0.1 carried an 11px rule here; delete it when they
111
+ // adopt kendo-widget 2.8.1.)
112
+ // ---------------------------------------------------------------------------
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Two fields in one row, one label wraps — keep the controls on one line.
116
+ //
117
+ // The label HOC stacks label over control in `.prc-label-field`; a row of two
118
+ // aligns them by their TOP, so a wrapped label pushes its control down a line.
119
+ // Aligning the controls by their BOTTOM fixes it at every width. Both
120
+ // restrictions are load-bearing: `[class*="prc-cl-"]` keeps it on a column
121
+ // SLOT (`:has()` is a descendant test), and `:not(.prc-flx-container)` excludes
122
+ // a slot that is itself a layout — a scroll pane carrying `prc-cl-fl` otherwise
123
+ // gets bottom-packed and its first section is pushed above the fold, measured.
124
+ // Durable home: core's ContainerRenderer styles.
125
+ // ---------------------------------------------------------------------------
126
+ .prc-flx-container:not(.prc-verticle):not(.prc-verticle-reverse)
127
+ > [class*="prc-cl-"]:not(.prc-flx-container):has(.prc-label-field) {
128
+ align-self: stretch;
129
+ display: flex;
130
+ flex-direction: column;
131
+ justify-content: flex-end;
132
+ }
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // App screens (src/Screens) — the loader, error and 404 elements main.tsx
136
+ // hands to <Core>. They reuse the core classes; only the colour and the
137
+ // missing height are restated here, as theme tokens.
138
+ // ---------------------------------------------------------------------------
139
+ .prc-spinner-arc.prc-spinner-arc {
140
+ border-top-color: var(--qo-border-primary);
141
+ border-right-color: var(--qo-border-primary);
142
+ }
143
+
144
+ .prc-404-container.app-screen-message {
145
+ min-height: 100dvh;
146
+ padding: var(--qo-space-lg);
147
+ }
148
+
149
+ @import "../../Factory/Widgets/SectionNav/style.scss";
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Application constants and the two registration enums.
3
+ *
4
+ * Every registry in this app resolves by string key and returns null on a miss
5
+ * — no error, no log. A new widget needs FOUR things: the WidgetTypes member,
6
+ * the component, the createWidgetMap() entry and the schema reference. Miss one
7
+ * and it renders nothing at all. Grep the key and confirm it appears at both
8
+ * the definition and the registration site before calling it done.
9
+ */
10
+
11
+ /**
12
+ * Must equal the code main.tsx builds `window.getAdapter<CODE>` from — the core
13
+ * resolves that global by name from this same env var.
14
+ */
15
+ export const APPLICATION_CODE = import.meta.env.VITE_APPLICATION_CODE;
16
+
17
+ export const baseUrl = import.meta.env.VITE_CENTRAL_SERVICE_BASE_URL;
18
+
19
+ export const getTokenFromCookie = (): string => {
20
+ const m = document.cookie.match(/(?:^|;\s*)AuthToken=([^;]*)/);
21
+ return m ? decodeURIComponent(m[1]) : "";
22
+ };
23
+
24
+ /**
25
+ * A hardcoded token (in .env.local, never .env) wins over the cookie. If every
26
+ * request 401s locally, check both of these first.
27
+ */
28
+ export const AuthToken = import.meta.env.VITE_HARDCODED_TOKEN ?? getTokenFromCookie();
29
+
30
+ /**
31
+ * Custom widget types. A member here is NOT evidence the widget works — it has
32
+ * to be in CustomWidgetFactory's createWidgetMap() too.
33
+ *
34
+ * Do not register a custom TEXTBOX, DATE, DATERANGE, RADIOGROUP, CHECKBOX or
35
+ * DIALOG: resolution order is custom -> template -> standard, so you would
36
+ * shadow the widget vs-template's grid toolbar depends on and visibly break the
37
+ * global-search box.
38
+ */
39
+ export enum WidgetTypes {
40
+ MY_WIDGET = "MY_WIDGET",
41
+ /** The left "Sections" rail on a long form or view screen — src/Factory/Widgets/SectionNav. */
42
+ SECTION_NAV = "SECTION_NAV",
43
+ }
44
+
45
+ /** Custom validation names, as referenced by `validations[].name` in a schema. */
46
+ export enum ValidationTypes {
47
+ MY_VALIDATION = "MY_VALIDATION",
48
+ }
49
+
50
+ /** Custom rule effects, as referenced by `ruleInterpreter[].rules[].effect`. */
51
+ export enum RuleEffectTypes {
52
+ CUS_READONLY = "CUS_READONLY",
53
+ }
@@ -0,0 +1,17 @@
1
+ import type { IconResolver, registerIcons } from "procode-lowcode-kendo-widget";
2
+
3
+ /**
4
+ * App icons, registered once by main.tsx. Every ICON widget, the Grid toolbar
5
+ * and the Grid filter cell resolve a name here first, then the library's
6
+ * aliases, then @progress/kendo-svg-icons by name — so this is only for a glyph
7
+ * Kendo lacks, or to change what a name draws app-wide. BUTTON role "icon" uses
8
+ * a font class and is not affected.
9
+ *
10
+ * Names are trimmed and lower-cased. A value is a Kendo SVGIcon:
11
+ *
12
+ * logo: { name: "logo", viewBox: "0 0 24 24", content: '<path d="…" />' },
13
+ *
14
+ * The library maps three names onto other Kendo glyphs: download -> import,
15
+ * filter -> sliders, star -> star-outline. Register the name to take it back.
16
+ */
17
+ export const appIcons: Exclude<Parameters<typeof registerIcons>[0], IconResolver | null | undefined> = {};
@@ -0,0 +1,48 @@
1
+ import { LocalizationKey, type LocalizationResolver } from "procode-vs-template";
2
+
3
+ /**
4
+ * Answers procode-vs-template's localization lookups. Registered once in
5
+ * main.tsx with registerLocalization().
6
+ *
7
+ * The template's widgets ask for values only the app can know — the signed-in
8
+ * user's date pattern, the organisation's currency symbol — by key, and this
9
+ * function decides where each one is read from (a token claim, a cached
10
+ * settings payload, a constant). The library never learns those shapes.
11
+ *
12
+ * It runs per lookup, at render time, so a value that arrives later (an async
13
+ * settings call) is still picked up on the next render. Returning undefined
14
+ * means "no value": the widget uses its own default. A resolver that throws is
15
+ * treated as "no value", so a lookup can never blank a screen.
16
+ *
17
+ * Switch on LocalizationKey, never on string literals — a misspelled key fails
18
+ * silently. Any other key works too; answering one is an edit to this file.
19
+ */
20
+
21
+ /** Kendo date pattern. Replace with the user's own (e.g. a token claim). */
22
+ const DATE_FORMAT = "MM/dd/yyyy";
23
+
24
+ export const localizationResolver: LocalizationResolver = (key) => {
25
+ switch (key) {
26
+ case LocalizationKey.DateFormat:
27
+ return DATE_FORMAT;
28
+
29
+ // The idle hint, e.g. "MM/DD/YYYY".
30
+ case LocalizationKey.DatePlaceholder:
31
+ return DATE_FORMAT.toUpperCase();
32
+
33
+ // Hints for a FOCUSED date input. Without them Kendo swaps the placeholder
34
+ // for its CLDR long form ("month/day/year") the moment the box takes focus.
35
+ case LocalizationKey.DateFormatSections:
36
+ return { year: "YYYY", month: "MM", day: "DD" };
37
+
38
+ // e.g. "€". Undefined keeps the plain numeric pattern.
39
+ case LocalizationKey.CurrencySymbol:
40
+ return undefined;
41
+
42
+ case LocalizationKey.CurrencyDecimals:
43
+ return 2;
44
+
45
+ default:
46
+ return undefined;
47
+ }
48
+ };
@@ -0,0 +1,136 @@
1
+ import ReactDOM from "react-dom/client";
2
+ import axios from "axios";
3
+ import WidgetFactory, {
4
+ registerIcons,
5
+ registerWidgetLogger,
6
+ registerWidgetResolver,
7
+ } from "procode-lowcode-kendo-widget";
8
+ import Core, { registerLogger, type CoreProps } from "procode-lowcode-core";
9
+ import TemplateWidgetFactory, { TemplateActionFactory, registerLocalization } from "procode-vs-template";
10
+
11
+ import { getAdapter } from "./AdapterProvider/AdapterFactory";
12
+ import CustomWidgetFactory from "./Factory/CustomWidgetFactory";
13
+ import CustomValidationFactory from "./Factory/CustomValidationFactory";
14
+ import CustomRuleEffectFactory from "./Factory/CustomRuleEffectFactory";
15
+ import CustomActionFactory from "./CustomAction";
16
+ import CustomAuthorizationProvider from "./CustomAuthorizationProvider";
17
+ import { getSchemaBySlugService } from "./Module";
18
+ import Notification from "./Factory/Widgets/Notification";
19
+ import { AppError, AppLoader, AppNotFound } from "./Screens";
20
+ import { localizationResolver } from "./localizationResolver";
21
+ import { appIcons } from "./icons";
22
+ import { customRoute } from "./routes";
23
+ import { responseFormatter } from "./Utils/responseFormatter";
24
+ import { fieldErrorMapper } from "./Utils/fieldErrorMapper";
25
+ import { errorMessageMapper } from "./Utils/errorMessages";
26
+ import { customStorages } from "./Utils/storages";
27
+ import { logSink } from "./Utils/logger";
28
+ import { APPLICATION_CODE, AuthToken } from "./constant";
29
+ import "./assets/styles/index.scss";
30
+
31
+ // ===========================================================================
32
+ // This file is the SINGLE integration point of a ProCode app, and the order of
33
+ // the numbered statements below is load-bearing — understand each one's
34
+ // comment before reordering anything here.
35
+ //
36
+ // It is also the ONLY file under src/ that may import axios. Anywhere else, a
37
+ // bare `import axios` or `fetch(` silently forfeits the loading counter and
38
+ // skeletons, status-code toasts, 422 -> field-error routing, action
39
+ // chainability, a schema-visible endpoint, and per-environment retargeting
40
+ // without a code change. Data access is apiConfig + a standard action.
41
+ // ===========================================================================
42
+
43
+ // (1) Let HTTP 4xx reach the core's SUCCESS interceptor so responseConfig
44
+ // entries (422 field errors, per-status toasts, the 401 handler) can run.
45
+ // This MUST execute before CentralService lazily calls axios.create() —
46
+ // after that the instance has already captured the default.
47
+ axios.defaults.validateStatus = (status) => status < 500;
48
+
49
+ // (2) One log sink for the core and the widget library.
50
+ registerLogger(logSink);
51
+ registerWidgetLogger(logSink);
52
+
53
+ // (3) Build the three widget lists in this order — the template list is
54
+ // composed FROM the other two, so both have to exist first.
55
+ const customWidgetList = CustomWidgetFactory.get();
56
+ const standardWidgetList = WidgetFactory.get();
57
+ const templateWidgetList = TemplateWidgetFactory.get(standardWidgetList, customWidgetList);
58
+
59
+ // (4) A widget inside the kendo library that needs another widget (the Grid's
60
+ // header bar needs a DROPDOWN) resolves it here instead of importing its
61
+ // sibling — so a custom override reaches those places too. Same
62
+ // custom -> template -> standard order the schema renderer uses.
63
+ registerWidgetResolver((type: string) =>
64
+ customWidgetList.getWidgets?.().get(type) ??
65
+ TemplateWidgetFactory.getTemplateWidgets?.()?.get(type) ??
66
+ standardWidgetList.getWidgets?.().get(type),
67
+ );
68
+
69
+ // (5) BEFORE render. The core resolves this global by NAME as
70
+ // "getAdapter" + VITE_APPLICATION_CODE. If the two diverge, EVERY adapter
71
+ // silently falls back to a dummy and no screen ever has data — on every
72
+ // screen at once, with nothing in the console.
73
+ (window as any)[`getAdapter${APPLICATION_CODE ?? ""}`] = getAdapter;
74
+
75
+ // (6) BEFORE render. vs-template's widgets ask this for the date pattern,
76
+ // currency symbol and similar per-user values by key; icons resolve the
77
+ // app's own glyphs first.
78
+ registerLocalization(localizationResolver);
79
+ registerIcons(appIcons);
80
+
81
+ // HTTP 401. The core calls this INSTEAD of its own redirect, and skips the toast
82
+ // and responseConfig for that response. Replace the redirect with a re-auth
83
+ // dialog or an SPA route if the app has one. The flag stops a burst of
84
+ // parallel 401s from redirecting more than once.
85
+ let signingOut = false;
86
+ const unauthorizedHandler = () => {
87
+ if (signingOut) return;
88
+ signingOut = true;
89
+ window.location.assign(import.meta.env.VITE_BASE_LOGOUT_URL || "/");
90
+ };
91
+
92
+ // The core reads these headers on every request. Mutate this object in place
93
+ // to change a header later (e.g. a refreshed token) — a new object is ignored.
94
+ const requestHeaders = { Authorization: `Bearer ${AuthToken}` };
95
+
96
+ // Every <Core> prop, typed. <Core> itself accepts `any`, so a misspelled prop
97
+ // written inline compiles and is silently dropped — building the props here
98
+ // makes TypeScript catch it. All of them are fixed at first mount: <Core> is
99
+ // rendered once, never inside React.StrictMode (its setup runs once and a
100
+ // second run is silently ignored).
101
+ const coreProps: CoreProps = {
102
+ // Widgets, resolved custom -> template -> standard.
103
+ widgetProvider: standardWidgetList,
104
+ templateWidgetProvider: templateWidgetList,
105
+ customWidgetProvider: customWidgetList,
106
+
107
+ // Actions, validations and rule effects — the same three-tier resolution.
108
+ // vs-template ships no validation or rule-effect factory, so there is no
109
+ // templateValidationProvider / templateRuleEffectProvider to pass.
110
+ templateActionProvider: TemplateActionFactory.getInstance(),
111
+ customActionProvider: new CustomActionFactory(),
112
+ customValidationProvider: new CustomValidationFactory(),
113
+ customRuleEffectProvider: new CustomRuleEffectFactory(),
114
+
115
+ authorizationProvider: new CustomAuthorizationProvider(),
116
+ getSchemaBySlugService,
117
+ customRoute,
118
+ customStorages,
119
+
120
+ // HTTP
121
+ requestHeaders,
122
+ responseFormatter,
123
+ fieldErrorMapper,
124
+ errorMessageMapper,
125
+ unauthorizedHandler,
126
+
127
+ // Screens the core renders itself — ELEMENTS, not components.
128
+ LoaderElement: AppLoader,
129
+ ErrorElement: AppError,
130
+ PageNotFoundElement: AppNotFound,
131
+
132
+ // A COMPONENT exposing showNotification() through a ref.
133
+ NotificationElement: Notification,
134
+ };
135
+
136
+ ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(<Core {...coreProps} />);