Skip to content

Commit 3d528bf

Browse files
fix(plugin-calendar): no resize handle in the week/day views without an end field; calendar-view forwards a host endEditable (#29292)
<!-- claude-projects-attribution: {"github_login":"zhuangjianguo"} --> _Requested by **Steedos** · [project thread](https://claude.ai/code/project/chan_018ETKaNwMZCNRYD7D8Q9Ubb?thread=cmsg_018ETKaNwMZCNRYD7D8Q9UbbWFEE4TZR1T1myKpHHno2iQ)_ Fixes #29197 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么**:日历视图没有配置结束日期字段(`endDateField`)时,周视图和日视图的事件上不再画任何调整大小的拖拽柄,顶部「调整开始」柄也去掉了;拖动事件本身照旧可以移动。另外,按页面节点编写的 `calendar-view` 现在会把宿主传入的 `endEditable` 交给日历组件,和 `ObjectCalendar` 行为一致。 - **为什么改**:没有结束字段时,事件只是一个时间点。拖顶部柄会改写开始时间,然后事件又按一小时重画,看起来就是一次移动,用户以为在调整时长,实际上把记录挪了位置。#29020 已经去掉了底部和月视图的「调整结束」柄,这次补上剩下的顶部柄和页面节点这条路。 - **风险与回滚**:低。配置了结束字段的视图两个柄都保留,行为不变;宿主不传 `endEditable` 时 `calendar-view` 的行为也不变。`endEditable` 是 #29020 新加、尚未发版的属性,这次只是让它的 `false` 也覆盖顶部柄,类型没变。回滚即还原本 PR。 - **先看**:`CalendarView.tsx` 里周/日视图顶部柄的条件和 `handleEventPointerDown` 开头新增的拦截;`calendar-view-renderer.tsx` 新增的 `resolveAuthoredEndEditable` 与转交处;`ObjectCalendar.endEditable-29020.test.tsx` 里两条改了预期的用例。 - **两分钟验证**:在 `ui/` 下运行 `pnpm exec vitest run packages/plugin-calendar/src/__tests__/CalendarView.noEndNoResize-29197.test.tsx`,预期 1 个文件、12 个测试全绿。 - **需要你做的**:无,席位落地。 ## What changes - **`CalendarView`, week and day views:** `endEditable={false}` now also hides the top "Resize start" handle, so with no stored end the time grid draws no resize handle at all. `handleEventPointerDown` ignores the top and bottom zones when `endEditable` is `false`; a body drag still moves the event. The month view is as #29020 left it (no "Resize event end" handle). The prop's doc now says what `false` means: the events have no stored end, so no view draws a resize handle. The type is unchanged. - **`ObjectCalendar`:** unchanged. It already passes `endEditable={Boolean(calendarConfig.endDateField)}`, so a view with no end field now draws no top handle either. - **`calendar-view` renderer:** forwards a host's `endEditable` as a declared host hatch, the way it forwards `slotMinutes`: a boolean reaches `CalendarView`, anything else gets the absent-key answer (the default `true`). No registry input, `@object-ui/types` key or metadata key is added. - **Docs:** one bullet under "Week / Day view" in `content/docs/frontend/plugins/plugin-calendar.mdx`. **Premise note (why the renderer forwards rather than derives).** A `calendar-view` node's `endDateField` defaults to `end`: `CalendarViewSchema.endDateField` carries `@default 'end'` in `@object-ui/types`, and the renderer reads `schema.endDateField || 'end'`. So every authored node names an end field, and deriving `endEditable` from it would always give `true`. Only the host knows its records keep no end, so the renderer forwards the host's boolean. Handles need a host `onEventDrop` in the first place (JSON cannot carry a function), so the hatch sits beside the callback that makes them appear. ## Tests New file `ui/packages/plugin-calendar/src/__tests__/CalendarView.noEndNoResize-29197.test.tsx`, 12 cases: - **`CalendarView` directly, week and day:** `endEditable={false}` draws neither the top nor the bottom handle. Control: the default draws both. - **`ObjectCalendar`, week and day:** a view with no end field (with `allDayField` declared, so the record is drawn timed) draws no top handle and no bottom handle. Control: a view with an end field draws both. - **`calendar-view` through `SchemaRenderer`:** a host `onEventDrop` with `endEditable: false` draws neither time-grid handle in the week view and no "Resize event end" handle in the month view. Controls: no `endEditable` from the host draws both handles, and a non-boolean `'false'` gets the default (both handles). Updated `ObjectCalendar.endEditable-29020.test.tsx`: the two cases that pinned the top handle staying with no end field (one through `ObjectCalendar`, one on `CalendarView` directly) now assert it is gone; the end-field controls are untouched. ## Acceptance notes - `.changeset/29020-calendar-resize-without-end-field.md` (unreleased) still says the top "Resize start" handle is unchanged. Both changesets ship in the same release, so the changelog will carry that line and this one's correction side by side. It is outside this claim's file surface and left as it is. - The pointer-down guard is not reachable from a test: with `endEditable` false the top and bottom zones have no element to press. It keeps the gesture closed if a handle is drawn by some later change. - `ui/packages/plugin-calendar/README.md` keeps its one-line summary ("Drag top/bottom edges to resize start/end times"), which stays true for views with an end field; it is outside this claim's file surface. ## Verification Readings on head `d28a86c7`, base `45e9b2f1`. <details><summary>Tests</summary> - New file and the updated #29020 file together: 2 files, 25 of 25 passed. - Red on main: with `CalendarView.tsx` and `calendar-view-renderer.tsx` restored from `45e9b2f1` (the tests import the source, no rebuild involved), the same two files ran 8 failed and 17 passed. Every case asserting the new behaviour failed (six in the new file, the two flipped #29020 cases); every control passed. Restored with `git checkout HEAD`, `git diff HEAD` empty, both blobs equal to HEAD. - Full package: `pnpm exec vitest run --maxWorkers=2 packages/plugin-calendar/` from `ui/`, under the verify lock: 68 files passed, 502 tests passed, 95 skipped (the zone-driven cases that run only in their forks child). - `pnpm exec turbo run type-check --filter=@object-ui/plugin-calendar`: 50 of 50 tasks successful. </details> <details><summary>Gates</summary> - `node scripts/pm/dispatch-gates.mjs --commands` derived 66 families; all 66 ran with exit 0 on head `d28a86c7`, the 65 unlocked ones with this body as the PR event (`check-adr-0087-registration`, `check-changeset-no-major`, `check-closing-keyword-parity`, `check-empty-changeset`, `check-sdui-manifest` among them). Two needed a second run: `check:skill-examples` first refused with exit 3 (`PREREQUISITE NOT MET`, `@objectstack/client-react` unbuilt) and ran exit 0 after building it; in `pnpm --dir ui exec vitest run scripts/` (locked, 171 files) one case, `check-hand-rolled-comment-mask` "exits 0 as a process", a whole-tree scan, timed out at 15 s in the full run, and its file alone then passed 22 of 22; the rest of the run was 168 files passed, 2 skipped. - `dispatch-gates --ran`: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN. - `node ui/scripts/check-new-cross-file-line-citations.mjs`: 0 new citations. - `eslint` on the four changed ui files: 0 errors; 25 warnings, the same counts (24 and 1) the two source files carry at the base, and none in the two test files. </details> --- _Generated by [Claude Code](https://claude.ai/code/session_01QsR2jRjkd6BVRcw6N7MyTu)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2626bdd commit 3d528bf

6 files changed

Lines changed: 264 additions & 16 deletions

File tree

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
'@object-ui/plugin-calendar': patch
3+
---
4+
5+
fix(plugin-calendar): a calendar with no end-date field draws no resize handle in the week and day views, and the `calendar-view` renderer forwards a host's `endEditable`
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
A calendar view that declares no `endDateField` shows each event as a point in time. The week and day views still drew the top "Resize start" handle on such an event: dragging it rewrote the start and redrew the event an hour long, so it read as a move.
11+
12+
- **No stored end, no resize handle.** `CalendarView`'s `endEditable={false}` now also hides the time grid's top "Resize start" handle, so the week and day views draw no resize handle at all; moving an event by its body is unchanged. `ObjectCalendar` already passes `false` when the view declares no `endDateField`. A view that declares one keeps both handles.
13+
- **`calendar-view` forwards `endEditable`.** A React host rendering a `calendar-view` node can pass a boolean `endEditable`, forwarded the way `slotMinutes` is; any other value gets the default. It is not derived from the node's `endDateField`, which defaults to `end`.

‎content/docs/frontend/plugins/plugin-calendar.mdx‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ grid. All gestures use pointer events and snap to `slotMinutes` (default
4343
start time. Refuses to cross the existing end.
4444
- **Resize end** — drag the bottom edge to adjust only the end time.
4545
Refuses to cross the existing start.
46+
- **No end field, no resize** — when the view declares no `endDateField`
47+
(`CalendarView`'s `endEditable={false}`), events have no stored end, so
48+
no resize handle is drawn in any view; moving an event still works.
4649
- **Drag-to-create** — click-drag on an empty area of the time grid to
4750
open the quick-create dialog with start/end pre-filled to the dragged
4851
time range.

‎ui/packages/plugin-calendar/src/CalendarView.tsx‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -159,11 +159,13 @@ export interface CalendarViewProps {
159159
* drag-to-move, drag-to-resize, and drag-to-create. Defaults to 30. */
160160
slotMinutes?: number
161161
/**
162-
* Whether a gesture may move an event's END on its own: the month view's
163-
* right-edge "Resize event end" handle and the week/day views' bottom resize
164-
* handle. Defaults to `true`. A host whose events have no stored end passes
165-
* `false` (`ObjectCalendar` does when the view declares no `endDateField`):
166-
* an end-only edit has nothing to write there, so neither handle is drawn.
162+
* Whether the events have a stored end, so that a gesture may change one
163+
* edge and keep the other: the month view's right-edge "Resize event end"
164+
* handle and the week/day views' top and bottom resize handles. Defaults to
165+
* `true`. A host whose events have no stored end passes `false`
166+
* (`ObjectCalendar` does when the view declares no `endDateField`): an event
167+
* is then a point in time, which a resize has nothing to change on, so no
168+
* resize handle is drawn in any view. Moving an event is unaffected.
167169
*/
168170
endEditable?: boolean
169171
className?: string
@@ -886,7 +888,7 @@ interface TimeGridViewProps {
886888
onEventClick?: (event: CalendarViewEvent) => void
887889
onEventDrop?: (event: CalendarViewEvent, newStart: Date, newEnd?: Date) => void
888890
onTimeRangeSelect?: (start: Date, end: Date) => void
889-
/** {@link CalendarViewProps.endEditable}: `false` draws no bottom resize handle. */
891+
/** {@link CalendarViewProps.endEditable}: `false` draws no resize handle, top or bottom. */
890892
endEditable: boolean
891893
}
892894

@@ -1320,6 +1322,8 @@ function TimeGridView({
13201322
) => {
13211323
if (!onEventDrop) return
13221324
if (e.button !== 0) return
1325+
// An event with no stored end has no edge to resize; only its body moves.
1326+
if (zone !== "body" && !endEditable) return
13231327
e.stopPropagation()
13241328
e.preventDefault()
13251329
const minutes = yToMinutes(e.clientY, dayIndex)
@@ -1598,8 +1602,9 @@ function TimeGridView({
15981602
<div className="text-[10px] opacity-80 truncate">
15991603
{formatTimeRange(day, entry.startMin, entry.endMin, locale)}
16001604
</div>
1601-
{/* Resize handles — only on the original days */}
1602-
{onEventDrop && entry.isStart && (
1605+
{/* Resize handles: only on the original days, and only
1606+
where the events have a stored end. */}
1607+
{onEventDrop && endEditable && entry.isStart && (
16031608
<div
16041609
onPointerDown={(e) => handleEventPointerDown(e, entry, dayIndex, "top")}
16051610
className="absolute top-0 left-0 right-0 h-1.5 cursor-ns-resize"
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
/**
2+
* ObjectUI
3+
* Copyright (c) 2024-present ObjectStack Inc.
4+
*
5+
* This source code is licensed under the MIT license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
/**
10+
* objectstack#29197 — an event with no stored end is a point in time, so no
11+
* view draws a resize handle on it.
12+
*
13+
* ── The rule ────────────────────────────────────────────────────────────────
14+
* `CalendarView`'s `endEditable={false}` says the events have no stored end.
15+
* The week and day views then draw neither the bottom "Resize end" handle nor
16+
* the top "Resize start" one: against an unstored end, dragging the top edge
17+
* only rewrote the start and redrew the event an hour long, so it read as a
18+
* move. Moving the event stays a drag of its body. `ObjectCalendar` passes
19+
* `false` when the view declares no `endDateField`.
20+
*
21+
* The `calendar-view` renderer forwards a host's boolean `endEditable` the way
22+
* it forwards `slotMinutes`. It does not derive one from `endDateField`: that
23+
* key defaults to `end`, so every node names an end field.
24+
*
25+
* A view whose events have a stored end keeps both handles.
26+
*/
27+
28+
import React from 'react';
29+
import { afterEach, describe, expect, it, vi } from 'vitest';
30+
import { cleanup, render, screen, within } from '@testing-library/react';
31+
import { LocalizationProvider } from '@object-ui/i18n';
32+
import { SchemaRenderer } from '@object-ui/react';
33+
import { ObjectCalendar } from '../ObjectCalendar';
34+
import { CalendarView, type CalendarViewEvent } from '../CalendarView';
35+
// Module scope: the `calendar-view` registration the renderer cases render
36+
// through (ui/AGENTS.md, test discipline: never inside a hook).
37+
import '../index';
38+
39+
const TITLE = 'Build homepage';
40+
41+
const MONTH_HANDLE = 'Resize event end';
42+
const GRID_HANDLE = { top: 'Resize start', bottom: 'Resize end' } as const;
43+
44+
/**
45+
* Thursday January 15th, 2026: the day the day view opens on, inside the week
46+
* of the 11th to the 17th. The `ObjectCalendar` records sit on it.
47+
*/
48+
const CLOCK = '2026-01-15T12:00:00.000Z';
49+
50+
const OBJECT_SCHEMA = {
51+
name: 'task',
52+
fields: {
53+
name: { type: 'text' },
54+
starts_at: { type: 'datetime' },
55+
ends_at: { type: 'datetime' },
56+
all_day: { type: 'boolean' },
57+
},
58+
};
59+
60+
/**
61+
* With no end field a record is all-day, drawn in the all-day lane with no
62+
* handle, unless the view declares `allDayField`: then a record flagged not
63+
* all-day is drawn timed, an hour long, in the time grid.
64+
*/
65+
const TIMED_NO_END = { startDateField: 'starts_at', allDayField: 'all_day' };
66+
67+
const timed: CalendarViewEvent = {
68+
id: 'e1',
69+
title: TITLE,
70+
start: new Date(2026, 0, 12, 9),
71+
end: new Date(2026, 0, 12, 11),
72+
};
73+
74+
afterEach(() => {
75+
cleanup();
76+
vi.useRealTimers();
77+
});
78+
79+
function handlesOn(piece: HTMLElement) {
80+
return {
81+
top: within(piece).queryByLabelText(GRID_HANDLE.top),
82+
bottom: within(piece).queryByLabelText(GRID_HANDLE.bottom),
83+
};
84+
}
85+
86+
function mountObjectCalendar(
87+
calendar: Record<string, unknown>,
88+
row: Record<string, unknown>,
89+
defaultView: 'week' | 'day',
90+
) {
91+
vi.useFakeTimers({ toFake: ['Date'] });
92+
vi.setSystemTime(new Date(CLOCK));
93+
const ds = {
94+
getObjectSchema: vi.fn(async () => OBJECT_SCHEMA),
95+
find: vi.fn(async () => ({ value: [{ id: 't1', name: TITLE, ...row }] })),
96+
update: vi.fn(async () => ({})),
97+
create: vi.fn(async () => ({ id: 't2' })),
98+
};
99+
const schema = {
100+
type: 'object-calendar',
101+
objectName: 'task',
102+
defaultView,
103+
calendar: { titleField: 'name', ...calendar },
104+
} as never;
105+
render(
106+
<LocalizationProvider value={{ locale: 'en-US' }}>
107+
<ObjectCalendar schema={schema} dataSource={ds as never} />
108+
</LocalizationProvider>,
109+
);
110+
return ds;
111+
}
112+
113+
describe('CalendarView endEditable={false}: no resize handle in the time grid (objectstack#29197)', () => {
114+
for (const view of ['week', 'day'] as const) {
115+
it(`${view} view: neither the top nor the bottom handle is drawn`, () => {
116+
render(
117+
<CalendarView
118+
events={[timed]}
119+
view={view}
120+
currentDate={new Date(2026, 0, 12)}
121+
onEventDrop={vi.fn()}
122+
endEditable={false}
123+
/>,
124+
);
125+
const { top, bottom } = handlesOn(screen.getByTitle(TITLE));
126+
expect(top).toBeNull();
127+
expect(bottom).toBeNull();
128+
});
129+
130+
it(`control: ${view} view, endEditable left at its default, draws both handles`, () => {
131+
render(
132+
<CalendarView events={[timed]} view={view} currentDate={new Date(2026, 0, 12)} onEventDrop={vi.fn()} />,
133+
);
134+
const { top, bottom } = handlesOn(screen.getByTitle(TITLE));
135+
expect(top).not.toBeNull();
136+
expect(bottom).not.toBeNull();
137+
});
138+
}
139+
});
140+
141+
describe('ObjectCalendar with no endDateField: no resize handle in the time grid (objectstack#29197)', () => {
142+
for (const view of ['week', 'day'] as const) {
143+
it(`${view} view: a view with no end field draws no top handle`, async () => {
144+
mountObjectCalendar(TIMED_NO_END, { starts_at: '2026-01-15T09:00:00.000Z', all_day: false }, view);
145+
const { top, bottom } = handlesOn(await screen.findByTitle(TITLE));
146+
expect(top).toBeNull();
147+
expect(bottom).toBeNull();
148+
});
149+
150+
it(`control: ${view} view, a view with an end field draws both handles`, async () => {
151+
mountObjectCalendar(
152+
{ startDateField: 'starts_at', endDateField: 'ends_at' },
153+
{ starts_at: '2026-01-15T09:00:00.000Z', ends_at: '2026-01-15T11:00:00.000Z' },
154+
view,
155+
);
156+
const { top, bottom } = handlesOn(await screen.findByTitle(TITLE));
157+
expect(top).not.toBeNull();
158+
expect(bottom).not.toBeNull();
159+
});
160+
}
161+
});
162+
163+
describe('calendar-view renderer forwards a host endEditable (objectstack#29197)', () => {
164+
/** A timed record with an end, on Monday January 12th, 2026. */
165+
const data = [
166+
{
167+
id: 'r1',
168+
title: TITLE,
169+
start: new Date(2026, 0, 12, 9).toISOString(),
170+
end: new Date(2026, 0, 12, 11).toISOString(),
171+
allDay: false,
172+
},
173+
];
174+
175+
function mountNode(view: 'month' | 'week', host: Record<string, unknown>) {
176+
render(
177+
<SchemaRenderer
178+
schema={{ type: 'plugin-calendar:calendar-view', id: 'n', view, currentDate: '2026-01-12', data } as never}
179+
// A React host's own props: `SchemaRenderer` hands them to the widget.
180+
{...host}
181+
/>,
182+
);
183+
}
184+
185+
it('week view: a host endEditable false draws neither time-grid handle', async () => {
186+
mountNode('week', { onEventDrop: vi.fn(), endEditable: false });
187+
const { top, bottom } = handlesOn(await screen.findByTitle(TITLE));
188+
expect(top).toBeNull();
189+
expect(bottom).toBeNull();
190+
});
191+
192+
it('month view: a host endEditable false draws no resize-end handle', async () => {
193+
mountNode('month', { onEventDrop: vi.fn(), endEditable: false });
194+
await screen.findAllByTitle(TITLE);
195+
expect(screen.queryAllByLabelText(MONTH_HANDLE)).toHaveLength(0);
196+
});
197+
198+
it('control: with no endEditable from the host, the week view draws both handles', async () => {
199+
mountNode('week', { onEventDrop: vi.fn() });
200+
const { top, bottom } = handlesOn(await screen.findByTitle(TITLE));
201+
expect(top).not.toBeNull();
202+
expect(bottom).not.toBeNull();
203+
});
204+
205+
it('control: a non-boolean endEditable gets the absent-key answer, both handles drawn', async () => {
206+
mountNode('week', { onEventDrop: vi.fn(), endEditable: 'false' });
207+
const { top, bottom } = handlesOn(await screen.findByTitle(TITLE));
208+
expect(top).not.toBeNull();
209+
expect(bottom).not.toBeNull();
210+
});
211+
});

‎ui/packages/plugin-calendar/src/__tests__/ObjectCalendar.endEditable-29020.test.tsx‎

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@
1414
* ── The rule ────────────────────────────────────────────────────────────────
1515
* With no end field the end is not stored, so an end-only edit has nothing to
1616
* write: the month view's "Resize event end" handle and the week and day
17-
* views' bottom "Resize end" handle are not drawn (`CalendarView`'s
18-
* `endEditable`, which `ObjectCalendar` sets from `endDateField`). A view that
19-
* declares an end field keeps both handles and keeps writing the end.
17+
* views' resize handles are not drawn (`CalendarView`'s `endEditable`, which
18+
* `ObjectCalendar` sets from `endDateField`). The time grid's top "Resize
19+
* start" handle goes too (objectstack#29197): against an unstored end it only
20+
* moved the event. A view that declares an end field keeps every handle and
21+
* keeps writing the end.
2022
*
2123
* `ObjectCalendar`'s default drop handler writes nothing when every date it
2224
* would write is where the record already has it: a write of unchanged values
@@ -212,11 +214,11 @@ describe('ObjectCalendar month view: the resize-end handle follows endDateField
212214
});
213215
});
214216

215-
describe('ObjectCalendar week view: the bottom resize handle follows endDateField (objectstack#29020)', () => {
216-
it('a view with no end field draws the top handle and no bottom handle', async () => {
217+
describe('ObjectCalendar week view: the resize handles follow endDateField (objectstack#29020, objectstack#29197)', () => {
218+
it('a view with no end field draws neither the top nor the bottom handle', async () => {
217219
mount(TIMED_NO_END, { starts_at: '2026-01-12T09:00:00.000Z', all_day: false }, 'week');
218220
const piece = await screen.findByTitle(TITLE);
219-
expect(within(piece).queryByLabelText(GRID_HANDLE.top)).not.toBeNull();
221+
expect(within(piece).queryByLabelText(GRID_HANDLE.top)).toBeNull();
220222
expect(within(piece).queryByLabelText(GRID_HANDLE.bottom)).toBeNull();
221223
});
222224

@@ -317,7 +319,7 @@ describe('CalendarView endEditable (objectstack#29020)', () => {
317319
expect(onEventDrop).not.toHaveBeenCalled();
318320
});
319321

320-
it('false: the week view draws the top handle and no bottom handle', () => {
322+
it('false: the week view draws neither the top nor the bottom handle (objectstack#29197)', () => {
321323
const timed: CalendarViewEvent = {
322324
id: 'e2',
323325
title: TITLE,
@@ -334,7 +336,7 @@ describe('CalendarView endEditable (objectstack#29020)', () => {
334336
/>,
335337
);
336338
const piece = screen.getByTitle(TITLE);
337-
expect(within(piece).queryByLabelText(GRID_HANDLE.top)).not.toBeNull();
339+
expect(within(piece).queryByLabelText(GRID_HANDLE.top)).toBeNull();
338340
expect(within(piece).queryByLabelText(GRID_HANDLE.bottom)).toBeNull();
339341
});
340342
});

‎ui/packages/plugin-calendar/src/calendar-view-renderer.tsx‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,18 @@ function resolveAuthoredSlotMinutes(raw: unknown): number | undefined {
155155
return typeof raw === 'number' && Number.isFinite(raw) && raw > 0 ? raw : undefined;
156156
}
157157

158+
/**
159+
* Resolve the `endEditable` hatch: does the host's data store an end for its
160+
* events? A declared `CalendarViewProps` key, host-only like `slotMinutes`, and
161+
* not derived from `endDateField`: that key defaults to `end`, so every node
162+
* names an end field and only the host knows its records keep none. Forwarded
163+
* only as a boolean; anything else gets the absent-key answer, the component's
164+
* default (`true`, resize handles drawn).
165+
*/
166+
function resolveAuthoredEndEditable(raw: unknown): boolean | undefined {
167+
return typeof raw === 'boolean' ? raw : undefined;
168+
}
169+
158170
/**
159171
* Resolve the authored `currentDate` into the type `CalendarView` declares.
160172
*
@@ -328,6 +340,7 @@ ComponentRegistry.register('calendar-view',
328340
const hostCallbacks = pickHostCallbacks(rest);
329341
const locale = resolveAuthoredLocale(rest.locale);
330342
const slotMinutes = resolveAuthoredSlotMinutes(rest.slotMinutes);
343+
const endEditable = resolveAuthoredEndEditable(rest.endEditable);
331344

332345
// The action channel, under the same declared-type rule as the callbacks:
333346
// `onAction` reaches this renderer through the very same props channel, so
@@ -374,6 +387,7 @@ ComponentRegistry.register('calendar-view',
374387
view={view}
375388
locale={locale}
376389
slotMinutes={slotMinutes}
390+
endEditable={endEditable}
377391
// The declared host hatches. Not a raw spread: `hostCallbacks` holds
378392
// only keys from `HOST_CALLBACKS`, and only where the value is a
379393
// function.

0 commit comments

Comments
 (0)