@pallasoft/ps-core-components 2.28.0 → 2.30.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 (44) hide show
  1. package/dist/components/ps-rating-display.d.ts +9 -0
  2. package/dist/components/ps-rating-display.js +12 -0
  3. package/dist/components/ps-sortable-list.d.ts +1 -1
  4. package/dist/components/ps-sortable-list.js +2 -2
  5. package/dist/constants/ps-optimistic-config.d.ts +3 -0
  6. package/dist/constants/ps-optimistic-config.js +6 -0
  7. package/dist/constants/ps-rating-display-config.d.ts +10 -0
  8. package/dist/constants/ps-rating-display-config.js +14 -0
  9. package/dist/constants/ps-rating-display-size.d.ts +5 -0
  10. package/dist/constants/ps-rating-display-size.js +9 -0
  11. package/dist/esm/components/ps-rating-display.js +8 -0
  12. package/dist/esm/components/ps-sortable-list.js +3 -3
  13. package/dist/esm/constants/ps-optimistic-config.js +3 -0
  14. package/dist/esm/constants/ps-rating-display-config.js +11 -0
  15. package/dist/esm/constants/ps-rating-display-size.js +6 -0
  16. package/dist/esm/hooks/use-ps-optimistic-fetcher.js +1 -1
  17. package/dist/esm/hooks/use-ps-optimistic-save.js +17 -0
  18. package/dist/esm/index.js +10 -0
  19. package/dist/esm/interfaces/ps-optimistic-save-input.js +1 -0
  20. package/dist/esm/interfaces/ps-optimistic-save-result.js +1 -0
  21. package/dist/esm/interfaces/ps-optimistic-save-spec.js +1 -0
  22. package/dist/esm/interfaces/ps-revalidation-args.js +1 -0
  23. package/dist/esm/services/ps-client-param-revalidation-service.js +26 -0
  24. package/dist/esm/services/ps-optimistic-save-service.js +64 -0
  25. package/dist/hooks/use-ps-optimistic-fetcher.js +1 -1
  26. package/dist/hooks/use-ps-optimistic-save.d.ts +3 -0
  27. package/dist/hooks/use-ps-optimistic-save.js +21 -0
  28. package/dist/index.d.ts +10 -0
  29. package/dist/index.js +10 -0
  30. package/dist/interfaces/ps-fetcher-submit-options.d.ts +1 -0
  31. package/dist/interfaces/ps-optimistic-save-input.d.ts +11 -0
  32. package/dist/interfaces/ps-optimistic-save-input.js +2 -0
  33. package/dist/interfaces/ps-optimistic-save-result.d.ts +5 -0
  34. package/dist/interfaces/ps-optimistic-save-result.js +2 -0
  35. package/dist/interfaces/ps-optimistic-save-spec.d.ts +11 -0
  36. package/dist/interfaces/ps-optimistic-save-spec.js +2 -0
  37. package/dist/interfaces/ps-revalidation-args.d.ts +7 -0
  38. package/dist/interfaces/ps-revalidation-args.js +2 -0
  39. package/dist/models/ps-sortable-list.d.ts +1 -0
  40. package/dist/services/ps-client-param-revalidation-service.d.ts +6 -0
  41. package/dist/services/ps-client-param-revalidation-service.js +30 -0
  42. package/dist/services/ps-optimistic-save-service.d.ts +11 -0
  43. package/dist/services/ps-optimistic-save-service.js +68 -0
  44. package/package.json +1 -1
