@protobase/ui 0.21.6 → 0.21.8
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/app.d.ts +3 -3
- package/dist/app/app.js +32 -13
- package/dist/app/comments/comment-body.js +13 -0
- package/dist/app/comments/comment-composer.js +149 -0
- package/dist/app/comments/comments-section.js +78 -0
- package/dist/app/comments/mention-text.js +45 -0
- package/dist/app/comments/use-mentionable.js +18 -0
- package/dist/app/comments/use-post-comment.js +15 -0
- package/dist/app/comments/use-record-comments.js +24 -0
- package/dist/app/create/create-page.js +4 -4
- package/dist/app/current-address.d.ts +4 -3
- package/dist/app/current-address.js +1 -0
- package/dist/app/custom-section.js +1 -1
- package/dist/app/displays/board-columns.js +1 -1
- package/dist/app/displays/board-display.js +1 -1
- package/dist/app/displays/calendar-controls.js +69 -0
- package/dist/app/displays/calendar-day.js +33 -0
- package/dist/app/displays/calendar-display.js +174 -0
- package/dist/app/displays/calendar-dnd.js +27 -0
- package/dist/app/displays/calendar-event.js +72 -0
- package/dist/app/displays/calendar-range.js +125 -0
- package/dist/app/displays/list-displays.js +8 -2
- package/dist/app/displays/map-display.js +1 -1
- package/dist/app/displays/record-card.js +2 -2
- package/dist/app/displays/use-board-column.js +1 -1
- package/dist/app/displays/use-board-move.js +1 -1
- package/dist/app/displays/use-calendar-move.js +68 -0
- package/dist/app/displays/use-calendar-rows.js +35 -0
- package/dist/app/displays/use-map-records.js +1 -1
- package/dist/app/embedded-list/embedded-list.js +26 -22
- package/dist/app/embedded-list/embedded-row-targets.js +65 -0
- package/dist/app/embedded-list/use-embedded-rows.js +1 -1
- package/dist/app/gallery/gallery-section.js +1 -1
- package/dist/app/gallery/lightbox.js +1 -1
- package/dist/app/hand-order/hand-sorted-list.js +3 -3
- package/dist/app/hand-order/use-hand-order.js +2 -2
- package/dist/app/import/import-dialog.js +1 -1
- package/dist/app/list-edit/use-list-edits.js +1 -1
- package/dist/app/list-page.js +20 -9
- package/dist/app/map/map-section.js +1 -1
- package/dist/app/pages/action-inputs/action-inputs-dialog.js +3 -3
- package/dist/app/pages/blocks/card-row.js +1 -1
- package/dist/app/pages/blocks/modal-form.js +3 -3
- package/dist/app/pages/blocks/record-card.js +1 -1
- package/dist/app/pages/field-value.js +1 -1
- package/dist/app/pages/use-run-action.js +9 -7
- package/dist/app/record-lines.js +1 -1
- package/dist/app/record-page.js +29 -15
- package/dist/app/record-row/record-row-body.js +1 -1
- package/dist/app/record-row/saved-record-row.js +1 -1
- package/dist/app/related-dots/related-dots.js +1 -1
- package/dist/app/related-opens.js +22 -0
- package/dist/app/related-section.js +5 -2
- package/dist/app/relation-picker.js +1 -1
- package/dist/app/search/search-model.js +1 -1
- package/dist/app/searches/saved-search-rows.js +2 -2
- package/dist/app/searches/saved-searches.js +2 -2
- package/dist/app/searches/update-search-dialog.js +1 -1
- package/dist/app/searches/use-save-search.js +1 -1
- package/dist/app/searches/use-saved-searches.js +1 -1
- package/dist/app/use-app-address.d.ts +3 -1
- package/dist/app/use-app-address.js +9 -10
- package/dist/assistant-dock/widget-view.js +1 -1
- package/dist/auth/auth-layout.d.ts +4 -2
- package/dist/auth/auth-layout.js +4 -3
- package/dist/auth/user-to-shell.js +3 -1
- package/dist/data/api-provider.d.ts +26 -0
- package/dist/data/query-keys.js +18 -1
- package/dist/data-grid/row-menu.js +8 -1
- package/dist/format/date.js +27 -1
- package/dist/forms/prove-human.js +24 -0
- package/dist/forms/public-form-page.js +142 -0
- package/dist/forms/use-form-address.js +11 -0
- package/dist/index.js +7 -7
- package/dist/live/columns-from-model.js +1 -1
- package/dist/live/use-filter-config.js +1 -1
- package/dist/package.js +1 -1
- package/dist/plugins/plugin-gate.js +1 -1
- package/dist/primitives/segmented.js +8 -3
- package/dist/primitives/textarea.d.ts +4 -3
- package/dist/primitives/textarea.js +9 -4
- package/dist/shell/assets/app-C01lazBK.js +69 -0
- package/dist/shell/assets/{export-worker-CVd5saLq.js → export-worker-BnOpS_4Q.js} +1 -1
- package/dist/shell/assets/index-CBGigFxb.css +2 -0
- package/dist/shell/assets/{index-BztYGbYc.js → index-xN6_oZU2.js} +1 -1
- package/dist/shell/assets/{maplibre-CVOEAJ62.js → maplibre-Ba9RapjY.js} +1 -1
- package/dist/shell/assets/{markdown-content-Bg8FC_aK.js → markdown-content-BUxrbovf.js} +1 -1
- package/dist/shell/assets/{record-map-ChUVZLR6.js → record-map-14zotZ2s.js} +1 -1
- package/dist/shell/assets/{records-map-BAI2D9VS.js → records-map-wi-0pXqF.js} +1 -1
- package/dist/shell/assets/shared-protobase-ui-BVwhKZk9.js +1 -0
- package/dist/shell/index.html +4 -4
- package/dist/shell/protobase.shell.json +1 -1
- package/package.json +5 -4
- package/dist/shell/assets/app-Dfqcc3td.js +0 -69
- package/dist/shell/assets/index-C1fzTv_X.css +0 -2
- package/dist/shell/assets/shared-protobase-ui-CzT3bJ35.js +0 -1
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { cn } from "../../lib/cn.js";
|
|
2
|
+
import { Skeleton } from "../../primitives/skeleton.js";
|
|
3
|
+
import { formatDate, formatWeekday } from "../../format/date.js";
|
|
4
|
+
import { ErrorBanner } from "../error-banner.js";
|
|
5
|
+
import { usePermissions } from "../meta-gate.js";
|
|
6
|
+
import { AppLink } from "../use-app-address.js";
|
|
7
|
+
import { can, rowAllows } from "../permissions.js";
|
|
8
|
+
import { collision } from "./board-dnd.js";
|
|
9
|
+
import { byDay, calendarView, gridDays, rangeOf } from "./calendar-range.js";
|
|
10
|
+
import { CalendarControls } from "./calendar-controls.js";
|
|
11
|
+
import { dayCoordinates, dayId } from "./calendar-dnd.js";
|
|
12
|
+
import { CalendarDay } from "./calendar-day.js";
|
|
13
|
+
import { CalendarEvent, EventChip, eventTitle } from "./calendar-event.js";
|
|
14
|
+
import { useCalendarRows } from "./use-calendar-rows.js";
|
|
15
|
+
import { movedRows, useCalendarMove } from "./use-calendar-move.js";
|
|
16
|
+
import { useRef, useState } from "react";
|
|
17
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
18
|
+
import { DndContext, DragOverlay, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors } from "@dnd-kit/core";
|
|
19
|
+
//#region src/app/displays/calendar-display.tsx
|
|
20
|
+
/** The events a month's day shows before a link to its week, which shows them all. */
|
|
21
|
+
const monthDayLimit = 3;
|
|
22
|
+
/**
|
|
23
|
+
* The list's records on the days of a month or a week, loaded for those days alone through the list API; an event
|
|
24
|
+
* dragged, or moved with the keyboard, to another day saves the record with its dates moved by the same days.
|
|
25
|
+
*/
|
|
26
|
+
const CalendarDisplay = ({ model, view, display, fields, filter, at, linkTo, now = Date.now() }) => {
|
|
27
|
+
const [local, setLocal] = useState();
|
|
28
|
+
const shown = calendarView(linkTo ? at : local, now);
|
|
29
|
+
const days = gridDays(shown);
|
|
30
|
+
const list = useCalendarRows({
|
|
31
|
+
model,
|
|
32
|
+
display,
|
|
33
|
+
range: rangeOf(shown),
|
|
34
|
+
fields,
|
|
35
|
+
filter
|
|
36
|
+
});
|
|
37
|
+
const { moves, move } = useCalendarMove(model, display);
|
|
38
|
+
const placed = byDay(movedRows(list.rows, moves), display, model, days);
|
|
39
|
+
const permissions = usePermissions(model.name);
|
|
40
|
+
const movable = can(permissions, "update") && [display.start, display.end].every((name) => name === void 0 || !model.fields[name]?.readOnly);
|
|
41
|
+
const [active, setActive] = useState();
|
|
42
|
+
const dragged = useRef(false);
|
|
43
|
+
const sensors = useSensors(useSensor(MouseSensor, { activationConstraint: { distance: 4 } }), useSensor(TouchSensor, { activationConstraint: {
|
|
44
|
+
delay: 250,
|
|
45
|
+
tolerance: 5
|
|
46
|
+
} }), useSensor(KeyboardSensor, {
|
|
47
|
+
keyboardCodes: {
|
|
48
|
+
start: ["Space"],
|
|
49
|
+
cancel: ["Escape"],
|
|
50
|
+
end: ["Space", "Enter"]
|
|
51
|
+
},
|
|
52
|
+
coordinateGetter: dayCoordinates
|
|
53
|
+
}));
|
|
54
|
+
const today = new Date(now).toISOString().slice(0, 10);
|
|
55
|
+
const week = shown.scale === "week";
|
|
56
|
+
const dayText = (id) => formatDate(Date.parse(`${String(id).slice(dayId("").length)}T00:00:00Z`));
|
|
57
|
+
const title = (picked) => eventTitle(model, view, display, picked.row);
|
|
58
|
+
const announcements = {
|
|
59
|
+
onDragStart: ({ active: picked }) => `Picked up ${title(picked.data.current)}.`,
|
|
60
|
+
onDragOver: ({ active: picked, over }) => `${title(picked.data.current)} is over ${over ? dayText(over.id) : "no day"}.`,
|
|
61
|
+
onDragEnd: ({ active: picked, over }) => over ? `${title(picked.data.current)} was moved to ${dayText(over.id)}.` : `${title(picked.data.current)} was put back.`,
|
|
62
|
+
onDragCancel: ({ active: picked }) => `Moving ${title(picked.data.current)} was cancelled.`
|
|
63
|
+
};
|
|
64
|
+
const release = () => {
|
|
65
|
+
setActive(void 0);
|
|
66
|
+
setTimeout(() => dragged.current = false);
|
|
67
|
+
};
|
|
68
|
+
const start = ({ active: picked }) => {
|
|
69
|
+
dragged.current = true;
|
|
70
|
+
setActive(picked.data.current);
|
|
71
|
+
};
|
|
72
|
+
const end = ({ active: picked, over }) => {
|
|
73
|
+
release();
|
|
74
|
+
const { row, day } = picked.data.current;
|
|
75
|
+
const target = over ? String(over.id).slice(dayId("").length) : void 0;
|
|
76
|
+
if (target && target !== day) move(row, day, target);
|
|
77
|
+
};
|
|
78
|
+
const more = (day, hidden) => {
|
|
79
|
+
const to = calendarView({
|
|
80
|
+
scale: "week",
|
|
81
|
+
day
|
|
82
|
+
}, now);
|
|
83
|
+
const text = `+${hidden} more`;
|
|
84
|
+
const className = "self-start rounded px-1 text-xs text-muted-foreground hover:text-foreground focus-visible:outline-2 focus-visible:outline-primary";
|
|
85
|
+
return linkTo ? /* @__PURE__ */ jsx(AppLink, {
|
|
86
|
+
to: linkTo(to),
|
|
87
|
+
className,
|
|
88
|
+
children: text
|
|
89
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
90
|
+
type: "button",
|
|
91
|
+
className,
|
|
92
|
+
onClick: () => setLocal(to),
|
|
93
|
+
children: text
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
97
|
+
className: "flex min-w-0 flex-1 flex-col gap-3",
|
|
98
|
+
children: [
|
|
99
|
+
/* @__PURE__ */ jsx(CalendarControls, {
|
|
100
|
+
view: shown,
|
|
101
|
+
now,
|
|
102
|
+
linkTo,
|
|
103
|
+
onView: setLocal
|
|
104
|
+
}),
|
|
105
|
+
list.error && /* @__PURE__ */ jsx(ErrorBanner, {
|
|
106
|
+
title: "Could not load rows",
|
|
107
|
+
error: list.error
|
|
108
|
+
}),
|
|
109
|
+
list.capped && /* @__PURE__ */ jsxs("p", {
|
|
110
|
+
className: "text-xs text-muted-foreground",
|
|
111
|
+
children: [
|
|
112
|
+
"Showing the first ",
|
|
113
|
+
500,
|
|
114
|
+
" records of this ",
|
|
115
|
+
shown.scale,
|
|
116
|
+
". Narrow the filter to see the rest."
|
|
117
|
+
]
|
|
118
|
+
}),
|
|
119
|
+
/* @__PURE__ */ jsxs(DndContext, {
|
|
120
|
+
sensors,
|
|
121
|
+
collisionDetection: collision,
|
|
122
|
+
onDragStart: start,
|
|
123
|
+
onDragEnd: end,
|
|
124
|
+
onDragCancel: release,
|
|
125
|
+
accessibility: {
|
|
126
|
+
announcements,
|
|
127
|
+
screenReaderInstructions: { draggable: "To move an event to another day, press Space, move it a day with the left and right arrow keys or a week with the up and down arrow keys, and press Space again to drop it there. Escape puts it back." }
|
|
128
|
+
},
|
|
129
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
130
|
+
className: "overflow-hidden rounded-lg border border-border",
|
|
131
|
+
children: [!week && /* @__PURE__ */ jsx("div", {
|
|
132
|
+
"aria-hidden": true,
|
|
133
|
+
className: "grid grid-cols-7 gap-px border-b border-border bg-border",
|
|
134
|
+
children: days.slice(0, 7).map((day) => /* @__PURE__ */ jsx("span", {
|
|
135
|
+
className: "bg-surface px-1.5 py-1 text-xs font-medium text-muted-foreground",
|
|
136
|
+
children: formatWeekday(Date.parse(`${day}T00:00:00Z`))
|
|
137
|
+
}, day))
|
|
138
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
139
|
+
className: cn("grid gap-px bg-border", week ? "grid-cols-1 sm:grid-cols-7" : "grid-cols-7"),
|
|
140
|
+
children: days.map((day) => {
|
|
141
|
+
const events = placed.get(day);
|
|
142
|
+
const visible = week ? events : events.slice(0, monthDayLimit);
|
|
143
|
+
return /* @__PURE__ */ jsx(CalendarDay, {
|
|
144
|
+
day,
|
|
145
|
+
count: events.length,
|
|
146
|
+
inMonth: week || day.startsWith(shown.day.slice(0, 7)),
|
|
147
|
+
today: day === today,
|
|
148
|
+
week,
|
|
149
|
+
more: visible.length < events.length && more(day, events.length - visible.length),
|
|
150
|
+
children: list.loading ? /* @__PURE__ */ jsx(Skeleton, { className: "h-4" }) : visible.map((row) => /* @__PURE__ */ jsx(CalendarEvent, {
|
|
151
|
+
model,
|
|
152
|
+
view,
|
|
153
|
+
display,
|
|
154
|
+
row,
|
|
155
|
+
day,
|
|
156
|
+
movable: movable && rowAllows(row.record, "update"),
|
|
157
|
+
dragged: () => dragged.current
|
|
158
|
+
}, row.key))
|
|
159
|
+
}, day);
|
|
160
|
+
})
|
|
161
|
+
})]
|
|
162
|
+
}), /* @__PURE__ */ jsx(DragOverlay, { children: active && /* @__PURE__ */ jsx(EventChip, {
|
|
163
|
+
model,
|
|
164
|
+
view,
|
|
165
|
+
display,
|
|
166
|
+
row: active.row,
|
|
167
|
+
day: active.day
|
|
168
|
+
}) })]
|
|
169
|
+
})
|
|
170
|
+
]
|
|
171
|
+
});
|
|
172
|
+
};
|
|
173
|
+
//#endregion
|
|
174
|
+
export { CalendarDisplay };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { addDays } from "./calendar-range.js";
|
|
2
|
+
//#region src/app/displays/calendar-dnd.ts
|
|
3
|
+
const dayPrefix = "day:";
|
|
4
|
+
/** The id a calendar's day is dropped on by. */
|
|
5
|
+
const dayId = (day) => `${dayPrefix}${day}`;
|
|
6
|
+
/** The id of an event on one of its days: a record over several days shows on each. */
|
|
7
|
+
const eventId = (key, day) => `${key}@${day}`;
|
|
8
|
+
/** The arrow keys move a picked-up event a day back or on (left and right) or a week (up and down), to that day's top. */
|
|
9
|
+
const dayCoordinates = (event, { context: { active, collisionRect, droppableRects, over } }) => {
|
|
10
|
+
const step = {
|
|
11
|
+
ArrowLeft: -1,
|
|
12
|
+
ArrowRight: 1,
|
|
13
|
+
ArrowUp: -7,
|
|
14
|
+
ArrowDown: 7
|
|
15
|
+
}[event.code];
|
|
16
|
+
const day = over ? String(over.id).slice(4) : active?.data.current?.day;
|
|
17
|
+
if (step === void 0 || !collisionRect || day === void 0) return void 0;
|
|
18
|
+
event.preventDefault();
|
|
19
|
+
const target = droppableRects.get(dayId(addDays(day, step)));
|
|
20
|
+
if (!target) return void 0;
|
|
21
|
+
return {
|
|
22
|
+
x: target.left + (target.width - collisionRect.width) / 2,
|
|
23
|
+
y: target.top + 4
|
|
24
|
+
};
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { dayCoordinates, dayId, eventId };
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { cn } from "../../lib/cn.js";
|
|
2
|
+
import { formatTime } from "../../format/date.js";
|
|
3
|
+
import { recordTitleField } from "../../live/model-helpers.js";
|
|
4
|
+
import { displayValue } from "../../live/display-value.js";
|
|
5
|
+
import { AppLink } from "../use-app-address.js";
|
|
6
|
+
import { useSeedRecord } from "../record-preview.js";
|
|
7
|
+
import { eventId } from "./calendar-dnd.js";
|
|
8
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { useDraggable } from "@dnd-kit/core";
|
|
10
|
+
//#region src/app/displays/calendar-event.tsx
|
|
11
|
+
/** The text an event shows: its `title` field, or the view's title, with the names of the records relations point at. */
|
|
12
|
+
const eventTitle = (model, view, display, row) => {
|
|
13
|
+
const name = display.title ?? recordTitleField(model, view);
|
|
14
|
+
const field = model.fields[name];
|
|
15
|
+
const text = row.labels[name] ?? (field ? displayValue(field, row.record[name], view?.fields[name]) : "");
|
|
16
|
+
return text === "" ? row.key : text;
|
|
17
|
+
};
|
|
18
|
+
const startTime = (model, display, row, day) => {
|
|
19
|
+
const value = row.record[display.start];
|
|
20
|
+
if (model.fields[display.start]?.type !== "timestamp" || typeof value !== "string") return void 0;
|
|
21
|
+
const ms = Date.parse(value);
|
|
22
|
+
return new Date(ms).toISOString().startsWith(day) ? formatTime(ms) : void 0;
|
|
23
|
+
};
|
|
24
|
+
/** An event as a one-line link to its record, its time first for a timestamp, styled as a small record card. */
|
|
25
|
+
const EventChip = ({ model, view, display, row, day }) => {
|
|
26
|
+
const seedRecord = useSeedRecord(model);
|
|
27
|
+
const time = startTime(model, display, row, day);
|
|
28
|
+
return /* @__PURE__ */ jsxs(AppLink, {
|
|
29
|
+
to: {
|
|
30
|
+
kind: "record",
|
|
31
|
+
resource: model.name,
|
|
32
|
+
key: row.key
|
|
33
|
+
},
|
|
34
|
+
onClick: () => seedRecord(row.record),
|
|
35
|
+
className: "flex min-w-0 items-baseline gap-1 rounded-md border border-border bg-background px-1.5 py-0.5 text-xs hover:border-border-strong hover:bg-surface focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary/30",
|
|
36
|
+
children: [time && /* @__PURE__ */ jsx("span", {
|
|
37
|
+
className: "shrink-0 tabular-nums text-muted-foreground",
|
|
38
|
+
children: time
|
|
39
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
40
|
+
className: "min-w-0 truncate",
|
|
41
|
+
children: eventTitle(model, view, display, row)
|
|
42
|
+
})]
|
|
43
|
+
});
|
|
44
|
+
};
|
|
45
|
+
/** An event on one of its days, picked up by dragging it or by Space on its link; one the user may not update stays put. */
|
|
46
|
+
const CalendarEvent = ({ movable, dragged, ...props }) => {
|
|
47
|
+
const { setNodeRef, setActivatorNodeRef, listeners, isDragging } = useDraggable({
|
|
48
|
+
id: eventId(props.row.key, props.day),
|
|
49
|
+
data: {
|
|
50
|
+
row: props.row,
|
|
51
|
+
day: props.day
|
|
52
|
+
},
|
|
53
|
+
disabled: !movable
|
|
54
|
+
});
|
|
55
|
+
return /* @__PURE__ */ jsx("div", {
|
|
56
|
+
ref: (element) => {
|
|
57
|
+
setNodeRef(element);
|
|
58
|
+
setActivatorNodeRef(element?.querySelector("a") ?? element);
|
|
59
|
+
},
|
|
60
|
+
...listeners,
|
|
61
|
+
onDragStart: (event) => event.preventDefault(),
|
|
62
|
+
onClickCapture: (event) => {
|
|
63
|
+
if (!dragged()) return;
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
event.stopPropagation();
|
|
66
|
+
},
|
|
67
|
+
className: cn("min-w-0", movable && "cursor-grab touch-manipulation", isDragging && "opacity-40"),
|
|
68
|
+
children: /* @__PURE__ */ jsx(EventChip, { ...props })
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
//#endregion
|
|
72
|
+
export { CalendarEvent, EventChip, eventTitle };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { printFilter, where } from "@protobase/schema";
|
|
2
|
+
//#region src/app/displays/calendar-range.ts
|
|
3
|
+
const dayMs = 864e5;
|
|
4
|
+
const dayOf = (ms) => new Date(ms).toISOString().slice(0, 10);
|
|
5
|
+
const startOf = (day) => Date.parse(`${day}T00:00:00Z`);
|
|
6
|
+
/** The day `count` days after `day`, or before it for a negative count. */
|
|
7
|
+
const addDays = (day, count) => dayOf(startOf(day) + count * dayMs);
|
|
8
|
+
const monday = (day) => addDays(day, -((new Date(startOf(day)).getUTCDay() + 6) % 7));
|
|
9
|
+
const monthOf = (day, step = 0) => {
|
|
10
|
+
const date = new Date(startOf(day));
|
|
11
|
+
return dayOf(Date.UTC(date.getUTCFullYear(), date.getUTCMonth() + step, 1));
|
|
12
|
+
};
|
|
13
|
+
const normalised = ({ scale, day }) => ({
|
|
14
|
+
scale,
|
|
15
|
+
day: scale === "month" ? monthOf(day) : monday(day)
|
|
16
|
+
});
|
|
17
|
+
/** The month or week a calendar shows: the address's, starting on the month's 1st or the week's Monday, else the month of `now`. */
|
|
18
|
+
const calendarView = (at, now) => normalised(at ?? {
|
|
19
|
+
scale: "month",
|
|
20
|
+
day: dayOf(now)
|
|
21
|
+
});
|
|
22
|
+
/** The month or week of `scale` that holds `now`. */
|
|
23
|
+
const todayView = (scale, now) => normalised({
|
|
24
|
+
scale,
|
|
25
|
+
day: dayOf(now)
|
|
26
|
+
});
|
|
27
|
+
/** The month or week before (-1) or after (1) `view`. */
|
|
28
|
+
const stepView = (view, step) => view.scale === "month" ? {
|
|
29
|
+
scale: "month",
|
|
30
|
+
day: monthOf(view.day, step)
|
|
31
|
+
} : {
|
|
32
|
+
scale: "week",
|
|
33
|
+
day: addDays(monday(view.day), 7 * step)
|
|
34
|
+
};
|
|
35
|
+
/** The days a calendar shows: whole weeks from the Monday on or before a month's 1st to the Sunday on or after its last day, or a week's seven. */
|
|
36
|
+
const gridDays = (view) => {
|
|
37
|
+
const first = monday(view.scale === "month" ? monthOf(view.day) : view.day);
|
|
38
|
+
const last = view.scale === "month" ? addDays(monday(addDays(monthOf(view.day, 1), -1)), 6) : addDays(first, 6);
|
|
39
|
+
const count = (startOf(last) - startOf(first)) / dayMs + 1;
|
|
40
|
+
return Array.from({ length: count }, (_, index) => addDays(first, index));
|
|
41
|
+
};
|
|
42
|
+
/** The days `view` shows, as a range from its first day to the day after its last. */
|
|
43
|
+
const rangeOf = (view) => {
|
|
44
|
+
const days = gridDays(view);
|
|
45
|
+
return {
|
|
46
|
+
from: days[0],
|
|
47
|
+
to: addDays(days.at(-1), 1)
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
const isTimestamp = (display, model) => model.fields[display.start]?.type === "timestamp";
|
|
51
|
+
/**
|
|
52
|
+
* The records on the days of `range`, as a list filter: those starting in it, and with an `end` those starting before
|
|
53
|
+
* it and ending in or after its first day. A timestamp's `end` is the instant it is over, so one ending at midnight is
|
|
54
|
+
* not on the day it starts. Each branch reads one indexed field.
|
|
55
|
+
*/
|
|
56
|
+
const rangeFilter = (display, model, range) => {
|
|
57
|
+
const timestamp = isTimestamp(display, model);
|
|
58
|
+
const bound = (day) => timestamp ? {
|
|
59
|
+
kind: "timestamp",
|
|
60
|
+
value: `${day}T00:00:00Z`,
|
|
61
|
+
span: {
|
|
62
|
+
start: 0,
|
|
63
|
+
end: 0
|
|
64
|
+
}
|
|
65
|
+
} : {
|
|
66
|
+
kind: "string",
|
|
67
|
+
value: day,
|
|
68
|
+
span: {
|
|
69
|
+
start: 0,
|
|
70
|
+
end: 0
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const within = where.and(where.gte(display.start, bound(range.from)), where.lt(display.start, bound(range.to)));
|
|
74
|
+
if (display.end === void 0) return printFilter(within);
|
|
75
|
+
const ending = timestamp ? where.gt(display.end, bound(range.from)) : where.gte(display.end, bound(range.from));
|
|
76
|
+
return printFilter(where.or(within, where.and(where.lt(display.start, bound(range.from)), ending)));
|
|
77
|
+
};
|
|
78
|
+
/** The list's filter, its search included, narrowed to the calendar's days. */
|
|
79
|
+
const withRange = (filter, range) => filter === "" ? range : `(${filter}) AND (${range})`;
|
|
80
|
+
const instantOf = (value) => {
|
|
81
|
+
if (typeof value !== "string") return void 0;
|
|
82
|
+
const ms = Date.parse(/^\d{4}-\d{2}-\d{2}$/.test(value) ? `${value}T00:00:00Z` : value);
|
|
83
|
+
return Number.isNaN(ms) ? void 0 : ms;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* The days of `days` a record is on: from its start's day through its end's, both included, where a timestamp `end`
|
|
87
|
+
* at midnight is over before that day starts; its start day alone without an end or with one before its start.
|
|
88
|
+
*/
|
|
89
|
+
const eventDays = (record, display, model, days) => {
|
|
90
|
+
const start = instantOf(record[display.start]);
|
|
91
|
+
if (start === void 0) return [];
|
|
92
|
+
const first = dayOf(start);
|
|
93
|
+
const end = display.end === void 0 ? void 0 : instantOf(record[display.end]);
|
|
94
|
+
const ending = end === void 0 ? first : dayOf(isTimestamp(display, model) ? end - 1 : end);
|
|
95
|
+
const last = ending < first ? first : ending;
|
|
96
|
+
return days.filter((day) => day >= first && day <= last);
|
|
97
|
+
};
|
|
98
|
+
/** The rows on each day of `days`, each day's in start order. */
|
|
99
|
+
const byDay = (rows, display, model, days) => {
|
|
100
|
+
const placed = new Map(days.map((day) => [day, []]));
|
|
101
|
+
const ordered = rows.toSorted((a, b) => (instantOf(a.record[display.start]) ?? 0) - (instantOf(b.record[display.start]) ?? 0));
|
|
102
|
+
for (const row of ordered) for (const day of eventDays(row.record, display, model, days)) placed.get(day).push(row);
|
|
103
|
+
return placed;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* The update that moves a record shown on `from` to `to`: its `start`, and its `end` when it has one, moved by the same
|
|
107
|
+
* number of days, a timestamp keeping its time of day.
|
|
108
|
+
*/
|
|
109
|
+
const movedPatch = (record, display, model, from, to) => {
|
|
110
|
+
const days = (startOf(to) - startOf(from)) / dayMs;
|
|
111
|
+
const timestamp = isTimestamp(display, model);
|
|
112
|
+
const moved = (value) => {
|
|
113
|
+
const ms = instantOf(value);
|
|
114
|
+
if (ms === void 0) return void 0;
|
|
115
|
+
return timestamp ? new Date(ms + days * dayMs).toISOString() : addDays(dayOf(ms), days);
|
|
116
|
+
};
|
|
117
|
+
const start = moved(record[display.start]);
|
|
118
|
+
const end = display.end === void 0 ? void 0 : moved(record[display.end]);
|
|
119
|
+
return {
|
|
120
|
+
...start !== void 0 && { [display.start]: start },
|
|
121
|
+
...end !== void 0 && { [display.end]: end }
|
|
122
|
+
};
|
|
123
|
+
};
|
|
124
|
+
//#endregion
|
|
125
|
+
export { addDays, byDay, calendarView, eventDays, gridDays, movedPatch, rangeFilter, rangeOf, stepView, todayView, withRange };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { recordKey, recordTitleField } from "../../live/model-helpers.js";
|
|
2
1
|
import { humanize } from "../../live/naming.js";
|
|
2
|
+
import { recordKey, recordTitleField } from "../../live/model-helpers.js";
|
|
3
3
|
import { listColumnNames } from "../../live/columns-from-model.js";
|
|
4
4
|
import { displayValue } from "../../live/display-value.js";
|
|
5
5
|
import { photosKey } from "../../live/row-photos.js";
|
|
@@ -62,7 +62,8 @@ const shownDisplay = (view, name) => {
|
|
|
62
62
|
const defaultIcons = {
|
|
63
63
|
cards: "cards",
|
|
64
64
|
map: "map",
|
|
65
|
-
board: "kanban"
|
|
65
|
+
board: "kanban",
|
|
66
|
+
calendar: "calendar"
|
|
66
67
|
};
|
|
67
68
|
const displayChoices = (view) => [{
|
|
68
69
|
name: tableDisplay,
|
|
@@ -132,6 +133,11 @@ const shownFields = (model, view, display) => {
|
|
|
132
133
|
...card.meta ?? []
|
|
133
134
|
].filter((name) => name !== void 0);
|
|
134
135
|
}
|
|
136
|
+
if (display.kind === "calendar") return [
|
|
137
|
+
display.start,
|
|
138
|
+
display.end,
|
|
139
|
+
display.title ?? recordTitleField(model, view)
|
|
140
|
+
].filter((name) => name !== void 0);
|
|
135
141
|
const { image, title, subtitle, amount, badge, meta, footer, flags } = cardFields(model, view, display);
|
|
136
142
|
return [
|
|
137
143
|
image,
|
|
@@ -2,9 +2,9 @@ import { cn } from "../../lib/cn.js";
|
|
|
2
2
|
import { Button } from "../../primitives/button.js";
|
|
3
3
|
import { Problem } from "../pages/problem.js";
|
|
4
4
|
import { humanize } from "../../live/naming.js";
|
|
5
|
-
import { useAdminMeta } from "../meta-gate.js";
|
|
6
5
|
import { ErrorBanner } from "../error-banner.js";
|
|
7
6
|
import { mapCards } from "./list-displays.js";
|
|
7
|
+
import { useAdminMeta } from "../meta-gate.js";
|
|
8
8
|
import { useDebounced } from "../../lib/use-debounced.js";
|
|
9
9
|
import { RecordCard } from "./record-card.js";
|
|
10
10
|
import { boundsAround, worldArea } from "../map/map-area.js";
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { Badge } from "../../primitives/badge.js";
|
|
2
2
|
import { CustomBoundary } from "../pages/custom-boundary.js";
|
|
3
|
-
import { recordKey } from "../../live/model-helpers.js";
|
|
4
3
|
import { humanize } from "../../live/naming.js";
|
|
5
|
-
import { useProjectUi } from "../pages/project-ui.js";
|
|
6
4
|
import { markdownExcerpt } from "../../markdown/markdown-excerpt.js";
|
|
7
5
|
import { isMarkdown } from "../../markdown/markdown-hint.js";
|
|
6
|
+
import { recordKey } from "../../live/model-helpers.js";
|
|
8
7
|
import { displayValue } from "../../live/display-value.js";
|
|
9
8
|
import { amountParts, cardFields, filledAmount, filledMeta } from "./list-displays.js";
|
|
10
9
|
import { AppLink } from "../use-app-address.js";
|
|
10
|
+
import { useProjectUi } from "../pages/project-ui.js";
|
|
11
11
|
import { shownWidget } from "../field-types/shown-widget.js";
|
|
12
12
|
import { WidgetPart } from "../field-types/widget-part.js";
|
|
13
13
|
import { Markdown } from "../../markdown/markdown.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useAdminMeta } from "../meta-gate.js";
|
|
2
1
|
import { columnFilter } from "./board-columns.js";
|
|
2
|
+
import { useAdminMeta } from "../meta-gate.js";
|
|
3
3
|
import { useLiveRows } from "../../live/use-live-rows.js";
|
|
4
4
|
import { useDisplayRows } from "./use-display-rows.js";
|
|
5
5
|
//#region src/app/displays/use-board-column.ts
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useClient } from "../../data/api-provider.js";
|
|
2
|
-
import { recordKey } from "../../live/model-helpers.js";
|
|
3
2
|
import { useToast } from "../../toasts/toast-provider.js";
|
|
3
|
+
import { recordKey } from "../../live/model-helpers.js";
|
|
4
4
|
import { refusalReason } from "./board-columns.js";
|
|
5
5
|
import { useRefreshResource } from "../pages/use-layout-data.js";
|
|
6
6
|
import { useState } from "react";
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { formatDate } from "../../format/date.js";
|
|
2
|
+
import { useClient } from "../../data/api-provider.js";
|
|
3
|
+
import { useToast } from "../../toasts/toast-provider.js";
|
|
4
|
+
import { recordKey } from "../../live/model-helpers.js";
|
|
5
|
+
import { refusalReason } from "./board-columns.js";
|
|
6
|
+
import { useRefreshResource } from "../pages/use-layout-data.js";
|
|
7
|
+
import { movedPatch } from "./calendar-range.js";
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
import { useMutation } from "@tanstack/react-query";
|
|
10
|
+
//#region src/app/displays/use-calendar-move.ts
|
|
11
|
+
/** The rows with each moving record's new dates, so an event shows on its new day at once. */
|
|
12
|
+
const movedRows = (rows, moves) => {
|
|
13
|
+
const patches = new Map(moves.map((move) => [move.key, move.patch]));
|
|
14
|
+
return rows.map((row) => patches.has(row.key) ? {
|
|
15
|
+
...row,
|
|
16
|
+
record: {
|
|
17
|
+
...row.record,
|
|
18
|
+
...patches.get(row.key)
|
|
19
|
+
}
|
|
20
|
+
} : row);
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Moving a calendar's event to another day: one update of the record with its `start` and `end` moved by the same days
|
|
24
|
+
* and the ETag it was listed with, as a named update action sends, so the resource's access rules and `validate`
|
|
25
|
+
* decide. The event shows on its new day at once; a refusal puts it back and says why, and either way every list of
|
|
26
|
+
* the resource reloads.
|
|
27
|
+
*/
|
|
28
|
+
const useCalendarMove = (model, display) => {
|
|
29
|
+
const client = useClient();
|
|
30
|
+
const toast = useToast();
|
|
31
|
+
const refresh = useRefreshResource();
|
|
32
|
+
const [moves, setMoves] = useState([]);
|
|
33
|
+
const settle = (key) => setMoves((current) => current.filter((move) => move.key !== key));
|
|
34
|
+
const mutation = useMutation({
|
|
35
|
+
mutationFn: ({ row, patch }) => {
|
|
36
|
+
const etag = typeof row.record.etag === "string" ? row.record.etag : "*";
|
|
37
|
+
return client.update(model.name, recordKey(model, row.record), patch, etag);
|
|
38
|
+
},
|
|
39
|
+
onMutate: ({ row, patch }) => setMoves((current) => [...current.filter((move) => move.key !== row.key), {
|
|
40
|
+
key: row.key,
|
|
41
|
+
patch
|
|
42
|
+
}]),
|
|
43
|
+
onError: (error, { row, to }) => {
|
|
44
|
+
settle(row.key);
|
|
45
|
+
toast.show({
|
|
46
|
+
state: "error",
|
|
47
|
+
title: `Could not move it to ${formatDate(Date.parse(`${to}T00:00:00Z`))}`,
|
|
48
|
+
description: refusalReason(error)
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
onSettled: async (_result, _error, { row }) => {
|
|
52
|
+
await refresh(model.name);
|
|
53
|
+
settle(row.key);
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
/** Moves the record of `row`, shown on the day `from`, to the day `to`. */
|
|
57
|
+
const move = (row, from, to) => mutation.mutate({
|
|
58
|
+
row,
|
|
59
|
+
patch: movedPatch(row.record, display, model, from, to),
|
|
60
|
+
to
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
moves,
|
|
64
|
+
move
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
//#endregion
|
|
68
|
+
export { movedRows, useCalendarMove };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useAdminMeta } from "../meta-gate.js";
|
|
2
|
+
import { useLiveRows } from "../../live/use-live-rows.js";
|
|
3
|
+
import { useDisplayRows } from "./use-display-rows.js";
|
|
4
|
+
import { rangeFilter, withRange } from "./calendar-range.js";
|
|
5
|
+
import { useEffect } from "react";
|
|
6
|
+
/**
|
|
7
|
+
* The records on the days a calendar shows, through the list API: the list's filter with the days added, in start
|
|
8
|
+
* order, page after page up to `calendarLimit`; `capped` when more remain. A change to the resource reloads them, as it
|
|
9
|
+
* does the table.
|
|
10
|
+
*/
|
|
11
|
+
const useCalendarRows = ({ model, display, range, fields, filter }) => {
|
|
12
|
+
const { resources, views } = useAdminMeta();
|
|
13
|
+
const rows = useLiveRows({
|
|
14
|
+
resources,
|
|
15
|
+
views,
|
|
16
|
+
model,
|
|
17
|
+
columns: fields,
|
|
18
|
+
filter: withRange(filter, rangeFilter(display, model, range)),
|
|
19
|
+
orderBy: `${display.start} asc`
|
|
20
|
+
});
|
|
21
|
+
const list = useDisplayRows(model, rows.page, rows.key, true);
|
|
22
|
+
const wanted = list.more && !list.loading && list.rows.length < 500;
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (wanted) list.loadMore();
|
|
25
|
+
}, [wanted, list.rows.length]);
|
|
26
|
+
const capped = list.rows.length > 500 || list.rows.length === 500 && list.more;
|
|
27
|
+
return {
|
|
28
|
+
rows: list.rows.slice(0, 500),
|
|
29
|
+
capped,
|
|
30
|
+
loading: list.loading,
|
|
31
|
+
error: list.error
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
//#endregion
|
|
35
|
+
export { useCalendarRows };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { keys } from "../../data/query-keys.js";
|
|
2
2
|
import { useClient } from "../../data/api-provider.js";
|
|
3
|
-
import { useAdminMeta } from "../meta-gate.js";
|
|
4
3
|
import { listPhotos, withPhotos } from "../../live/row-photos.js";
|
|
5
4
|
import { displayRow } from "./list-displays.js";
|
|
5
|
+
import { useAdminMeta } from "../meta-gate.js";
|
|
6
6
|
import { listLabels, withLabels } from "../../live/relation-labels.js";
|
|
7
7
|
import { areaFilter, requestZoom } from "../map/map-area.js";
|
|
8
8
|
import { keepPreviousData, useQuery } from "@tanstack/react-query";
|