@object-ui/providers 3.3.1 → 3.3.3

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # @object-ui/providers — Changelog
2
2
 
3
+ ## 3.3.3
4
+
5
+ ### Patch Changes
6
+
7
+ - a2d7023: End-user feature batch — forms, designer history, import/export, and PWA offline sync.
8
+
9
+ **Forms (`@object-ui/fields`, `@object-ui/providers`)**
10
+ - `FileField`: native `<input capture="environment">` camera capture for mobile devices, plus a uploading-progress indicator driven by `UploadProvider`.
11
+ - `ImageField`: per-image inline crop/rotate via the lazy-loaded `ImageCropperDialog` (canvas-based, zero new deps).
12
+ - New `UploadProvider` in `@object-ui/providers` with pluggable adapters for S3 and Azure Blob (plus the default object-URL adapter for local previews). XHR-based with progress, abort, and retry.
13
+ - `LookupField`: `lookup.dependsOn: string | string[]` to chain dependent lookups (e.g. State depends on Country); the trigger is gated until parent values are present and the OData `$filter` is built automatically.
14
+
15
+ **Container-aware widget widths (`@object-ui/components`)**
16
+ - New `useResizeObserver(ref)` hook exposing `{ width, height }` of any element. SSR-safe; reads the initial size via `getBoundingClientRect`.
17
+ - `plugin-gantt` and `plugin-kanban` now react to their container size instead of `window.innerWidth`, so they behave correctly inside split panels and dashboards.
18
+
19
+ **Designer history (`@object-ui/plugin-designer`)**
20
+ - `useUndoRedo` (and therefore `useDesignerHistory`) gains `persistKey` + `storage` options to round-trip the undo/redo stack through `sessionStorage`, plus a `clearPersisted()` cleanup helper. Drafts now survive accidental tab refreshes.
21
+ - New `<HistoryPanel>` component renders the timeline visually with one-click jump-to-checkpoint via the new `jumpTo(index)` API.
22
+
23
+ **Import wizard (`@object-ui/plugin-grid`)**
24
+ - Saved column-mapping templates: name, save, re-apply, and delete via a new template bar in the mapping step. Persisted under `objectui:import-templates:${objectName}` (override via `templateStorageKey` / `templateStorage`).
25
+ - Inline validation correction: cells with errors in the preview step are now editable; corrections feed straight into the import without requiring a re-upload, with green-bar status indicators for fixed rows.
26
+
27
+ **PWA offline sync (`@object-ui/mobile`)**
28
+ - New `MemoryOfflineQueue` / `IndexedDbOfflineQueue` (`createOfflineQueue()` picks the best backend) backed by IndexedDB.
29
+ - `createOfflineDataSource(inner, { queue })` wraps any DataSource so mutations issued while offline (or that fail with a network-style error) are queued and replayed in order on reconnect. Includes `replay()`, `drop()`, `clear()`, `pending()`, an `onChange` notifier, and an opt-in `resolveConflict` hook for stale-write conflicts.
30
+ - New `useOfflineSync(source)` hook exposes `{ isOnline, pending, isReplaying, replay, drop, clear }` and auto-replays on the browser's `online` event.
31
+ - `getServiceWorkerSource(opts)` emits a customisable Service Worker that pre-caches the app shell, applies network-first to API requests, and broadcasts `REPLAY_QUEUE` to clients on Background Sync. `requestBackgroundSync(tag)` registers a one-shot sync from the page.
32
+
33
+ - Updated dependencies [f1ca238]
34
+ - Updated dependencies [de881ef]
35
+ - @object-ui/types@3.4.0
36
+
37
+ ## 3.3.2
38
+
39
+ ### Patch Changes
40
+
41
+ - @object-ui/types@3.3.2
42
+
3
43
  ## 3.3.1
4
44
 
5
45
  ### Patch Changes
