@protobase/ui 0.16.0 → 0.16.1
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/app/current-address.d.ts +5 -1
- package/dist/app/current-address.js +2 -1
- package/dist/app/displays/record-card.js +28 -16
- package/dist/app/embedded-list/embedded-display.js +7 -3
- package/dist/app/embedded-list/embedded-list.js +5 -4
- package/dist/app/list-page.js +4 -2
- package/dist/app/pages/blocks/record-row.js +51 -0
- package/dist/app/pages/render-node.js +2 -0
- package/dist/app/record-row/record-row-body.js +114 -0
- package/dist/app/record-row/record-row.js +93 -0
- package/dist/app/record-row/row-window.js +20 -0
- package/dist/app/record-row/saved-record-row.js +42 -0
- package/dist/app/record-row/use-row-scroll.js +51 -0
- package/dist/app/searches/filter-chip-list.js +28 -0
- package/dist/app/searches/save-search-button.js +72 -8
- package/dist/app/searches/saved-search-rows.js +73 -0
- package/dist/app/searches/saved-search-section.js +91 -73
- package/dist/app/searches/saved-search.js +7 -3
- package/dist/app/searches/saved-searches.js +3 -5
- package/dist/app/searches/search-controls.js +13 -2
- package/dist/app/searches/search-diff.js +97 -0
- package/dist/app/searches/search-name-dialog.js +62 -0
- package/dist/app/searches/update-search-dialog.js +196 -0
- package/dist/app/searches/use-opened-search.js +15 -0
- package/dist/app/searches/use-save-search.js +64 -18
- package/dist/app/searches/use-search-row.js +56 -0
- package/dist/package.js +1 -1
- package/dist/primitives/dialog.js +3 -2
- package/dist/primitives/responsive-dialog.d.ts +3 -1
- package/dist/primitives/responsive-dialog.js +2 -1
- package/dist/shell/assets/app-sDEk9LFu.js +66 -0
- package/dist/shell/assets/index-D0AzKE47.css +2 -0
- package/dist/shell/assets/{index-LBhMBiK4.js → index-L7nXyA_7.js} +1 -1
- package/dist/shell/assets/{maplibre-Dy0ZTnFZ.js → maplibre-AOh9A0NP.js} +1 -1
- package/dist/shell/assets/{record-map-BiWSbqqT.js → record-map-DbwHSPf3.js} +1 -1
- package/dist/shell/assets/{records-map-DabYc3ad.js → records-map-2U_CQ4RG.js} +1 -1
- package/dist/shell/assets/{shared-protobase-ui-BURelnPG.js → shared-protobase-ui-BMcStVNP.js} +1 -1
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +4 -4
- package/dist/shell/assets/app-DJAZP1yB.js +0 -66
- package/dist/shell/assets/index-C5pYkUfv.css +0 -2
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { Skeleton } from "../../primitives/skeleton.js";
|
|
2
|
+
import { Button } from "../../primitives/button.js";
|
|
3
|
+
import { useClient } from "../../data/api-provider.js";
|
|
4
|
+
import { humanize } from "../../live/naming.js";
|
|
5
|
+
import { defaultOrderBy } from "../list-order.js";
|
|
6
|
+
import { ResponsiveDialog } from "../../primitives/responsive-dialog.js";
|
|
7
|
+
import { countQuery } from "../pages/use-layout-data.js";
|
|
8
|
+
import { EmbeddedList } from "../embedded-list/embedded-list.js";
|
|
9
|
+
import { addedFilter, filterChanges, netChange, removedFilter } from "./search-diff.js";
|
|
10
|
+
import { useFilterChips } from "./use-filter-chips.js";
|
|
11
|
+
import { useMemo } from "react";
|
|
12
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
13
|
+
import { useQuery } from "@tanstack/react-query";
|
|
14
|
+
import { tableDisplay } from "@protobase/schema/internal";
|
|
15
|
+
//#region src/app/searches/update-search-dialog.tsx
|
|
16
|
+
/** How many records each side of the diff shows; its count says how many there are in all. */
|
|
17
|
+
const shownLimit = 12;
|
|
18
|
+
const changeStyle = {
|
|
19
|
+
added: {
|
|
20
|
+
word: "Added",
|
|
21
|
+
className: "text-success-text"
|
|
22
|
+
},
|
|
23
|
+
removed: {
|
|
24
|
+
word: "Removed",
|
|
25
|
+
className: "text-danger-text"
|
|
26
|
+
},
|
|
27
|
+
changed: {
|
|
28
|
+
word: "Changed",
|
|
29
|
+
className: "text-primary-text"
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
const FilterChanges = ({ changes }) => /* @__PURE__ */ jsx("ul", {
|
|
33
|
+
"aria-label": "Filter changes",
|
|
34
|
+
className: "divide-y rounded-md border text-[13px]",
|
|
35
|
+
children: changes.map((change) => /* @__PURE__ */ jsxs("li", {
|
|
36
|
+
className: "grid grid-cols-[72px_1fr] gap-3 px-3 py-2",
|
|
37
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
38
|
+
className: `font-medium ${changeStyle[change.kind].className}`,
|
|
39
|
+
children: changeStyle[change.kind].word
|
|
40
|
+
}), change.kind === "changed" ? /* @__PURE__ */ jsxs("span", { children: [
|
|
41
|
+
/* @__PURE__ */ jsx("span", {
|
|
42
|
+
className: "text-muted-foreground line-through",
|
|
43
|
+
children: change.before
|
|
44
|
+
}),
|
|
45
|
+
" ",
|
|
46
|
+
/* @__PURE__ */ jsx("span", {
|
|
47
|
+
className: "font-medium",
|
|
48
|
+
children: change.label
|
|
49
|
+
})
|
|
50
|
+
] }) : /* @__PURE__ */ jsx("span", {
|
|
51
|
+
className: change.kind === "removed" ? "text-muted-foreground line-through" : "font-medium",
|
|
52
|
+
children: change.label
|
|
53
|
+
})]
|
|
54
|
+
}, `${change.kind}:${change.label}`))
|
|
55
|
+
});
|
|
56
|
+
/** How many records `filter` finds, none for a filter that can find none (`null`). */
|
|
57
|
+
const useDiffCount = (model, filter) => {
|
|
58
|
+
const query = useQuery({
|
|
59
|
+
...countQuery(useClient(), model, filter ?? ""),
|
|
60
|
+
enabled: filter !== null
|
|
61
|
+
});
|
|
62
|
+
return filter === null ? 0 : query.data;
|
|
63
|
+
};
|
|
64
|
+
const count = (n, singular, plural) => `${n} ${n === 1 ? singular : plural}`;
|
|
65
|
+
/** One side of the diff: its title and count, and its first records as the list shows them. */
|
|
66
|
+
const RecordsSide = ({ model, view, title, filter, total, plural }) => {
|
|
67
|
+
const cards = view?.list?.displays?.find((display) => display.kind === "cards");
|
|
68
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
69
|
+
"aria-label": title,
|
|
70
|
+
className: "flex flex-col gap-2",
|
|
71
|
+
children: [
|
|
72
|
+
/* @__PURE__ */ jsxs("h3", {
|
|
73
|
+
className: "text-[13px] font-semibold",
|
|
74
|
+
children: [
|
|
75
|
+
title,
|
|
76
|
+
" ",
|
|
77
|
+
/* @__PURE__ */ jsxs("span", {
|
|
78
|
+
className: "font-normal text-muted-foreground",
|
|
79
|
+
children: [
|
|
80
|
+
"(",
|
|
81
|
+
total ?? "…",
|
|
82
|
+
")"
|
|
83
|
+
]
|
|
84
|
+
})
|
|
85
|
+
]
|
|
86
|
+
}),
|
|
87
|
+
total !== void 0 && total > shownLimit && /* @__PURE__ */ jsxs("p", {
|
|
88
|
+
className: "text-xs text-muted-foreground",
|
|
89
|
+
children: [
|
|
90
|
+
"The first ",
|
|
91
|
+
shownLimit,
|
|
92
|
+
" of ",
|
|
93
|
+
total,
|
|
94
|
+
"."
|
|
95
|
+
]
|
|
96
|
+
}),
|
|
97
|
+
filter === null || total === 0 ? /* @__PURE__ */ jsxs("p", {
|
|
98
|
+
className: "text-[13px] text-muted-foreground",
|
|
99
|
+
children: [
|
|
100
|
+
"No ",
|
|
101
|
+
plural,
|
|
102
|
+
"."
|
|
103
|
+
]
|
|
104
|
+
}) : /* @__PURE__ */ jsx(EmbeddedList, {
|
|
105
|
+
model,
|
|
106
|
+
view,
|
|
107
|
+
filter,
|
|
108
|
+
sort: defaultOrderBy(model, view),
|
|
109
|
+
limit: shownLimit,
|
|
110
|
+
display: cards?.name ?? tableDisplay,
|
|
111
|
+
row: true,
|
|
112
|
+
link: false,
|
|
113
|
+
empty: `No ${plural}.`
|
|
114
|
+
})
|
|
115
|
+
]
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Asks before a saved search takes the list's filter: what changes in its filters, how many more or fewer records it
|
|
120
|
+
* finds, and the records it would add and drop, each found by the list API (`addedFilter`, `removedFilter`).
|
|
121
|
+
*/
|
|
122
|
+
const UpdateSearchDialog = ({ model, view, title, before, after, saveNewLabel, updating, creating, onUpdate, onSaveNew, onCancel }) => {
|
|
123
|
+
const changes = filterChanges(useFilterChips(model, before), useFilterChips(model, after));
|
|
124
|
+
const added = useMemo(() => addedFilter(before, after), [before, after]);
|
|
125
|
+
const removed = useMemo(() => removedFilter(before, after), [before, after]);
|
|
126
|
+
const addedCount = useDiffCount(model, added);
|
|
127
|
+
const removedCount = useDiffCount(model, removed);
|
|
128
|
+
const singular = (view?.names?.singular ?? humanize(model.name)).toLowerCase();
|
|
129
|
+
const plural = (view?.names?.plural ?? humanize(model.name)).toLowerCase();
|
|
130
|
+
const counted = addedCount !== void 0 && removedCount !== void 0;
|
|
131
|
+
return /* @__PURE__ */ jsx(ResponsiveDialog, {
|
|
132
|
+
open: true,
|
|
133
|
+
wide: true,
|
|
134
|
+
title,
|
|
135
|
+
onClose: onCancel,
|
|
136
|
+
actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
137
|
+
/* @__PURE__ */ jsx(Button, {
|
|
138
|
+
onClick: onCancel,
|
|
139
|
+
children: "Cancel"
|
|
140
|
+
}),
|
|
141
|
+
/* @__PURE__ */ jsx(Button, {
|
|
142
|
+
loading: creating,
|
|
143
|
+
disabled: updating,
|
|
144
|
+
onClick: onSaveNew,
|
|
145
|
+
children: saveNewLabel
|
|
146
|
+
}),
|
|
147
|
+
/* @__PURE__ */ jsx(Button, {
|
|
148
|
+
variant: "primary",
|
|
149
|
+
loading: updating,
|
|
150
|
+
disabled: creating,
|
|
151
|
+
onClick: onUpdate,
|
|
152
|
+
children: "Update"
|
|
153
|
+
})
|
|
154
|
+
] }),
|
|
155
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
156
|
+
className: "flex flex-col gap-4",
|
|
157
|
+
children: [
|
|
158
|
+
changes.length > 0 && /* @__PURE__ */ jsx(FilterChanges, { changes }),
|
|
159
|
+
counted ? /* @__PURE__ */ jsxs("p", {
|
|
160
|
+
className: "text-[13px]",
|
|
161
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
162
|
+
className: "font-semibold",
|
|
163
|
+
children: netChange(addedCount, removedCount, singular, plural)
|
|
164
|
+
}), /* @__PURE__ */ jsxs("span", {
|
|
165
|
+
className: "text-muted-foreground",
|
|
166
|
+
children: [
|
|
167
|
+
": ",
|
|
168
|
+
count(addedCount, singular, plural),
|
|
169
|
+
" added, ",
|
|
170
|
+
removedCount,
|
|
171
|
+
" removed"
|
|
172
|
+
]
|
|
173
|
+
})]
|
|
174
|
+
}) : /* @__PURE__ */ jsx(Skeleton, { className: "h-4 w-48" }),
|
|
175
|
+
/* @__PURE__ */ jsx(RecordsSide, {
|
|
176
|
+
model,
|
|
177
|
+
view,
|
|
178
|
+
title: "Added",
|
|
179
|
+
filter: added,
|
|
180
|
+
total: addedCount,
|
|
181
|
+
plural
|
|
182
|
+
}),
|
|
183
|
+
/* @__PURE__ */ jsx(RecordsSide, {
|
|
184
|
+
model,
|
|
185
|
+
view,
|
|
186
|
+
title: "Removed",
|
|
187
|
+
filter: removed,
|
|
188
|
+
total: removedCount,
|
|
189
|
+
plural
|
|
190
|
+
})
|
|
191
|
+
]
|
|
192
|
+
})
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
//#endregion
|
|
196
|
+
export { UpdateSearchDialog };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useAppAddress } from "../use-app-address.js";
|
|
2
|
+
//#region src/app/searches/use-opened-search.ts
|
|
3
|
+
/**
|
|
4
|
+
* The saved search the list was opened from, by its key, as the address names it ("View all results" does), and `open`
|
|
5
|
+
* to name another, such as a search just saved.
|
|
6
|
+
*/
|
|
7
|
+
const useOpenedSearch = () => {
|
|
8
|
+
const { address, set } = useAppAddress();
|
|
9
|
+
return {
|
|
10
|
+
key: address?.kind === "list" ? address.savedSearch : void 0,
|
|
11
|
+
open: (key) => set({ savedSearch: key })
|
|
12
|
+
};
|
|
13
|
+
};
|
|
14
|
+
//#endregion
|
|
15
|
+
export { useOpenedSearch };
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { useClient } from "../../data/api-provider.js";
|
|
2
2
|
import { recordKey } from "../../live/model-helpers.js";
|
|
3
|
+
import { humanize } from "../../live/naming.js";
|
|
3
4
|
import { useToast } from "../../toasts/toast-provider.js";
|
|
4
5
|
import { useAdminMeta } from "../meta-gate.js";
|
|
5
6
|
import { useAppAddress } from "../use-app-address.js";
|
|
6
7
|
import { can } from "../permissions.js";
|
|
7
8
|
import { useRefreshResource } from "../pages/use-layout-data.js";
|
|
9
|
+
import { useOpenedSearch } from "./use-opened-search.js";
|
|
8
10
|
import { useSavedSearches } from "./use-saved-searches.js";
|
|
9
11
|
import { useMutation } from "@tanstack/react-query";
|
|
10
12
|
import { encodeKey } from "@protobase/schema/internal";
|
|
@@ -12,15 +14,18 @@ import { encodeKey } from "@protobase/schema/internal";
|
|
|
12
14
|
const message = (error) => error instanceof Error ? error.message : "Something went wrong";
|
|
13
15
|
/**
|
|
14
16
|
* A list page's "Save search" button for its `filter`: `save` for someone who can list, create and delete the saved
|
|
15
|
-
* searches, `sign-in` for a visitor their resource asks to sign in (`prompt`), nothing otherwise.
|
|
16
|
-
* filter
|
|
17
|
+
* searches, `sign-in` for a visitor their resource asks to sign in (`prompt`), nothing otherwise. `saved` while a search
|
|
18
|
+
* has the filter, and `remove` takes it off again; otherwise `saveNew` saves the filter as a search, with the name the
|
|
19
|
+
* user gave (`naming` says how to ask for it) or none. While the list was opened from a saved search (`opened`) whose
|
|
20
|
+
* filter differs, `update` gives that search the list's filter.
|
|
17
21
|
*/
|
|
18
|
-
const useSaveSearch = (search, filter
|
|
22
|
+
const useSaveSearch = (search, filter) => {
|
|
19
23
|
const meta = useAdminMeta();
|
|
20
24
|
const client = useClient();
|
|
21
25
|
const toast = useToast();
|
|
22
26
|
const refresh = useRefreshResource();
|
|
23
27
|
const { href, open } = useAppAddress();
|
|
28
|
+
const opened = useOpenedSearch();
|
|
24
29
|
const { resource } = search;
|
|
25
30
|
const model = meta.resources[resource];
|
|
26
31
|
const of = meta.views[resource]?.searchesOf;
|
|
@@ -28,18 +33,20 @@ const useSaveSearch = (search, filter, name) => {
|
|
|
28
33
|
const usable = model !== void 0 && of !== void 0 && permissions !== void 0 && can(permissions, "create") && can(permissions, "delete");
|
|
29
34
|
const prompt = meta.signIn?.[resource]?.signIn;
|
|
30
35
|
const saved = useSavedSearches(resource, usable);
|
|
36
|
+
const keyOf = (row) => encodeKey(recordKey(model, row));
|
|
31
37
|
const current = of && saved.data?.find((row) => row[of.filter] === filter);
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
}
|
|
40
|
-
},
|
|
41
|
-
onSuccess: (
|
|
42
|
-
|
|
38
|
+
const edited = current || opened.key === void 0 || !permissions || !can(permissions, "update") ? void 0 : saved.data?.find((row) => keyOf(row) === opened.key);
|
|
39
|
+
const names = meta.views[resource]?.names;
|
|
40
|
+
const plural = names?.plural ?? "saved searches";
|
|
41
|
+
const nameField = of?.name === void 0 ? void 0 : model?.fields[of.name];
|
|
42
|
+
const create = useMutation({
|
|
43
|
+
mutationFn: (name) => client.create(resource, {
|
|
44
|
+
[of.filter]: filter,
|
|
45
|
+
...of.name && { [of.name]: name }
|
|
46
|
+
}),
|
|
47
|
+
onSuccess: (created) => {
|
|
48
|
+
opened.open(keyOf(created.record));
|
|
49
|
+
toast.show({
|
|
43
50
|
state: "success",
|
|
44
51
|
title: "Search saved",
|
|
45
52
|
action: {
|
|
@@ -52,19 +59,58 @@ const useSaveSearch = (search, filter, name) => {
|
|
|
52
59
|
}
|
|
53
60
|
});
|
|
54
61
|
},
|
|
55
|
-
onError: (error
|
|
62
|
+
onError: (error) => toast.show({
|
|
63
|
+
state: "error",
|
|
64
|
+
title: "Could not save the search",
|
|
65
|
+
description: message(error)
|
|
66
|
+
}),
|
|
67
|
+
onSettled: () => refresh(resource)
|
|
68
|
+
});
|
|
69
|
+
const remove = useMutation({
|
|
70
|
+
mutationFn: (row) => client.remove(resource, keyOf(row)),
|
|
71
|
+
onError: (error) => toast.show({
|
|
56
72
|
state: "error",
|
|
57
|
-
title:
|
|
73
|
+
title: "Could not take the search off",
|
|
58
74
|
description: message(error)
|
|
59
75
|
}),
|
|
60
76
|
onSettled: () => refresh(resource)
|
|
61
77
|
});
|
|
78
|
+
const update = useMutation({
|
|
79
|
+
mutationFn: (row) => client.update(resource, keyOf(row), { [of.filter]: filter }, row.etag ?? "*"),
|
|
80
|
+
onSuccess: () => toast.show({
|
|
81
|
+
state: "success",
|
|
82
|
+
title: "Search updated"
|
|
83
|
+
}),
|
|
84
|
+
onError: (error) => toast.show({
|
|
85
|
+
state: "error",
|
|
86
|
+
title: "Could not update the search",
|
|
87
|
+
description: message(error)
|
|
88
|
+
}),
|
|
89
|
+
onSettled: () => refresh(resource)
|
|
90
|
+
});
|
|
91
|
+
const pending = create.isPending || remove.isPending || update.isPending;
|
|
62
92
|
return {
|
|
63
93
|
mode: usable ? "save" : prompt ? "sign-in" : void 0,
|
|
64
94
|
prompt,
|
|
65
95
|
saved: current !== void 0,
|
|
66
|
-
|
|
67
|
-
|
|
96
|
+
/** The search the list was opened from and differs from: its filter, and its name when it has one. */
|
|
97
|
+
opened: edited && {
|
|
98
|
+
filter: String(edited[of.filter] ?? ""),
|
|
99
|
+
name: of.name && typeof edited[of.name] === "string" && edited[of.name] !== "" ? String(edited[of.name]) : void 0
|
|
100
|
+
},
|
|
101
|
+
/** What one search is called, as the saved searches' view names it. */
|
|
102
|
+
noun: (names?.singular ?? humanize(resource)).toLowerCase(),
|
|
103
|
+
/** How to ask for a new search's name, from its field and view; none when searches have no name. */
|
|
104
|
+
naming: nameField && {
|
|
105
|
+
label: meta.views[resource]?.fields[nameField.name]?.label ?? humanize(nameField.name),
|
|
106
|
+
required: !nameField.nullable
|
|
107
|
+
},
|
|
108
|
+
ready: saved.data !== void 0 && !pending,
|
|
109
|
+
updating: update.isPending,
|
|
110
|
+
creating: create.isPending,
|
|
111
|
+
remove: () => current && remove.mutate(current),
|
|
112
|
+
saveNew: (name, options) => create.mutate(name, options),
|
|
113
|
+
update: (options) => edited && update.mutate(edited, options)
|
|
68
114
|
};
|
|
69
115
|
};
|
|
70
116
|
//#endregion
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { useClient } from "../../data/api-provider.js";
|
|
2
|
+
import { humanize } from "../../live/naming.js";
|
|
3
|
+
import { countQuery, useRefreshResource } from "../pages/use-layout-data.js";
|
|
4
|
+
import { isUnseen, unseenFilter } from "./saved-search.js";
|
|
5
|
+
import { chipsText } from "./filter-chip-list.js";
|
|
6
|
+
import { useFilterChips } from "./use-filter-chips.js";
|
|
7
|
+
import { useMutation, useQuery } from "@tanstack/react-query";
|
|
8
|
+
//#region src/app/searches/use-search-row.ts
|
|
9
|
+
/**
|
|
10
|
+
* One saved search as a row of what it finds: its name, if it has one, its filters as chips, its title (the name, or
|
|
11
|
+
* the chips as words), and the `RecordRowBody` of its records: with `unseen` newest first, those new to the user marked
|
|
12
|
+
* "New", saying so when none are, and "View all results", which opens the list from the search and counts as having
|
|
13
|
+
* seen them.
|
|
14
|
+
*/
|
|
15
|
+
const useSearchRow = ({ model, of, target, targetView, row, recordKey }) => {
|
|
16
|
+
const client = useClient();
|
|
17
|
+
const refresh = useRefreshResource();
|
|
18
|
+
const filter = String(row[of.filter] ?? "");
|
|
19
|
+
const chips = useFilterChips(target, filter);
|
|
20
|
+
const stored = of.name === void 0 ? void 0 : row[of.name];
|
|
21
|
+
const name = typeof stored === "string" && stored !== "" ? stored : void 0;
|
|
22
|
+
const unseen = of.unseen;
|
|
23
|
+
const markSeen = useMutation({
|
|
24
|
+
mutationFn: (seenAt) => client.update(model.name, recordKey, { [unseen.seenAt]: seenAt }, row.etag ?? "*"),
|
|
25
|
+
onSettled: () => refresh(model.name)
|
|
26
|
+
});
|
|
27
|
+
const newCount = useQuery({
|
|
28
|
+
...countQuery(client, target, unseen ? unseenFilter(filter, unseen.field, row[unseen.seenAt]) : ""),
|
|
29
|
+
enabled: unseen !== void 0
|
|
30
|
+
});
|
|
31
|
+
const plural = (targetView?.names?.plural ?? humanize(target.name)).toLowerCase();
|
|
32
|
+
const body = {
|
|
33
|
+
model: target,
|
|
34
|
+
view: targetView,
|
|
35
|
+
filter,
|
|
36
|
+
sort: unseen ? `${unseen.field} desc` : void 0,
|
|
37
|
+
limit: of.limit,
|
|
38
|
+
chips: name !== void 0,
|
|
39
|
+
empty: `No ${plural} match.`,
|
|
40
|
+
note: unseen && newCount.data === 0 && `No new ${plural} since you last looked.`,
|
|
41
|
+
isNew: unseen && ((record) => isUnseen(record.record[unseen.field], row[unseen.seenAt])),
|
|
42
|
+
viewAll: {
|
|
43
|
+
label: "View all results",
|
|
44
|
+
savedSearch: recordKey,
|
|
45
|
+
...unseen && { onOpen: () => markSeen.mutate((/* @__PURE__ */ new Date()).toISOString()) }
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return {
|
|
49
|
+
name,
|
|
50
|
+
chips,
|
|
51
|
+
title: name ?? chipsText(chips),
|
|
52
|
+
body
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
//#endregion
|
|
56
|
+
export { useSearchRow };
|
package/dist/package.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { cn } from "../lib/cn.js";
|
|
1
2
|
import { useFocusTrap } from "../lib/use-focus-trap.js";
|
|
2
3
|
import { useRef } from "react";
|
|
3
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
5
|
//#region src/primitives/dialog.tsx
|
|
5
6
|
/** A modal with a title, optional body and action buttons. Closes on Escape and on a click outside. */
|
|
6
|
-
const Dialog = ({ title, description, onClose, actions, children }) => {
|
|
7
|
+
const Dialog = ({ title, description, onClose, actions, children, wide = false }) => {
|
|
7
8
|
const panel = useRef(null);
|
|
8
9
|
useFocusTrap(panel, true, onClose);
|
|
9
10
|
return /* @__PURE__ */ jsx("div", {
|
|
@@ -15,7 +16,7 @@ const Dialog = ({ title, description, onClose, actions, children }) => {
|
|
|
15
16
|
role: "dialog",
|
|
16
17
|
"aria-modal": "true",
|
|
17
18
|
"aria-label": title,
|
|
18
|
-
className: "outline-none max-h-[90vh] w-full
|
|
19
|
+
className: cn("outline-none max-h-[90vh] w-full overflow-y-auto rounded-lg border bg-background p-4 shadow-pop sm:p-5", wide ? "max-w-3xl" : "max-w-md"),
|
|
19
20
|
children: [
|
|
20
21
|
/* @__PURE__ */ jsx("h2", {
|
|
21
22
|
className: "text-[15px] font-semibold",
|
|
@@ -8,10 +8,12 @@ export type ResponsiveDialogProps = {
|
|
|
8
8
|
/** Buttons: right-aligned in the modal, stacked full width at the bottom of the sheet. */
|
|
9
9
|
actions: ReactNode;
|
|
10
10
|
children?: ReactNode;
|
|
11
|
+
/** Room for a row of cards or a table in the modal's body. */
|
|
12
|
+
wide?: boolean;
|
|
11
13
|
};
|
|
12
14
|
/**
|
|
13
15
|
* A modal centred on the screen from the `md` breakpoint up, and a sheet rising from the bottom on a phone, switching as
|
|
14
16
|
* the viewport does. Both close on Escape and on a press outside, and keep focus inside while open.
|
|
15
17
|
*/
|
|
16
|
-
export declare const ResponsiveDialog: ({ title, description, open, onClose, actions, children }: ResponsiveDialogProps) => import("react").JSX.Element | null;
|
|
18
|
+
export declare const ResponsiveDialog: ({ title, description, open, onClose, actions, children, wide }: ResponsiveDialogProps) => import("react").JSX.Element | null;
|
|
17
19
|
//#endregion
|
|
@@ -7,7 +7,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
7
7
|
* A modal centred on the screen from the `md` breakpoint up, and a sheet rising from the bottom on a phone, switching as
|
|
8
8
|
* the viewport does. Both close on Escape and on a press outside, and keep focus inside while open.
|
|
9
9
|
*/
|
|
10
|
-
const ResponsiveDialog = ({ title, description, open, onClose, actions, children }) => {
|
|
10
|
+
const ResponsiveDialog = ({ title, description, open, onClose, actions, children, wide }) => {
|
|
11
11
|
const desktop = useIsDesktop();
|
|
12
12
|
if (!open) return null;
|
|
13
13
|
if (desktop) return /* @__PURE__ */ jsx(Dialog, {
|
|
@@ -15,6 +15,7 @@ const ResponsiveDialog = ({ title, description, open, onClose, actions, children
|
|
|
15
15
|
description,
|
|
16
16
|
onClose,
|
|
17
17
|
actions,
|
|
18
|
+
wide,
|
|
18
19
|
children
|
|
19
20
|
});
|
|
20
21
|
return /* @__PURE__ */ jsx(Sheet, {
|