Skip to content

fix(plugin-calendar): no resize-end handle without an end field; a drop that changes no date writes nothing - #29146

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-8vj6zf
Nov 2, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-8vj6zf

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Steedos · project thread

Fixes #29020

Clause-②: yes (widening)
Metadata-protocol: none

维护者速读

  • 改了什么:日历视图没有配置结束日期字段(endDateField)时,月视图卡片右侧不再画「调整结束」拖拽柄,周/日视图事件底部的「调整结束」柄也不再画;移动卡片、顶部「调整开始」柄照旧。另外,日历默认的拖放保存在「要写的日期与记录现值完全相同」时不再发写请求。做法:CalendarView 新增可选属性 endEditable(默认 true),ObjectCalendar 按是否声明 endDateField 传入。
  • 为什么改:只有开始日期的日历(如 showcase 任务日历)上,拖结束柄什么都不改,却把原样的开始日期写回去:updated_at 变了,所有「记录更新后」触发器和流程都为一次没人做的修改跑了一遍(通知、审计行、集成)。周视图里在事件上按下再松开(不移动)也会这样写一次。
  • 风险与回滚:低。声明了结束字段的视图两个柄都保留、照旧写结束日期(结束值为空的记录也能拖出结束日期);独立使用 CalendarView 不传新属性时行为不变。回滚即还原本 PR。
  • 先看:ObjectCalendar.tsx 的 handleEventDropDefault 开头新增的「无变化不写」判断与 sameInstant;CalendarView.tsx 月视图 showResizeHandle 与周/日视图底部柄的条件;ObjectCalendar.tsx 渲染 CalendarView 处新增的 endEditable。
  • 两分钟验证:在 ui/ 下运行 pnpm exec vitest run packages/plugin-calendar/src/__tests__/ObjectCalendar.endEditable-29020.test.tsx,预期 1 个文件、13 个测试全绿。
  • 需要你做的:无,席位落地。

What changes

  • CalendarViewProps.endEditable?: boolean (default true; the type is exported from the @object-ui/plugin-calendar entry, hence the widening declaration). When false:
    • the month view draws no "Resize event end" handle, and a resize-end drag payload dropped anyway hands nothing to onEventDrop;
    • the week and day views draw no bottom "Resize end" handle.
      Moving a card and the time grid's top "Resize start" handle are unchanged.
  • ObjectCalendar passes endEditable={Boolean(calendarConfig.endDateField)}. A view that declares an end field keeps both handles and keeps writing the end, including on a record whose end is empty.
  • No write for a drop that changes no date. ObjectCalendar's default drop handler (handleEventDropDefault) returns before the optimistic update and dataSource.update when the start it would write is the record's start, and either it writes no end or that end is the record's end. Both are compared as instants, read the way the grid placed the event (toDisplayDate), so a stored spelling that differs from the one written back still counts as unchanged; an empty or unparseable stored value never matches, so it still writes. This closes the second door to the same write: a press and release on a week-view event, without moving it, calls onEventDrop with the event's own dates and wrote them back, with or without an end field. A host that supplies its own onEventDrop is unaffected.

Tests

New file ui/packages/plugin-calendar/src/__tests__/ObjectCalendar.endEditable-29020.test.tsx, 13 cases:

  • Month view, ObjectCalendar: no end field draws no "Resize event end" handle; no end field still moves the card and writes the start alone. Controls: with an end field the handle is drawn and dragging it writes the new end; with an end field and an empty end on the record, the handle is drawn and writes the end.
  • Week view, ObjectCalendar: no end field (with allDayField declared, so the record is drawn timed) draws the top handle and no bottom handle. Control: with an end field, both handles.
  • No-op drop: a press and release on a week-view event writes nothing, both with no end field and with one. Control: the same event moved two hours writes its new start.
  • CalendarView directly: endEditable defaults to drawing the handle; false hides the month handle while the card still moves; false refuses a resize-end payload; false hides the week view's bottom handle and keeps the top one.

Acceptance notes

  • With no endDateField and no allDayField, ObjectCalendar marks every record all-day, so the week and day views draw it in the all-day lane, which has no handles. The bottom-handle change matters only when the view also declares allDayField and a record is flagged not all-day.
  • With no end field, the time grid's top "Resize start" handle still writes the dragged start, and the event is then drawn an hour long from it, so it reads as a move rather than a resize. It writes a real change, so it is left as it is and reported to the seat.
  • calendar-view-renderer.tsx (the schema-driven calendar-view element) does not forward the new prop, so authored calendar-view nodes keep the default true, as before.