@@ -0,0 +1,115 @@
1
+ /**
2
+ * UploadProvider — pluggable file upload context for ObjectUI.
3
+ *
4
+ * Why: `FileField` and `ImageField` historically used `URL.createObjectURL(file)`
5
+ * which only works for the lifetime of the browser tab and never persists the
6
+ * file anywhere. Production deployments need to push the bytes to S3, Azure
7
+ * Blob, or another object store and replace the temporary blob URL with a
8
+ * canonical one.
9
+ *
10
+ * Design:
11
+ * - `UploadProvider` exposes an `upload(file, opts)` function via context.
12
+ * - Apps inject an adapter (`createS3Adapter`, `createAzureBlobAdapter`, or any
13
+ * custom one implementing `UploadAdapter`).
14
+ * - The default adapter (`createObjectUrlAdapter`) preserves the legacy
15
+ * "blob: URL" behaviour so the field widgets keep working when no provider
16
+ * is mounted.
17
+ * - Upload progress and resumable retries are first-class concerns: each
18
+ * adapter implementation can chunk + resume; the public API surfaces
19
+ * `onProgress` and an `abortSignal`.
20
+ */
21
+ import React, { type ReactNode } from 'react';
22
+ export interface UploadResult {
23
+ /** Canonical, shareable URL (e.g. https://my-bucket.s3.amazonaws.com/avatars/abc.png). */
24
+ url: string;
25
+ /** Stored object name / key. */
26
+ name: string;
27
+ size: number;
28
+ mimeType: string;
29
+ /** Adapter-specific metadata (etag, version, etc.). */
30
+ meta?: Record<string, unknown>;
31
+ }
32
+ export interface UploadOptions {
33
+ /** Logical folder / key prefix (e.g. "avatars/"). */
34
+ path?: string;
35
+ /** Progress callback in 0–1. */
36
+ onProgress?: (ratio: number) => void;
37
+ /** External abort signal — cancels the upload (and any retry attempts). */
38
+ signal?: AbortSignal;
39
+ /** Number of retry attempts on network failure. @default 3 */
40
+ maxRetries?: number;
41
+ /** Initial backoff in ms; exponential. @default 500 */
42
+ retryDelayMs?: number;
43
+ }
44
+ export interface UploadAdapter {
45
+ readonly name: string;
46
+ upload(file: File | Blob, opts?: UploadOptions): Promise<UploadResult>;
47
+ }
48
+ interface UploadContextValue {
49
+ upload: (file: File | Blob, opts?: UploadOptions) => Promise<UploadResult>;
50
+ adapter: UploadAdapter;
51
+ }
52
+ /**
53
+ * Default adapter — wraps the file in an object URL. Identical to the legacy
54
+ * field-widget behaviour; safe fallback when the app hasn't configured a real
55
+ * upload destination yet.
56
+ */
57
+ export declare function createObjectUrlAdapter(): UploadAdapter;
58
+ export interface S3AdapterOptions {
59
+ /**
60
+ * URL of an endpoint that returns `{ uploadUrl, publicUrl, headers? }`
61
+ * for a given `{ name, type, size }`. Implementations typically presign an
62
+ * S3 PUT URL server-side.
63
+ */
64
+ getPresignedUrl: (input: {
65
+ name: string;
66
+ type: string;
67
+ size: number;
68
+ path?: string;
69
+ }) => Promise<{
70
+ uploadUrl: string;
71
+ publicUrl: string;
72
+ headers?: Record<string, string>;
73
+ }>;
74
+ /** Override fetch (useful for tests and ServiceWorker offline queues). */
75
+ fetchImpl?: typeof fetch;
76
+ }
77
+ /**
78
+ * S3-style adapter — relies on the app to provide a presign endpoint so that
79
+ * AWS credentials never leak to the browser. Works with any S3-compatible
80
+ * object store (AWS S3, Cloudflare R2, MinIO, …).
81
+ */
82
+ export declare function createS3Adapter(opts: S3AdapterOptions): UploadAdapter;
83
+ export interface AzureBlobAdapterOptions {
84
+ /**
85
+ * URL of an endpoint that returns `{ uploadUrl, publicUrl }` — typically a
86
+ * short-lived SAS URL minted server-side.
87
+ */
88
+ getSasUrl: (input: {
89
+ name: string;
90
+ type: string;
91
+ size: number;
92
+ path?: string;
93
+ }) => Promise<{
94
+ uploadUrl: string;
95
+ publicUrl: string;
96
+ }>;
97
+ fetchImpl?: typeof fetch;
98
+ }
99
+ /**
100
+ * Azure Blob Storage adapter using SAS tokens. Single PUT — for files larger
101
+ * than ~256 MiB, callers should issue chunked SAS URLs (block list + commit)
102
+ * via their backend.
103
+ */
104
+ export declare function createAzureBlobAdapter(opts: AzureBlobAdapterOptions): UploadAdapter;
105
+ export interface UploadProviderProps {
106
+ adapter?: UploadAdapter;
107
+ children: ReactNode;
108
+ }
109
+ export declare const UploadProvider: React.FC<UploadProviderProps>;
110
+ /**
111
+ * Hook returning the configured upload function. Falls back to a fresh
112
+ * object-URL adapter so widgets never crash when used outside a provider.
113
+ */
114
+ export declare function useUpload(): UploadContextValue;
115
+ export {};
@@ -0,0 +1,211 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * UploadProvider — pluggable file upload context for ObjectUI.
4
+ *
5
+ * Why: `FileField` and `ImageField` historically used `URL.createObjectURL(file)`
6
+ * which only works for the lifetime of the browser tab and never persists the
7
+ * file anywhere. Production deployments need to push the bytes to S3, Azure
8
+ * Blob, or another object store and replace the temporary blob URL with a
9
+ * canonical one.
10
+ *
11
+ * Design:
12
+ * - `UploadProvider` exposes an `upload(file, opts)` function via context.
13
+ * - Apps inject an adapter (`createS3Adapter`, `createAzureBlobAdapter`, or any
14
+ * custom one implementing `UploadAdapter`).
15
+ * - The default adapter (`createObjectUrlAdapter`) preserves the legacy
16
+ * "blob: URL" behaviour so the field widgets keep working when no provider
17
+ * is mounted.
18
+ * - Upload progress and resumable retries are first-class concerns: each
19
+ * adapter implementation can chunk + resume; the public API surfaces
20
+ * `onProgress` and an `abortSignal`.
21
+ */
22
+ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, } from 'react';
23
+ const UploadContext = createContext(null);
24
+ /**
25
+ * Default adapter — wraps the file in an object URL. Identical to the legacy
26
+ * field-widget behaviour; safe fallback when the app hasn't configured a real
27
+ * upload destination yet.
28
+ */
29
+ export function createObjectUrlAdapter() {
30
+ return {
31
+ name: 'object-url',
32
+ async upload(file) {
33
+ const f = file;
34
+ const name = ('name' in f && f.name) || 'upload';
35
+ return {
36
+ url: URL.createObjectURL(file),
37
+ name,
38
+ size: file.size,
39
+ mimeType: file.type || 'application/octet-stream',
40
+ };
41
+ },
42
+ };
43
+ }
44
+ /**
45
+ * S3-style adapter — relies on the app to provide a presign endpoint so that
46
+ * AWS credentials never leak to the browser. Works with any S3-compatible
47
+ * object store (AWS S3, Cloudflare R2, MinIO, …).
48
+ */
49
+ export function createS3Adapter(opts) {
50
+ const fetchFn = opts.fetchImpl ?? fetch;
51
+ return {
52
+ name: 's3',
53
+ async upload(file, options = {}) {
54
+ const f = file;
55
+ const name = ('name' in f && f.name) || 'upload';
56
+ const presigned = await opts.getPresignedUrl({
57
+ name,
58
+ type: file.type || 'application/octet-stream',
59
+ size: file.size,
60
+ path: options.path,
61
+ });
62
+ await uploadWithProgress(fetchFn, presigned.uploadUrl, file, {
63
+ method: 'PUT',
64
+ headers: presigned.headers,
65
+ ...options,
66
+ });
67
+ return {
68
+ url: presigned.publicUrl,
69
+ name,
70
+ size: file.size,
71
+ mimeType: file.type || 'application/octet-stream',
72
+ };
73
+ },
74
+ };
75
+ }
76
+ /**
77
+ * Azure Blob Storage adapter using SAS tokens. Single PUT — for files larger
78
+ * than ~256 MiB, callers should issue chunked SAS URLs (block list + commit)
79
+ * via their backend.
80
+ */
81
+ export function createAzureBlobAdapter(opts) {
82
+ const fetchFn = opts.fetchImpl ?? fetch;
83
+ return {
84
+ name: 'azure-blob',
85
+ async upload(file, options = {}) {
86
+ const f = file;
87
+ const name = ('name' in f && f.name) || 'upload';
88
+ const sas = await opts.getSasUrl({
89
+ name,
90
+ type: file.type || 'application/octet-stream',
91
+ size: file.size,
92
+ path: options.path,
93
+ });
94
+ await uploadWithProgress(fetchFn, sas.uploadUrl, file, {
95
+ method: 'PUT',
96
+ headers: {
97
+ 'x-ms-blob-type': 'BlockBlob',
98
+ 'Content-Type': file.type || 'application/octet-stream',
99
+ },
100
+ ...options,
101
+ });
102
+ return {
103
+ url: sas.publicUrl,
104
+ name,
105
+ size: file.size,
106
+ mimeType: file.type || 'application/octet-stream',
107
+ };
108
+ },
109
+ };
110
+ }
111
+ /**
112
+ * Internal helper — performs a single PUT/POST with progress reporting,
113
+ * exponential-backoff retry, and abort propagation.
114
+ */
115
+ async function uploadWithProgress(fetchFn, url, body, options) {
116
+ const maxRetries = options.maxRetries ?? 3;
117
+ const baseDelay = options.retryDelayMs ?? 500;
118
+ let attempt = 0;
119
+ // eslint-disable-next-line no-constant-condition
120
+ while (true) {
121
+ if (options.signal?.aborted)
122
+ throw new DOMException('aborted', 'AbortError');
123
+ try {
124
+ // Fetch's standard progress reporting is limited; use XHR when we need
125
+ // per-byte progress. Falls back to fetch for environments without XHR
126
+ // (e.g. Cloudflare Workers used by Service Workers).
127
+ if (typeof XMLHttpRequest !== 'undefined' && options.onProgress) {
128
+ await xhrUpload(url, body, options);
129
+ }
130
+ else {
131
+ const res = await fetchFn(url, {
132
+ method: options.method,
133
+ body,
134
+ headers: options.headers,
135
+ signal: options.signal,
136
+ });
137
+ if (!res.ok)
138
+ throw new Error(`Upload failed with status ${res.status}`);
139
+ options.onProgress?.(1);
140
+ }
141
+ return;
142
+ }
143
+ catch (err) {
144
+ if (err?.name === 'AbortError')
145
+ throw err;
146
+ if (attempt >= maxRetries)
147
+ throw err;
148
+ const delay = baseDelay * Math.pow(2, attempt);
149
+ attempt++;
150
+ await new Promise((r) => setTimeout(r, delay));
151
+ }
152
+ }
153
+ }
154
+ function xhrUpload(url, body, options) {
155
+ return new Promise((resolve, reject) => {
156
+ const xhr = new XMLHttpRequest();
157
+ xhr.open(options.method, url);
158
+ if (options.headers) {
159
+ for (const [k, v] of Object.entries(options.headers))
160
+ xhr.setRequestHeader(k, v);
161
+ }
162
+ xhr.upload.onprogress = (e) => {
163
+ if (e.lengthComputable && options.onProgress)
164
+ options.onProgress(e.loaded / e.total);
165
+ };
166
+ xhr.onload = () => {
167
+ if (xhr.status >= 200 && xhr.status < 300) {
168
+ options.onProgress?.(1);
169
+ resolve();
170
+ }
171
+ else {
172
+ reject(new Error(`Upload failed with status ${xhr.status}`));
173
+ }
174
+ };
175
+ xhr.onerror = () => reject(new Error('Network error during upload'));
176
+ xhr.onabort = () => reject(new DOMException('aborted', 'AbortError'));
177
+ if (options.signal) {
178
+ const abortHandler = () => xhr.abort();
179
+ if (options.signal.aborted) {
180
+ xhr.abort();
181
+ }
182
+ else {
183
+ options.signal.addEventListener('abort', abortHandler, { once: true });
184
+ }
185
+ }
186
+ xhr.send(body);
187
+ });
188
+ }
189
+ export const UploadProvider = ({ adapter, children }) => {
190
+ const adapterRef = useRef(adapter ?? createObjectUrlAdapter());
191
+ useEffect(() => {
192
+ if (adapter)
193
+ adapterRef.current = adapter;
194
+ }, [adapter]);
195
+ const upload = useCallback((file, opts) => adapterRef.current.upload(file, opts), []);
196
+ const value = useMemo(() => ({ upload, adapter: adapterRef.current }), [upload]);
197
+ return _jsx(UploadContext.Provider, { value: value, children: children });
198
+ };
199
+ /**
200
+ * Hook returning the configured upload function. Falls back to a fresh
201
+ * object-URL adapter so widgets never crash when used outside a provider.
202
+ */
203
+ export function useUpload() {
204
+ const ctx = useContext(UploadContext);
205
+ // Always call useMemo to honour the rules of hooks; cheap when ctx is set.
206
+ const fallback = useMemo(() => {
207
+ const a = createObjectUrlAdapter();
208
+ return { upload: (f, o) => a.upload(f, o), adapter: a };
209
+ }, []);
210
+ return ctx ?? fallback;
211
+ }
package/dist/index.d.ts CHANGED
@@ -6,4 +6,6 @@
6
6
  export { DataSourceProvider, useDataSource } from './DataSourceProvider';