@@ -0,0 +1,9 @@
1
+ import { PsRatingDisplaySize } from '../constants/ps-rating-display-size';
2
+ export interface PsRatingDisplayProps {
3
+ className?: string;
4
+ label: string;
5
+ max?: number;
6
+ score: number;
7
+ size?: PsRatingDisplaySize;
8
+ }
9
+ export declare const PsRatingDisplay: ({ className, label, max, score, size, }: PsRatingDisplayProps) => import("react").JSX.Element;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsRatingDisplay = 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 ps_rating_display_config_1 = require("../constants/ps-rating-display-config");
8
+ const ps_rating_display_size_1 = require("../constants/ps-rating-display-size");
9
+ const PsRatingDisplay = ({ className, label, max = ps_rating_display_config_1.PsRatingDisplayConfig.defaultMax, score, size = ps_rating_display_size_1.PsRatingDisplaySize.Medium, }) => ((0, jsx_runtime_1.jsx)("div", { "aria-label": label, className: ps_design_system_1.PallasoftClassNameService.merge('flex items-center gap-1', className), role: 'img', children: Array.from({ length: max }, (_, index) => index + 1).map((value) => ((0, jsx_runtime_1.jsx)(ps_icons_1.PsIcon, { className: ps_design_system_1.PallasoftClassNameService.merge(ps_rating_display_config_1.PsRatingDisplayConfig.iconClassNames[size], value <= score
10
+ ? ps_rating_display_config_1.PsRatingDisplayConfig.filledClassName
11
+ : ps_rating_display_config_1.PsRatingDisplayConfig.emptyClassName), iconKey: ps_icons_1.PsIconKey.Star }, value))) }));
12
+ exports.PsRatingDisplay = PsRatingDisplay;
@@ -19,4 +19,4 @@ export interface PsSortableListRowProps<TItem extends PsSortableListItem> {
19
19
  item: TItem;
20
20
  renderItem: (options: PsSortableListRenderOptions<TItem>) => JSX.Element | null;
21
21
  }
22
- export declare const PsSortableList: <TItem extends PsSortableListItem>({ announcements: texts, className, getHandleLabel, getItemClassName, handleClassName, handleIcon, instructions, role, items, onMove, renderItem, }: PsSortableListProps<TItem>) => JSX.Element;
22
+ export declare const PsSortableList: <TItem extends PsSortableListItem>({ announcements: texts, className, getHandleLabel, getItemClassName, handleClassName, handleIcon, instructions, isHorizontal, role, items, onMove, renderItem, }: PsSortableListProps<TItem>) => JSX.Element;
@@ -30,7 +30,7 @@ const PsSortableListRow = ({ className, handleClassName, handleIcon, getHandleLa
30
30
  transition: isReduced ? undefined : transition,
31
31
  }, children: rendered }));
32
32
  };