Verification

All readings on head 88ee38a1 (fix ce572f2f, changeset declaration 50f1987b, test fixture 88ee38a1), base 5d3667e2.

Tests
  • New file alone: 13 of 13 passed.
  • Red on main: with CalendarView.tsx and ObjectCalendar.tsx restored from 5d3667e2 (the tests import the source, no rebuild involved), the new file ran 7 failed and 6 passed. Every case asserting the new behaviour failed; the six controls passed. Restored with git checkout HEAD, git diff HEAD empty, blob equal to HEAD.
  • Ablation of the no-op guard: scripts/ablation-replace.mjs turned the guard's first condition into false && … (anchor 1 to 0, blob moved); the two press-and-release cases failed, 11 passed; restored, git diff HEAD empty.
  • Full package: pnpm --dir ui exec vitest run --maxWorkers=2 packages/plugin-calendar/ under the verify lock: 67 files passed, 490 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.
Gates
  • node scripts/pm/dispatch-gates.mjs --commands derived 34 families; all 34 ran with exit 0 on head 88ee38a1, the 33 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), and pnpm --dir ui exec vitest run scripts/ locked: 169 files passed, 2 skipped (4,900 tests passed).
  • dispatch-gates --ran: 34 derived, 34 run, 0 NOT-MEASURED, 0 UNRUN.
  • node ui/scripts/check-new-cross-file-line-citations.mjs: 0 new citations.
  • eslint on the three changed ui files: 0 errors; 52 warnings, the same counts (24 and 28) the two source files carry at the base, and none in the new test file.

Generated by Claude Code

…op that changes no date writes nothing

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jTP5DLZwaztvKMgUJf
…the event is drawn timed

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QsR2jTP5DLZwaztvKMgUJf
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tooling labels Nov 2, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:ui#1 · zhuangjianguo · session_01QsR2jUJ19MstFvFhjgiYTu

席位核对 · ACCEPT · 2026-11-02T08:37Z

Head-sha: 88ee38a19eec4f0438a57a088fbd1d56bf5d0609

  • Shape:
    • The PR is a draft against main. The first card line is Fixes #29020, and no other card sits beside a closing keyword.
    • 4 files, +406 / −3, inside the claimed surface (6571837928): CalendarView.tsx, ObjectCalendar.tsx, a new test and a root changeset. check-governed-merges --pr 29146: 409 changed lines, under the human-merge threshold.
    • Clause-②: yes (widening) and Metadata-protocol: none start their lines in the body and the changeset, as claimed: CalendarViewProps is exported from the package's . entry and gains the optional endEditable (default true). The changeset is patch. The body does not cite the claim id because the id reached the dev after pr_create; its declaration lines match the claim.
  • Read in the diff:
    • ObjectCalendar passes endEditable={Boolean(endDateField)}. With false, CalendarView draws no month resize-end handle and no week or day bottom handle, and it refuses a resize-end payload.
    • The default drop handler returns without writing when every date it would write is the instant the record already holds (toDisplayDate). An empty or unparseable stored value never matches, so a drop onto a record with no date still writes.
    • Views with an end field keep both handles and still write the end.
  • Gates (check-runs on this head): 44 success, 11 skipped, 0 failed or pending. mergeable_state is clean.
  • Tests (dev): new file 13 of 13. Against main's source, 7 fail and the 6 controls pass. Guard ablation reds 2. plugin-calendar 67 files and 490 tests; type-check 50 of 50. Derived gates 34 of 34.
  • NOT MEASURED: the seat ran no live session.
  • Findings: the week and day Resize start handle with no end field, and calendar-view-renderer not forwarding endEditable, are filed separately.
  • Landing: ordinary queue landing, pr_ready then automerge_enable. Fixes #29020 closes the card on merge.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head 88ee38a19eec4f0438a57a088fbd1d56bf5d0609 · 2026-11-02T10:02Z

Taken under references/contract-review.md rule (b): this head has waited over 60 minutes with no record, claim or hand-off on it. At TIER_DEFAULT.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract review

