@loj-lang/rdsl-runtime 0.5.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 (71) hide show
  1. package/README.md +19 -0
  2. package/dist/components/Badge.d.ts +6 -0
  3. package/dist/components/Badge.d.ts.map +1 -0
  4. package/dist/components/Badge.js +5 -0
  5. package/dist/components/ConfirmDialog.d.ts +8 -0
  6. package/dist/components/ConfirmDialog.d.ts.map +1 -0
  7. package/dist/components/ConfirmDialog.js +11 -0
  8. package/dist/components/DataTable.d.ts +34 -0
  9. package/dist/components/DataTable.d.ts.map +1 -0
  10. package/dist/components/DataTable.js +58 -0
  11. package/dist/components/FilterBar.d.ts +13 -0
  12. package/dist/components/FilterBar.d.ts.map +1 -0
  13. package/dist/components/FilterBar.js +41 -0
  14. package/dist/components/FormField.d.ts +22 -0
  15. package/dist/components/FormField.d.ts.map +1 -0
  16. package/dist/components/FormField.js +66 -0
  17. package/dist/components/GroupedDataTable.d.ts +26 -0
  18. package/dist/components/GroupedDataTable.d.ts.map +1 -0
  19. package/dist/components/GroupedDataTable.js +67 -0
  20. package/dist/components/Pagination.d.ts +7 -0
  21. package/dist/components/Pagination.d.ts.map +1 -0
  22. package/dist/components/Pagination.js +12 -0
  23. package/dist/components/PivotDataTable.d.ts +25 -0
  24. package/dist/components/PivotDataTable.d.ts.map +1 -0
  25. package/dist/components/PivotDataTable.js +72 -0
  26. package/dist/components/Tag.d.ts +6 -0
  27. package/dist/components/Tag.d.ts.map +1 -0
  28. package/dist/components/Tag.js +5 -0
  29. package/dist/components/WorkflowSummary.d.ts +5 -0
  30. package/dist/components/WorkflowSummary.d.ts.map +1 -0
  31. package/dist/components/WorkflowSummary.js +17 -0
  32. package/dist/hooks/navigation.d.ts +20 -0
  33. package/dist/hooks/navigation.d.ts.map +1 -0
  34. package/dist/hooks/navigation.js +135 -0
  35. package/dist/hooks/resourceClient.d.ts +36 -0
  36. package/dist/hooks/resourceClient.d.ts.map +1 -0
  37. package/dist/hooks/resourceClient.js +259 -0
  38. package/dist/hooks/resourceStore.d.ts +25 -0
  39. package/dist/hooks/resourceStore.d.ts.map +1 -0
  40. package/dist/hooks/resourceStore.js +164 -0
  41. package/dist/hooks/resourceTarget.d.ts +2 -0
  42. package/dist/hooks/resourceTarget.d.ts.map +1 -0
  43. package/dist/hooks/resourceTarget.js +22 -0
  44. package/dist/hooks/useAuth.d.ts +16 -0
  45. package/dist/hooks/useAuth.d.ts.map +1 -0
  46. package/dist/hooks/useAuth.js +20 -0
  47. package/dist/hooks/useCollectionView.d.ts +28 -0
  48. package/dist/hooks/useCollectionView.d.ts.map +1 -0
  49. package/dist/hooks/useCollectionView.js +73 -0
  50. package/dist/hooks/useDocumentMetadata.d.ts +13 -0
  51. package/dist/hooks/useDocumentMetadata.d.ts.map +1 -0
  52. package/dist/hooks/useDocumentMetadata.js +111 -0
  53. package/dist/hooks/useGroupedCollectionView.d.ts +26 -0
  54. package/dist/hooks/useGroupedCollectionView.d.ts.map +1 -0
  55. package/dist/hooks/useGroupedCollectionView.js +82 -0
  56. package/dist/hooks/useReadModel.d.ts +16 -0
  57. package/dist/hooks/useReadModel.d.ts.map +1 -0
  58. package/dist/hooks/useReadModel.js +104 -0
  59. package/dist/hooks/useResource.d.ts +36 -0
  60. package/dist/hooks/useResource.d.ts.map +1 -0
  61. package/dist/hooks/useResource.js +69 -0
  62. package/dist/hooks/useToast.d.ts +18 -0
  63. package/dist/hooks/useToast.d.ts.map +1 -0
  64. package/dist/hooks/useToast.js +31 -0
  65. package/dist/index.d.ts +42 -0
  66. package/dist/index.d.ts.map +1 -0
  67. package/dist/index.js +22 -0
  68. package/dist/policies/can.d.ts +15 -0
  69. package/dist/policies/can.d.ts.map +1 -0
  70. package/dist/policies/can.js +160 -0
  71. package/package.json +55 -0
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ const AuthContext = React.createContext(undefined);
3
+ function readGlobalAuth() {
4
+ if (typeof window === 'undefined') {
5
+ return { currentUser: null };
6
+ }
7
+ const value = window.__RDSL_AUTH__;
8
+ if (value && typeof value === 'object') {
9
+ return {
10
+ currentUser: value.currentUser ?? null,
11
+ };
12
+ }
13
+ return { currentUser: null };
14
+ }
15
+ export function AuthProvider({ value, children }) {
16
+ return React.createElement(AuthContext.Provider, { value }, children);
17
+ }
18
+ export function useAuth() {
19
+ return React.useContext(AuthContext) ?? readGlobalAuth();
20
+ }
@@ -0,0 +1,28 @@
1
+ export interface CollectionPaginationState {
2
+ page: number;
3
+ totalPages: number;
4
+ }
5
+ export interface CollectionSortState {
6
+ field: string;
7
+ direction: 'asc' | 'desc';
8
+ }
9
+ export interface UseCollectionViewOptions {
10
+ pageSize?: number;
11
+ paginate?: boolean;
12
+ }
13
+ export interface UseCollectionViewResult<T extends {
14
+ id: string;
15
+ }> {
16
+ data: T[];
17
+ allData: T[];
18
+ filters: Record<string, string>;
19
+ setFilters: (next: Record<string, string>) => void;
20
+ sort: CollectionSortState | null;
21
+ setSort: (next: CollectionSortState | null) => void;
22
+ pagination: CollectionPaginationState;
23
+ setPagination: (page: number) => void;
24
+ }
25
+ export declare function useCollectionView<T extends {
26
+ id: string;
27
+ }>(items: readonly T[], options?: UseCollectionViewOptions): UseCollectionViewResult<T>;
28
+ //# sourceMappingURL=useCollectionView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCollectionView.d.ts","sourceRoot":"","sources":["../../src/hooks/useCollectionView.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,uBAAuB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE;IAC/D,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,OAAO,EAAE,CAAC,EAAE,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;IACnD,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACjC,OAAO,EAAE,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,KAAK,IAAI,CAAC;IACpD,UAAU,EAAE,yBAAyB,CAAC;IACtC,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAoCD,wBAAgB,iBAAiB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EACxD,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,OAAO,GAAE,wBAA6B,GACrC,uBAAuB,CAAC,CAAC,CAAC,CAoD5B"}
@@ -0,0 +1,73 @@
1
+ import React from 'react';
2
+ function compareValues(left, right) {
3
+ if (typeof left === 'number' && typeof right === 'number') {
4
+ return left - right;
5
+ }
6
+ return String(left ?? '').localeCompare(String(right ?? ''));
7
+ }
8
+ function filterItems(items, filters) {
9
+ return items.filter((item) => Object.entries(filters).every(([key, rawFilter]) => {
10
+ const filterValue = rawFilter.trim();
11
+ if (filterValue === '')
12
+ return true;
13
+ const value = item[key];
14
+ return String(value ?? '').toLowerCase().includes(filterValue.toLowerCase());
15
+ }));
16
+ }
17
+ function sortItems(items, sort) {
18
+ if (!sort)
19
+ return [...items];
20
+ const sorted = [...items];
21
+ sorted.sort((left, right) => {
22
+ const comparison = compareValues(left[sort.field], right[sort.field]);
23
+ return sort.direction === 'asc' ? comparison : comparison * -1;
24
+ });
25
+ return sorted;
26
+ }
27
+ export function useCollectionView(items, options = {}) {
28
+ const pageSize = options.pageSize ?? 20;
29
+ const paginate = options.paginate ?? true;
30
+ const [filters, setFiltersState] = React.useState({});
31
+ const [sort, setSortState] = React.useState(null);
32
+ const [page, setPage] = React.useState(1);
33
+ const filteredItems = React.useMemo(() => filterItems(items, filters), [items, filters]);
34
+ const sortedItems = React.useMemo(() => sortItems(filteredItems, sort), [filteredItems, sort]);
35
+ const totalPages = paginate ? Math.max(1, Math.ceil(sortedItems.length / pageSize)) : 1;
36
+ const currentPage = paginate ? Math.min(page, totalPages) : 1;
37
+ const data = React.useMemo(() => {
38
+ if (!paginate) {
39
+ return sortedItems;
40
+ }
41
+ const start = (currentPage - 1) * pageSize;
42
+ return sortedItems.slice(start, start + pageSize);
43
+ }, [currentPage, pageSize, paginate, sortedItems]);
44
+ React.useEffect(() => {
45
+ if (currentPage !== page) {
46
+ setPage(currentPage);
47
+ }
48
+ }, [currentPage, page]);
49
+ const setFilters = React.useCallback((next) => {
50
+ setFiltersState(next);
51
+ setPage(1);
52
+ }, []);
53
+ const setSort = React.useCallback((next) => {
54
+ setSortState(next);
55
+ setPage(1);
56
+ }, []);
57
+ const setPagination = React.useCallback((nextPage) => {
58
+ setPage(paginate ? Math.max(1, nextPage) : 1);
59
+ }, [paginate]);
60
+ return {
61
+ data,
62
+ allData: items,
63
+ filters,
64
+ setFilters,
65
+ sort,
66
+ setSort,
67
+ pagination: {
68
+ page: currentPage,
69
+ totalPages,
70
+ },
71
+ setPagination,
72
+ };
73
+ }
@@ -0,0 +1,13 @@
1
+ export interface DocumentMetadata {
2
+ title?: string | null;
3
+ defaultTitle?: string | null;
4
+ titleTemplate?: string | null;
5
+ description?: string | null;
6
+ canonicalPath?: string | null;
7
+ image?: string | null;
8
+ favicon?: string | null;
9
+ noIndex?: boolean;
10
+ siteName?: string | null;
11
+ }
12
+ export declare function useDocumentMetadata(metadata: DocumentMetadata): void;
13
+ //# sourceMappingURL=useDocumentMetadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDocumentMetadata.d.ts","sourceRoot":"","sources":["../../src/hooks/useDocumentMetadata.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,gBAAgB;IAC/B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1B;AAED,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,gBAAgB,GAAG,IAAI,CA8BpE"}
@@ -0,0 +1,111 @@
1
+ import React from 'react';
2
+ import { prefixAppBasePath } from './navigation.js';
3
+ export function useDocumentMetadata(metadata) {
4
+ React.useEffect(() => {
5
+ if (typeof document === 'undefined') {
6
+ return;
7
+ }
8
+ const resolvedTitle = applyTitleTemplate(metadata.title, metadata.defaultTitle ?? null, metadata.titleTemplate ?? null);
9
+ if (resolvedTitle) {
10
+ document.title = resolvedTitle;
11
+ }
12
+ setNamedMeta('description', metadata.description ?? null);
13
+ setNamedMeta('robots', metadata.noIndex ? 'noindex,nofollow' : null);
14
+ setPropertyMeta('og:title', resolvedTitle);
15
+ setPropertyMeta('og:description', metadata.description ?? null);
16
+ setPropertyMeta('og:image', toAbsoluteUrl(metadata.image ?? null));
17
+ setPropertyMeta('og:site_name', metadata.siteName ?? null);
18
+ setCanonicalHref(metadata.canonicalPath ?? null);
19
+ setFaviconHref(metadata.favicon ?? null);
20
+ }, [
21
+ metadata.title,
22
+ metadata.defaultTitle,
23
+ metadata.titleTemplate,
24
+ metadata.description,
25
+ metadata.canonicalPath,
26
+ metadata.image,
27
+ metadata.favicon,
28
+ metadata.noIndex,
29
+ metadata.siteName,
30
+ ]);
31
+ }
32
+ function applyTitleTemplate(title, defaultTitle, titleTemplate) {
33
+ const effectiveTitle = title && title.trim().length > 0 ? title : defaultTitle;
34
+ if (!effectiveTitle) {
35
+ return null;
36
+ }
37
+ if (!titleTemplate || !titleTemplate.includes('{title}')) {
38
+ return effectiveTitle;
39
+ }
40
+ return titleTemplate.replace(/\{title\}/g, effectiveTitle);
41
+ }
42
+ function setNamedMeta(name, content) {
43
+ const selector = `meta[name="${name}"]`;
44
+ let element = document.head.querySelector(selector);
45
+ if (!content) {
46
+ element?.remove();
47
+ return;
48
+ }
49
+ if (!element) {
50
+ element = document.createElement('meta');
51
+ element.setAttribute('name', name);
52
+ document.head.appendChild(element);
53
+ }
54
+ element.setAttribute('content', content);
55
+ }
56
+ function setPropertyMeta(name, content) {
57
+ const selector = `meta[property="${name}"]`;
58
+ let element = document.head.querySelector(selector);
59
+ if (!content) {
60
+ element?.remove();
61
+ return;
62
+ }
63
+ if (!element) {
64
+ element = document.createElement('meta');
65
+ element.setAttribute('property', name);
66
+ document.head.appendChild(element);
67
+ }
68
+ element.setAttribute('content', content);
69
+ }
70
+ function setCanonicalHref(path) {
71
+ const selector = 'link[rel="canonical"]';
72
+ let element = document.head.querySelector(selector);
73
+ if (!path) {
74
+ element?.remove();
75
+ return;
76
+ }
77
+ if (!element) {
78
+ element = document.createElement('link');
79
+ element.setAttribute('rel', 'canonical');
80
+ document.head.appendChild(element);
81
+ }
82
+ element.setAttribute('href', toAbsoluteUrl(path) ?? path);
83
+ }
84
+ function setFaviconHref(path) {
85
+ const selector = 'link[rel="icon"]';
86
+ let element = document.head.querySelector(selector);
87
+ if (!path) {
88
+ element?.remove();
89
+ return;
90
+ }
91
+ if (!element) {
92
+ element = document.createElement('link');
93
+ element.setAttribute('rel', 'icon');
94
+ document.head.appendChild(element);
95
+ }
96
+ element.setAttribute('href', path);
97
+ }
98
+ function toAbsoluteUrl(path) {
99
+ if (!path || typeof window === 'undefined') {
100
+ return path;
101
+ }
102
+ if (/^https?:\/\//.test(path)) {
103
+ return path;
104
+ }
105
+ try {
106
+ return new URL(path.startsWith('/') ? prefixAppBasePath(path) : path, window.location.origin).toString();
107
+ }
108
+ catch {
109
+ return path;
110
+ }
111
+ }
@@ -0,0 +1,26 @@
1
+ import type { CollectionPaginationState, CollectionSortState } from './useCollectionView.js';
2
+ export interface GroupedCollectionViewGroup<T extends {
3
+ id: string;
4
+ }> {
5
+ id: string;
6
+ values: Record<string, unknown>;
7
+ rows: T[];
8
+ }
9
+ export interface UseGroupedCollectionViewOptions {
10
+ pageSize?: number;
11
+ paginate?: boolean;
12
+ }
13
+ export interface UseGroupedCollectionViewResult<T extends {
14
+ id: string;
15
+ }> {
16
+ groups: Array<GroupedCollectionViewGroup<T>>;
17
+ allGroups: Array<GroupedCollectionViewGroup<T>>;
18
+ sort: CollectionSortState | null;
19
+ setSort: (next: CollectionSortState | null) => void;
20
+ pagination: CollectionPaginationState;
21
+ setPagination: (page: number) => void;
22
+ }
23
+ export declare function useGroupedCollectionView<T extends {
24
+ id: string;
25
+ }>(items: readonly T[], groupBy: readonly string[], options?: UseGroupedCollectionViewOptions): UseGroupedCollectionViewResult<T>;
26
+ //# sourceMappingURL=useGroupedCollectionView.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useGroupedCollectionView.d.ts","sourceRoot":"","sources":["../../src/hooks/useGroupedCollectionView.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,yBAAyB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAE7F,MAAM,WAAW,0BAA0B,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE;IAClE,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,IAAI,EAAE,CAAC,EAAE,CAAC;CACX;AAED,MAAM,WAAW,+BAA+B;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,8BAA8B,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE;IACtE,MAAM,EAAE,KAAK,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7C,SAAS,EAAE,KAAK,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,IAAI,EAAE,mBAAmB,GAAG,IAAI,CAAC;IACjC,OAAO,EAAE,CAAC,IAAI,EAAE,mBAAmB,GAAG,IAAI,KAAK,IAAI,CAAC;IACpD,UAAU,EAAE,yBAAyB,CAAC;IACtC,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC;AAuDD,wBAAgB,wBAAwB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAC/D,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,OAAO,EAAE,SAAS,MAAM,EAAE,EAC1B,OAAO,GAAE,+BAAoC,GAC5C,8BAA8B,CAAC,CAAC,CAAC,CA4CnC"}
@@ -0,0 +1,82 @@
1
+ import React from 'react';
2
+ function compareValues(left, right) {
3
+ if (typeof left === 'number' && typeof right === 'number') {
4
+ return left - right;
5
+ }
6
+ return String(left ?? '').localeCompare(String(right ?? ''));
7
+ }
8
+ function sortItems(items, sort) {
9
+ if (!sort)
10
+ return [...items];
11
+ const sorted = [...items];
12
+ sorted.sort((left, right) => {
13
+ const comparison = compareValues(left[sort.field], right[sort.field]);
14
+ return sort.direction === 'asc' ? comparison : comparison * -1;
15
+ });
16
+ return sorted;
17
+ }
18
+ function groupItems(items, groupBy) {
19
+ if (groupBy.length === 0) {
20
+ return items.map((item) => ({
21
+ id: String(item.id),
22
+ values: {},
23
+ rows: [item],
24
+ }));
25
+ }
26
+ const groups = new Map();
27
+ for (const item of items) {
28
+ const values = Object.fromEntries(groupBy.map((field) => [field, item[field]]));
29
+ const id = JSON.stringify(groupBy.map((field) => [field, values[field]]));
30
+ const existing = groups.get(id);
31
+ if (existing) {
32
+ existing.rows.push(item);
33
+ continue;
34
+ }
35
+ groups.set(id, {
36
+ id,
37
+ values,
38
+ rows: [item],
39
+ });
40
+ }
41
+ return Array.from(groups.values());
42
+ }
43
+ export function useGroupedCollectionView(items, groupBy, options = {}) {
44
+ const pageSize = options.pageSize ?? 20;
45
+ const paginate = options.paginate ?? true;
46
+ const [sort, setSortState] = React.useState(null);
47
+ const [page, setPage] = React.useState(1);
48
+ const sortedItems = React.useMemo(() => sortItems(items, sort), [items, sort]);
49
+ const allGroups = React.useMemo(() => groupItems(sortedItems, groupBy), [sortedItems, groupBy]);
50
+ const totalPages = paginate ? Math.max(1, Math.ceil(allGroups.length / pageSize)) : 1;
51
+ const currentPage = paginate ? Math.min(page, totalPages) : 1;
52
+ const groups = React.useMemo(() => {
53
+ if (!paginate) {
54
+ return allGroups;
55
+ }
56
+ const start = (currentPage - 1) * pageSize;
57
+ return allGroups.slice(start, start + pageSize);
58
+ }, [allGroups, currentPage, pageSize, paginate]);
59
+ React.useEffect(() => {
60
+ if (currentPage !== page) {
61
+ setPage(currentPage);
62
+ }
63
+ }, [currentPage, page]);
64
+ const setSort = React.useCallback((next) => {
65
+ setSortState(next);
66
+ setPage(1);
67
+ }, []);
68
+ const setPagination = React.useCallback((nextPage) => {
69
+ setPage(paginate ? Math.max(1, nextPage) : 1);
70
+ }, [paginate]);
71
+ return {
72
+ groups,
73
+ allGroups,
74
+ sort,
75
+ setSort,
76
+ pagination: {
77
+ page: currentPage,
78
+ totalPages,
79
+ },
80
+ setPagination,
81
+ };
82
+ }
@@ -0,0 +1,16 @@
1
+ export interface UseReadModelOptions {
2
+ enabled?: boolean;
3
+ }
4
+ export interface UseReadModelResult<T extends {
5
+ id: string;
6
+ }> {
7
+ data: T[];
8
+ allData: T[];
9
+ loading: boolean;
10
+ error: unknown;
11
+ refresh: () => Promise<void>;
12
+ }
13
+ export declare function useReadModel<T extends {
14
+ id: string;
15
+ }>(api: string, query: Record<string, string>, options?: UseReadModelOptions): UseReadModelResult<T>;
16
+ //# sourceMappingURL=useReadModel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useReadModel.d.ts","sourceRoot":"","sources":["../../src/hooks/useReadModel.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE;IAC1D,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,OAAO,EAAE,CAAC,EAAE,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAmDD,wBAAgB,YAAY,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EACnD,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC7B,OAAO,GAAE,mBAAwB,GAChC,kBAAkB,CAAC,CAAC,CAAC,CA2DvB"}
@@ -0,0 +1,104 @@
1
+ import React from 'react';
2
+ import { useResourceClient } from './resourceClient.js';
3
+ import { matchesResourceTarget } from './resourceTarget.js';
4
+ function isRecord(value) {
5
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
6
+ }
7
+ function readModelLeaf(api) {
8
+ const parts = api.split('/').filter(Boolean);
9
+ return parts[parts.length - 1] ?? 'item';
10
+ }
11
+ function normalizeReadModelItems(api, payload) {
12
+ const rows = Array.isArray(payload)
13
+ ? payload
14
+ : isRecord(payload) && Array.isArray(payload.items)
15
+ ? payload.items
16
+ : isRecord(payload) && Array.isArray(payload.data)
17
+ ? payload.data
18
+ : null;
19
+ if (!rows) {
20
+ throw new Error(`Invalid read-model response for ${api}: expected an array or { items: [] }`);
21
+ }
22
+ const syntheticPrefix = readModelLeaf(api);
23
+ return rows.map((row, index) => {
24
+ if (!isRecord(row)) {
25
+ throw new Error(`Invalid read-model row for ${api}: expected an object`);
26
+ }
27
+ return {
28
+ ...row,
29
+ id: row.id == null ? `${syntheticPrefix}-${index + 1}` : String(row.id),
30
+ };
31
+ });
32
+ }
33
+ function buildReadModelUrl(api, query) {
34
+ const params = new URLSearchParams();
35
+ for (const [key, value] of Object.entries(query)) {
36
+ const trimmed = String(value ?? '').trim();
37
+ if (trimmed !== '') {
38
+ params.set(key, trimmed);
39
+ }
40
+ }
41
+ const queryString = params.toString();
42
+ if (queryString === '') {
43
+ return api;
44
+ }
45
+ return api.includes('?') ? `${api}&${queryString}` : `${api}?${queryString}`;
46
+ }
47
+ export function useReadModel(api, query, options = {}) {
48
+ const client = useResourceClient();
49
+ const enabled = options.enabled ?? true;
50
+ const [allData, setAllData] = React.useState([]);
51
+ const [loading, setLoading] = React.useState(false);
52
+ const [error, setError] = React.useState(null);
53
+ const requestUrl = React.useMemo(() => buildReadModelUrl(api, query), [api, query]);
54
+ const load = React.useCallback(async () => {
55
+ if (!enabled) {
56
+ setAllData([]);
57
+ setLoading(false);
58
+ setError(null);
59
+ return;
60
+ }
61
+ setLoading(true);
62
+ try {
63
+ const payload = await client.get(requestUrl);
64
+ setAllData(normalizeReadModelItems(api, payload));
65
+ setError(null);
66
+ }
67
+ catch (nextError) {
68
+ setAllData([]);
69
+ setError(nextError);
70
+ }
71
+ finally {
72
+ setLoading(false);
73
+ }
74
+ }, [api, client, enabled, requestUrl]);
75
+ React.useEffect(() => {
76
+ void load();
77
+ }, [load]);
78
+ React.useEffect(() => {
79
+ if (typeof window === 'undefined')
80
+ return undefined;
81
+ const handleRefresh = (event) => {
82
+ const target = event.detail?.target;
83
+ if (matchesResourceTarget(api, target)) {
84
+ void load();
85
+ }
86
+ };
87
+ window.addEventListener('rdsl:refresh', handleRefresh);
88
+ window.addEventListener('rdsl:invalidate', handleRefresh);
89
+ return () => {
90
+ window.removeEventListener('rdsl:refresh', handleRefresh);
91
+ window.removeEventListener('rdsl:invalidate', handleRefresh);
92
+ };
93
+ }, [api, load]);
94
+ const refresh = React.useCallback(async () => {
95
+ await load();
96
+ }, [load]);
97
+ return {
98
+ data: allData,
99
+ allData,
100
+ loading,
101
+ error,
102
+ refresh,
103
+ };
104
+ }
@@ -0,0 +1,36 @@
1
+ export interface ResourcePaginationState {
2
+ page: number;
3
+ totalPages: number;
4
+ }
5
+ export interface UseResourceOptions {
6
+ pageSize?: number;
7
+ }
8
+ export interface UseResourceResult<T extends {
9
+ id: string;
10
+ }> {
11
+ data: T[];
12
+ allData: T[];
13
+ loading: boolean;
14
+ error: unknown;
15
+ filters: Record<string, string>;
16
+ setFilters: (next: Record<string, string>) => void;
17
+ sort: {
18
+ field: string;
19
+ direction: 'asc' | 'desc';
20
+ } | null;
21
+ setSort: (next: {
22
+ field: string;
23
+ direction: 'asc' | 'desc';
24
+ } | null) => void;
25
+ pagination: ResourcePaginationState;
26
+ setPagination: (page: number) => void;
27
+ getById: (id: string) => T | undefined;
28
+ createItem: (input: Partial<T>) => Promise<T>;
29
+ updateItem: (id: string, input: Partial<T>) => Promise<T>;
30
+ deleteItem: (id: string) => Promise<void>;
31
+ refresh: () => void | Promise<void>;
32
+ }
33
+ export declare function useResource<T extends {
34
+ id: string;
35
+ }>(api: string, options?: UseResourceOptions): UseResourceResult<T>;
36
+ //# sourceMappingURL=useResource.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useResource.d.ts","sourceRoot":"","sources":["../../src/hooks/useResource.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE;IACzD,IAAI,EAAE,CAAC,EAAE,CAAC;IACV,OAAO,EAAE,CAAC,EAAE,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAChC,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAK,IAAI,CAAC;IACnD,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1D,OAAO,EAAE,CAAC,IAAI,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,KAAK,GAAG,MAAM,CAAA;KAAE,GAAG,IAAI,KAAK,IAAI,CAAC;IAC7E,UAAU,EAAE,uBAAuB,CAAC;IACpC,aAAa,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,OAAO,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,CAAC,GAAG,SAAS,CAAC;IACvC,UAAU,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC9C,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC1D,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1C,OAAO,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACrC;AAYD,wBAAgB,WAAW,CAAC,CAAC,SAAS;IAAE,EAAE,EAAE,MAAM,CAAA;CAAE,EAClD,GAAG,EAAE,MAAM,EACX,OAAO,GAAE,kBAAuB,GAC/B,iBAAiB,CAAC,CAAC,CAAC,CAwEtB"}
@@ -0,0 +1,69 @@
1
+ import React from 'react';
2
+ import { useResourceClient } from './resourceClient.js';
3
+ import { getResourceStore } from './resourceStore.js';
4
+ import { matchesResourceTarget } from './resourceTarget.js';
5
+ import { useCollectionView } from './useCollectionView.js';
6
+ export function useResource(api, options = {}) {
7
+ const pageSize = options.pageSize ?? 20;
8
+ const client = useResourceClient();
9
+ const store = React.useMemo(() => getResourceStore(client, api), [api, client]);
10
+ const [, setRevision] = React.useState(0);
11
+ React.useEffect(() => {
12
+ return store.subscribe(() => {
13
+ setRevision((value) => value + 1);
14
+ });
15
+ }, [store]);
16
+ React.useEffect(() => {
17
+ void store.load(false).catch(() => undefined);
18
+ }, [store]);
19
+ React.useEffect(() => {
20
+ if (typeof window === 'undefined')
21
+ return undefined;
22
+ const handleRefresh = (event) => {
23
+ const target = event?.detail?.target;
24
+ if (matchesResourceTarget(api, target)) {
25
+ void store.load(true).catch(() => undefined);
26
+ }
27
+ };
28
+ window.addEventListener('rdsl:refresh', handleRefresh);
29
+ window.addEventListener('rdsl:invalidate', handleRefresh);
30
+ return () => {
31
+ window.removeEventListener('rdsl:refresh', handleRefresh);
32
+ window.removeEventListener('rdsl:invalidate', handleRefresh);
33
+ };
34
+ }, [api, store]);
35
+ const snapshot = store.getSnapshot();
36
+ const collection = useCollectionView(snapshot.items, { pageSize, paginate: true });
37
+ const getById = React.useCallback((id) => {
38
+ return store.getById(id);
39
+ }, [store]);
40
+ const createItem = React.useCallback((input) => {
41
+ return store.createItem(input);
42
+ }, [store]);
43
+ const updateItem = React.useCallback((id, input) => {
44
+ return store.updateItem(id, input);
45
+ }, [store]);
46
+ const deleteItem = React.useCallback((id) => {
47
+ return store.deleteItem(id);
48
+ }, [store]);
49
+ const refresh = React.useCallback(() => {
50
+ return store.load(true).then(() => undefined);
51
+ }, [store]);
52
+ return {
53
+ data: collection.data,
54
+ allData: snapshot.items,
55
+ loading: snapshot.pendingCount > 0 || !snapshot.resolvedOnce,
56
+ error: snapshot.error,
57
+ filters: collection.filters,
58
+ setFilters: collection.setFilters,
59
+ sort: collection.sort,
60
+ setSort: collection.setSort,
61
+ pagination: collection.pagination,
62
+ setPagination: collection.setPagination,
63
+ getById,
64
+ createItem,
65
+ updateItem,
66
+ deleteItem,
67
+ refresh,
68
+ };
69
+ }
@@ -0,0 +1,18 @@
1
+ import type { MessageDescriptor, MessageDescriptorValue, MessageLike } from '@loj-lang/shared-contracts';
2
+ export type ToastMessageValue = MessageDescriptorValue;
3
+ export type ToastMessageDescriptor = MessageDescriptor<ToastMessageValue>;
4
+ export type ToastMessage = MessageLike<ToastMessageValue>;
5
+ export interface ToastApi {
6
+ success: (message: ToastMessage) => void;
7
+ error: (message: ToastMessage) => void;
8
+ info: (message: ToastMessage) => void;
9
+ }
10
+ interface ToastProviderProps {
11
+ value: ToastApi;
12
+ children?: React.ReactNode;
13
+ }
14
+ export declare function resolveToastMessage(message: ToastMessage): string;
15
+ export declare function ToastProvider({ value, children }: ToastProviderProps): any;
16
+ export declare function useToast(): ToastApi;
17
+ export {};
18
+ //# sourceMappingURL=useToast.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useToast.d.ts","sourceRoot":"","sources":["../../src/hooks/useToast.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAEzG,MAAM,MAAM,iBAAiB,GAAG,sBAAsB,CAAC;AACvD,MAAM,MAAM,sBAAsB,GAAG,iBAAiB,CAAC,iBAAiB,CAAC,CAAC;AAC1E,MAAM,MAAM,YAAY,GAAG,WAAW,CAAC,iBAAiB,CAAC,CAAC;AAE1D,MAAM,WAAW,QAAQ;IACvB,OAAO,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;IACzC,KAAK,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;IACvC,IAAI,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,IAAI,CAAC;CACvC;AAED,UAAU,kBAAkB;IAC1B,KAAK,EAAE,QAAQ,CAAC;IAChB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC5B;AAID,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,YAAY,GAAG,MAAM,CAEjE;AAuBD,wBAAgB,aAAa,CAAC,EAAE,KAAK,EAAE,QAAQ,EAAE,EAAE,kBAAkB,OAEpE;AAED,wBAAgB,QAAQ,IAAI,QAAQ,CAEnC"}
@@ -0,0 +1,31 @@
1
+ import React from 'react';
2
+ import { resolveMessageText } from '@loj-lang/shared-contracts';
3
+ const ToastContext = React.createContext(undefined);
4
+ export function resolveToastMessage(message) {
5
+ return resolveMessageText(message);
6
+ }
7
+ function emitToast(level, message) {
8
+ const resolvedMessage = resolveToastMessage(message);
9
+ if (typeof window !== 'undefined') {
10
+ window.dispatchEvent(new CustomEvent('rdsl:toast', {
11
+ detail: {
12
+ level,
13
+ message: resolvedMessage,
14
+ descriptor: typeof message === 'string' ? undefined : message,
15
+ },
16
+ }));
17
+ }
18
+ const logger = level === 'error' ? console.error : console.info;
19
+ logger(`[rdsl:${level}] ${resolvedMessage}`);
20
+ }
21
+ const fallbackToastApi = {
22
+ success: (message) => emitToast('success', message),
23
+ error: (message) => emitToast('error', message),
24
+ info: (message) => emitToast('info', message),
25
+ };
26
+ export function ToastProvider({ value, children }) {
27
+ return React.createElement(ToastContext.Provider, { value }, children);
28
+ }
29
+ export function useToast() {
30
+ return React.useContext(ToastContext) ?? fallbackToastApi;
31
+ }