33
- const PsSortableList = ({ announcements: texts, className, getHandleLabel, getItemClassName, handleClassName, handleIcon, instructions, role, items, onMove, renderItem, }) => {
33
+ const PsSortableList = ({ announcements: texts, className, getHandleLabel, getItemClassName, handleClassName, handleIcon, instructions, isHorizontal = false, role, items, onMove, renderItem, }) => {
34
34
  const [activeId, setActiveId] = (0, react_1.useState)(null);
35
35
  const sensors = (0, use_ps_drag_sensors_1.usePsDragSensors)();
36
36
  const activeIndex = items.findIndex((item) => item.id === activeId);
@@ -66,7 +66,7 @@ const PsSortableList = ({ announcements: texts, className, getHandleLabel, getIt
66
66
  onMove(String(active.id), from, to);
67
67
  };
68
68
  const activeGroup = activeItem === null ? undefined : activeItem.groupKey;
69
- return ((0, jsx_runtime_1.jsxs)(core_1.DndContext, { accessibility: { announcements, screenReaderInstructions: { draggable: instructions } }, collisionDetection: core_1.closestCenter, onDragCancel: () => setActiveId(null), onDragEnd: handleDragEnd, onDragStart: ({ active }) => setActiveId(String(active.id)), sensors: sensors, children: [(0, jsx_runtime_1.jsx)(sortable_1.SortableContext, { items: items.map((item) => item.id), strategy: sortable_1.verticalListSortingStrategy, children: (0, jsx_runtime_1.jsx)("div", { className: className, role: role, children: items.map((item, index) => ((0, jsx_runtime_1.jsx)(PsSortableListRow, { className: itemClassName(item), getHandleLabel: getHandleLabel, handleClassName: handleClassName, handleIcon: handleIcon, index: index, isDisabled: activeItem !== null && item.groupKey !== activeGroup, item: item, renderItem: renderItem }, item.id))) }) }), (0, jsx_runtime_1.jsx)(ps_drag_overlay_1.PsDragOverlay, { className: ps_drag_config_1.PsDragClasses.lifted, frames: ps_drag_config_1.PsDragListLiftFrames, children: activeItem !== null
69
+ return ((0, jsx_runtime_1.jsxs)(core_1.DndContext, { accessibility: { announcements, screenReaderInstructions: { draggable: instructions } }, collisionDetection: core_1.closestCenter, onDragCancel: () => setActiveId(null), onDragEnd: handleDragEnd, onDragStart: ({ active }) => setActiveId(String(active.id)), sensors: sensors, children: [(0, jsx_runtime_1.jsx)(sortable_1.SortableContext, { items: items.map((item) => item.id), strategy: isHorizontal ? sortable_1.horizontalListSortingStrategy : sortable_1.verticalListSortingStrategy, children: (0, jsx_runtime_1.jsx)("div", { className: className, role: role, children: items.map((item, index) => ((0, jsx_runtime_1.jsx)(PsSortableListRow, { className: itemClassName(item), getHandleLabel: getHandleLabel, handleClassName: handleClassName, handleIcon: handleIcon, index: index, isDisabled: activeItem !== null && item.groupKey !== activeGroup, item: item, renderItem: renderItem }, item.id))) }) }), (0, jsx_runtime_1.jsx)(ps_drag_overlay_1.PsDragOverlay, { className: ps_drag_config_1.PsDragClasses.lifted, frames: ps_drag_config_1.PsDragListLiftFrames, children: activeItem !== null
70
70
  ? renderItem({
71
71
  handle: ((0, jsx_runtime_1.jsx)(PsSortableListHandle, { className: handleClassName, icon: handleIcon, label: getHandleLabel(activeItem.label) })),
72
72
  index: activeIndex,
@@ -0,0 +1,3 @@
1
+ export declare const PsOptimisticConfig: {
2
+ readonly pendingPrefix: "pending-";
3
+ };
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsOptimisticConfig = void 0;
4
+ exports.PsOptimisticConfig = {
5
+ pendingPrefix: 'pending-',
6
+ };
@@ -0,0 +1,10 @@
1
+ export declare const PsRatingDisplayConfig: {
2
+ readonly defaultMax: 5;
3
+ readonly filledClassName: "fill-current text-primary";
4
+ readonly emptyClassName: "text-ink/30 dark:text-white/30";
5
+ readonly iconClassNames: {
6
+ readonly large: "h-8 w-8";
7
+ readonly medium: "h-6 w-6";
8
+ readonly small: "h-4 w-4";
9
+ };
10
+ };
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsRatingDisplayConfig = void 0;
4
+ const ps_rating_display_size_1 = require("./ps-rating-display-size");
5
+ exports.PsRatingDisplayConfig = {
6
+ defaultMax: 5,
7
+ filledClassName: 'fill-current text-primary',
8
+ emptyClassName: 'text-ink/30 dark:text-white/30',
9
+ iconClassNames: {
10
+ [ps_rating_display_size_1.PsRatingDisplaySize.Large]: 'h-8 w-8',
11
+ [ps_rating_display_size_1.PsRatingDisplaySize.Medium]: 'h-6 w-6',
12
+ [ps_rating_display_size_1.PsRatingDisplaySize.Small]: 'h-4 w-4',
13
+ },
14
+ };
@@ -0,0 +1,5 @@
1
+ export declare enum PsRatingDisplaySize {
2
+ Large = "large",
3
+ Medium = "medium",
4
+ Small = "small"
5
+ }
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PsRatingDisplaySize = void 0;
4
+ var PsRatingDisplaySize;
5
+ (function (PsRatingDisplaySize) {
6
+ PsRatingDisplaySize["Large"] = "large";
7
+ PsRatingDisplaySize["Medium"] = "medium";
8
+ PsRatingDisplaySize["Small"] = "small";
9
+ })(PsRatingDisplaySize || (exports.PsRatingDisplaySize = PsRatingDisplaySize = {}));
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { PallasoftClassNameService } from '@pallasoft/ps-design-system';
3
+ import { PsIcon, PsIconKey } from '@pallasoft/ps-icons';
4
+ import { PsRatingDisplayConfig } from '../constants/ps-rating-display-config';
5
+ import { PsRatingDisplaySize } from '../constants/ps-rating-display-size';
6
+ export const PsRatingDisplay = ({ className, label, max = PsRatingDisplayConfig.defaultMax, score, size = PsRatingDisplaySize.Medium, }) => (_jsx("div", { "aria-label": label, className: PallasoftClassNameService.merge('flex items-center gap-1', className), role: 'img', children: Array.from({ length: max }, (_, index) => index + 1).map((value) => (_jsx(PsIcon, { className: PallasoftClassNameService.merge(PsRatingDisplayConfig.iconClassNames[size], value <= score
7
+ ? PsRatingDisplayConfig.filledClassName
8
+ : PsRatingDisplayConfig.emptyClassName), iconKey: PsIconKey.Star }, value))) }));
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useState } from 'react';
3
3
  import { DndContext, closestCenter, } from '@dnd-kit/core';
4
- import { SortableContext, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable';
4
+ import { SortableContext, horizontalListSortingStrategy, useSortable, verticalListSortingStrategy, } from '@dnd-kit/sortable';
5
5
  import { CSS } from '@dnd-kit/utilities';
6
6
  import { PsDragClasses, PsDragConfig, PsDragListLiftFrames } from '../constants/ps-drag-config';
7
7
  import { usePsDragSensors } from '../hooks/use-ps-drag-sensors';
@@ -27,7 +27,7 @@ const PsSortableListRow = ({ className, handleClassName, handleIcon, getHandleLa
27
27
  transition: isReduced ? undefined : transition,
28
28
  }, children: rendered }));
29
29
  };
30
- export const PsSortableList = ({ announcements: texts, className, getHandleLabel, getItemClassName, handleClassName, handleIcon, instructions, role, items, onMove, renderItem, }) => {
30
+ export const PsSortableList = ({ announcements: texts, className, getHandleLabel, getItemClassName, handleClassName, handleIcon, instructions, isHorizontal = false, role, items, onMove, renderItem, }) => {
31
31
  const [activeId, setActiveId] = useState(null);
32
32
  const sensors = usePsDragSensors();
33
33
  const activeIndex = items.findIndex((item) => item.id === activeId);
@@ -63,7 +63,7 @@ export const PsSortableList = ({ announcements: texts, className, getHandleLabel
63
63
  onMove(String(active.id), from, to);
64
64
  };
65
65
  const activeGroup = activeItem === null ? undefined : activeItem.groupKey;
66
- return (_jsxs(DndContext, { accessibility: { announcements, screenReaderInstructions: { draggable: instructions } }, collisionDetection: closestCenter, onDragCancel: () => setActiveId(null), onDragEnd: handleDragEnd, onDragStart: ({ active }) => setActiveId(String(active.id)), sensors: sensors, children: [_jsx(SortableContext, { items: items.map((item) => item.id), strategy: verticalListSortingStrategy, children: _jsx("div", { className: className, role: role, children: items.map((item, index) => (_jsx(PsSortableListRow, { className: itemClassName(item), getHandleLabel: getHandleLabel, handleClassName: handleClassName, handleIcon: handleIcon, index: index, isDisabled: activeItem !== null && item.groupKey !== activeGroup, item: item, renderItem: renderItem }, item.id))) }) }), _jsx(PsDragOverlay, { className: PsDragClasses.lifted, frames: PsDragListLiftFrames, children: activeItem !== null
66
+ return (_jsxs(DndContext, { accessibility: { announcements, screenReaderInstructions: { draggable: instructions } }, collisionDetection: closestCenter, onDragCancel: () => setActiveId(null), onDragEnd: handleDragEnd, onDragStart: ({ active }) => setActiveId(String(active.id)), sensors: sensors, children: [_jsx(SortableContext, { items: items.map((item) => item.id), strategy: isHorizontal ? horizontalListSortingStrategy : verticalListSortingStrategy, children: _jsx("div", { className: className, role: role, children: items.map((item, index) => (_jsx(PsSortableListRow, { className: itemClassName(item), getHandleLabel: getHandleLabel, handleClassName: handleClassName, handleIcon: handleIcon, index: index, isDisabled: activeItem !== null && item.groupKey !== activeGroup, item: item, renderItem: renderItem }, item.id))) }) }), _jsx(PsDragOverlay, { className: PsDragClasses.lifted, frames: PsDragListLiftFrames, children: activeItem !== null
67
67
  ? renderItem({
68
68
  handle: (_jsx(PsSortableListHandle, { className: handleClassName, icon: handleIcon, label: getHandleLabel(activeItem.label) })),
69
69
  index: activeIndex,
@@ -0,0 +1,3 @@
1
+ export const PsOptimisticConfig = {
2
+ pendingPrefix: 'pending-',
3
+ };
@@ -0,0 +1,11 @@
1
+ import { PsRatingDisplaySize } from './ps-rating-display-size';
2
+ export const PsRatingDisplayConfig = {
3
+ defaultMax: 5,
4
+ filledClassName: 'fill-current text-primary',
5
+ emptyClassName: 'text-ink/30 dark:text-white/30',
6
+ iconClassNames: {
7
+ [PsRatingDisplaySize.Large]: 'h-8 w-8',
8
+ [PsRatingDisplaySize.Medium]: 'h-6 w-6',
9
+ [PsRatingDisplaySize.Small]: 'h-4 w-4',
10
+ },
11
+ };
@@ -0,0 +1,6 @@
1
+ export var PsRatingDisplaySize;
2
+ (function (PsRatingDisplaySize) {
3
+ PsRatingDisplaySize["Large"] = "large";
4
+ PsRatingDisplaySize["Medium"] = "medium";
5
+ PsRatingDisplaySize["Small"] = "small";
6
+ })(PsRatingDisplaySize || (PsRatingDisplaySize = {}));
@@ -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
@@ -76,6 +76,14 @@ export * from './interfaces/ps-change-queue-input';
76
76
  export * from './interfaces/ps-change-queue-result';
77
77
  export * from './hooks/use-ps-optimistic-rows';
78
78
  export * from './hooks/use-ps-optimistic-fetcher';
79
+ export * from './hooks/use-ps-optimistic-save';
80
+ export * from './constants/ps-optimistic-config';
81
+ export * from './interfaces/ps-optimistic-save-input';
82
+ export * from './interfaces/ps-optimistic-save-result';
83
+ export * from './interfaces/ps-optimistic-save-spec';
84
+ export * from './interfaces/ps-revalidation-args';
85
+ export * from './services/ps-optimistic-save-service';
86
+ export * from './services/ps-client-param-revalidation-service';
79
87
  export * from './interfaces/ps-fetcher-like';
80
88
  export * from './interfaces/ps-fetcher-submit-options';
81
89
  export * from './interfaces/ps-optimistic-fetcher-input';
@@ -154,3 +162,5 @@ export * from './hooks/use-ps-drag-sensors';
154
162
  export * from './hooks/use-ps-reduced-motion';
155
163
  export * from './components/ps-divider';
156
164
  export * from './models/ps-divider-orientation';
165
+ export * from './components/ps-rating-display';
166
+ export * from './constants/ps-rating-display-size';
@@ -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
@@ -76,6 +76,14 @@ export * from './interfaces/ps-change-queue-input';
76
76
  export * from './interfaces/ps-change-queue-result';
77
77
  export * from './hooks/use-ps-optimistic-rows';
78
78
  export * from './hooks/use-ps-optimistic-fetcher';
79
+ export * from './hooks/use-ps-optimistic-save';
80
+ export * from './constants/ps-optimistic-config';
81
+ export * from './interfaces/ps-optimistic-save-input';
82
+ export * from './interfaces/ps-optimistic-save-result';
83
+ export * from './interfaces/ps-optimistic-save-spec';
84
+ export * from './interfaces/ps-revalidation-args';
85
+ export * from './services/ps-optimistic-save-service';
86
+ export * from './services/ps-client-param-revalidation-service';
79
87
  export * from './interfaces/ps-fetcher-like';
80
88
  export * from './interfaces/ps-fetcher-submit-options';
81
89
  export * from './interfaces/ps-optimistic-fetcher-input';
@@ -154,3 +162,5 @@ export * from './hooks/use-ps-drag-sensors';
154
162
  export * from './hooks/use-ps-reduced-motion';
155
163
  export * from './components/ps-divider';
156
164
  export * from './models/ps-divider-orientation';
165
+ export * from './components/ps-rating-display';
166
+ export * from './constants/ps-rating-display-size';
package/dist/index.js CHANGED
@@ -92,6 +92,14 @@ __exportStar(require("./interfaces/ps-change-queue-input"), exports);
92
92
  __exportStar(require("./interfaces/ps-change-queue-result"), exports);
93
93
  __exportStar(require("./hooks/use-ps-optimistic-rows"), exports);
94
94
  __exportStar(require("./hooks/use-ps-optimistic-fetcher"), exports);
95
+ __exportStar(require("./hooks/use-ps-optimistic-save"), exports);
96
+ __exportStar(require("./constants/ps-optimistic-config"), exports);
97
+ __exportStar(require("./interfaces/ps-optimistic-save-input"), exports);
98
+ __exportStar(require("./interfaces/ps-optimistic-save-result"), exports);
99
+ __exportStar(require("./interfaces/ps-optimistic-save-spec"), exports);
100
+ __exportStar(require("./interfaces/ps-revalidation-args"), exports);
101
+ __exportStar(require("./services/ps-optimistic-save-service"), exports);
102
+ __exportStar(require("./services/ps-client-param-revalidation-service"), exports);
95
103
  __exportStar(require("./interfaces/ps-fetcher-like"), exports);
96
104
  __exportStar(require("./interfaces/ps-fetcher-submit-options"), exports);
97
105
  __exportStar(require("./interfaces/ps-optimistic-fetcher-input"), exports);
@@ -170,3 +178,5 @@ __exportStar(require("./hooks/use-ps-drag-sensors"), exports);
170
178
  __exportStar(require("./hooks/use-ps-reduced-motion"), exports);
171
179
  __exportStar(require("./components/ps-divider"), exports);
172
180
  __exportStar(require("./models/ps-divider-orientation"), exports);
181
+ __exportStar(require("./components/ps-rating-display"), exports);
182
+ __exportStar(require("./constants/ps-rating-display-size"), exports);
@@ -1,3 +1,4 @@
1
1
  export interface PsFetcherSubmitOptions {
2
+ defaultShouldRevalidate: boolean;
2
3
  method: 'post';
3
4
  }
@@ -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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,5 @@
1
+ export interface PsOptimisticSaveResult<Row, Values> {
2
+ remove: (ids: string[]) => void;
3
+ rows: Row[];
4
+ save: (values: Values, existingId: string) => void;
5
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,7 @@
1
+ export interface PsRevalidationArgs {
2
+ currentUrl: URL;
3
+ defaultShouldRevalidate: boolean;
4
+ formAction?: string;
5
+ formMethod?: string;
6
+ nextUrl: URL;
7
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -27,6 +27,7 @@ export interface PsSortableListProps<TItem extends PsSortableListItem> {
27
27
  handleIcon?: JSX.Element;
28
28
  instructions: string;
29
29
  items: TItem[];
30
+ isHorizontal?: boolean;
30
31
  onMove: (itemId: string, fromIndex: number, toIndex: number) => void;
31
32
  renderItem: (options: PsSortableListRenderOptions<TItem>) => JSX.Element | null;
32
33
  }
@@ -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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pallasoft/ps-core-components",
3
- "version": "2.28.0",
3
+ "version": "2.30.0",
4
4
  "private": false,
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/esm/index.js",