Served-tier: TIER_DEFAULT
Head-sha: 88ee38a19eec4f0438a57a088fbd1d56bf5d0609
Local-runs: none

① Derived judgments

  • Face, right. The committed tree is 4 files: .changeset/29020-calendar-resize-without-end-field.md, ui/packages/plugin-calendar/src/CalendarView.tsx, ui/packages/plugin-calendar/src/ObjectCalendar.tsx and one new test. None is under packages/spec/src/** and none is a governed surface (AGENTS.md Prime Directive feat: Comprehensive CRM example demonstrating all ObjectStack protocol features #14). There is no (narrowing) arm. The review is owed by the Clause-②: yes (widening) line alone, so this is the declaration face at TIER_DEFAULT.
  • Public surface: CalendarViewProps.endEditable?: boolean (default true), right. CalendarViewProps is re-exported from the package's only exports entry . (src/index.tsx, the export type { CalendarViewProps, CalendarViewEvent } from './CalendarView' line), so an optional member widens the published type. It is additive and optional, and its default keeps a standalone CalendarView host's behaviour unchanged. MonthViewProps and TimeGridViewProps gain a required endEditable: boolean, but neither is exported (the file's only exports are CalendarViewEvent, CalendarViewProps, CalendarView and the test-only __resolveEventColorForTest), so that change is internal.
  • The handle is gated on the declared field, right. ObjectCalendar passes endEditable={Boolean(calendarConfig.endDateField)}. calendarConfig is getCalendarConfig(schema), which reads the declared calendar block only (the endField alias is retired with a refusal, objectui#8355). It is the same endDateField that handleEventDropDefault uses to decide whether to write an end, so whether a handle is drawn and whether an end is written follow one source. At the head, every gesture that moves the end on its own is gated:
    • the month-view handle: showResizeHandle = !!onEventDrop && endEditable && isSpanEnd;
    • the month-view drop: a resize-end payload returns before onEventDrop when !endEditable;
    • the time-grid bottom handle: onEventDrop && endEditable && entry.isEnd.
      handleEventPointerDown sets resize-bottom only from that handle's "bottom" zone, and nothing else produces one. The top "Resize start" handle and the body move are left ungated on purpose (see ③). This matches the card's expected behaviour: no resize handle when the view declares no end field.
  • No new key; Metadata-protocol: none, right. endEditable is a TypeScript component prop and not something an author can write in metadata:
    • packages/spec is untouched. CalendarConfigSchema stays a strictObject of the four keys startDateField, endDateField, titleField and colorField.
    • @object-ui/types is untouched.
    • ObjectCalendar computes the prop from the existing endDateField and never reads it from the schema.
    • calendar-view-renderer.tsx forwards a closed set of CalendarViewProps keys with no raw spread, so an authored calendar-view node cannot set the prop either.
      At the head, git grep endEditable outside the tests hits only CalendarView.tsx, ObjectCalendar.tsx and the changeset. No key, accepted value or metadata type is added, so this is not a protocol addition.
  • Accept set: the no-op drop guard, right, and not a surface change. sameInstant and the early return in handleEventDropDefault stop the default handler from writing when the start it would write, and any end it would write, already equal the record's values read through toDisplayDate. An empty or unparseable stored value never matches, so a drop onto an empty date still writes. A host's own onEventDrop is not affected. It narrows no published input: it removes a write of unchanged values, which is the card's actual defect (updated_at bumps and after-update triggers fire).
  • Gates. All 57 check-runs on this head are complete: 46 success, 11 skipped, 0 failed, 0 pending. These include Check Changeset, Type Check UI, Test UI (1/16 through 16/16), Lint UI, Build & E2E UI and Governed Surface Queue Guard.

② Semver level

  • Clause-②: yes (widening) and Metadata-protocol: none each start their own line, in both the PR body and the changeset body. Both are right: the optional exported prop widens the public surface, and no metadata author can write anything new.
  • The changeset is '@object-ui/plugin-calendar': patch, which is right. AGENTS.md's Post-Task Checklist step 3 sets the level by the protocol kind alone: kind none takes patch, and "a non-protocol change, widening or breaking, ships patch". Nothing is breaking, so no BREAKING banner, migration or ADR-0087 marker is owed. Check Changeset passed on this head.

③ Boundary flags

Implemented-by: claude/project-thread-8vj6zf
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: PASS


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tooling

Projects

None yet

1 participant