Skip to content

feat(meeting-types): use the workplan grid on the create form - #147

Merged
asm0dey merged 11 commits into
mainfrom
feat/create-form-workplan-grid
Aug 22, 2026
Merged

feat(meeting-types): use the workplan grid on the create form#147
asm0dey merged 11 commits into
mainfrom
feat/create-form-workplan-grid

Conversation

@asm0dey

@asm0dey asm0dey commented Aug 22, 2026

Copy link
Copy Markdown
Owner

The meeting-type create form had a stripped-down working-hours block — seven fixed rows, one ruleStart/ruleEnd pair per weekday — with its own server-side parser, while the edit page and /me/availability both used the richer workplan grid. The create form now renders that same grid, and AdminResource.persistFrames becomes the app's only frame parser.

Closes #120.

What changed

  • meetingTypes.html — the ruleDay/ruleStart/ruleEnd rows are replaced by the workplan grid: per-day cards, + Frame, copy-to-all-days, copy-to-weekdays, clear-day, a <template data-frame-template> and /workplan.js. data-workplan goes on the create <form> itself, since the page is one big form and that is what workplan.js's closest("[data-workplan]") resolves against.
  • AdminResourcecreateMeetingType calls persistFrames(ownerId, typeId, form); the second parser createInitialWorkingHours is deleted. Nothing in src/main reads ruleDay/ruleStart/ruleEnd any more.
  • i18n — four adm_meetingTypes_* keys, with de and he values copied verbatim from their existing adm_detail_* twins.

Progressive enhancement

One blank frame per weekday is seeded server-side, so a JS-off browser posts exactly what the old form posted. noJsSubmitOfTheSeededGridCreatesTheTypedRules submits all seven seeded rows over HTTP to prove it, rather than asserting on markup alone.

Hardening that came along for the ride

Reviews surfaced three things worth naming:

  • persistFrames(ownerId, typeId, …) ended up next to createInitialDateOverride(typeId, ownerId, …) — both Long, so a future swap would compile clean and write a cross-tenant ownerId. Both helpers now read (ownerId, typeId, form).
  • The create form's date override and its time windows now skip unparseable values instead of rejecting the whole submission. Previously one bad optional date meant the meeting type itself was never created. The shared persistWindows guard also covers time windows added from the date-override pages.
  • A non-numeric meetingTypeId on POST /me/date-overrides returned 500; it now returns 400. (Investigating that corrected a false premise: unparseable dates were already returning 400 via the global MalformedDateTimeMapper — the bug reports claiming 500 for those were wrong, and the beans now say so.)

One grid, not four

