obm-upload-form-ui 0.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 (59) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +153 -0
  3. package/dist/_plugin-vue_export-helper-DQLEQ__A.js +44 -0
  4. package/dist/_plugin-vue_export-helper-DQLEQ__A.js.map +1 -0
  5. package/dist/core/client/auth.d.ts +37 -0
  6. package/dist/core/client/errors.d.ts +31 -0
  7. package/dist/core/client/formApi.d.ts +23 -0
  8. package/dist/core/client/http.d.ts +10 -0
  9. package/dist/core/config.d.ts +25 -0
  10. package/dist/core/geo/wkt.d.ts +7 -0
  11. package/dist/core/i18n/messages.d.ts +13 -0
  12. package/dist/core/index.d.ts +16 -0
  13. package/dist/core/schema/guards.d.ts +12 -0
  14. package/dist/core/schema/normalize.d.ts +10 -0
  15. package/dist/core/schema/types.d.ts +119 -0
  16. package/dist/core/submit/linked.d.ts +58 -0
  17. package/dist/core/submit/metadata.d.ts +37 -0
  18. package/dist/core/submit/submitClient.d.ts +40 -0
  19. package/dist/core/validation/engine.d.ts +18 -0
  20. package/dist/core/validation/rules.d.ts +29 -0
  21. package/dist/core/validation/types.d.ts +48 -0
  22. package/dist/core.js +377 -0
  23. package/dist/core.js.map +1 -0
  24. package/dist/custom-elements/obm-upload-form-ui.css +1 -0
  25. package/dist/custom-elements/obm-upload-form.iife.js +21 -0
  26. package/dist/custom-elements/obm-upload-form.iife.js.map +1 -0
  27. package/dist/custom-elements/obm-upload-form.js +16889 -0
  28. package/dist/custom-elements/obm-upload-form.js.map +1 -0
  29. package/dist/engine-7r1HChwT.js +174 -0
  30. package/dist/engine-7r1HChwT.js.map +1 -0
  31. package/dist/obm-upload-form-ui.css +1 -0
  32. package/dist/vue/FieldWrapper.vue.d.ts +30 -0
  33. package/dist/vue/UploadForm.vue.d.ts +58 -0
  34. package/dist/vue/ValidationSummary.vue.d.ts +13 -0
  35. package/dist/vue/fieldRegistry.d.ts +5 -0
  36. package/dist/vue/fields/ArrayField.vue.d.ts +14 -0
  37. package/dist/vue/fields/AutocompleteField.vue.d.ts +15 -0
  38. package/dist/vue/fields/BooleanField.vue.d.ts +14 -0
  39. package/dist/vue/fields/NumericField.vue.d.ts +14 -0
  40. package/dist/vue/fields/SelectField.vue.d.ts +15 -0
  41. package/dist/vue/fields/TemporalField.vue.d.ts +14 -0
  42. package/dist/vue/fields/TextAreaField.vue.d.ts +14 -0
  43. package/dist/vue/fields/TextField.vue.d.ts +14 -0
  44. package/dist/vue/fields/TimeIntervalField.vue.d.ts +14 -0
  45. package/dist/vue/fields/UnsupportedField.vue.d.ts +8 -0
  46. package/dist/vue/fields/WktTextField.vue.d.ts +14 -0
  47. package/dist/vue/index.d.ts +19 -0
  48. package/dist/vue/types.d.ts +22 -0
  49. package/dist/vue/uiText.d.ts +3 -0
  50. package/dist/vue/useFormState.d.ts +13 -0
  51. package/dist/vue-leaflet/GeometryField.vue.d.ts +14 -0
  52. package/dist/vue-leaflet/index.d.ts +9 -0
  53. package/dist/vue-leaflet.js +117 -0
  54. package/dist/vue-leaflet.js.map +1 -0
  55. package/dist/vue.js +813 -0
  56. package/dist/vue.js.map +1 -0
  57. package/dist/wkt-C0dkXG1x.js +359 -0
  58. package/dist/wkt-C0dkXG1x.js.map +1 -0
  59. package/package.json +113 -0
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # obm-upload-form-ui
2
+
3
+ [![npm version](https://img.shields.io/npm/v/obm-upload-form-ui.svg)](https://www.npmjs.com/package/obm-upload-form-ui)
4
+ [![pipeline status](https://gitlab.com/openbiomaps/obm-upload-form-ui-library/badges/main/pipeline.svg)](https://gitlab.com/openbiomaps/obm-upload-form-ui-library/-/pipelines)
5
+ [![license](https://img.shields.io/npm/l/obm-upload-form-ui.svg)](./LICENSE)
6
+
7
+ A reusable UI component library for rendering and submitting [OpenBioMaps](https://openbiomaps.org)
8
+ upload forms, built on the `obm-project-api` v3 form schema
9
+ (`GET /forms`, `GET /forms/{publishedFormId}`, `POST /forms/{formId}`).
10
+
11
+ > **Status: v0, work in progress.** Photo/attachment upload (`file_id`/`photo_id` fields) and the
12
+ > `spatial`/`custom_check` validation rules are explicitly out of scope for this version and render
13
+ > or report as "unsupported".
14
+
15
+ ## Installation
16
+
17
+ ```sh
18
+ npm install obm-upload-form-ui
19
+ ```
20
+
21
+ The `/custom-elements` entry point is also published to npm, so no-build hosts can load it straight
22
+ from a CDN mirror instead of installing it, e.g.:
23
+
24
+ ```html
25
+ <script
26
+ type="module"
27
+ src="https://cdn.jsdelivr.net/npm/obm-upload-form-ui/dist/custom-elements/obm-upload-form.js"
28
+ ></script>
29
+ ```
30
+
31
+ ## Packages in one repo
32
+
33
+ | Entry point | What it is | Dependencies |
34
+ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
35
+ | `obm-upload-form-ui` | Framework-free core: typed form schema, API client, validation engine, submit client, WKT↔GeoJSON utilities | none |
36
+ | `obm-upload-form-ui/vue` | Vue 3 field widgets and the `<UploadForm>` orchestrator | `vue` (peer) |
37
+ | `obm-upload-form-ui/vue-leaflet` | Leaflet-based geometry field widget | `vue`, `leaflet`, `leaflet-draw` (peers) |
38
+ | `obm-upload-form-ui/custom-elements` | Self-contained `<obm-upload-form>` custom element (ES + IIFE bundles, Vue inlined) for no-build hosts | none |
39
+
40
+ ## Quick start (core)
41
+
42
+ ```ts
43
+ import {
44
+ createFormApi,
45
+ cookieAuth,
46
+ validateForm,
47
+ canSubmit,
48
+ submitObservation,
49
+ buildMetadata,
50
+ } from 'obm-upload-form-ui';
51
+
52
+ const config = {
53
+ baseUrl: 'https://example.org/projects/my_project/api/v3',
54
+ language: 'en', // sent as Accept-Language; the API uses the first 2 characters
55
+ appVersion: 'my-app 1.0.0',
56
+ auth: cookieAuth(),
57
+ };
58
+
59
+ const api = createFormApi(config);
60
+ const forms = await api.listForms();
61
+ const schema = await api.getForm(forms[0].published_form_id);
62
+
63
+ const data = { species: 'Lanius collurio', number: 5 };
64
+ const result = validateForm(schema, data, 'final');
65
+ if (canSubmit(result)) {
66
+ const live = await api.resolveLiveForm(schema.header.published_form_id);
67
+ await submitObservation(config, {
68
+ formId: live.formId,
69
+ data,
70
+ metadata: buildMetadata({ formVersion: live.formVersion, appVersion: config.appVersion }),
71
+ });
72
+ }
73
+ ```
74
+
75
+ ### Validation model
76
+
77
+ The API currently performs **no field-level validation** on write (it proxies to the legacy
78
+ upload engine), so this library's validation engine is the enforcement layer. See
79
+ `docs/adr/0002-validation-policy-model.md`:
80
+
81
+ - Document states: `draft` (skips checks by default, matching existing OBM client behavior) and
82
+ `final` (enforces).
83
+ - `soft` rules warn but never block.
84
+ - `ValidationPolicy.enforcement` can be set to `'warn'` to make all validation non-blocking once
85
+ server-side validation exists.
86
+
87
+ ### Linked submissions (repeated groups)
88
+
89
+ The form schema has no repeated/nested field groups. The established OBM convention is separate
90
+ linked forms sharing a client-generated UUID as a foreign key. `submitLinked()` orchestrates this:
91
+ one parent POST plus N child POSTs, all stamped with the shared id. **There is no server-side
92
+ transaction** — partial failure is reported per item so callers can retry the remainder.
93
+
94
+ ## Development
95
+
96
+ ```sh
97
+ npm install
98
+ npm test # unit + component tests (integration tests skip without OBM_API_BASE)
99
+ npm run dev # Storybook
100
+ npm run build # library + custom-elements bundles into dist/
101
+ npm run typecheck
102
+ npm run lint
103
+ ```
104
+
105
+ ### Testing against a live API
106
+
107
+ Contract fixtures under `tests/fixtures/forms/` prefixed `synthetic-` are hand-derived from the
108
+ API source, not captured responses. Replace them with real captures:
109
+
110
+ ```sh
111
+ OBM_API_BASE=https://example.org/projects/my_project/api/v3 OBM_ACCESS_TOKEN=... npm run capture:fixtures
112
+ OBM_API_BASE=... npm run test:integration
113
+ ```
114
+
115
+ Note: the API allows `localhost`/`127.0.0.1` origins only when its `ENV != PROD`.
116
+
117
+ ### Demo app
118
+
119
+ `demo/` is a Vite app deployable into an OBM project via the `spa_integration` module. See
120
+ `docs/spa-integration-deployment.md` for build/packaging constraints and known host-side bugs.
121
+
122
+ ```sh
123
+ npm run demo:dev # local dev server
124
+ npm run demo:build # dist + dist-archives/<appName>.zip ready for upload
125
+ ```
126
+
127
+ ## Releases
128
+
129
+ Versioning, `CHANGELOG.md`, GitLab Releases, and npm publishing are all automated by
130
+ [semantic-release](https://semantic-release.gitbook.io/) from commit messages on `main` — see
131
+ `.releaserc.json`. Use [Conventional Commits](https://www.conventionalcommits.org/) prefixes
132
+ (`feat:`, `fix:`, `docs:`, `refactor:`, etc.) so the pipeline can compute the next version
133
+ correctly; a `feat:` commit triggers a minor release, `fix:`/`docs:`/`refactor:`/... trigger a
134
+ patch release, and a `BREAKING CHANGE:` footer (or `!` after the type) triggers a major release.
135
+ See `docs/adr/0004-release-and-publish-strategy.md` for the full pipeline design.
136
+
137
+ Publishing to npm uses [Trusted Publishing (OIDC)](https://docs.npmjs.com/trusted-publishers) —
138
+ the CI pipeline exchanges a short-lived GitLab-issued token for a one-time npm publish token, so no
139
+ long-lived `NPM_TOKEN` is stored in the project. See
140
+ `docs/adr/0005-npm-trusted-publishing.md` for the setup and the one-time manual bootstrap it
141
+ required.
142
+
143
+ ## Design documents
144
+
145
+ - Architecture decisions: `docs/adr/`
146
+ - Live-verification checklist (what automated tests cannot cover): `docs/live-verification.md`
147
+ - Upstream feasibility study: `web-app/docs/architecture/upload-form-component-library-feasibility-study.md`
148
+
149
+ ## License
150
+
151
+ GPL-3.0-or-later, matching the OpenBioMaps ecosystem. Note: GPL on a library means consumers must
152
+ be GPL-compatible; if embedding into non-GPL third-party apps ever becomes a goal, LGPL would be
153
+ the deliberate alternative — relicensing later requires all contributors' consent (see ADR 0001).
@@ -0,0 +1,44 @@
1
+ const n = {
2
+ saveDraft: "Save draft",
3
+ finalize: "Finalize",
4
+ required: "required",
5
+ unsupportedField: "This field is not supported in this interface yet.",
6
+ unsupportedFieldType: "Unsupported field type: {type}",
7
+ validationSummaryTitle: "The form cannot be finalized:",
8
+ warningsTitle: "Warnings:",
9
+ noticesTitle: "Not checked client-side:",
10
+ addItem: "Add",
11
+ removeItem: "Remove",
12
+ invalidWkt: "The value is not valid WKT geometry text.",
13
+ noOptions: "No options"
14
+ }, r = {
15
+ saveDraft: "Piszkozat mentése",
16
+ finalize: "Véglegesítés",
17
+ required: "kötelező",
18
+ unsupportedField: "Ezt a mezőt ez a felület még nem támogatja.",
19
+ unsupportedFieldType: "Nem támogatott mezőtípus: {type}",
20
+ validationSummaryTitle: "Az űrlap nem véglegesíthető:",
21
+ warningsTitle: "Figyelmeztetések:",
22
+ noticesTitle: "Kliensoldalon nem ellenőrzött:",
23
+ addItem: "Hozzáadás",
24
+ removeItem: "Eltávolítás",
25
+ invalidWkt: "Az érték nem érvényes WKT geometria.",
26
+ noOptions: "Nincs választható elem"
27
+ }, d = { en: n, hu: r };
28
+ function m(e, i = "en", t = {}) {
29
+ return ((d[i.slice(0, 2).toLowerCase()] ?? n)[e] ?? n[e]).replace(
30
+ /\{(\w+)\}/g,
31
+ (s, l) => l in t ? String(t[l]) : s
32
+ );
33
+ }
34
+ const p = (e, i) => {
35
+ const t = e.__vccOpts || e;
36
+ for (const [o, a] of i)
37
+ t[o] = a;
38
+ return t;
39
+ };
40
+ export {
41
+ p as _,
42
+ m as u
43
+ };
44
+ //# sourceMappingURL=_plugin-vue_export-helper-DQLEQ__A.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"_plugin-vue_export-helper-DQLEQ__A.js","sources":["../src/vue/uiText.ts"],"sourcesContent":["/** Small catalog for UI strings the widgets themselves render. */\n\nexport type UiTextKey =\n | 'saveDraft'\n | 'finalize'\n | 'required'\n | 'unsupportedField'\n | 'unsupportedFieldType'\n | 'validationSummaryTitle'\n | 'warningsTitle'\n | 'noticesTitle'\n | 'addItem'\n | 'removeItem'\n | 'invalidWkt'\n | 'noOptions';\n\ntype UiCatalog = Record<UiTextKey, string>;\n\nconst en: UiCatalog = {\n saveDraft: 'Save draft',\n finalize: 'Finalize',\n required: 'required',\n unsupportedField: 'This field is not supported in this interface yet.',\n unsupportedFieldType: 'Unsupported field type: {type}',\n validationSummaryTitle: 'The form cannot be finalized:',\n warningsTitle: 'Warnings:',\n noticesTitle: 'Not checked client-side:',\n addItem: 'Add',\n removeItem: 'Remove',\n invalidWkt: 'The value is not valid WKT geometry text.',\n noOptions: 'No options',\n};\n\nconst hu: UiCatalog = {\n saveDraft: 'Piszkozat mentése',\n finalize: 'Véglegesítés',\n required: 'kötelező',\n unsupportedField: 'Ezt a mezőt ez a felület még nem támogatja.',\n unsupportedFieldType: 'Nem támogatott mezőtípus: {type}',\n validationSummaryTitle: 'Az űrlap nem véglegesíthető:',\n warningsTitle: 'Figyelmeztetések:',\n noticesTitle: 'Kliensoldalon nem ellenőrzött:',\n addItem: 'Hozzáadás',\n removeItem: 'Eltávolítás',\n invalidWkt: 'Az érték nem érvényes WKT geometria.',\n noOptions: 'Nincs választható elem',\n};\n\nconst catalogs: Record<string, UiCatalog> = { en, hu };\n\nexport function uiText(\n key: UiTextKey,\n language = 'en',\n params: Record<string, unknown> = {},\n): string {\n const catalog = catalogs[language.slice(0, 2).toLowerCase()] ?? en;\n const template = catalog[key] ?? en[key];\n return template.replace(/\\{(\\w+)\\}/g, (match, name: string) =>\n name in params ? String(params[name]) : match,\n );\n}\n"],"names":["en","hu","catalogs","uiText","key","language","params","match","name"],"mappings":"AAkBA,MAAMA,IAAgB;AAAA,EACpB,WAAW;AAAA,EACX,UAAU;AAAA,EACV,UAAU;AAAA,EACV,kBAAkB;AAAA,EAClB,sBAAsB;AAAA,EACtB,wBAAwB;AAAA,EACxB,eAAe;AAAA,EACf,cAAc;AAAA,EACd,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,WAAW;AACb,GAEMC,IAAgB;AAAA,EACpB,WAAW;AAAA,EACX,UAAU;AAAA,EACV,UAAU;AAAA,EACV,kBAAkB;AAAA,EAClB,sBAAsB;AAAA,EACtB,wBAAwB;AAAA,EACxB,eAAe;AAAA,EACf,cAAc;AAAA,EACd,SAAS;AAAA,EACT,YAAY;AAAA,EACZ,YAAY;AAAA,EACZ,WAAW;AACb,GAEMC,IAAsC,EAAE,IAAAF,GAAI,IAAAC,EAAA;AAE3C,SAASE,EACdC,GACAC,IAAW,MACXC,IAAkC,CAAA,GAC1B;AAGR,WAFgBJ,EAASG,EAAS,MAAM,GAAG,CAAC,EAAE,YAAA,CAAa,KAAKL,GACvCI,CAAG,KAAKJ,EAAGI,CAAG,GACvB;AAAA,IAAQ;AAAA,IAAc,CAACG,GAAOC,MAC5CA,KAAQF,IAAS,OAAOA,EAAOE,CAAI,CAAC,IAAID;AAAA,EAAA;AAE5C;;;;;;;"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Pluggable authentication strategies. Which one applies depends entirely on
3
+ * where the library is hosted (same-origin page, separate SPA, no-build host),
4
+ * so auth is injected via ObmClientConfig, never assumed.
5
+ */
6
+ export interface AuthStrategy {
7
+ /** Adjust the outgoing request (headers/credentials) before it is sent. */
8
+ apply(init: RequestInit & {
9
+ headers: Headers;
10
+ }): void | Promise<void>;
11
+ /**
12
+ * Called once when a request gets HTTP 401. Return true to retry the
13
+ * request (e.g. after refreshing a token).
14
+ */
15
+ onUnauthorized?(): Promise<boolean>;
16
+ }
17
+ /**
18
+ * Same-origin session-cookie auth — the proven path for pages hosted inside
19
+ * an OBM project (the API also accepts the `access_token` cookie).
20
+ */
21
+ export declare function cookieAuth(): AuthStrategy;
22
+ /** Opaque or pre-obtained bearer token, supplied by the host. */
23
+ export declare function bearerAuth(tokenProvider: () => string | Promise<string>): AuthStrategy;
24
+ export interface JwtAuthOptions {
25
+ /**
26
+ * Endpoint returning a JWT for the current session, e.g. the spa_integration
27
+ * host pattern `https://host/projects/<project>/<appName>/ajax?jwt`.
28
+ * Fetched with credentials included (the session cookie authenticates it).
29
+ */
30
+ tokenUrl: string;
31
+ fetchImpl?: typeof fetch;
32
+ }
33
+ /**
34
+ * JWT auth that lazily fetches (and on 401 refreshes) its token from a host
35
+ * endpoint. The refresh is single-flighted: concurrent 401s share one fetch.
36
+ */
37
+ export declare function jwtAuth(options: JwtAuthOptions): AuthStrategy;
@@ -0,0 +1,31 @@
1
+ /** Base class for every error thrown by this library. */
2
+ export declare class ObmError extends Error {
3
+ constructor(message: string, options?: ErrorOptions);
4
+ }
5
+ /** The request never produced an HTTP response (network failure, CORS, abort). */
6
+ export declare class ObmNetworkError extends ObmError {
7
+ }
8
+ /** RFC 9457 Problem Details, the API's error body format. */
9
+ export interface ProblemDetails {
10
+ type?: string;
11
+ title?: string;
12
+ detail?: string;
13
+ status?: number;
14
+ instance?: string;
15
+ [extension: string]: unknown;
16
+ }
17
+ /** A non-2xx HTTP response from the API. */
18
+ export declare class ObmApiError extends ObmError {
19
+ readonly status: number;
20
+ /** Parsed Problem JSON body, when the response carried one. */
21
+ readonly problem: ProblemDetails | null;
22
+ /** Raw response body text, for non-Problem error responses. */
23
+ readonly body: string;
24
+ constructor(status: number, body: string, problem: ProblemDetails | null);
25
+ }
26
+ /** The API returned a payload whose structure this library cannot interpret. */
27
+ export declare class ObmSchemaError extends ObmError {
28
+ }
29
+ /** WKT/GeoJSON conversion failure. */
30
+ export declare class ObmGeometryError extends ObmError {
31
+ }
@@ -0,0 +1,23 @@
1
+ import { ObmClientConfig } from '../config';
2
+ import { FormListItem, FormSchema } from '../schema/types';
3
+ export interface LiveFormRef {
4
+ /** The live form id POST /forms/{formId} expects. */
5
+ formId: number;
6
+ /** The form's last_modification_timestamp, sent as metadata.form_version. */
7
+ formVersion: number;
8
+ }
9
+ export interface FormApi {
10
+ /** GET /forms — memoized. */
11
+ listForms(): Promise<FormListItem[]>;
12
+ /** GET /forms/{publishedFormId}, normalized — memoized per id. */
13
+ getForm(publishedFormId: number): Promise<FormSchema>;
14
+ /**
15
+ * Resolve a stable published form id to the live form id + version via the
16
+ * form list. Host configs should store published ids; uploads need this
17
+ * mapping (thr_vizfolyasok getFormMapping precedent).
18
+ */
19
+ resolveLiveForm(publishedFormId: number): Promise<LiveFormRef>;
20
+ /** Drop all memoized responses. */
21
+ invalidate(): void;
22
+ }
23
+ export declare function createFormApi(config: ObmClientConfig): FormApi;
@@ -0,0 +1,10 @@
1
+ import { ObmClientConfig } from '../config';
2
+ /**
3
+ * Thin fetch wrapper shared by every API call: applies the configured auth
4
+ * strategy and Accept-Language, retries once after a successful
5
+ * auth.onUnauthorized() on 401, and turns non-2xx responses into ObmApiError
6
+ * (parsing RFC 9457 Problem JSON bodies when present).
7
+ */
8
+ export declare function apiFetch(config: ObmClientConfig, path: string, init?: RequestInit): Promise<Response>;
9
+ /** apiFetch + JSON body parsing. */
10
+ export declare function apiFetchJson<T>(config: ObmClientConfig, path: string, init?: RequestInit): Promise<T>;
@@ -0,0 +1,25 @@
1
+ import { AuthStrategy } from './client/auth';
2
+ /**
3
+ * Project configuration, injected explicitly wherever the library talks to
4
+ * the API — never read from globals (a deliberate break from the
5
+ * thr_vizfolyasok precedent of module-level config constants).
6
+ */
7
+ export interface ObmClientConfig {
8
+ /**
9
+ * Base URL of the project API, e.g.
10
+ * "https://example.org/projects/my_project/api/v3". Trailing slash tolerated.
11
+ */
12
+ baseUrl: string;
13
+ /**
14
+ * Language sent as Accept-Language on reads. The API uses the first two
15
+ * characters and falls back to the project language, then Hungarian.
16
+ */
17
+ language?: string;
18
+ /** Client application identifier reported in upload metadata (`app_version`). */
19
+ appVersion?: string;
20
+ /** Authentication strategy; defaults to cookie/session auth. */
21
+ auth?: AuthStrategy;
22
+ /** Custom fetch implementation (tests, Node hosts). Defaults to globalThis.fetch. */
23
+ fetchImpl?: typeof fetch;
24
+ }
25
+ export declare function normalizeBaseUrl(baseUrl: string): string;
@@ -0,0 +1,7 @@
1
+ import { Geometry } from 'geojson';
2
+ /** Parse a WKT string into a GeoJSON geometry. Throws ObmGeometryError on invalid input. */
3
+ export declare function wktToGeoJSON(wkt: string): Geometry;
4
+ /** Serialize a GeoJSON geometry to WKT. Throws ObmGeometryError on failure. */
5
+ export declare function geoJSONToWkt(geometry: Geometry): string;
6
+ /** True when the string parses as (E)WKT. */
7
+ export declare function isWkt(value: string): boolean;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Message catalog for strings the library itself generates (validation
3
+ * messages). Hosts can override individual keys per call via MessageOverrides.
4
+ */
5
+ export type MessageKey = 'validation.required' | 'validation.regexp' | 'validation.minmax.belowMin' | 'validation.minmax.aboveMax' | 'validation.minmax.notNumber' | 'validation.invalidRule' | 'validation.notSupported';
6
+ export type MessageCatalog = Record<MessageKey, string>;
7
+ export type MessageOverrides = Partial<MessageCatalog>;
8
+ export declare const DEFAULT_LANGUAGE = "en";
9
+ /**
10
+ * Resolve a message for the given language ('en' fallback for unknown
11
+ * languages/keys) and interpolate `{param}` placeholders.
12
+ */
13
+ export declare function formatMessage(key: MessageKey, params?: Record<string, unknown>, language?: string, overrides?: MessageOverrides): string;
@@ -0,0 +1,16 @@
1
+ /** Public API of the framework-free core (`obm-upload-form-ui`). */
2
+ export { normalizeBaseUrl, type ObmClientConfig } from './config';
3
+ export { ObmApiError, ObmError, ObmGeometryError, ObmNetworkError, ObmSchemaError, type ProblemDetails, } from './client/errors';
4
+ export { bearerAuth, cookieAuth, jwtAuth, type AuthStrategy, type JwtAuthOptions, } from './client/auth';
5
+ export { apiFetch, apiFetchJson } from './client/http';
6
+ export { createFormApi, type FormApi, type LiveFormRef } from './client/formApi';
7
+ export { FORM_COLUMN_TYPES, UNKNOWN_COLUMN_TYPE, type ColumnSchema, type ColumnValidation, type FormApiParameters, type FormColumnType, type FormHeader, type FormListElement, type FormListItem, type FormSchema, type ImportantField, type ListDefinition, type NormalizedColumnType, type ObservationListSettings, type RawFormDetails, type ValidationControlType, } from './schema/types';
8
+ export { normalizeFormDetails } from './schema/normalize';
9
+ export { DEFAULT_VALIDATION_POLICY, type DocumentState, type IssueSeverity, type ValidationIssue, type ValidationOptions, type ValidationPolicy, type ValidationResult, type ValidationRuleName, } from './validation/types';
10
+ export { canSubmit, validateField, validateForm } from './validation/engine';
11
+ export { isEmpty, parseMinmaxBounds } from './validation/rules';
12
+ export { buildMetadata, generateUuid, type BuildMetadataOptions, type UploadMetadata, } from './submit/metadata';
13
+ export { submitObservation, ValidationRefusedError, type SubmitObservationOptions, type SubmitResult, type SubmitValidationGate, } from './submit/submitClient';
14
+ export { submitLinked, type LinkedItemFailure, type LinkedItemRef, type LinkedItemSuccess, type LinkedSubmissionItem, type LinkedSubmissionPlan, type LinkedSubmissionResult, } from './submit/linked';
15
+ export { geoJSONToWkt, isWkt, wktToGeoJSON } from './geo/wkt';
16
+ export { DEFAULT_LANGUAGE, formatMessage, type MessageCatalog, type MessageKey, type MessageOverrides, } from './i18n/messages';
@@ -0,0 +1,12 @@
1
+ import { FormColumnType } from './types';
2
+ export declare function isFormColumnType(value: unknown): value is FormColumnType;
3
+ export declare function isPlainObject(value: unknown): value is Record<string, unknown>;
4
+ /**
5
+ * Tolerant boolean coercion for values that legacy data may deliver as
6
+ * 0/1, '0'/'1', 't'/'f' or 'true'/'false' instead of JSON booleans.
7
+ */
8
+ export declare function toBoolean(value: unknown, fallback?: boolean): boolean;
9
+ /** Tolerant numeric coercion; returns the fallback for non-numeric input. */
10
+ export declare function toNumber(value: unknown, fallback?: number): number;
11
+ export declare function toNullableString(value: unknown): string | null;
12
+ export declare function toStringArray(value: unknown): string[];
@@ -0,0 +1,10 @@
1
+ import { FormSchema } from './types';
2
+ /**
3
+ * Normalize a raw GET /forms/{publishedFormId} response into a FormSchema.
4
+ *
5
+ * Structural problems (not an object, missing form_body array) throw
6
+ * ObmSchemaError. Per-column oddities never throw: the schema is re-derived
7
+ * server-side from legacy data, so unknown types, missing blocks and loosely
8
+ * typed booleans are smoothed into safe defaults instead.
9
+ */
10
+ export declare function normalizeFormDetails(raw: unknown): FormSchema;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Typed model of the obm-project-api form schema.
3
+ *
4
+ * Field names mirror the API's JSON serialization exactly
5
+ * (obm-project-api src/Models/FormDetails/**), except where a normalized
6
+ * shape is explicitly documented (FormSchema).
7
+ */
8
+ /** Field types returned by GET /forms/{publishedFormId} (obm-project-api FormColumnType enum). */
9
+ export declare const FORM_COLUMN_TYPES: readonly ["array", "autocomplete", "autocompletelist", "boolean", "crings", "custom_list", "date", "datetime", "file_id", "line", "list", "numeric", "photo_id", "point", "polygon", "text", "textarea", "time", "time_interval", "timetominutes", "wkt"];
10
+ export type FormColumnType = (typeof FORM_COLUMN_TYPES)[number];
11
+ /**
12
+ * Sentinel used by normalization when the API reports a type this library
13
+ * version does not know. Such columns render as "unsupported" instead of failing.
14
+ */
15
+ export declare const UNKNOWN_COLUMN_TYPE = "__unknown";
16
+ export type NormalizedColumnType = FormColumnType | typeof UNKNOWN_COLUMN_TYPE;
17
+ /** obm-project-api ControlType enum (the `validate.type` field). */
18
+ export type ValidationControlType = 'custom_check' | 'minmax' | 'nocheck' | 'regexp' | 'spatial';
19
+ export interface ColumnValidation {
20
+ required: boolean;
21
+ /** Warn-but-allow tier: failures are reported as warnings and never block. */
22
+ soft: boolean;
23
+ type: ValidationControlType;
24
+ /** Rule payload: regexp pattern, "min,max" bounds, custom function name, ... */
25
+ value: string | null;
26
+ }
27
+ export interface FormApiParameters {
28
+ sticky: boolean;
29
+ hidden: boolean;
30
+ readonly: boolean;
31
+ list_elements_as_buttons: boolean;
32
+ once_field: boolean;
33
+ unfolding_list: boolean;
34
+ /** Only present in the JSON when non-null server-side. */
35
+ auto_geometry?: unknown;
36
+ }
37
+ export interface ListDefinition {
38
+ custom_list: boolean;
39
+ extendable: boolean;
40
+ /** Columns whose picklists should refresh when this column changes. */
41
+ trigger_target_column: string[];
42
+ selected: unknown;
43
+ multiselect: boolean;
44
+ /** The API spreads extra snake_case keys (pre_filter_*, ...) into this object. */
45
+ [passthrough: string]: unknown;
46
+ }
47
+ export interface FormListElement {
48
+ value: string;
49
+ /** The API falls back to `value` when the label is empty; normalization guarantees it here. */
50
+ label: string;
51
+ filter_value?: string;
52
+ }
53
+ /** One item of `form_body`, normalized (see `normalizeFormDetails`). */
54
+ export interface ColumnSchema {
55
+ column: string;
56
+ short_name: string;
57
+ description: string | null;
58
+ type: NormalizedColumnType;
59
+ /** Raw type string as received; diagnostic aid when `type` is `__unknown`. */
60
+ raw_type: string;
61
+ default_value: unknown;
62
+ relation: string | null;
63
+ position_order: number;
64
+ validate: ColumnValidation;
65
+ pseudo_column: string | null;
66
+ api_params: FormApiParameters;
67
+ has_empty_line: boolean;
68
+ list_definition: ListDefinition | null;
69
+ generated_list_elements: FormListElement[];
70
+ /** Options per trigger value, keyed by the triggering column's value. */
71
+ filterable_list_elements: Record<string, FormListElement[]> | null;
72
+ }
73
+ export interface ObservationListSettings {
74
+ mode?: string;
75
+ time_limit?: number;
76
+ [passthrough: string]: unknown;
77
+ }
78
+ export interface ImportantField {
79
+ column: string;
80
+ type: string;
81
+ [passthrough: string]: unknown;
82
+ }
83
+ /** `form_header` of GET /forms/{publishedFormId}. */
84
+ export interface FormHeader {
85
+ user?: unknown;
86
+ form_id: number;
87
+ published_form_id: number;
88
+ name: string;
89
+ description: string | null;
90
+ project_table: string;
91
+ destination_table: string;
92
+ tracklog_mode: string | null;
93
+ observation_list: ObservationListSettings | null;
94
+ periodic_notification?: unknown;
95
+ important_fields: ImportantField[];
96
+ highlighted_fields: string[];
97
+ }
98
+ /**
99
+ * Normalized form schema produced by `normalizeFormDetails` from the raw
100
+ * `{form_header, form_body}` API response: columns are sorted by
101
+ * `position_order` and per-column oddities are smoothed over.
102
+ */
103
+ export interface FormSchema {
104
+ header: FormHeader;
105
+ columns: ColumnSchema[];
106
+ }
107
+ /** One row of GET /forms. */
108
+ export interface FormListItem {
109
+ form_id: number;
110
+ published_form_id: number;
111
+ /** Unix timestamp; doubles as the `form_version` sent in upload metadata. */
112
+ last_modification_timestamp: number;
113
+ name: string;
114
+ }
115
+ /** Raw, un-normalized response shape of GET /forms/{publishedFormId}. */
116
+ export interface RawFormDetails {
117
+ form_header: Record<string, unknown>;
118
+ form_body: unknown[];
119
+ }
@@ -0,0 +1,58 @@
1
+ import { FormApi } from '../client/formApi';
2
+ import { ObmClientConfig } from '../config';
3
+ import { SubmitResult } from './submitClient';
4
+ /**
5
+ * Linked submissions generalize the OBM convention for repeated/nested field
6
+ * groups: the form schema is flat, so "add another X" data goes into separate
7
+ * linked forms whose rows carry a client-generated shared UUID as a foreign
8
+ * key (thr_vizfolyasok precedent: one parent POST + N child POSTs).
9
+ */
10
+ export interface LinkedSubmissionItem {
11
+ /** Stable published form id; resolved to the live form id at submit time. */
12
+ publishedFormId: number;
13
+ data: Record<string, unknown>;
14
+ }
15
+ export interface LinkedSubmissionPlan {
16
+ /** Data column that carries the shared UUID in the parent and every child. */
17
+ sharedIdColumn: string;
18
+ /** Shared UUID; generated when omitted. */
19
+ sharedId?: string;
20
+ parent: LinkedSubmissionItem;
21
+ children: LinkedSubmissionItem[];
22
+ /** Access rules included in every item's metadata, e.g. `{ write: [...] }`. */
23
+ rules?: Record<string, unknown>;
24
+ }
25
+ export type LinkedItemRef = 'parent' | number;
26
+ export interface LinkedItemSuccess {
27
+ item: LinkedItemRef;
28
+ publishedFormId: number;
29
+ result: SubmitResult;
30
+ }
31
+ export interface LinkedItemFailure {
32
+ item: LinkedItemRef;
33
+ publishedFormId: number;
34
+ error: unknown;
35
+ }
36
+ export interface LinkedSubmissionResult {
37
+ sharedId: string;
38
+ /** True when the parent and every child were accepted. */
39
+ complete: boolean;
40
+ succeeded: LinkedItemSuccess[];
41
+ /** The first failing item; submission stops there. */
42
+ failed?: LinkedItemFailure;
43
+ /** Child indexes that were not attempted because of the failure. */
44
+ remaining: number[];
45
+ }
46
+ /**
47
+ * Submit a parent record and its linked children, all stamped with a shared
48
+ * UUID, sequentially (parent first). Each POST gets fresh metadata with its
49
+ * own id and the target form's version.
50
+ *
51
+ * There is NO server-side transaction — each POST hits the legacy upload
52
+ * engine independently. On failure this resolves (never rejects) with
53
+ * `failed` + `remaining` so the caller can retry the remainder with the same
54
+ * `sharedId`.
55
+ */
56
+ export declare function submitLinked(config: ObmClientConfig, plan: LinkedSubmissionPlan, dependencies?: {
57
+ formApi?: FormApi;
58
+ }): Promise<LinkedSubmissionResult>;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Upload metadata for POST /forms/{formId}. Field semantics follow the
3
+ * working thr_vizfolyasok client (getBaseMetadata): epoch-second timestamps,
4
+ * timezone as signed hours (UTC+2 → 2), a fresh UUID per observation.
5
+ */
6
+ export interface UploadMetadata {
7
+ /** Unique id of this observation upload; the API requires it. */
8
+ id: string;
9
+ app_version: string;
10
+ /** The form's last_modification_timestamp (see FormApi.resolveLiveForm). */
11
+ form_version: number | null;
12
+ /** Signed offset from UTC in hours (UTC+2 → 2). */
13
+ timezone: number;
14
+ /** Epoch seconds. */
15
+ started_at: number;
16
+ /** Epoch seconds. */
17
+ finished_at: number;
18
+ rules?: Record<string, unknown>;
19
+ [extension: string]: unknown;
20
+ }
21
+ export interface BuildMetadataOptions {
22
+ /** The form's last_modification_timestamp; null only when genuinely unknown. */
23
+ formVersion: number | null;
24
+ /** Client application identifier; falls back to the library's own name. */
25
+ appVersion?: string;
26
+ /** Epoch seconds; defaults to now. */
27
+ startedAt?: number;
28
+ /** Epoch seconds; defaults to now. */
29
+ finishedAt?: number;
30
+ /** Access rules, e.g. `{ write: [...user groups] }`. */
31
+ rules?: Record<string, unknown>;
32
+ /** Extra metadata fields spread into the payload (e.g. observation_list). */
33
+ extra?: Record<string, unknown>;
34
+ }
35
+ /** crypto.randomUUID with a getRandomValues-based fallback for older webviews. */
36
+ export declare function generateUuid(): string;
37
+ export declare function buildMetadata(options: BuildMetadataOptions): UploadMetadata;
@@ -0,0 +1,40 @@
1
+ import { ObmError } from '../client/errors';
2
+ import { ObmClientConfig } from '../config';
3
+ import { FormSchema } from '../schema/types';
4
+ import { DocumentState, ValidationOptions, ValidationResult } from '../validation/types';
5
+ import { UploadMetadata } from './metadata';
6
+ /** Thrown by submitObservation when its validation gate blocks the payload. */
7
+ export declare class ValidationRefusedError extends ObmError {
8
+ readonly result: ValidationResult;
9
+ constructor(result: ValidationResult);
10
+ }
11
+ export interface SubmitValidationGate {
12
+ schema: FormSchema;
13
+ state: DocumentState;
14
+ options?: ValidationOptions;
15
+ }
16
+ export interface SubmitObservationOptions {
17
+ /** The live form id (not the published id — see FormApi.resolveLiveForm). */
18
+ formId: number;
19
+ data: Record<string, unknown>;
20
+ metadata: UploadMetadata;
21
+ /**
22
+ * Optional validation gate: when present, the payload is validated first
23
+ * and a blocking result throws ValidationRefusedError. The refuse-vs-warn
24
+ * knob lives in the gate's ValidationPolicy, not here.
25
+ */
26
+ validate?: SubmitValidationGate;
27
+ }
28
+ export interface SubmitResult {
29
+ status: number;
30
+ /** The API responds 201 with a text/plain confirmation message. */
31
+ message: string;
32
+ /** Present when a validation gate ran (blocking or not). */
33
+ validation?: ValidationResult;
34
+ }
35
+ /**
36
+ * POST /forms/{formId} with the `{data, metadata}` JSON body. Success is
37
+ * HTTP 201 with a text/plain body — not JSON. API errors arrive as
38
+ * RFC 9457 Problem JSON and are thrown as ObmApiError by the HTTP layer.
39
+ */
40
+ export declare function submitObservation(config: ObmClientConfig, options: SubmitObservationOptions): Promise<SubmitResult>;