7
7
  export { MetadataProvider, useMetadata } from './MetadataProvider';
8
8
  export { ThemeProvider, useTheme } from './ThemeProvider';
9
+ export { UploadProvider, useUpload, createObjectUrlAdapter, createS3Adapter, createAzureBlobAdapter, } from './UploadProvider';
9
10
  export type { DataSourceProviderProps, MetadataProviderProps, ThemeProviderProps, Theme, } from './types';
11
+ export type { UploadAdapter, UploadOptions, UploadResult, UploadProviderProps, S3AdapterOptions, AzureBlobAdapterOptions, } from './UploadProvider';
package/dist/index.js CHANGED
@@ -6,3 +6,4 @@
6
6
  export { DataSourceProvider, useDataSource } from './DataSourceProvider';
7
7
  export { MetadataProvider, useMetadata } from './MetadataProvider';
8
8
  export { ThemeProvider, useTheme } from './ThemeProvider';
9
+ export { UploadProvider, useUpload, createObjectUrlAdapter, createS3Adapter, createAzureBlobAdapter, } from './UploadProvider';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@object-ui/providers",
3
- "version": "3.3.1",
3
+ "version": "3.3.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Reusable context providers for ObjectUI applications",
@@ -24,7 +24,7 @@
24
24
  }
25
25
  },
26
26
  "dependencies": {
27
- "@object-ui/types": "3.3.1"
27
+ "@object-ui/types": "3.4.0"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "react": "^18.0.0 || ^19.0.0",