@remit/ui 0.0.157 → 0.0.159
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/agenda-composer.tsx +1 -0
- package/src/components/event-detail.render.test.ts +31 -0
- package/src/components/event-detail.stories.tsx +17 -0
- package/src/components/event-detail.tsx +28 -19
- package/src/components/event-editor.render.test.ts +17 -1
- package/src/components/event-editor.stories.tsx +24 -0
- package/src/components/event-editor.tsx +20 -4
- package/src/components/nav-sidebar.tsx +3 -3
package/package.json
CHANGED
|
@@ -124,3 +124,34 @@ describe("EventDetail", () => {
|
|
|
124
124
|
assert.match(html, /Recent mail · Priya Natarajan/);
|
|
125
125
|
});
|
|
126
126
|
});
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* A host that cannot write yet — the calendar before its API lands — passes
|
|
130
|
+
* neither action. A button that leads nowhere is worse than no button, so the
|
|
131
|
+
* header has to come back without them rather than with two that do nothing.
|
|
132
|
+
*/
|
|
133
|
+
describe("EventDetail with nowhere to write", () => {
|
|
134
|
+
const readOnly = renderToString(
|
|
135
|
+
createElement(EventDetail, {
|
|
136
|
+
event,
|
|
137
|
+
calendar,
|
|
138
|
+
whenText: "Wednesday 10 June · 10:00 – 11:30",
|
|
139
|
+
onClose: () => undefined,
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
it("offers neither Edit nor Delete", () => {
|
|
144
|
+
assert.doesNotMatch(readOnly, />Edit</);
|
|
145
|
+
assert.doesNotMatch(readOnly, />Delete</);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("still names the event, its calendar and when it is", () => {
|
|
149
|
+
assert.match(readOnly, /Q3 roadmap review/);
|
|
150
|
+
assert.match(readOnly, /Northwind/);
|
|
151
|
+
assert.match(readOnly, /10:00 – 11:30/);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("keeps the way out of the pane", () => {
|
|
155
|
+
assert.match(readOnly, /aria-label="Close event"/);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
@@ -176,3 +176,20 @@ export const AmbiguousZone: Story = {
|
|
|
176
176
|
/>
|
|
177
177
|
),
|
|
178
178
|
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* The state the app itself renders while the calendar has no API to write
|
|
182
|
+
* through: the facts, the way back to the thread, and no control that would
|
|
183
|
+
* lead nowhere.
|
|
184
|
+
*/
|
|
185
|
+
export const NoActions: Story = {
|
|
186
|
+
render: () => (
|
|
187
|
+
<EventDetail
|
|
188
|
+
event={base}
|
|
189
|
+
calendar={calendar}
|
|
190
|
+
whenText="Wednesday 10 June · 10:00 – 11:30"
|
|
191
|
+
onClose={() => {}}
|
|
192
|
+
onOpenThread={() => {}}
|
|
193
|
+
/>
|
|
194
|
+
),
|
|
195
|
+
};
|
|
@@ -23,8 +23,13 @@ export interface EventDetailProps {
|
|
|
23
23
|
calendar: CalendarDescriptor;
|
|
24
24
|
/** Already formatted by the caller — the component owns no clock. */
|
|
25
25
|
whenText: string;
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Absent where the surface cannot write yet. A control that leads nowhere is
|
|
28
|
+
* worse than no control, so the header renders each one only when it has
|
|
29
|
+
* somewhere to go.
|
|
30
|
+
*/
|
|
31
|
+
onEdit?: () => void;
|
|
32
|
+
onDelete?: () => void;
|
|
28
33
|
/** Absent when the event was never born from a mail. */
|
|
29
34
|
onOpenThread?: () => void;
|
|
30
35
|
/** Puts the pane back to whatever it shows with nothing selected. */
|
|
@@ -171,23 +176,27 @@ export function EventDetail({
|
|
|
171
176
|
<span className="min-w-0 flex-1 truncate text-xs text-fg-muted">
|
|
172
177
|
{calendar.name} · {calendar.accountLabel}
|
|
173
178
|
</span>
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
179
|
+
{onEdit && (
|
|
180
|
+
<Button
|
|
181
|
+
variant="ghost"
|
|
182
|
+
size="sm"
|
|
183
|
+
icon={<Pencil className="size-3.5" />}
|
|
184
|
+
onClick={onEdit}
|
|
185
|
+
>
|
|
186
|
+
Edit
|
|
187
|
+
</Button>
|
|
188
|
+
)}
|
|
189
|
+
{onDelete && (
|
|
190
|
+
<Button
|
|
191
|
+
variant="ghost"
|
|
192
|
+
size="sm"
|
|
193
|
+
icon={<Trash2 className="size-3.5" />}
|
|
194
|
+
onClick={onDelete}
|
|
195
|
+
className="text-danger"
|
|
196
|
+
>
|
|
197
|
+
Delete
|
|
198
|
+
</Button>
|
|
199
|
+
)}
|
|
191
200
|
{onClose && (
|
|
192
201
|
<button
|
|
193
202
|
type="button"
|
|
@@ -78,11 +78,27 @@ describe("EventEditor", () => {
|
|
|
78
78
|
it("reveals the rest on request", () => {
|
|
79
79
|
const html = render({ expanded: true });
|
|
80
80
|
assert.match(html, /aria-label="Location"/);
|
|
81
|
-
assert.match(html, /aria-label="Guests"/);
|
|
82
81
|
assert.match(html, /aria-label="Repeat"/);
|
|
83
82
|
assert.match(html, /Fewer details/);
|
|
84
83
|
});
|
|
85
84
|
|
|
85
|
+
it("takes no guests until a surface says it can store them", () => {
|
|
86
|
+
assert.doesNotMatch(render({ expanded: true }), /aria-label="Guests"/);
|
|
87
|
+
assert.match(
|
|
88
|
+
render({ expanded: true, guestsEditable: true }),
|
|
89
|
+
/aria-label="Guests"/,
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("drops the calendar picker where the event cannot be moved", () => {
|
|
94
|
+
assert.match(render({}), /Northwind/);
|
|
95
|
+
assert.doesNotMatch(render({ calendarEditable: false }), /Northwind/);
|
|
96
|
+
assert.doesNotMatch(
|
|
97
|
+
render({ expanded: true, calendarEditable: false }),
|
|
98
|
+
/Northwind/,
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
86
102
|
it("drops the clock fields for an all-day entry", () => {
|
|
87
103
|
const html = render({ draft: { ...draft, allDay: true } });
|
|
88
104
|
assert.doesNotMatch(html, /aria-label="Start time"/);
|
|
@@ -64,9 +64,13 @@ const backwards: EventDraft = {
|
|
|
64
64
|
function Live({
|
|
65
65
|
startExpanded,
|
|
66
66
|
seed: initial = seed,
|
|
67
|
+
guestsEditable,
|
|
68
|
+
calendarEditable,
|
|
67
69
|
}: {
|
|
68
70
|
startExpanded: boolean;
|
|
69
71
|
seed?: EventDraft;
|
|
72
|
+
guestsEditable?: boolean;
|
|
73
|
+
calendarEditable?: boolean;
|
|
70
74
|
}) {
|
|
71
75
|
const [draft, setDraft] = useState(initial);
|
|
72
76
|
const [expanded, setExpanded] = useState(startExpanded);
|
|
@@ -78,6 +82,8 @@ function Live({
|
|
|
78
82
|
calendars={calendars}
|
|
79
83
|
expanded={expanded}
|
|
80
84
|
onToggleExpanded={() => setExpanded((open) => !open)}
|
|
85
|
+
guestsEditable={guestsEditable}
|
|
86
|
+
calendarEditable={calendarEditable}
|
|
81
87
|
onSave={() => {}}
|
|
82
88
|
onCancel={() => {}}
|
|
83
89
|
/>
|
|
@@ -99,6 +105,24 @@ export const EndBeforeStart: Story = {
|
|
|
99
105
|
render: () => <Live startExpanded={false} seed={backwards} />,
|
|
100
106
|
};
|
|
101
107
|
|
|
108
|
+
/**
|
|
109
|
+
* Guests are opt-in. A store with nowhere to put them leaves the field out, so
|
|
110
|
+
* the default form has none: a box that takes names and drops them is worse
|
|
111
|
+
* than no box, because the reader only finds out later.
|
|
112
|
+
*/
|
|
113
|
+
export const WithGuests: Story = {
|
|
114
|
+
render: () => <Live startExpanded guestsEditable />,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Editing an event that already exists. The collection a resource lives in is
|
|
119
|
+
* part of its address, so the calendar is read-only here rather than a picker
|
|
120
|
+
* that changes nothing.
|
|
121
|
+
*/
|
|
122
|
+
export const WithoutTheCalendarPicker: Story = {
|
|
123
|
+
render: () => <Live startExpanded calendarEditable={false} />,
|
|
124
|
+
};
|
|
125
|
+
|
|
102
126
|
/** All day takes the clock fields away, so there is no range left to reject. */
|
|
103
127
|
export const AllDay: Story = {
|
|
104
128
|
render: () => (
|
|
@@ -276,6 +276,18 @@ export interface EventEditorProps {
|
|
|
276
276
|
* a single instance reads the rule back instead of offering to change it.
|
|
277
277
|
*/
|
|
278
278
|
repeatEditable?: boolean;
|
|
279
|
+
/**
|
|
280
|
+
* Whether guests can be written here. Off unless a surface says otherwise: a
|
|
281
|
+
* box that takes names and drops them is worse than no box, because the
|
|
282
|
+
* reader only finds out later, from an event with nobody on it.
|
|
283
|
+
*/
|
|
284
|
+
guestsEditable?: boolean;
|
|
285
|
+
/**
|
|
286
|
+
* Whether the event can be moved to another calendar. An edit cannot: the
|
|
287
|
+
* collection a resource lives in is part of its address, so a picker that
|
|
288
|
+
* changes it and saves nothing is a control that lies.
|
|
289
|
+
*/
|
|
290
|
+
calendarEditable?: boolean;
|
|
279
291
|
/** Opens the custom-rule editor. Absent leaves the derived choices alone. */
|
|
280
292
|
onCustomRepeat?: () => void;
|
|
281
293
|
/** Grows the controls for a rail. */
|
|
@@ -302,6 +314,8 @@ export function EventEditor({
|
|
|
302
314
|
saveLabel = "Add",
|
|
303
315
|
header,
|
|
304
316
|
repeatEditable = true,
|
|
317
|
+
guestsEditable = false,
|
|
318
|
+
calendarEditable = true,
|
|
305
319
|
onCustomRepeat,
|
|
306
320
|
touch,
|
|
307
321
|
className,
|
|
@@ -363,10 +377,12 @@ export function EventEditor({
|
|
|
363
377
|
{header}
|
|
364
378
|
<EventField label="Title">{title}</EventField>
|
|
365
379
|
<EventField label="When">{when}</EventField>
|
|
366
|
-
|
|
380
|
+
{calendarEditable && (
|
|
381
|
+
<EventField label="Calendar">{calendarPicker}</EventField>
|
|
382
|
+
)}
|
|
367
383
|
<EventField label="Repeat">{repeat}</EventField>
|
|
368
384
|
<EventField label="Location">{location}</EventField>
|
|
369
|
-
<EventField label="Guests">{guests}</EventField>
|
|
385
|
+
{guestsEditable && <EventField label="Guests">{guests}</EventField>}
|
|
370
386
|
<EventField label="Notes">{notes}</EventField>
|
|
371
387
|
{actions}
|
|
372
388
|
</div>
|
|
@@ -381,7 +397,7 @@ export function EventEditor({
|
|
|
381
397
|
|
|
382
398
|
{title}
|
|
383
399
|
{when}
|
|
384
|
-
{calendarPicker}
|
|
400
|
+
{calendarEditable && calendarPicker}
|
|
385
401
|
|
|
386
402
|
{onToggleExpanded && (
|
|
387
403
|
<button
|
|
@@ -405,7 +421,7 @@ export function EventEditor({
|
|
|
405
421
|
{!folded && (
|
|
406
422
|
<div className="flex flex-col gap-2 border-t border-line pt-3">
|
|
407
423
|
{location}
|
|
408
|
-
{guests}
|
|
424
|
+
{guestsEditable && guests}
|
|
409
425
|
{repeat}
|
|
410
426
|
{notes}
|
|
411
427
|
</div>
|
|
@@ -560,9 +560,9 @@ export interface NavSidebarProps
|
|
|
560
560
|
*/
|
|
561
561
|
linkComponent?: NavLinkComponent;
|
|
562
562
|
/**
|
|
563
|
-
* Whether the calendar destination sits under the daily brief.
|
|
564
|
-
*
|
|
565
|
-
*
|
|
563
|
+
* Whether the calendar destination sits under the daily brief. Hidden by
|
|
564
|
+
* default so a host with no calendar route behind it does not offer an entry
|
|
565
|
+
* that leads nowhere.
|
|
566
566
|
*/
|
|
567
567
|
calendarNav?: "hidden" | "shown";
|
|
568
568
|
/** Every saved query, most recently saved first. See `SavedSearchesGroup`. */
|