Follow-up review asked why the component was copied rather than reused — a fair hit. The grid markup existed four times over (availability.html, meetingTypeDetail.html, sharedAvailability.html, and this PR's create form), character-identical except for its i18n namespace and card background class, and the same four button strings existed in three key families.

It is now one AdminResource/_workplanGrid.html, included by all four pages and parameterised by the row list and the card class, with a single adm_workplan_* key family replacing the adm_availability_* / adm_detail_* / adm_meetingTypes_* frame keys (13 keys removed across the bundle and both locale files). workplan.js was not touched — if the markup contract had drifted, it would have needed to be.

The create page normalises onto the same List<WeekRow> the other three pass, via WeekRow.blank(), so the fragment has no "is this the create page" branching. Net −102 lines.

Testing

Full suite green: 905 tests, 0 failures, 0 errors, mvn spotless:check clean.

New tests cover the rendered grid markers (including that each seeded row carries its own day), a blank frame skipped, two frames on one day, an inverted frame dropped rather than 500ing, the no-JS submit, and the malformed-input cases above.

Driven in a real browser against the dev server and Postgres: + Frame, Copy to all days, Copy to weekdays, × remove and Remove availability each behave correctly and none of them submits the form; a create with a split Monday (09:00–12:00 + 13:00–17:00) round-tripped into the edit page through the same shared fragment.

Notes

  • The changelog bullets for this work are on the docs-site branch and are not part of this PR; their PR links are #TBD pending this number.
  • The first two commits (83de3dc, 4f3e052) are the fork point from docs/drop-flyway-version-range, included deliberately.
  • Follow-ups filed rather than folded in: calit-o9rp (the grid's time inputs have no accessible name), calit-x921 (the daysOfWeek template parameter is dead now that the fragment takes week), and calit-qec2's sibling endpoints. calit-spfu was folded in — it is the shared-fragment work above.

🤖 Generated with Claude Code

https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug

asm0dey and others added 11 commits August 22, 2026 11:48
The line claimed migrations V1…V25 while V26 and V27 already exist. That
range is the one fact in the section that rots on every migration added,
and it carries no information a directory listing does not — the rules
around it (validate-only Hibernate, never edit an applied migration, add
a new V*.sql) are what actually matter and do not change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QF9JXW4V2AFDBicwBfV5zH
Two working documents, no code change.

calit-7a6t records the two-week trial of JetBrains Context as a semantic
search layer: the measured 2-win/1-miss result against grep, the usage
baseline, keep-or-remove criteria for 2026-09-07, and the removal steps.
Its .properties blind spot is the reason the verdict is not a foregone
conclusion. The trial's own config (CLAUDE.md block, settings.json hooks,
.mcp.json entry) is deliberately left uncommitted.

The plan implements calit-9d76: point the meeting-type create form at the
same workplan grid the edit page uses, and delete the second
ruleDay/ruleStart/ruleEnd parser it needed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QF9JXW4V2AFDBicwBfV5zH
The create form had a stripped-down working-hours block — seven fixed rows,
one start/end pair per weekday — while the edit page and /me/availability
both use the workplan grid with multiple frames per day and copy buttons.
The create form now renders the same grid, and the create handler reuses
persistFrames instead of a second ruleDay/ruleStart/ruleEnd parser, which
is deleted. One blank frame per day is still seeded server-side, so the
form works unchanged without JavaScript.

Closes #120

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
…ded frameDay

createInitialDateOverride still took (typeId, ownerId) while the new
persistFrames call right above it takes (ownerId, typeId) - both Long,
so a future swap between the two adjacent calls would compile clean and
write a cross-tenant ownerId. Reorder createInitialDateOverride's
parameters to (ownerId, typeId, form), matching persistFrames and the
AvailabilityRule.delete("ownerId = ?1 and meetingTypeId = ?2", ...)
convention used elsewhere; no behaviour change.

Also pin the create form's seeded frameDay to its own day: nothing
previously asserted that the hidden input's value matches the day card
it renders inside, so a typo'd constant there would have passed every
test in the file (the no-JS test hand-writes its own frameDay params
instead of deriving them from the rendered markup).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
Marks calit-9d76 completed with a summary of what landed, and files the
three items the final code review surfaced but left out of scope: a shared
workplan Qute fragment (with the duplicated i18n key families collapsed),
the unguarded overrideDate parse that 500s the create endpoint, and the
missing accessible names on the grid time inputs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
…ge input

createInitialDateOverride called LocalDate.parse(date) with no guard, so a
crafted overrideDate on POST /me/meeting-types threw DateTimeParseException
out of the transaction and 500ed instead of dropping the value. Pre-existing,
but the workplan-grid change made the asymmetry conspicuous: the working-hours
half of the same form now routes through persistFrames, which catches parse
failures and skips them, while the date-override half directly below it still
crashed on garbage.

Guard the overrideDate parse the same way persistFrames does (catch and skip,
keep the meeting type). persistWindows - the shared helper this path and two
other AdminResource endpoints call - had the identical unguarded LocalTime.parse
for window start/end; guarded that too, which closes the window-time hole for
its other two callers as a side effect.

Investigated whether a "bulk-save" date-override endpoint already guards, per
calit-8nlx's notes: none exists. Two sibling single-add endpoints
(addTypeOverride, createOverride) still parse overrideDate unguarded and share
the crash risk, but fixing + testing them touches two more resource methods
and two other test files outside this bean's scope - left alone and noted as
a follow-up candidate in the bean. SharedMeetingsResource.addOverride already
guards both date and window parsing via its own helpers, confirming this is
the established safe pattern elsewhere in the codebase.

Closes calit-8nlx.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
calit-qec2 claimed addTypeOverride and createOverride both 500 on a
malformed date, and that createOverride also 500s on a non-numeric
meetingTypeId. Verified empirically with real requests instead of
propagating the assumption: the malformed-date half was already wrong.
MalformedDateTimeMapper is a globally-registered
ExceptionMapper<DateTimeParseException>, so both endpoints already
return 400 on a bad date, not 500.

The meetingTypeId half was real: Long.valueOf(meetingTypeId) had no
mapper for NumberFormatException, so a non-numeric id genuinely 500ed.
createOverride now catches it and throws BadRequestException, matching
the existing parseLocationType precedent (AdminResource.java:671) - no
new i18n key needed, since that precedent already uses a plain message.

Added tests pinning the actual behaviour for all three inputs (kept
regardless of which way the premise turned out): a garbage date on
addTypeOverride, a garbage date on createOverride, and a garbage
meetingTypeId on createOverride - all now assert 400 and no persisted
row.

Corrected calit-8nlx's Summary of Changes to match: its fix's value was
never "500 -> 200" - the pre-fix status was already 400, but because
createInitialDateOverride shared its transaction with the meeting
type's own persist(), that 400 rolled back the entire create, not just
the date override. Corrected the docs-site changelog bullet
(worktree /home/finkel/work_self/calit-docs-site) to describe "whole
request rejected -> bad value skipped, rest saves" instead of "server
error", amending the unpushed commit that first stated it.

Closes calit-qec2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
The title still asserted a 500 the body now documents as never having
happened; the create path rejected the whole form with a 400 from the
global DateTimeParseException mapper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
…ragment

The working-hours weekly grid existed verbatim in four templates
(availability.html, meetingTypeDetail.html, sharedAvailability.html, and
meetingTypes.html's create form), each with its own i18n key twin family
for the same four button strings. Reviewed on PR #147 and asked for the
opposite of what landed there.

Extracts the day-cards + <template data-frame-template> markup into
AdminResource/_workplanGrid.html, parameterised by `week` (List<WeekRow>)
and `cardClass`; all four templates now include it instead of carrying
their own copy. The including page still owns the <form> (data-workplan,
action, CSRF).

The create form previously seeded rows from a bare DayOfWeek[]; it now
gets a List<WeekRow> via new WeekRow.blank() (one unpersisted, blank
AvailabilityRule per day), matching the shape the three bulk-edit pages
already used. The fragment guards the frame-time value expressions with
{#if fr.startTime}...{/if} so a blank frame renders value="", never the
literal text "null" -- a no-op for the three pages with real (NOT NULL)
frame times.

Collapses the three duplicated adm_detail_*/adm_availability_*/
adm_meetingTypes_* frame-button key families onto the existing
adm_workplan_* family (adm_workplan_clear_day already lived there),
removing the 13 now-orphaned keys from AdminMessages.java and both
adm_de.properties/adm_he.properties. adm_detail_to and adm_meetingTypes_to
stay -- still used by the date-override window rows.

Pure refactor: no test was edited to make the suite pass. Full suite
(mvn test): 905 tests, 0 failures, 0 errors, 0 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
… in tests

Review follow-up on the workplan-grid extraction (098f561):

- WeekRow.blank()'s AvailabilityRule.dayOfWeek assignment was dead --
  _workplanGrid.html emits the hidden frameDay from row.day, never
  fr.dayOfWeek. Dropped it rather than keep an assignment nothing reads.
- AdminMeetingTypeFormTest.createFormRendersWorkplanGrid now asserts
  name="frameStart" value="" on the seeded blank frame, pinning that the
  {#if fr.startTime} guard renders an empty value and not the literal
  text "null" -- the most fragile part of the no-JS guarantee, previously
  unpinned by any committed test.

mvn test -Dtest=AdminMeetingTypeFormTest,AdminTypeHoursPrefillTest,\
AdminAvailabilityBulkTest,AdminAvailabilityTest,MultiHostMessageParityTest:
14+3+6+2+4 = 29 tests, 0 failures, 0 errors, 0 skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
The shared-fragment work is done; the leftover DayOfWeek[] template
parameter that no page reads any more gets its own bean, since dropping
it spans four templates and three Templates signatures.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
@sonarqubecloud

Copy link
Copy Markdown

@asm0dey
asm0dey merged commit 16fb6e2 into main Aug 22, 2026
11 checks passed
asm0dey added a commit that referenced this pull request Aug 22, 2026
Two Unreleased bullets for PR #147: the meeting-type create form now uses
the same working-hours grid as the edit page, and malformed dates, times
or meeting-type ids on that form no longer reject the whole submission.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HnLuwUFvipixJum9fR8xug
@asm0dey
asm0dey deleted the feat/create-form-workplan-grid branch August 22, 2026 11:53
@asm0dey asm0dey mentioned this pull request Aug 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Use the same working-hours editor when creating a meeting type as when editing

1 participant