@remit/web-client 0.0.313 → 0.0.314
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/package.json +1 -1
- package/src/components/calendar/CalendarComposePane.tsx +15 -2
- package/src/components/calendar/CalendarWaiting.tsx +3 -0
- package/src/components/calendar/ChangeCalendarSuggestion.stories.tsx +182 -0
- package/src/components/calendar/ChangeCalendarSuggestion.tsx +159 -0
- package/src/components/calendar/PendingSuggestions.tsx +35 -5
- package/src/components/calendar/calendar-story-server.tsx +5 -0
- package/src/components/calendar/change-suggestion-first.render.test.ts +299 -0
- package/src/components/calendar/pending-suggestions.render.test.ts +68 -3
- package/src/hooks/calendar/draft.ts +1 -1
- package/src/hooks/calendar/index.ts +7 -0
- package/src/hooks/calendar/suggestion-draft.ts +77 -0
- package/src/hooks/calendar/useCalendarSuggestions.ts +59 -1
- package/src/hooks/intelligence-calendar.render.test.ts +135 -0
- package/src/hooks/useIntelligenceCalendar.ts +36 -5
- package/src/lib/calendar-suggestion.test.ts +10 -1
- package/src/lib/calendar-suggestion.ts +12 -2
- package/src/routeTree.gen.ts +23 -0
- package/src/routes/calendar/$view.$date/suggestion.$suggestionId.tsx +22 -0
- package/src/routes/calendar/$view.$date.tsx +9 -3
- package/src/routes/calendar/-calendar-tree.test.ts +18 -0
- package/src/routing/calendar.ts +36 -1
- package/src/routing/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remit/web-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.314",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Remit web client, published as composable primitives — the app shell, auth shells, and runtime config. A distributor imports what it composes and bundles it.",
|
|
6
6
|
"exports": {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
EventEditor,
|
|
7
7
|
EventEditorPane,
|
|
8
8
|
} from "@remit/ui";
|
|
9
|
-
import { AlertCircle } from "lucide-react";
|
|
9
|
+
import { AlertCircle, AlertTriangle } from "lucide-react";
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
12
|
* Writing an event, and editing one.
|
|
@@ -25,6 +25,7 @@ export interface CalendarComposePaneProps {
|
|
|
25
25
|
title: string;
|
|
26
26
|
/** The one line of context the title cannot carry — which day, which series. */
|
|
27
27
|
subtitle?: string;
|
|
28
|
+
notice?: string;
|
|
28
29
|
calendars: CalendarDescriptor[];
|
|
29
30
|
draft: EventDraft;
|
|
30
31
|
onChange: (draft: EventDraft) => void;
|
|
@@ -58,6 +59,7 @@ export interface CalendarComposePaneProps {
|
|
|
58
59
|
export function CalendarComposePane({
|
|
59
60
|
title,
|
|
60
61
|
subtitle,
|
|
62
|
+
notice = "",
|
|
61
63
|
calendars,
|
|
62
64
|
draft,
|
|
63
65
|
onChange,
|
|
@@ -83,8 +85,19 @@ export function CalendarComposePane({
|
|
|
83
85
|
onSave={onSave}
|
|
84
86
|
onCancel={onCancel}
|
|
85
87
|
header={
|
|
86
|
-
problem === "" &&
|
|
88
|
+
problem === "" &&
|
|
89
|
+
notice === "" &&
|
|
90
|
+
clashes === undefined ? undefined : (
|
|
87
91
|
<div className="space-y-2">
|
|
92
|
+
{notice !== "" && (
|
|
93
|
+
<p className="flex items-start gap-2 rounded-md border border-warning/40 bg-warning-soft px-3 py-2 text-sm text-fg">
|
|
94
|
+
<AlertTriangle
|
|
95
|
+
className="mt-0.5 size-4 shrink-0 text-warning"
|
|
96
|
+
aria-hidden="true"
|
|
97
|
+
/>
|
|
98
|
+
<span className="min-w-0 flex-1 break-words">{notice}</span>
|
|
99
|
+
</p>
|
|
100
|
+
)}
|
|
88
101
|
{problem !== "" && (
|
|
89
102
|
<div
|
|
90
103
|
role="alert"
|
|
@@ -28,6 +28,7 @@ export interface CalendarWaitingProps {
|
|
|
28
28
|
/** Why nothing can be added yet, with the way past it; holds Add. */
|
|
29
29
|
addBlocked?: ReactNode;
|
|
30
30
|
onAdd: (suggestionId: string) => void;
|
|
31
|
+
onChangeFirst?: (suggestionId: string) => void;
|
|
31
32
|
onDismiss: (suggestionId: string) => void;
|
|
32
33
|
}
|
|
33
34
|
|
|
@@ -38,6 +39,7 @@ export function CalendarWaiting({
|
|
|
38
39
|
reportHref,
|
|
39
40
|
addBlocked,
|
|
40
41
|
onAdd,
|
|
42
|
+
onChangeFirst,
|
|
41
43
|
onDismiss,
|
|
42
44
|
}: CalendarWaitingProps) {
|
|
43
45
|
const top = suggestions[0];
|
|
@@ -83,6 +85,7 @@ export function CalendarWaiting({
|
|
|
83
85
|
suggestion={top.suggestion}
|
|
84
86
|
whenText={top.whenText}
|
|
85
87
|
onAdd={() => onAdd(top.suggestion.id)}
|
|
88
|
+
onReview={onChangeFirst && (() => onChangeFirst(top.suggestion.id))}
|
|
86
89
|
onDismiss={() => onDismiss(top.suggestion.id)}
|
|
87
90
|
addLabel="Add to calendar"
|
|
88
91
|
busy={busy}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { RemitImapCalendarSuggestionResponse } from "@remit/api-http-client/types.gen.ts";
|
|
2
|
+
import type { Meta, StoryObj } from "@storybook/react-vite";
|
|
3
|
+
import { expect, userEvent, within } from "storybook/test";
|
|
4
|
+
import { useCalendarNavigation, useChangingSuggestionId } from "@/routing";
|
|
5
|
+
import { ChangeCalendarSuggestion } from "./ChangeCalendarSuggestion";
|
|
6
|
+
import { calendars, STORY_WEEK } from "./calendar-story-fixtures";
|
|
7
|
+
import {
|
|
8
|
+
type CalendarServer,
|
|
9
|
+
CalendarStory,
|
|
10
|
+
json,
|
|
11
|
+
} from "./calendar-story-server";
|
|
12
|
+
import { PendingSuggestions } from "./PendingSuggestions";
|
|
13
|
+
|
|
14
|
+
const SUGGESTION = "cccccccc-cccc-4ccc-8ccc-cccccccccccc";
|
|
15
|
+
|
|
16
|
+
const suggestion = (
|
|
17
|
+
overrides: Partial<RemitImapCalendarSuggestionResponse> = {},
|
|
18
|
+
): RemitImapCalendarSuggestionResponse => ({
|
|
19
|
+
suggestionId: SUGGESTION,
|
|
20
|
+
accountConfigId: "cfg-1",
|
|
21
|
+
messageId: "msg-1",
|
|
22
|
+
bodyPartId: "part-1",
|
|
23
|
+
icalUid: "uid-kickoff",
|
|
24
|
+
sequence: 0,
|
|
25
|
+
method: "Request",
|
|
26
|
+
source: "IcalendarPart",
|
|
27
|
+
state: "Pending",
|
|
28
|
+
summary: "Billing migration kickoff",
|
|
29
|
+
dtStart: "2026-06-11T14:00:00+02:00",
|
|
30
|
+
dtEnd: "2026-06-11T15:00:00+02:00",
|
|
31
|
+
allDay: false,
|
|
32
|
+
location: "Room Noord",
|
|
33
|
+
organizer: "priya@example.invalid",
|
|
34
|
+
zoneCertainty: "Explicit",
|
|
35
|
+
acceptedCalendarObjectId: "",
|
|
36
|
+
createdAt: 0,
|
|
37
|
+
updatedAt: 0,
|
|
38
|
+
...overrides,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const serverWith =
|
|
42
|
+
(pending: RemitImapCalendarSuggestionResponse[]): CalendarServer =>
|
|
43
|
+
(request) => {
|
|
44
|
+
const url = new URL(request.url);
|
|
45
|
+
if (url.pathname.endsWith("/calendars")) return json({ items: calendars });
|
|
46
|
+
if (url.pathname.endsWith("/calendar-suggestions"))
|
|
47
|
+
return json({ items: pending });
|
|
48
|
+
if (url.pathname.endsWith("/accept"))
|
|
49
|
+
return json({ ...pending[0], state: "Accepted" });
|
|
50
|
+
return json({ items: [] });
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function WaitingThenEditor() {
|
|
54
|
+
const { changeSuggestionFirst, closeEvent } = useCalendarNavigation();
|
|
55
|
+
const changing = useChangingSuggestionId();
|
|
56
|
+
return (
|
|
57
|
+
<div className="flex h-dvh bg-canvas">
|
|
58
|
+
<PendingSuggestions onChangeFirst={changeSuggestionFirst} />
|
|
59
|
+
<div className="min-w-0 flex-1 border-l border-line">
|
|
60
|
+
{changing !== undefined && (
|
|
61
|
+
<ChangeCalendarSuggestion
|
|
62
|
+
suggestionId={changing}
|
|
63
|
+
onClose={closeEvent}
|
|
64
|
+
/>
|
|
65
|
+
)}
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function Editor({
|
|
72
|
+
pending,
|
|
73
|
+
openedFrom = "address",
|
|
74
|
+
}: {
|
|
75
|
+
pending: RemitImapCalendarSuggestionResponse[];
|
|
76
|
+
openedFrom?: "address" | "card";
|
|
77
|
+
}) {
|
|
78
|
+
if (openedFrom === "card")
|
|
79
|
+
return (
|
|
80
|
+
<CalendarStory
|
|
81
|
+
entry={STORY_WEEK}
|
|
82
|
+
server={serverWith(pending)}
|
|
83
|
+
pane={<WaitingThenEditor />}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
return (
|
|
87
|
+
<div className="h-dvh max-w-2xl border-l border-line bg-canvas">
|
|
88
|
+
<CalendarStory
|
|
89
|
+
entry={`${STORY_WEEK}/suggestion/${SUGGESTION}`}
|
|
90
|
+
server={serverWith(pending)}
|
|
91
|
+
pane={
|
|
92
|
+
<ChangeCalendarSuggestion
|
|
93
|
+
suggestionId={SUGGESTION}
|
|
94
|
+
onClose={() => undefined}
|
|
95
|
+
/>
|
|
96
|
+
}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const meta: Meta<typeof Editor> = {
|
|
103
|
+
title: "Playground/Shipped/Calendar/Change a suggestion first",
|
|
104
|
+
component: Editor,
|
|
105
|
+
parameters: { layout: "fullscreen" },
|
|
106
|
+
};
|
|
107
|
+
export default meta;
|
|
108
|
+
|
|
109
|
+
type Story = StoryObj<typeof Editor>;
|
|
110
|
+
|
|
111
|
+
export const SeededFromTheMail: Story = {
|
|
112
|
+
args: { pending: [suggestion()] },
|
|
113
|
+
play: async ({ canvasElement }) => {
|
|
114
|
+
const canvas = within(canvasElement);
|
|
115
|
+
await expect(await canvas.findByLabelText("Title")).toHaveValue(
|
|
116
|
+
"Billing migration kickoff",
|
|
117
|
+
);
|
|
118
|
+
await expect(canvas.getByLabelText("Location")).toHaveValue("Room Noord");
|
|
119
|
+
await expect(canvas.getByLabelText("Date")).toHaveValue("2026-06-11");
|
|
120
|
+
await expect(canvas.getByRole("button", { name: "Add" })).toBeVisible();
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export const ZoneNobodyCouldPlace: Story = {
|
|
125
|
+
args: {
|
|
126
|
+
pending: [
|
|
127
|
+
suggestion({
|
|
128
|
+
dtStart: "2026-06-11T09:00:00+00:00",
|
|
129
|
+
dtEnd: "2026-06-11T10:00:00+00:00",
|
|
130
|
+
zoneCertainty: "Ambiguous",
|
|
131
|
+
}),
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
play: async ({ canvasElement }) => {
|
|
135
|
+
const canvas = within(canvasElement);
|
|
136
|
+
await expect(await canvas.findByLabelText("Start time")).toHaveValue(
|
|
137
|
+
"09:00",
|
|
138
|
+
);
|
|
139
|
+
await expect(
|
|
140
|
+
canvas.getByText(/named a time zone nothing could resolve/),
|
|
141
|
+
).toBeVisible();
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
export const NoLongerWaiting: Story = {
|
|
146
|
+
args: { pending: [] },
|
|
147
|
+
play: async ({ canvasElement }) => {
|
|
148
|
+
const canvas = within(canvasElement);
|
|
149
|
+
await expect(
|
|
150
|
+
await canvas.findByText(/no longer waiting on you/),
|
|
151
|
+
).toBeVisible();
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
export const SaysWhatIsMissing: Story = {
|
|
156
|
+
args: { pending: [suggestion()] },
|
|
157
|
+
play: async ({ canvasElement }) => {
|
|
158
|
+
const canvas = within(canvasElement);
|
|
159
|
+
const title = await canvas.findByLabelText("Title");
|
|
160
|
+
await userEvent.clear(title);
|
|
161
|
+
await userEvent.click(canvas.getByRole("button", { name: "Add" }));
|
|
162
|
+
await expect(await canvas.findByRole("alert")).toHaveTextContent(
|
|
163
|
+
"Give the event a title",
|
|
164
|
+
);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
export const OpenedFromTheWaitingCard: Story = {
|
|
169
|
+
args: { pending: [suggestion()], openedFrom: "card" },
|
|
170
|
+
globals: { viewport: { value: "desktop" } },
|
|
171
|
+
play: async ({ canvasElement }) => {
|
|
172
|
+
const canvas = within(canvasElement);
|
|
173
|
+
await userEvent.click(
|
|
174
|
+
await canvas.findByRole("button", { name: "Change first" }),
|
|
175
|
+
);
|
|
176
|
+
await expect(await canvas.findByLabelText("Title")).toHaveValue(
|
|
177
|
+
"Billing migration kickoff",
|
|
178
|
+
);
|
|
179
|
+
await expect(canvas.getByLabelText("Location")).toHaveValue("Room Noord");
|
|
180
|
+
await expect(canvas.getByLabelText("Date")).toHaveValue("2026-06-11");
|
|
181
|
+
},
|
|
182
|
+
};
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { RemitImapCalendarSuggestionResponse } from "@remit/api-http-client/types.gen.ts";
|
|
2
|
+
import {
|
|
3
|
+
CalendarFailureNote,
|
|
4
|
+
type EventDraft,
|
|
5
|
+
EventEditorPane,
|
|
6
|
+
} from "@remit/ui";
|
|
7
|
+
import { useEffect, useState } from "react";
|
|
8
|
+
import { CalendarComposePane } from "@/components/calendar/CalendarComposePane";
|
|
9
|
+
import { EmptyState } from "@/components/ui/EmptyState";
|
|
10
|
+
import {
|
|
11
|
+
acceptEditedInputFromDrafts,
|
|
12
|
+
anchorZoneFor,
|
|
13
|
+
deviceTimeZone,
|
|
14
|
+
draftFromSuggestion,
|
|
15
|
+
UNZONED_CALENDAR,
|
|
16
|
+
useCalendarSuggestionAnswers,
|
|
17
|
+
useCalendars,
|
|
18
|
+
useDraftClashes,
|
|
19
|
+
usePendingCalendarSuggestion,
|
|
20
|
+
} from "@/hooks/calendar";
|
|
21
|
+
import { calendarReportHref } from "@/lib/calendar-report";
|
|
22
|
+
import { suggestionWhen } from "@/lib/calendar-suggestion";
|
|
23
|
+
|
|
24
|
+
const TITLE = "Add from mail";
|
|
25
|
+
|
|
26
|
+
const AMBIGUOUS_NOTICE =
|
|
27
|
+
"The invitation named a time zone nothing could resolve. The time below is as the mail wrote it — check it before adding.";
|
|
28
|
+
|
|
29
|
+
export interface ChangeCalendarSuggestionProps {
|
|
30
|
+
suggestionId: string;
|
|
31
|
+
onClose: () => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function ChangeCalendarSuggestion({
|
|
35
|
+
suggestionId,
|
|
36
|
+
onClose,
|
|
37
|
+
}: ChangeCalendarSuggestionProps) {
|
|
38
|
+
const {
|
|
39
|
+
suggestion: pending,
|
|
40
|
+
isLoading,
|
|
41
|
+
error,
|
|
42
|
+
} = usePendingCalendarSuggestion(suggestionId);
|
|
43
|
+
const found = pending ?? undefined;
|
|
44
|
+
const [held, setHeld] = useState<
|
|
45
|
+
RemitImapCalendarSuggestionResponse | undefined
|
|
46
|
+
>(undefined);
|
|
47
|
+
if (found !== undefined && held === undefined) setHeld(found);
|
|
48
|
+
const suggestion = held ?? found;
|
|
49
|
+
|
|
50
|
+
if (suggestion !== undefined)
|
|
51
|
+
return (
|
|
52
|
+
<SuggestionEditor
|
|
53
|
+
key={suggestion.suggestionId}
|
|
54
|
+
suggestion={suggestion}
|
|
55
|
+
onClose={onClose}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (error !== null) {
|
|
60
|
+
const text = "Couldn't read what your mail is waiting on.";
|
|
61
|
+
return (
|
|
62
|
+
<EventEditorPane title={TITLE} onClose={onClose}>
|
|
63
|
+
<CalendarFailureNote
|
|
64
|
+
text={`${text} Close this and try again.`}
|
|
65
|
+
reportHref={calendarReportHref(text)}
|
|
66
|
+
className="m-row-inset"
|
|
67
|
+
/>
|
|
68
|
+
</EventEditorPane>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<EventEditorPane title={TITLE} onClose={onClose}>
|
|
74
|
+
<EmptyState
|
|
75
|
+
message={
|
|
76
|
+
isLoading
|
|
77
|
+
? "Reading the suggestion…"
|
|
78
|
+
: "This suggestion is no longer waiting on you. It was answered, or a later message replaced it."
|
|
79
|
+
}
|
|
80
|
+
/>
|
|
81
|
+
</EventEditorPane>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
interface SuggestionEditorProps {
|
|
86
|
+
suggestion: RemitImapCalendarSuggestionResponse;
|
|
87
|
+
onClose: () => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function SuggestionEditor({ suggestion, onClose }: SuggestionEditorProps) {
|
|
91
|
+
const {
|
|
92
|
+
calendars: held,
|
|
93
|
+
defaultCalendarId,
|
|
94
|
+
timeZoneByCalendarId,
|
|
95
|
+
} = useCalendars();
|
|
96
|
+
const calendars = held.filter((calendar) => !calendar.readOnly);
|
|
97
|
+
const answers = useCalendarSuggestionAnswers();
|
|
98
|
+
const [seed] = useState<EventDraft>(() =>
|
|
99
|
+
draftFromSuggestion(suggestion, "", deviceTimeZone()),
|
|
100
|
+
);
|
|
101
|
+
const [draft, setDraft] = useState<EventDraft>(seed);
|
|
102
|
+
const [problem, setProblem] = useState("");
|
|
103
|
+
const clashes = useDraftClashes(draft);
|
|
104
|
+
|
|
105
|
+
const initialCalendarId = calendars.some(
|
|
106
|
+
(calendar) => calendar.id === defaultCalendarId,
|
|
107
|
+
)
|
|
108
|
+
? defaultCalendarId
|
|
109
|
+
: (calendars[0]?.id ?? "");
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
if (initialCalendarId === "") return;
|
|
112
|
+
setDraft((current) =>
|
|
113
|
+
current.calendarId === ""
|
|
114
|
+
? { ...current, calendarId: initialCalendarId }
|
|
115
|
+
: current,
|
|
116
|
+
);
|
|
117
|
+
}, [initialCalendarId]);
|
|
118
|
+
|
|
119
|
+
const save = () => {
|
|
120
|
+
const built = acceptEditedInputFromDrafts(suggestion, seed, draft, {
|
|
121
|
+
clock: deviceTimeZone(),
|
|
122
|
+
anchor: anchorZoneFor(
|
|
123
|
+
timeZoneByCalendarId[draft.calendarId] ?? UNZONED_CALENDAR,
|
|
124
|
+
),
|
|
125
|
+
});
|
|
126
|
+
if (!built.ok) {
|
|
127
|
+
setProblem(built.problem);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
setProblem("");
|
|
131
|
+
void answers
|
|
132
|
+
.acceptEdited(suggestion.suggestionId, built.input)
|
|
133
|
+
.then((answer) => {
|
|
134
|
+
if (answer.kind === "answered") {
|
|
135
|
+
onClose();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
setProblem(`Couldn't add this to your calendar: ${answer.message}`);
|
|
139
|
+
});
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<CalendarComposePane
|
|
144
|
+
title={TITLE}
|
|
145
|
+
subtitle={suggestionWhen(suggestion)}
|
|
146
|
+
notice={suggestion.zoneCertainty === "Ambiguous" ? AMBIGUOUS_NOTICE : ""}
|
|
147
|
+
calendars={calendars}
|
|
148
|
+
draft={draft}
|
|
149
|
+
onChange={setDraft}
|
|
150
|
+
problem={problem}
|
|
151
|
+
saveLabel="Add"
|
|
152
|
+
isSaving={answers.isAnswering}
|
|
153
|
+
repeatEditable={false}
|
|
154
|
+
clashes={clashes}
|
|
155
|
+
onSave={save}
|
|
156
|
+
onCancel={onClose}
|
|
157
|
+
/>
|
|
158
|
+
);
|
|
159
|
+
}
|
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
usePendingCalendarSuggestions,
|
|
8
8
|
} from "@/hooks/calendar";
|
|
9
9
|
import { calendarReportHref } from "@/lib/calendar-report";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
suggestionDate,
|
|
12
|
+
suggestionWhen,
|
|
13
|
+
toEventSuggestion,
|
|
14
|
+
} from "@/lib/calendar-suggestion";
|
|
11
15
|
import { calendarUnavailable, calendarWriteGate } from "./CalendarUnavailable";
|
|
12
16
|
import { CalendarWaiting } from "./CalendarWaiting";
|
|
13
17
|
|
|
@@ -19,7 +23,11 @@ const refusal = (what: string, answer: SuggestionAnswer): string =>
|
|
|
19
23
|
* through it. Nothing is drawn when nothing is waiting: an empty column beside
|
|
20
24
|
* the grid is width the week could have had.
|
|
21
25
|
*/
|
|
22
|
-
export
|
|
26
|
+
export interface PendingSuggestionsProps {
|
|
27
|
+
onChangeFirst: (suggestionId: string, date: string) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function PendingSuggestions({ onChangeFirst }: PendingSuggestionsProps) {
|
|
23
31
|
const { suggestions, error } = usePendingCalendarSuggestions();
|
|
24
32
|
const calendars = useCalendars();
|
|
25
33
|
const { defaultCalendarId } = calendars;
|
|
@@ -45,6 +53,23 @@ export function PendingSuggestions() {
|
|
|
45
53
|
readFailure || failure || "pending invitations could not be answered",
|
|
46
54
|
);
|
|
47
55
|
|
|
56
|
+
const changeFirst = (suggestionId: string) => {
|
|
57
|
+
const suggestion = suggestions.find(
|
|
58
|
+
(candidate) => candidate.suggestionId === suggestionId,
|
|
59
|
+
);
|
|
60
|
+
if (suggestion) onChangeFirst(suggestionId, suggestionDate(suggestion));
|
|
61
|
+
};
|
|
62
|
+
const unplaced = (suggestionId: string): boolean => {
|
|
63
|
+
const suggestion = suggestions.find(
|
|
64
|
+
(candidate) => candidate.suggestionId === suggestionId,
|
|
65
|
+
);
|
|
66
|
+
return (
|
|
67
|
+
suggestion?.zoneCertainty === "Ambiguous" &&
|
|
68
|
+
suggestion.method !== "Cancel"
|
|
69
|
+
);
|
|
70
|
+
};
|
|
71
|
+
const topIsCancellation = suggestions[0]?.method === "Cancel";
|
|
72
|
+
|
|
48
73
|
const answer = (what: string, request: () => Promise<SuggestionAnswer>) => {
|
|
49
74
|
setFailure("");
|
|
50
75
|
void request().then((result) => setFailure(refusal(what, result)));
|
|
@@ -57,11 +82,16 @@ export function PendingSuggestions() {
|
|
|
57
82
|
failure={readFailure || failure}
|
|
58
83
|
reportHref={reportHref}
|
|
59
84
|
addBlocked={calendarUnavailable(calendarWriteGate(calendars), reportHref)}
|
|
60
|
-
onAdd={(suggestionId) =>
|
|
85
|
+
onAdd={(suggestionId) => {
|
|
86
|
+
if (unplaced(suggestionId)) {
|
|
87
|
+
changeFirst(suggestionId);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
61
90
|
answer("add this to your calendar", () =>
|
|
62
91
|
answers.accept(suggestionId, defaultCalendarId),
|
|
63
|
-
)
|
|
64
|
-
}
|
|
92
|
+
);
|
|
93
|
+
}}
|
|
94
|
+
onChangeFirst={topIsCancellation ? undefined : changeFirst}
|
|
65
95
|
onDismiss={(suggestionId) =>
|
|
66
96
|
answer("dismiss this", () => answers.dismiss(suggestionId, false))
|
|
67
97
|
}
|
|
@@ -84,6 +84,11 @@ function storyRouter(
|
|
|
84
84
|
path: "new",
|
|
85
85
|
component: leaf,
|
|
86
86
|
}),
|
|
87
|
+
createRoute({
|
|
88
|
+
getParentRoute: () => viewRoute,
|
|
89
|
+
path: "suggestion/$suggestionId",
|
|
90
|
+
component: leaf,
|
|
91
|
+
}),
|
|
87
92
|
eventRoute.addChildren([
|
|
88
93
|
createRoute({
|
|
89
94
|
getParentRoute: () => eventRoute,
|