@pallasoft/ps-core-components 2.29.0 → 2.31.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.
- package/dist/components/ps-document-row.d.ts +19 -0
- package/dist/components/ps-document-row.js +17 -0
- package/dist/components/ps-selection-bar.d.ts +9 -0
- package/dist/components/ps-selection-bar.js +8 -0
- package/dist/constants/ps-optimistic-config.d.ts +3 -0
- package/dist/constants/ps-optimistic-config.js +6 -0
- package/dist/esm/components/ps-document-row.js +13 -0
- package/dist/esm/components/ps-selection-bar.js +4 -0
- package/dist/esm/constants/ps-optimistic-config.js +3 -0
- package/dist/esm/hooks/use-ps-optimistic-fetcher.js +1 -1
- package/dist/esm/hooks/use-ps-optimistic-save.js +17 -0
- package/dist/esm/index.js +10 -0
- package/dist/esm/interfaces/ps-optimistic-save-input.js +1 -0
- package/dist/esm/interfaces/ps-optimistic-save-result.js +1 -0
- package/dist/esm/interfaces/ps-optimistic-save-spec.js +1 -0
- package/dist/esm/interfaces/ps-revalidation-args.js +1 -0
- package/dist/esm/services/ps-client-param-revalidation-service.js +26 -0
- package/dist/esm/services/ps-optimistic-save-service.js +64 -0
- package/dist/hooks/use-ps-optimistic-fetcher.js +1 -1
- package/dist/hooks/use-ps-optimistic-save.d.ts +3 -0
- package/dist/hooks/use-ps-optimistic-save.js +21 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +10 -0
- package/dist/interfaces/ps-fetcher-submit-options.d.ts +1 -0
- package/dist/interfaces/ps-optimistic-save-input.d.ts +11 -0
- package/dist/interfaces/ps-optimistic-save-input.js +2 -0
- package/dist/interfaces/ps-optimistic-save-result.d.ts +5 -0
- package/dist/interfaces/ps-optimistic-save-result.js +2 -0
- package/dist/interfaces/ps-optimistic-save-spec.d.ts +11 -0
- package/dist/interfaces/ps-optimistic-save-spec.js +2 -0
- package/dist/interfaces/ps-revalidation-args.d.ts +7 -0
- package/dist/interfaces/ps-revalidation-args.js +2 -0
- package/dist/services/ps-client-param-revalidation-service.d.ts +6 -0
- package/dist/services/ps-client-param-revalidation-service.js +30 -0
- package/dist/services/ps-optimistic-save-service.d.ts +11 -0
- package/dist/services/ps-optimistic-save-service.js +68 -0
- package/package.json +1 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export interface PsDocumentRowProps {
|
|
3
|
+
actions?: ReactNode;
|
|
4
|
+
amount?: ReactNode;
|
|
5
|
+
badges?: ReactNode;
|
|
6
|
+
className?: string;
|
|
7
|
+
date?: string;
|
|
8
|
+
isSelected?: boolean;
|
|
9
|
+
note?: ReactNode;
|
|
10
|
+
onOpen?: () => void;
|
|
11
|
+
onPrefetch?: () => void;
|
|
12
|
+
select?: ReactNode;
|
|
13
|
+
subtitle?: string;
|
|
14
|
+
thumbnailAlt?: string;
|
|
15
|
+
thumbnailFallback?: ReactNode;
|
|
16
|
+
thumbnailSrc?: string | null;
|
|
17
|
+
title: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const PsDocumentRow: ({ actions, amount, badges, className, date, isSelected, note, onOpen, onPrefetch, select, subtitle, thumbnailAlt, thumbnailFallback, thumbnailSrc, title, }: PsDocumentRowProps) => import("react").JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PsDocumentRow = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const ps_design_system_1 = require("@pallasoft/ps-design-system");
|
|
6
|
+
const PsDocumentRow = ({ actions, amount, badges, className, date, isSelected = false, note, onOpen, onPrefetch, select, subtitle, thumbnailAlt = '', thumbnailFallback, thumbnailSrc, title, }) => {
|
|
7
|
+
const Main = onOpen ? 'button' : 'div';
|
|
8
|
+
return ((0, jsx_runtime_1.jsxs)("div", { className: ps_design_system_1.PallasoftClassNameService.merge('flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2 border-b border-line/70 px-3 py-3 transition-colors last:border-b-0 sm:flex-nowrap', isSelected ? 'bg-primary/5' : 'hover:bg-mist/40', className), children: [select ? (0, jsx_runtime_1.jsx)("span", { className: 'flex shrink-0 items-center', children: select }) : null, (0, jsx_runtime_1.jsxs)(Main, { className: 'ps-focus-ring grid min-w-0 flex-1 basis-0 grid-cols-[auto_minmax(0,1fr)] items-center gap-x-3 gap-y-1 rounded-ps-control text-left sm:grid-cols-[auto_minmax(0,1fr)_auto]', ...(onOpen
|
|
9
|
+
? {
|
|
10
|
+
onClick: onOpen,
|
|
11
|
+
onFocus: onPrefetch,
|
|
12
|
+
onPointerEnter: onPrefetch,
|
|
13
|
+
type: 'button',
|
|
14
|
+
}
|
|
15
|
+
: {}), children: [(0, jsx_runtime_1.jsx)("span", { className: 'row-span-2 flex h-14 w-11 shrink-0 items-center justify-center overflow-hidden rounded-ps-control border border-line/60 bg-mist/40 sm:row-span-1', children: thumbnailSrc ? ((0, jsx_runtime_1.jsx)("img", { alt: thumbnailAlt, className: 'h-full w-full object-cover object-top', decoding: 'async', loading: 'lazy', src: thumbnailSrc })) : (thumbnailFallback) }), (0, jsx_runtime_1.jsxs)("span", { className: 'flex min-w-0 flex-col gap-0.5', children: [(0, jsx_runtime_1.jsx)("span", { className: 'truncate text-sm font-semibold text-ink', children: title }), subtitle ? (0, jsx_runtime_1.jsx)("span", { className: 'truncate text-xs text-ink/55', children: subtitle }) : null, badges ? ((0, jsx_runtime_1.jsx)("span", { className: 'flex flex-wrap items-center gap-1.5 pt-1', children: badges })) : null, note ? ((0, jsx_runtime_1.jsx)("span", { className: 'line-clamp-2 break-words pt-0.5 text-xs text-ink/70', children: note })) : null] }), amount || date ? ((0, jsx_runtime_1.jsxs)("span", { className: 'col-start-2 flex items-baseline gap-2 sm:col-start-3 sm:flex-col sm:items-end sm:gap-0.5', children: [amount ? ((0, jsx_runtime_1.jsx)("span", { className: 'text-sm font-semibold tabular-nums text-ink', children: amount })) : null, date ? (0, jsx_runtime_1.jsx)("span", { className: 'text-xs tabular-nums text-ink/55', children: date }) : null] })) : null] }), actions ? ((0, jsx_runtime_1.jsx)("div", { className: 'flex w-full shrink-0 items-center justify-end gap-2 sm:w-auto', children: actions })) : null] }));
|
|
16
|
+
};
|
|
17
|
+
exports.PsDocumentRow = PsDocumentRow;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
export interface PsSelectionBarProps {
|
|
3
|
+
children: ReactNode;
|
|
4
|
+
className?: string;
|
|
5
|
+
clearLabel: string;
|
|
6
|
+
countLabel: string;
|
|
7
|
+
onClear: () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare const PsSelectionBar: ({ children, className, clearLabel, countLabel, onClear, }: PsSelectionBarProps) => import("react").JSX.Element;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PsSelectionBar = void 0;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const ps_design_system_1 = require("@pallasoft/ps-design-system");
|
|
6
|
+
const ps_icons_1 = require("@pallasoft/ps-icons");
|
|
7
|
+
const PsSelectionBar = ({ children, className, clearLabel, countLabel, onClear, }) => ((0, jsx_runtime_1.jsxs)("div", { "aria-live": 'polite', className: ps_design_system_1.PallasoftClassNameService.merge('sticky bottom-3 z-10 flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2 rounded-ps-card border border-line bg-surface px-4 py-2 shadow-lg', className), role: 'toolbar', children: [(0, jsx_runtime_1.jsx)("span", { className: 'text-sm font-semibold text-ink', children: countLabel }), (0, jsx_runtime_1.jsx)("div", { className: 'flex min-w-0 flex-1 flex-wrap items-center gap-2', children: children }), (0, jsx_runtime_1.jsx)("button", { "aria-label": clearLabel, className: 'ps-focus-ring ps-control-square inline-flex shrink-0 items-center justify-center rounded-ps-control text-ink/65 transition-colors hover:bg-ink/5 hover:text-ink', onClick: onClear, title: clearLabel, type: 'button', children: (0, jsx_runtime_1.jsx)(ps_icons_1.PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: ps_icons_1.PsIconKey.Close }) })] }));
|
|
8
|
+
exports.PsSelectionBar = PsSelectionBar;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
+
export const PsDocumentRow = ({ actions, amount, badges, className, date, isSelected = false, note, onOpen, onPrefetch, select, subtitle, thumbnailAlt = '', thumbnailFallback, thumbnailSrc, title, }) => {
|
|
4
|
+
const Main = onOpen ? 'button' : 'div';
|
|
5
|
+
return (_jsxs("div", { className: PallasoftClassNameService.merge('flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2 border-b border-line/70 px-3 py-3 transition-colors last:border-b-0 sm:flex-nowrap', isSelected ? 'bg-primary/5' : 'hover:bg-mist/40', className), children: [select ? _jsx("span", { className: 'flex shrink-0 items-center', children: select }) : null, _jsxs(Main, { className: 'ps-focus-ring grid min-w-0 flex-1 basis-0 grid-cols-[auto_minmax(0,1fr)] items-center gap-x-3 gap-y-1 rounded-ps-control text-left sm:grid-cols-[auto_minmax(0,1fr)_auto]', ...(onOpen
|
|
6
|
+
? {
|
|
7
|
+
onClick: onOpen,
|
|
8
|
+
onFocus: onPrefetch,
|
|
9
|
+
onPointerEnter: onPrefetch,
|
|
10
|
+
type: 'button',
|
|
11
|
+
}
|
|
12
|
+
: {}), children: [_jsx("span", { className: 'row-span-2 flex h-14 w-11 shrink-0 items-center justify-center overflow-hidden rounded-ps-control border border-line/60 bg-mist/40 sm:row-span-1', children: thumbnailSrc ? (_jsx("img", { alt: thumbnailAlt, className: 'h-full w-full object-cover object-top', decoding: 'async', loading: 'lazy', src: thumbnailSrc })) : (thumbnailFallback) }), _jsxs("span", { className: 'flex min-w-0 flex-col gap-0.5', children: [_jsx("span", { className: 'truncate text-sm font-semibold text-ink', children: title }), subtitle ? _jsx("span", { className: 'truncate text-xs text-ink/55', children: subtitle }) : null, badges ? (_jsx("span", { className: 'flex flex-wrap items-center gap-1.5 pt-1', children: badges })) : null, note ? (_jsx("span", { className: 'line-clamp-2 break-words pt-0.5 text-xs text-ink/70', children: note })) : null] }), amount || date ? (_jsxs("span", { className: 'col-start-2 flex items-baseline gap-2 sm:col-start-3 sm:flex-col sm:items-end sm:gap-0.5', children: [amount ? (_jsx("span", { className: 'text-sm font-semibold tabular-nums text-ink', children: amount })) : null, date ? _jsx("span", { className: 'text-xs tabular-nums text-ink/55', children: date }) : null] })) : null] }), actions ? (_jsx("div", { className: 'flex w-full shrink-0 items-center justify-end gap-2 sm:w-auto', children: actions })) : null] }));
|
|
13
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
|
|
3
|
+
import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
|
|
4
|
+
export const PsSelectionBar = ({ children, className, clearLabel, countLabel, onClear, }) => (_jsxs("div", { "aria-live": 'polite', className: PallasoftClassNameService.merge('sticky bottom-3 z-10 flex min-w-0 flex-wrap items-center gap-x-3 gap-y-2 rounded-ps-card border border-line bg-surface px-4 py-2 shadow-lg', className), role: 'toolbar', children: [_jsx("span", { className: 'text-sm font-semibold text-ink', children: countLabel }), _jsx("div", { className: 'flex min-w-0 flex-1 flex-wrap items-center gap-2', children: children }), _jsx("button", { "aria-label": clearLabel, className: 'ps-focus-ring ps-control-square inline-flex shrink-0 items-center justify-center rounded-ps-control text-ink/65 transition-colors hover:bg-ink/5 hover:text-ink', onClick: onClear, title: clearLabel, type: 'button', children: _jsx(PsIcon, { "aria-hidden": 'true', className: 'size-4', iconKey: PsIconKey.Close }) })] }));
|
|
@@ -33,7 +33,7 @@ export const usePsOptimisticFetcher = ({ apply, fetcher, isHolding = false, isSu
|
|
|
33
33
|
settle.current = resolve;
|
|
34
34
|
sawBusy.current = false;
|
|
35
35
|
dataBefore.current = dataSeen.current;
|
|
36
|
-
void fetcher.submit(values, { method: 'post' });
|
|
36
|
+
void fetcher.submit(values, { defaultShouldRevalidate: false, method: 'post' });
|
|
37
37
|
}), [fetcher]);
|
|
38
38
|
const send = useCallback(async (queued) => {
|
|
39
39
|
for (const submission of merge === undefined ? queued : merge(queued)) {
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { useCallback } from 'react';
|
|
2
|
+
import { PsOptimisticSaveService } from '../services/ps-optimistic-save-service';
|
|
3
|
+
import { usePsOptimisticFetcher } from './use-ps-optimistic-fetcher';
|
|
4
|
+
export const usePsOptimisticSave = ({ fetcher, isSuccess, revalidator, rows, scopeId, spec, }) => {
|
|
5
|
+
const apply = useCallback((base, changes) => PsOptimisticSaveService.apply(base, changes, spec), [spec]);
|
|
6
|
+
const optimistic = usePsOptimisticFetcher({
|
|
7
|
+
apply,
|
|
8
|
+
fetcher,
|
|
9
|
+
isSuccess,
|
|
10
|
+
revalidator,
|
|
11
|
+
rows,
|
|
12
|
+
});
|
|
13
|
+
const { submit } = optimistic;
|
|
14
|
+
const save = useCallback((values, existingId) => submit(PsOptimisticSaveService.saveValues(spec, values, existingId, scopeId)), [scopeId, spec, submit]);
|
|
15
|
+
const remove = useCallback((ids) => submit(PsOptimisticSaveService.removeValues(spec, ids)), [spec, submit]);
|
|
16
|
+
return { remove, rows: optimistic.rows, save };
|
|
17
|
+
};
|
package/dist/esm/index.js
CHANGED
|
@@ -25,6 +25,8 @@ export * from './models/ps-sortable-list';
|
|
|
25
25
|
export * from './components/ps-empty-state';
|
|
26
26
|
export * from './components/ps-fab';
|
|
27
27
|
export * from './components/ps-infinite-scroll';
|
|
28
|
+
export * from './components/ps-document-row';
|
|
29
|
+
export * from './components/ps-selection-bar';
|
|
28
30
|
export * from './components/ps-list-tile';
|
|
29
31
|
export * from './components/ps-metric-card';
|
|
30
32
|
export * from './components/ps-pill';
|
|
@@ -76,6 +78,14 @@ export * from './interfaces/ps-change-queue-input';
|
|
|
76
78
|
export * from './interfaces/ps-change-queue-result';
|
|
77
79
|
export * from './hooks/use-ps-optimistic-rows';
|
|
78
80
|
export * from './hooks/use-ps-optimistic-fetcher';
|
|
81
|
+
export * from './hooks/use-ps-optimistic-save';
|
|
82
|
+
export * from './constants/ps-optimistic-config';
|
|
83
|
+
export * from './interfaces/ps-optimistic-save-input';
|
|
84
|
+
export * from './interfaces/ps-optimistic-save-result';
|
|
85
|
+
export * from './interfaces/ps-optimistic-save-spec';
|
|
86
|
+
export * from './interfaces/ps-revalidation-args';
|
|
87
|
+
export * from './services/ps-optimistic-save-service';
|
|
88
|
+
export * from './services/ps-client-param-revalidation-service';
|
|
79
89
|
export * from './interfaces/ps-fetcher-like';
|
|
80
90
|
export * from './interfaces/ps-fetcher-submit-options';
|
|
81
91
|
export * from './interfaces/ps-optimistic-fetcher-input';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class PsClientParamRevalidationService {
|
|
2
|
+
static isClientOnlyChange(currentUrl, nextUrl, clientParams) {
|
|
3
|
+
if (currentUrl.href === nextUrl.href)
|
|
4
|
+
return false;
|
|
5
|
+
const without = (url) => {
|
|
6
|
+
const params = new URLSearchParams(url.search);
|
|
7
|
+
for (const name of clientParams)
|
|
8
|
+
params.delete(name);
|
|
9
|
+
params.sort();
|
|
10
|
+
return `${url.pathname}?${params.toString()}`;
|
|
11
|
+
};
|
|
12
|
+
return without(currentUrl) === without(nextUrl);
|
|
13
|
+
}
|
|
14
|
+
static shouldRevalidate({ currentUrl, defaultShouldRevalidate, formMethod, nextUrl }, clientParams) {
|
|
15
|
+
if (formMethod === undefined &&
|
|
16
|
+
PsClientParamRevalidationService.isClientOnlyChange(currentUrl, nextUrl, clientParams))
|
|
17
|
+
return false;
|
|
18
|
+
return defaultShouldRevalidate;
|
|
19
|
+
}
|
|
20
|
+
static shouldRevalidateAfterAction(args, quietPaths) {
|
|
21
|
+
if (args.formMethod === undefined || args.formAction === undefined)
|
|
22
|
+
return args.defaultShouldRevalidate;
|
|
23
|
+
const path = new URL(args.formAction, args.currentUrl).pathname;
|
|
24
|
+
return quietPaths.includes(path) ? false : args.defaultShouldRevalidate;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { PsOptimisticConfig } from '../constants/ps-optimistic-config';
|
|
2
|
+
export class PsOptimisticSaveService {
|
|
3
|
+
static isPending(id) {
|
|
4
|
+
return id.startsWith(PsOptimisticConfig.pendingPrefix);
|
|
5
|
+
}
|
|
6
|
+
static pendingId() {
|
|
7
|
+
return `${PsOptimisticConfig.pendingPrefix}${crypto.randomUUID()}`;
|
|
8
|
+
}
|
|
9
|
+
static saveValues(spec, values, existingId, scopeId) {
|
|
10
|
+
const fields = spec.isFlat
|
|
11
|
+
? Object.fromEntries(Object.entries(values).map(([key, value]) => [key, String(value)]))
|
|
12
|
+
: { values: JSON.stringify(values) };
|
|
13
|
+
return {
|
|
14
|
+
...fields,
|
|
15
|
+
[spec.idField]: existingId,
|
|
16
|
+
intent: spec.intent,
|
|
17
|
+
pendingId: PsOptimisticSaveService.pendingId(),
|
|
18
|
+
scopeId,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
static removeValues(spec, ids) {
|
|
22
|
+
return spec.remove === undefined
|
|
23
|
+
? {}
|
|
24
|
+
: { [spec.remove.idsField]: JSON.stringify(ids), intent: spec.remove.intent };
|
|
25
|
+
}
|
|
26
|
+
static apply(rows, changes, spec) {
|
|
27
|
+
return changes.reduce((current, change) => {
|
|
28
|
+
if (change.intent === spec.intent)
|
|
29
|
+
return PsOptimisticSaveService.saved(current, change, spec);
|
|
30
|
+
if (spec.remove !== undefined && change.intent === spec.remove.intent)
|
|
31
|
+
return PsOptimisticSaveService.removed(current, change, spec, spec.remove.idsField);
|
|
32
|
+
return current;
|
|
33
|
+
}, rows);
|
|
34
|
+
}
|
|
35
|
+
static read(raw) {
|
|
36
|
+
if (raw === undefined)
|
|
37
|
+
return null;
|
|
38
|
+
try {
|
|
39
|
+
return JSON.parse(raw);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
static saved(rows, change, spec) {
|
|
46
|
+
const parsed = spec.isFlat ? change : PsOptimisticSaveService.read(change.values);
|
|
47
|
+
if (parsed === null || typeof parsed !== 'object')
|
|
48
|
+
return rows;
|
|
49
|
+
const existingId = change[spec.idField];
|
|
50
|
+
const id = existingId === '' ? change.pendingId : existingId;
|
|
51
|
+
const previous = rows.find((row) => spec.getId(row) === existingId);
|
|
52
|
+
const built = spec.build(parsed, id, previous === undefined ? null : previous, change.scopeId);
|
|
53
|
+
if (built === null)
|
|
54
|
+
return rows;
|
|
55
|
+
return previous === undefined
|
|
56
|
+
? [...rows, built]
|
|
57
|
+
: rows.map((row) => (spec.getId(row) === existingId ? built : row));
|
|
58
|
+
}
|
|
59
|
+
static removed(rows, change, spec, idsField) {
|
|
60
|
+
const parsed = PsOptimisticSaveService.read(change[idsField]);
|
|
61
|
+
const ids = new Set(Array.isArray(parsed) ? parsed : []);
|
|
62
|
+
return rows.filter((row) => !ids.has(spec.getId(row)));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -36,7 +36,7 @@ const usePsOptimisticFetcher = ({ apply, fetcher, isHolding = false, isSuccess,
|
|
|
36
36
|
settle.current = resolve;
|
|
37
37
|
sawBusy.current = false;
|
|
38
38
|
dataBefore.current = dataSeen.current;
|
|
39
|
-
void fetcher.submit(values, { method: 'post' });
|
|
39
|
+
void fetcher.submit(values, { defaultShouldRevalidate: false, method: 'post' });
|
|
40
40
|
}), [fetcher]);
|
|
41
41
|
const send = (0, react_1.useCallback)(async (queued) => {
|
|
42
42
|
for (const submission of merge === undefined ? queued : merge(queued)) {
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { PsOptimisticSaveInput } from '../interfaces/ps-optimistic-save-input';
|
|
2
|
+
import type { PsOptimisticSaveResult } from '../interfaces/ps-optimistic-save-result';
|
|
3
|
+
export declare const usePsOptimisticSave: <Row, Values, Data>({ fetcher, isSuccess, revalidator, rows, scopeId, spec, }: PsOptimisticSaveInput<Row, Values, Data>) => PsOptimisticSaveResult<Row, Values>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.usePsOptimisticSave = void 0;
|
|
4
|
+
const react_1 = require("react");
|
|
5
|
+
const ps_optimistic_save_service_1 = require("../services/ps-optimistic-save-service");
|
|
6
|
+
const use_ps_optimistic_fetcher_1 = require("./use-ps-optimistic-fetcher");
|
|
7
|
+
const usePsOptimisticSave = ({ fetcher, isSuccess, revalidator, rows, scopeId, spec, }) => {
|
|
8
|
+
const apply = (0, react_1.useCallback)((base, changes) => ps_optimistic_save_service_1.PsOptimisticSaveService.apply(base, changes, spec), [spec]);
|
|
9
|
+
const optimistic = (0, use_ps_optimistic_fetcher_1.usePsOptimisticFetcher)({
|
|
10
|
+
apply,
|
|
11
|
+
fetcher,
|
|
12
|
+
isSuccess,
|
|
13
|
+
revalidator,
|
|
14
|
+
rows,
|
|
15
|
+
});
|
|
16
|
+
const { submit } = optimistic;
|
|
17
|
+
const save = (0, react_1.useCallback)((values, existingId) => submit(ps_optimistic_save_service_1.PsOptimisticSaveService.saveValues(spec, values, existingId, scopeId)), [scopeId, spec, submit]);
|
|
18
|
+
const remove = (0, react_1.useCallback)((ids) => submit(ps_optimistic_save_service_1.PsOptimisticSaveService.removeValues(spec, ids)), [spec, submit]);
|
|
19
|
+
return { remove, rows: optimistic.rows, save };
|
|
20
|
+
};
|
|
21
|
+
exports.usePsOptimisticSave = usePsOptimisticSave;
|
package/dist/index.d.ts
CHANGED
|
@@ -25,6 +25,8 @@ export * from './models/ps-sortable-list';
|
|
|
25
25
|
export * from './components/ps-empty-state';
|
|
26
26
|
export * from './components/ps-fab';
|
|
27
27
|
export * from './components/ps-infinite-scroll';
|
|
28
|
+
export * from './components/ps-document-row';
|
|
29
|
+
export * from './components/ps-selection-bar';
|
|
28
30
|
export * from './components/ps-list-tile';
|
|
29
31
|
export * from './components/ps-metric-card';
|
|
30
32
|
export * from './components/ps-pill';
|
|
@@ -76,6 +78,14 @@ export * from './interfaces/ps-change-queue-input';
|
|
|
76
78
|
export * from './interfaces/ps-change-queue-result';
|
|
77
79
|
export * from './hooks/use-ps-optimistic-rows';
|
|
78
80
|
export * from './hooks/use-ps-optimistic-fetcher';
|
|
81
|
+
export * from './hooks/use-ps-optimistic-save';
|
|
82
|
+
export * from './constants/ps-optimistic-config';
|
|
83
|
+
export * from './interfaces/ps-optimistic-save-input';
|
|
84
|
+
export * from './interfaces/ps-optimistic-save-result';
|
|
85
|
+
export * from './interfaces/ps-optimistic-save-spec';
|
|
86
|
+
export * from './interfaces/ps-revalidation-args';
|
|
87
|
+
export * from './services/ps-optimistic-save-service';
|
|
88
|
+
export * from './services/ps-client-param-revalidation-service';
|
|
79
89
|
export * from './interfaces/ps-fetcher-like';
|
|
80
90
|
export * from './interfaces/ps-fetcher-submit-options';
|
|
81
91
|
export * from './interfaces/ps-optimistic-fetcher-input';
|
package/dist/index.js
CHANGED
|
@@ -41,6 +41,8 @@ __exportStar(require("./models/ps-sortable-list"), exports);
|
|
|
41
41
|
__exportStar(require("./components/ps-empty-state"), exports);
|
|
42
42
|
__exportStar(require("./components/ps-fab"), exports);
|
|
43
43
|
__exportStar(require("./components/ps-infinite-scroll"), exports);
|
|
44
|
+
__exportStar(require("./components/ps-document-row"), exports);
|
|
45
|
+
__exportStar(require("./components/ps-selection-bar"), exports);
|
|
44
46
|
__exportStar(require("./components/ps-list-tile"), exports);
|
|
45
47
|
__exportStar(require("./components/ps-metric-card"), exports);
|
|
46
48
|
__exportStar(require("./components/ps-pill"), exports);
|
|
@@ -92,6 +94,14 @@ __exportStar(require("./interfaces/ps-change-queue-input"), exports);
|
|
|
92
94
|
__exportStar(require("./interfaces/ps-change-queue-result"), exports);
|
|
93
95
|
__exportStar(require("./hooks/use-ps-optimistic-rows"), exports);
|
|
94
96
|
__exportStar(require("./hooks/use-ps-optimistic-fetcher"), exports);
|
|
97
|
+
__exportStar(require("./hooks/use-ps-optimistic-save"), exports);
|
|
98
|
+
__exportStar(require("./constants/ps-optimistic-config"), exports);
|
|
99
|
+
__exportStar(require("./interfaces/ps-optimistic-save-input"), exports);
|
|
100
|
+
__exportStar(require("./interfaces/ps-optimistic-save-result"), exports);
|
|
101
|
+
__exportStar(require("./interfaces/ps-optimistic-save-spec"), exports);
|
|
102
|
+
__exportStar(require("./interfaces/ps-revalidation-args"), exports);
|
|
103
|
+
__exportStar(require("./services/ps-optimistic-save-service"), exports);
|
|
104
|
+
__exportStar(require("./services/ps-client-param-revalidation-service"), exports);
|
|
95
105
|
__exportStar(require("./interfaces/ps-fetcher-like"), exports);
|
|
96
106
|
__exportStar(require("./interfaces/ps-fetcher-submit-options"), exports);
|
|
97
107
|
__exportStar(require("./interfaces/ps-optimistic-fetcher-input"), exports);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PsFetcherLike } from './ps-fetcher-like';
|
|
2
|
+
import type { PsOptimisticSaveSpec } from './ps-optimistic-save-spec';
|
|
3
|
+
import type { PsRevalidatorLike } from './ps-revalidator-like';
|
|
4
|
+
export interface PsOptimisticSaveInput<Row, Values, Data> {
|
|
5
|
+
fetcher: PsFetcherLike<Data>;
|
|
6
|
+
isSuccess: (data: Data | undefined) => boolean;
|
|
7
|
+
revalidator: PsRevalidatorLike;
|
|
8
|
+
rows: Row[];
|
|
9
|
+
scopeId: string;
|
|
10
|
+
spec: PsOptimisticSaveSpec<Row, Values>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface PsOptimisticSaveSpec<Row, Values> {
|
|
2
|
+
build: (values: Values, id: string, previous: Row | null, scopeId: string) => Row | null;
|
|
3
|
+
getId: (row: Row) => string;
|
|
4
|
+
idField: string;
|
|
5
|
+
intent: string;
|
|
6
|
+
isFlat: boolean;
|
|
7
|
+
remove?: {
|
|
8
|
+
idsField: string;
|
|
9
|
+
intent: string;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { PsRevalidationArgs } from '../interfaces/ps-revalidation-args';
|
|
2
|
+
export declare class PsClientParamRevalidationService {
|
|
3
|
+
static isClientOnlyChange(currentUrl: URL, nextUrl: URL, clientParams: readonly string[]): boolean;
|
|
4
|
+
static shouldRevalidate({ currentUrl, defaultShouldRevalidate, formMethod, nextUrl }: PsRevalidationArgs, clientParams: readonly string[]): boolean;
|
|
5
|
+
static shouldRevalidateAfterAction(args: PsRevalidationArgs, quietPaths: readonly string[]): boolean;
|
|
6
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PsClientParamRevalidationService = void 0;
|
|
4
|
+
class PsClientParamRevalidationService {
|
|
5
|
+
static isClientOnlyChange(currentUrl, nextUrl, clientParams) {
|
|
6
|
+
if (currentUrl.href === nextUrl.href)
|
|
7
|
+
return false;
|
|
8
|
+
const without = (url) => {
|
|
9
|
+
const params = new URLSearchParams(url.search);
|
|
10
|
+
for (const name of clientParams)
|
|
11
|
+
params.delete(name);
|
|
12
|
+
params.sort();
|
|
13
|
+
return `${url.pathname}?${params.toString()}`;
|
|
14
|
+
};
|
|
15
|
+
return without(currentUrl) === without(nextUrl);
|
|
16
|
+
}
|
|
17
|
+
static shouldRevalidate({ currentUrl, defaultShouldRevalidate, formMethod, nextUrl }, clientParams) {
|
|
18
|
+
if (formMethod === undefined &&
|
|
19
|
+
PsClientParamRevalidationService.isClientOnlyChange(currentUrl, nextUrl, clientParams))
|
|
20
|
+
return false;
|
|
21
|
+
return defaultShouldRevalidate;
|
|
22
|
+
}
|
|
23
|
+
static shouldRevalidateAfterAction(args, quietPaths) {
|
|
24
|
+
if (args.formMethod === undefined || args.formAction === undefined)
|
|
25
|
+
return args.defaultShouldRevalidate;
|
|
26
|
+
const path = new URL(args.formAction, args.currentUrl).pathname;
|
|
27
|
+
return quietPaths.includes(path) ? false : args.defaultShouldRevalidate;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
exports.PsClientParamRevalidationService = PsClientParamRevalidationService;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PsOptimisticSaveSpec } from '../interfaces/ps-optimistic-save-spec';
|
|
2
|
+
export declare class PsOptimisticSaveService {
|
|
3
|
+
static isPending(id: string): boolean;
|
|
4
|
+
static pendingId(): string;
|
|
5
|
+
static saveValues<Row, Values>(spec: PsOptimisticSaveSpec<Row, Values>, values: Values, existingId: string, scopeId: string): Record<string, string>;
|
|
6
|
+
static removeValues<Row, Values>(spec: PsOptimisticSaveSpec<Row, Values>, ids: string[]): Record<string, string>;
|
|
7
|
+
static apply<Row, Values>(rows: Row[], changes: Record<string, string>[], spec: PsOptimisticSaveSpec<Row, Values>): Row[];
|
|
8
|
+
private static read;
|
|
9
|
+
private static saved;
|
|
10
|
+
private static removed;
|
|
11
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PsOptimisticSaveService = void 0;
|
|
4
|
+
const ps_optimistic_config_1 = require("../constants/ps-optimistic-config");
|
|
5
|
+
class PsOptimisticSaveService {
|
|
6
|
+
static isPending(id) {
|
|
7
|
+
return id.startsWith(ps_optimistic_config_1.PsOptimisticConfig.pendingPrefix);
|
|
8
|
+
}
|
|
9
|
+
static pendingId() {
|
|
10
|
+
return `${ps_optimistic_config_1.PsOptimisticConfig.pendingPrefix}${crypto.randomUUID()}`;
|
|
11
|
+
}
|
|
12
|
+
static saveValues(spec, values, existingId, scopeId) {
|
|
13
|
+
const fields = spec.isFlat
|
|
14
|
+
? Object.fromEntries(Object.entries(values).map(([key, value]) => [key, String(value)]))
|
|
15
|
+
: { values: JSON.stringify(values) };
|
|
16
|
+
return {
|
|
17
|
+
...fields,
|
|
18
|
+
[spec.idField]: existingId,
|
|
19
|
+
intent: spec.intent,
|
|
20
|
+
pendingId: PsOptimisticSaveService.pendingId(),
|
|
21
|
+
scopeId,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
static removeValues(spec, ids) {
|
|
25
|
+
return spec.remove === undefined
|
|
26
|
+
? {}
|
|
27
|
+
: { [spec.remove.idsField]: JSON.stringify(ids), intent: spec.remove.intent };
|
|
28
|
+
}
|
|
29
|
+
static apply(rows, changes, spec) {
|
|
30
|
+
return changes.reduce((current, change) => {
|
|
31
|
+
if (change.intent === spec.intent)
|
|
32
|
+
return PsOptimisticSaveService.saved(current, change, spec);
|
|
33
|
+
if (spec.remove !== undefined && change.intent === spec.remove.intent)
|
|
34
|
+
return PsOptimisticSaveService.removed(current, change, spec, spec.remove.idsField);
|
|
35
|
+
return current;
|
|
36
|
+
}, rows);
|
|
37
|
+
}
|
|
38
|
+
static read(raw) {
|
|
39
|
+
if (raw === undefined)
|
|
40
|
+
return null;
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(raw);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
static saved(rows, change, spec) {
|
|
49
|
+
const parsed = spec.isFlat ? change : PsOptimisticSaveService.read(change.values);
|
|
50
|
+
if (parsed === null || typeof parsed !== 'object')
|
|
51
|
+
return rows;
|
|
52
|
+
const existingId = change[spec.idField];
|
|
53
|
+
const id = existingId === '' ? change.pendingId : existingId;
|
|
54
|
+
const previous = rows.find((row) => spec.getId(row) === existingId);
|
|
55
|
+
const built = spec.build(parsed, id, previous === undefined ? null : previous, change.scopeId);
|
|
56
|
+
if (built === null)
|
|
57
|
+
return rows;
|
|
58
|
+
return previous === undefined
|
|
59
|
+
? [...rows, built]
|
|
60
|
+
: rows.map((row) => (spec.getId(row) === existingId ? built : row));
|
|
61
|
+
}
|
|
62
|
+
static removed(rows, change, spec, idsField) {
|
|
63
|
+
const parsed = PsOptimisticSaveService.read(change[idsField]);
|
|
64
|
+
const ids = new Set(Array.isArray(parsed) ? parsed : []);
|
|
65
|
+
return rows.filter((row) => !ids.has(spec.getId(row)));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.PsOptimisticSaveService = PsOptimisticSaveService;
|