Skip to content

[scheduler] enable multi-resource event creation and editing - #23313

Merged
rita-codes merged 16 commits into
mui:masterfrom
mustafajw07:feat/23016-multi-resource-event-dialog
Aug 18, 2026
Merged

[scheduler] enable multi-resource event creation and editing#23313
rita-codes merged 16 commits into
mui:masterfrom
mustafajw07:feat/23016-multi-resource-event-dialog

Conversation

@mustafajw07

@mustafajw07 mustafajw07 commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Closes #23016

Changelog

New: multi-resource events, end to end

  • The event dialog's resource picker now supports selecting multiple resources, not just one.
  • New canHaveMultipleResources option on eventCreation controls whether new events — and existing events whose resource is null/unset — get a single- or multi-select picker. When not set, it's inferred from your events data: the first event with a defined resource decides (a string means single, an array means multiple), and data with no resource at all defaults to multiple.
  • New events created through the Event Calendar now default to resource: [] in multi-resource mode (previously undefined/a single id). The Event Timeline still pre-selects the resource of the row you clicked in, but that only seeds an entry — canHaveMultipleResources decides the picker mode either way.
  • shouldEventRequireResource now validates that the selection is a non-empty array (at least one resource), instead of just non-null.

Behavior change: saving preserves shape

  • Saving an existing event never changes the shape of its resource: one that arrives as a plain string is always saved back as a string (or undefined once cleared), and one that arrives as an array (including []) is always saved back as an array ([] once cleared). Only a new event, or an existing one with no resource shape to begin with, is subject to canHaveMultipleResources.
  • Note: editing any event through the dialog while in multi-resource mode — including one that previously had no resource at all — now saves it back as resource: [] rather than leaving it undefined.

Fixed

  • Event Timeline: a multi-resource event now renders with each row's own eventColor instead of always taking its primary resource's color in every row. An event's own color property still wins everywhere, in every row.

  • Event dialog: an event referencing a resource id no longer present in resources (e.g. a deleted resource) no longer shows the dashed "no resource" swatch — that state is now correctly distinguished from having nothing selected.

  • I have followed (at least) the PR section of the contributing guide.

@code-infra-dashboard

code-infra-dashboard Bot commented Aug 6, 2026

Copy link
Copy Markdown

Deploy preview

Bundle size

Bundle Parsed size Gzip size
@mui/x-data-grid 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-pro 0B(0.00%) 0B(0.00%)
@mui/x-data-grid-premium 0B(0.00%) 0B(0.00%)
@mui/x-charts 0B(0.00%) 0B(0.00%)
@mui/x-charts-pro 0B(0.00%) 0B(0.00%)
@mui/x-charts-premium 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers 0B(0.00%) 0B(0.00%)
@mui/x-date-pickers-pro 0B(0.00%) 0B(0.00%)
@mui/x-tree-view 0B(0.00%) 0B(0.00%)
@mui/x-tree-view-pro 0B(0.00%) 0B(0.00%)
@mui/x-scheduler 🔺+953B(+0.25%) 🔺+294B(+0.28%)
@mui/x-scheduler-premium 🔺+1.01KB(+0.19%) 🔺+294B(+0.20%)
@mui/x-chat 0B(0.00%) 0B(0.00%)
@mui/x-license 0B(0.00%) 0B(0.00%)

Details of bundle changes


Check out the code infra dashboard for more information about this PR.

@mustafajw07

Copy link
Copy Markdown
Contributor Author

Hey @rita-codes, can I get a review on this PR?

@rita-codes rita-codes added type: new feature Expand the scope of the product to solve a new problem. scope: scheduler Changes related to the scheduler. labels Aug 10, 2026
@rita-codes

Copy link
Copy Markdown
Member

PR review

The core fix is right: the old form seeded resourceId from getPrimaryResourceId(occurrence.resource) and wrote it straight back, so every save collapsed a multi-resource event down to its first resource. That's gone, and the [] creation default matches what #23016 specifies. Tests, typecheck and lint pass on the branch (x-scheduler 341 passed, x-scheduler-premium 259 passed, tsgo clean). Two things are merge-blocking, both consequences of this being the activation step: saving through the dialog rewrites the resource shape of events the user never touched, and a multi-resource event renders in every Timeline row with the primary resource's color. The rest is a stale docs paragraph and a missing test for the exact regression the PR closes.

Bugs (3)

1. 🔴 Saving through the dialog rewrites resource from a plain id to an array

Location: packages/x-scheduler/src/internals/components/event-editing/FormContent.tsx:220

resource: values.resourceIds,
color: values.color === null ? undefined : values.color,

To be clear about what is not the issue: [] as the creation default is specified (#23016"Event creation defaults resource to [] in EventCalendar; to the row's resource in EventTimeline") and the PR implements it correctly.

The issue is that the same line also applies to edits. An event that arrived as resource: 'personal' is written back as ['personal'] after any save, even when the user only fixed a typo in the title and never opened the resource picker. That contradicts the central decision in #18613:

Mode is inferred only from the value: array = multi-resource, string/null = single-resource. No "force mode" prop.

If the value is the mode, then rewriting the value silently flips an existing event from single-resource to multi-resource mode without the user asking — and since there is deliberately no prop to force a mode, the developer has no way to opt out.

It also puts the dialog out of step with the other two write paths, which already preserve the caller's shape:

  • useDropTarget.ts:240 — dragging an existing event writes a plain id when originalResource is not an array, and only produces an array when it already was one (the non-destructive reassignment from [scheduler] EventTimeline: multi-row occurrence correctness #23017, complete with dedupe).
  • useDropTarget.ts:311 — creating an event by dropping from outside writes a plain id.

So after this PR, dragging an event with resource: 'personal' keeps it a string, while opening that same event in the dialog and pressing Save turns it into ['personal']. Two of the three write paths follow one rule and the third follows another.

Worth noting that normalizing here doesn't buy uniform data either: only events someone happened to open in the dialog become arrays, so a consumer still ends up with mixed shapes — just split along an invisible axis ("did a user click this event?") instead of a meaningful one ("does this event have several resources?"). For reference, Bryntum never rewrites the shape the developer supplied; its event fields are input projections over a separate assignment store, and a record loaded with singular resourceId still serializes as resourceId even after it becomes multi-assigned.

Failure scenario: An app stores events with resource: 'team-a' and doesn't use multi-resource at all. A user renames an event and saves — onEventsChange returns resource: ['team-a'], and every event that never had a resource comes back with a new resource: [] key. The app persists a shape it never wrote, with no upgrade note telling it to normalize, and with no prop available to prevent it.

Fix: Preserve the incoming shape and only widen when the user actually opts into multi-resource:

  • creating → array ([] in the Calendar, the row's resource in the Timeline), as specified;
  • the event's resource was already an array → array;
  • the event's resource was a string / null / absent → keep single-resource: write the plain id, or undefined when cleared. Widen to an array only once the selection holds two or more entries.

occurrence.resource and rawPlaceholder?.type === 'creation' are both already in scope at that point. This also brings the dialog in line with useDropTarget instead of against it.

2. 🔴 In the Timeline, a multi-resource event takes the primary resource's color in every row

Location: packages/x-scheduler-premium/src/event-timeline-premium/content/timeline-event/EventTimelinePremiumEvent.tsx:149

const color = useStore(store, schedulerEventSelectors.color, occurrence.id);

schedulerEventSelectors.color resolves through getPrimaryResourceId (schedulerEventSelectors.ts:84-97) and is called with occurrence.id alone, so the same occurrence rendered in three resource rows takes resource[0]'s color in all three. EventList already holds the row's resourceId (EventTimelinePremiumContent.tsx:481) and doesn't pass it down.

This predates the PR, but it belongs here: this is the activation step, the point at which a user can create a multi-resource event from the UI for the first time. Shipping activation means shipping the first events that hit this path.

#18613 lists it under Decisions as "Color = first resource", citing Bryntum, and the original description flags it as "debatable, that's what Bryntum is doing". That citation doesn't hold for the Timeline. Verified on Bryntum's own Scheduler multi-assign demo: its resources ship without eventColor, so nothing differentiates by default; giving colors to the three resources of the multi-assigned event (id: 1, resourceIds: ['r1','r2','r8']) makes each of its three rendered elements take its own row's color — r1 red rgb(137,34,32), r2 green rgb(40,96,43), r8 orange rgb(151,84,0). Events carrying their own eventColor keep it in every row. The per-row behavior is Bryntum Scheduler (resource rows, one render per assignment); first-resource is Bryntum Calendar, which renders once — which is where that decision came from.

Failure scenario: An event assigned to "Team A" (green) and "Team B" (pink) renders green in both rows. In the Team B row it reads as a stray Team A event, and the color — whose only job on a resource-row axis is resource identity — actively misinforms.

Fix: Resolve the color against the row, not the primary resource, and leave the rest of the precedence chain alone:

  1. event.color if set → same color in every row, unchanged;
  2. otherwise the eventColor of a resource — but read from the row's resource instead of the event's primary resource, which is what resolveEventProperty's resourceId argument resolves to today;
  3. otherwise the component default, unchanged.

Concretely: add an optional resourceId to schedulerEventSelectors.color, defaulting to getPrimaryResourceId(event.resource) so every other caller keeps its current behavior; then have EventTimelinePremiumEvent pass the row's id. No prop drilling needed — it renders inside TimelineGrid.EventRow, which already exposes resourceId via TimelineGridEventRowContext (TimelineGridEvent.tsx:64 consumes it exactly that way); passing it from EventList works too. Only resolveEventProperty's getValueInResource step changes.

The Event Calendar keeps first-resource: it renders once and has no row identity, so a single color has to win.

This needs the "Color = first resource" line in #18613 amended to be per-component. Worth a test: an event with no color of its own, assigned to two resources with different eventColor, renders a different data-palette per row; an event with its own color keeps it in both.

One related item that can stay a follow-up: the creation placeholder still previews in a single row, because usePushPlaceholder pushes only getPrimaryResourceId(values.resourceIds). Fixing that needs a change to the placeholder model (it carries one resourceId), unlike the color, which doesn't.

3. 🟡 An unknown resource id now renders as the "no resource" dot

Location: packages/x-scheduler/src/internals/components/event-dialog/ResourceAndColorSection.tsx:122

data-no-resource={resource == null}

resource is resourcesOptions.find(...) || null, so it is null in two different situations: the event has no resource, and the event references an id that isn't in resources. The old expression Boolean(resource?.value === null) was false for the unknown-id case, because only the synthetic no-resource option carried value: null. The dashed "no resource" styling now fires for both.

Failure scenario: An event carries resource: 'deleted-team'. The trigger renders the dashed "no resource" swatch next to the text "Invalid resource" — the dot says unset, the label says set-but-broken.

Fix: Key the flag off the selection instead of the lookup: data-no-resource={resourceIds.length === 0}.

Tests (2)

1. 🟡 The regression this PR closes has no test

Location: packages/x-scheduler-premium/src/event-calendar-premium/tests/EventDialog.test.tsx:2262

await user.click(await screen.findByRole('option', { name: /work/i }));
await user.keyboard('{Escape}');

Every touched test starts from an event with a single resource or none, and was adapted to the new array shape. #23016 is specifically about an event that already has several resources losing all but the first one when it goes through the dialog. No test seeds an occurrence with resource: ['personal', 'work'].

Failure scenario: A future refactor of the seeding path re-collapses a multi-resource event to its primary on save and the whole suite stays green — the exact bug this PR fixes ships again unnoticed.

Fix: Add a test that renders an occurrence with resource: [personalResource.id, workResource.id], asserts both labels show in the combobox, edits only the title, saves, and asserts payload.resource still deep-equals both ids. Once finding 1 is addressed, this is also where the shape rule belongs: a single-resource event round-trips as a plain id, and only widens to an array once a second resource is selected.

2. ℹ️ Renamed test claims "never" but only covers the true case

Location: packages/x-scheduler-premium/src/event-calendar-premium/tests/EventDialog.test.tsx:621

it('should never render a dedicated "No resource" option in the dropdown', async () => {

The test still renders with shouldEventRequireResource set (it lives inside that describe), so "never" is asserted for one of the two configurations. The false case is only covered indirectly by the following test, which asserts on the combobox text rather than on the absence of the option.

Fix: Either scope the title back to the configuration under test, or move it out of the shouldEventRequireResource describe and assert both configurations.

Simplifications (1)

1. 🟡 The selected-state check mark bypasses the file's styling convention

Location: packages/x-scheduler/src/internals/components/event-dialog/ResourceAndColorSection.tsx:271

{resourceIds.includes(resourceOption.value) && (
  <CheckIcon fontSize="small" style={{ marginLeft: 'auto' }} />
)}

Every other visual in this file goes through styled(..., { name: 'MuiEventDialog', slot: '...' }) plus a class from useEventEditingStyledContextResourceMenuItem, ResourceMenuColorDot, ResourceMenuListSubheader. This one is a bare icon with an inline style, so it has no slot name and no class, and users can't theme or override it the way they can every neighbouring element. The inline style also wins over any CSS they write.

Failure scenario: Someone theming MuiEventDialog slots can restyle the menu item, the color dot and the subheader, but has no handle on the check mark.

Fix: Wrap it in a styled slot with marginLeft: 'auto' in its style object and give it a class from the styled context, like its siblings. Worth checking whether it's needed at all — MUI already applies .Mui-selected to selected items in a multiple Select.

Docs (3)

1. 🟡 The "Require a resource" section still describes the removed "No resource" option

Location: docs/data/scheduler/event-calendar/resources/resources.md:111

By default, an event on the Event Calendar can be saved without a resource — the edit dialog includes a "No resource" option in the resource picker.
Set `shouldEventRequireResource` to `true` to make the resource mandatory: the "No resource" option is hidden and the form cannot be submitted with an empty resource.

Both sentences are now wrong: there is no "No resource" option in either configuration, and shouldEventRequireResource={true} no longer hides anything — it only blocks submit on an empty selection. The PR adds a new section below this paragraph but leaves the paragraph itself untouched. The timeline page has a milder version of the same drift at docs/data/scheduler/event-timeline/resources/resources.md:118 ("Set it to false to allow clearing the resource").

Fix: Rewrite the paragraph for the multi-select behavior: resources are cleared by deselecting them, and shouldEventRequireResource={true} makes an empty selection invalid.

2. 🟡 The color-resolution list doesn't say which resource wins

Location: docs/data/scheduler/event-calendar/resources/resources.md:150

2. The `eventColor` property assigned to the event's resource

Unambiguous while an event had one resource; ambiguous now that the docs three sections above teach resource: ['team-a', 'team-b']. Same line on the timeline page, where the answer differs.

Fix: Spell out step 2 per component: on the Event Calendar, the first resource in the array; on the Event Timeline, the resource of the row the event is being rendered in (per finding 2). Step 1 is worth stating explicitly too — an event's own color wins everywhere, including in every Timeline row.

3. 🟡 The ## Changelog section is empty

Location: PR description

#18613 calls out the [] creation default as "the one user-visible behavior change" and says explicitly that it must be documented and communicated on release, since this is the step where the feature is announced. The PR body's ## Changelog section has no entry.

Fix: Add a changelog line covering the creation default (new events start with resource: []), plus whatever shape rule comes out of finding 1.

Nit, docs/data/scheduler/event-calendar/resources/resources.md:122: the new sentence uses an unspaced em dash ((`resource: []`)—assign) while the rest of the page uses spaced ones.

Verdict

Request changes — the dialog rewrites the resource shape of events the user never touched, and the Timeline paints multi-resource events with the wrong row colors; both need to land before the feature is switched on.


🤖 Review generated with Claude Code

@rita-codes

rita-codes commented Aug 10, 2026

Copy link
Copy Markdown
Member

Design decision: how single vs multiple resource selection is resolved

@mustafajw07 — after discussing this with @flaviendelangle we've settled on the model below. It supersedes finding 1 in my review above (the one about the save path rewriting resource into an array): the rules here replace it, so please implement this instead.

1. New prop: canHaveMultipleResources

A new boolean on the existing event creation config (SchedulerEventCreationConfig in packages/x-scheduler-internals/src/models/event.ts:480, alongside interaction and duration), set as eventCreation={{ canHaveMultipleResources: true }}. It decides whether the resource picker is a single Select or a multiple one for newly created events, letting the developer choose which mode their app works in.

2. Editing follows the data

When editing an existing event, the mode is not decided by the prop — it comes from that event's own resource value:

  • resource is a string → single select
  • resource is an array → multiple select, including [], which is an event declared as multi-resource with nothing selected yet

Both shapes may coexist in the same dataset, and we never convert one into the other. An event that arrived as 'team-a' is saved back as 'team-a'; an event that arrived as ['team-a', 'team-b'] — or as [] — is saved back as an array.

3. Event whose resource is null or not defined at all

These are the only two values that carry no shape to infer from, so they're the ones that fall back to canHaveMultipleResources. Note this is not the same as "the event has no resource": [] also means no resource, but it is an array and therefore already resolved by point 2.

4. canHaveMultipleResources not set

Inferred from the data. Scan the events in order until one is found whose resource is either a string or an array, and take the mode from it — string means single select, array means multiple select. If no event in the data has a resource at all, use multiple select.

This inference only decides event creation and the point 3 fallback. It is never applied per event: existing events always follow their own value per point 2.

It also covers the cases where there is no creation config to read the prop from — eventCreation={false}, or a read-only scheduler, where creationConfig resolves to false. Editing still needs a mode for point 3 in those cases, and the inference above provides it.

What this means for the write path

  • single mode → write the plain resource id, or undefined when cleared
  • multiple mode → write an array, [] when cleared

The important part is the invariant: saving an event never changes the shape of its resource. That's also what the drag-and-drop paths already do (useDropTarget.ts:240), so the dialog ends up consistent with them.

Thanks for bearing with us while we settled this one 🙇‍♀️

@rita-codes
rita-codes self-requested a review August 10, 2026 14:14
@mustafajw07

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review and for clarifying the single vs. multiple resource behavior.

I've addressed the review feedback:

  • Preserved the resource shape when editing existing events.
  • Added canHaveMultipleResources for determining the mode for newly created events.
  • Added the required resource-mode inference when the prop is not provided.
  • Fixed Timeline resource colors to resolve against the rendered row's resource.
  • Fixed the no-resource styling for unknown resource IDs.
  • Added regression coverage for preserving multiple resources through the event dialog.
  • Updated the resource documentation and changelog.
  • Cleaned up the selected-resource check mark styling.

Ready for another review.

@rita-codes

rita-codes commented Aug 12, 2026

Copy link
Copy Markdown
Member

PR review

Thank you for the work on this one — we took a design decision that landed mid-review and rebuilt the whole resolution model around it, and the result is faithful to it. The shape of resource drives the mode, saving preserves it, the Timeline resolves color against the row, the dashed dot no longer fires for unknown ids, and #23016 has a proper regression test. The comments you left in getResourceSelectionMode and in the color selector explain the why, not the what, which made this much faster to review. Thanks also for the patience across two rounds.

Verified on the branch: tsgo clean across the four scheduler packages, pnpm test:unit --project "x-scheduler*" 2162 passed / 32 skipped, eslint clean on the touched folders, pnpm proptypes + pnpm docs:api produce no diff, prettier and vale clean.

One thing is merge-blocking, and it's a case we under-specified rather than something you got wrong: on the Event Timeline, canHaveMultipleResources is ignored at creation time. The rest is docs — including a demo, which every other section of both pages has.

Bugs (2)

1. 🔴 On the Event Timeline, canHaveMultipleResources is ignored when creating an event

Location: packages/x-scheduler/src/internals/components/event-dialog/ResourceAndColorSection.tsx:170

const mode = getResourceSelectionMode(occurrence.resource, canHaveMultipleResources);

The Timeline's creation placeholder carries a shape. usePlaceholderInRow.ts:43 sets resource: rawPlaceholder.resourceId ?? originalEvent?.resource, and for a type: 'creation' placeholder in a row that is the row's id — a string. So getResourceSelectionMode returns 'single' and never reaches the fallback.

Confirmed in the browser against this branch, with <EventTimelinePremium eventCreation={{ canHaveMultipleResources: true }} />: clicking inside the DevOps row opens the dialog with a single-select picker, and picking "Product" replaces DevOps instead of adding to it.

It doesn't stop at creation. Saving in single mode writes values.resourceIds[0], a plain string, so reopening that event keeps it single, and useDropTarget.ts:240 doesn't widen either (it only produces an array when the value already was one). Net effect: on the Event Timeline no event created through the UI can ever hold more than one resource — in the component where multi-resource is the thing you actually see. The docs currently describe this as the intended behavior (docs/data/scheduler/event-timeline/resources/resources.md:124, "…until you turn it into an array yourself"), but that isn't reachable from the dialog, only by editing the data by hand.

To be clear about where this came from: the decision comment said the prop decides the picker "for newly created events" and separately that editing follows the value, and it didn't spell out what happens when a newly created event already has a seeded resource. You resolved that gap by letting the value win everywhere, which is a defensible reading. We've now settled the other way, so the rule is:

  • creating → the mode comes from canHaveMultipleResources (or the inference when it isn't set), and the row's resource only pre-selects an entry. In multiple mode that means the new event starts as ['engineering'] rather than 'engineering'.
  • editing an existing event → unchanged, the value's shape wins exactly as it does today.

That also makes the Timeline consistent with the Event Calendar, where the prop already decides because the placeholder carries no resource.

Fix: The good news is that this is one decision point, not a new code path. The seeding already works in both modes — getEventResourceIds('engineering') returns ['engineering'] (event-utils.ts:162) — and the write path already emits an array in multiple mode (FormContent.tsx:233). The only change is choosing the mode: when the placeholder is a creation, skip the shape and use canHaveMultipleResources. schedulerOccurrencePlaceholderSelectors.isCreating is already in the store.

Nothing new opens up on the empty-selection side: if the user deselects the row's resource and lands on [], shouldEventRequireResource already defaults to true on the Timeline and blocks submit.

This needs Timeline creation tests with the prop set both ways — there are none today.

2. 🟡 The comment promises a stable mode that the code doesn't provide

Location: packages/x-scheduler/src/internals/components/event-dialog/ResourceAndColorSection.tsx:166

// ... It's fixed for the lifetime of this mount: it must not react
// to the user's in-progress selection, only to the data the occurrence started with.

canHaveMultipleResources comes from useStore, so it's a live subscription to state.eventCreation + eventIdList + processedEventLookup. For an occurrence with resource == null and the prop unset, a change to events can flip the inference while the dialog is open, and mode flips with it: the Select goes from multiple={false} to multiple={true} and its value from string to array mid-flight.

Failure scenario: An app loads events in two batches. The user opens a resourceless event while only single-resource events are loaded, so the picker is single. The second batch arrives, its first event with a resource carries an array, the inference flips to "multiple", and the picker transforms under the cursor.

Fix: Freeze it for real — seed it once via a lazy useState initializer. The dialog already remounts on key={occurrence.key}, so the initial value is exactly "the data the occurrence started with" and the comment becomes true.

Tests (2)

1. 🟡 The two new Timeline tests don't follow the "should …" convention

Location: packages/x-scheduler-premium/src/event-timeline-premium/EventTimelinePremium.test.tsx:119

it('resolves a colorless multi-resource event against each row resource, not just the primary one', () => {

Same on line 132 ('keeps the event own color in every row…'). Every other it(...) in the file starts with "should".

2. ℹ️ The new x-scheduler-internals logic is only covered indirectly

Location: packages/x-scheduler-internals/src/scheduler-selectors/schedulerEventSelectors.ts:96

getResourceSelectionMode and the canHaveMultipleResources selector are pure logic with several branches (object prop / true / false / absent, inference order, default to multiple), and today they're only exercised through the dialog in x-scheduler-premium. Both test files already exist (event-utils.test.ts, and schedulerEventSelectors.test.ts with a describe per selector).

Fix: A describe('canHaveMultipleResources') next to describe('creationConfig'), covering eventCreation={true} in particular (the boolean path no test touches today) and the inference order.

Simplifications (1)

1. 🟡 The new selector doesn't use the idiom of the selector right above it

Location: packages/x-scheduler-internals/src/scheduler-selectors/schedulerEventSelectors.ts:104

const configured =
  typeof eventCreation === 'object' && eventCreation != null
    ? eventCreation.canHaveMultipleResources
    : undefined;

defaultEventDuration, thirty lines up, solves the identical problem ("read the raw prop, bypassing creationConfig") with typeof eventCreation === 'boolean' ? … : eventCreation?.duration ?? …. The != null guard is also unreachable: the state type is Partial<SchedulerEventCreationConfig> | boolean, never null.

Fix: Mirror the neighbouring selector so two adjacent selectors read the same way.

Docs (4)

1. 🟡 The new section is the only one on either page without a demo

Location: docs/data/scheduler/event-calendar/resources/resources.md:117

Both resources pages give every feature a demo — NestedResources, DefaultCollapsedResources, DefaultVisibleResources, ColorPalettes, TitleProperty, plus ResourceColumnLabel on the timeline. "Multiple resources per event" ships with snippets only, and it's the section that needs one most: what the reader has to understand is that the picker changes shape depending on which event they open, and a code block can't show that.

Fix: One demo per page with a mixed dataset — an event with resource: ['team-a', 'team-b'], another with resource: 'team-a', and eventCreation={{ canHaveMultipleResources: true }}. Open one and you get multi-select, open the other and you get single, create one and you get the prop's mode. On the timeline page that same demo also shows the per-row color, which is the other headline change here and currently has nothing visual either — worth giving the two resources distinct eventColors so the difference is immediate.

2. 🟡 The ## Changelog section is still empty

Location: PR description

Listed as done in the comment above, but the section only holds the contributing-guide checkbox. #18613 asks explicitly for the user-visible behavior change to be documented and communicated on release, and there are two now: the creation default (resource: [] when the inference resolves to multiple) and the new canHaveMultipleResources prop.

Fix: One entry covering both, plus a note that an event with no resource comes back as [] after it's been edited.

3. 🟡 The 🧪 marker on the section

Location: docs/data/scheduler/event-calendar/resources/resources.md:117

The only other 🧪 in the scheduler docs is Responsiveness, where it marks a whole page and has a matching nav entry in docs/data/pages.ts:54. A lone section marker inside an unmarked page implies the rest of that page is stable, which isn't the case while the scheduler is pre-stable — and the marker means "this may change", which shouldn't apply if the feature is complete with this PR.

Fix: Drop it from both pages.

4. 🟡 The array snippet is orphaned at the end of the section

Location: docs/data/scheduler/event-calendar/resources/resources.md:133

When `canHaveMultipleResources` isn't set, it's inferred from the `events` prop: …

const event = {
  // ...
  resource: ['team-a', 'team-b'],
};

The snippet illustrates the array shape, not the inference that precedes it, so it reads as a non sequitur — and the section opens with "An event can be associated with more than one resource" without ever showing how you declare that. Same pattern on the timeline page (resources.md:139).

Fix: Move the snippet up under the opening sentence with a lead-in, and let the inference paragraph close the section without code.

Verdict

Request changes — only for the Timeline creation rule; everything else here is docs and polish.


🤖 Review generated with Claude Code

@mustafajw07

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review and for clarifying the Timeline creation behavior.

I've addressed the review feedback:

  • Fixed Timeline creation to respect canHaveMultipleResources, while using the row's resource only as the initial selection.
  • Preserved the resource shape when editing existing events.
  • Stabilized the resource selection mode for the lifetime of the dialog.
  • Updated the selector implementation to follow the existing code pattern.
  • Added/updated tests for Timeline creation and canHaveMultipleResources inference.
  • Updated the test names to follow the repository convention.
  • Added direct selector test coverage.
  • Updated the Event Calendar and Event Timeline documentation with demos and the revised resource-selection behavior.
  • Updated the changelog and removed the experimental section marker.
  • Reworked the resource-array documentation example as suggested.

The requested changes are now addressed. Ready for another review.

@rita-codes

Copy link
Copy Markdown
Member

PR review

The Timeline creation rule is fixed properly: getResourceSelectionMode now takes isCreating, the mode is frozen at mount in both components that derive it, and there are two Timeline creation tests covering canHaveMultipleResources both ways. The selector idiom, the "should …" naming, the direct selector/util coverage, the demos, the 🧪 marker and the orphaned snippet are all addressed. Verified on the branch: tsgo clean across the four scheduler packages, pnpm test:unit --project "x-scheduler*" 2303 passed / 36 skipped, eslint clean on the touched folders, pnpm proptypes + pnpm docs:api + pnpm docs:typescript:formatted produce no diff, vale clean.

Nothing left in the implementation — everything below is docs. One is merge-blocking and it's a side effect of the fix itself: the Event Timeline page still describes the old creation behavior, the exact bug this round fixed, right under a demo configured to do the opposite. The rest is the two new demos not quite landing visually, and the changelog entry that's still missing after being asked twice.

Bugs (0)

No findings.

Tests (1)

1. 🟡 Clearing the resource in single mode is asserted in the UI but never in the payload

Location: packages/x-scheduler-premium/src/event-calendar-premium/tests/EventDialog.test.tsx:696

await user.click(await screen.findByRole('option', { name: /no resource/i }));

expect(screen.getByRole('combobox', { name: /resource/i }).textContent).to.match(
  /no resource/i,
);

The test stops at the combobox text. FormContent.tsx:240 writes values.resourceIds[0] in single mode, and the comment above it promises "the plain id (or undefined once cleared)" — the undefined half is never asserted anywhere. The multi-resource equivalent is covered (resource: [] at line 661) and the string round-trip is covered (line 1198), so this is the one write path in the shape matrix with no payload assertion.

Failure scenario: A refactor makes the cleared single-select save [] or null instead of undefined. A single-resource app that never opted into arrays gets a shape it never wrote — the regression this PR blocked on in the first round — and the suite stays green.

Fix: Extend that test: save after picking "No resource" and assert resource is undefined.

Simplifications (1)

1. 🟡 The mode is derived and frozen twice, and the two copies have to agree

Location: packages/x-scheduler/src/internals/components/event-editing/FormContent.tsx:213

const [resourceSelectionMode] = React.useState<ResourceSelectionMode>(() =>
  getResourceSelectionMode(
    occurrence.resource,
    canHaveMultipleResources,
    rawPlaceholder?.type === 'creation',
  ),
);

ResourceAndColorSection.tsx:178 computes the same thing from the same three inputs, with isCreating read through schedulerOccurrencePlaceholderSelectors.isCreating instead of off rawPlaceholder — one drives the Select's multiple, the other drives what handleSubmit writes. They agree today only because GeneralTab renders with hidden rather than unmounting (GeneralTab.tsx:26), so both freeze in the same commit and neither ever re-initializes.

That's a real invariant resting on an unrelated implementation detail, and the failure is silent: if the tab panel ever became conditionally mounted, switching tabs would re-freeze the section's mode against a drifted canHaveMultipleResources while FormContent kept the old one — a multi-select UI whose save path writes values.resourceIds[0] and drops everything after the first resource.

Failure scenario: Nothing user-visible today; the cost is that two components have to stay in lockstep with no mechanism enforcing it, and the comment in each one explains the freeze without mentioning that the other exists.

Fix: Derive it once where initialValues is already frozen, in the outer FormContent, and pass it down — it has the same lifetime and the same "captured at mount" semantics the form provider already documents for the seed. Both useState freezes and both long comments then collapse into one.

Docs (6)

1. 🔴 The Event Timeline page still documents the creation bug this commit fixed

Location: docs/data/scheduler/event-timeline/resources/resources.md:145

A new event created by clicking inside a resource's row starts assigned to that row's resource (a string), so it's edited as single-resource until you turn it into an array yourself. For an event whose `resource` is `null` or not set otherwise (and for any other newly created event), use `canHaveMultipleResources` on `eventCreation` to choose the mode:

This paragraph is the old behavior, written back when the row's string resource decided the picker. After the fix, creating in a row seeds ['team-a'] and the picker is multi-select whenever canHaveMultipleResources resolves to true — which is exactly what the new test at event-timeline-premium/tests/EventDialog.test.tsx:126 asserts (resource deep-equals [engineering.id, design.id]), and what the demo directly above this paragraph is configured for, with // A newly created event also gets the multi-select picker. in its source.

So the page shows a demo doing one thing and, two blocks later, tells the reader it can't be done. The second half compounds it: it scopes canHaveMultipleResources to events with no resource "otherwise", when creation is now the case where the prop always wins.

Failure scenario: A reader evaluating the Event Timeline reads this section, concludes multi-resource events can only be produced by hand-editing the data, and doesn't set the prop.

Fix: Replace the paragraph with the rule the code now implements: creating uses canHaveMultipleResources (or the inference), and the row the user clicked in only pre-selects an entry — in multiple mode the new event starts as ['team-a'].

2. 🟡 The shape rule reads as unconditional but only covers editing

Location: docs/data/scheduler/event-calendar/resources/resources.md:136

- An event whose `resource` is a string is edited as single-resource — the picker shows one entry at a time.
- An event whose `resource` is an array (including `[]`, …) is edited as multi-resource.

Saving never changes that shape: an event that arrives as a string is always saved back as a string …

Same block on the timeline page (line 143). "Always" isn't true on the creation path: a Timeline creation placeholder carries a string and, in multiple mode, is saved as an array. On the Event Calendar it happens to hold because creation placeholders never carry a resource, but the sentence is stated as a general rule on both pages, and it's the reason finding 1 reads the way it does.

Fix: Say the shape rule applies to existing events, and let the creation paragraph state its own rule.

3. 🟡 canHaveMultipleResources's JSDoc describes the pre-fix rule

Location: packages/x-scheduler-internals/src/models/event.ts:501

/**
 * Whether newly created events can be assigned more than one resource.
 * Only decides the mode for events created with no resource yet (and, when an existing
 * event's own `resource` is `null` or not set, which picker to show it with) — an event
 * whose `resource` is already a string or an array always keeps that shape.
 */

"Only decides the mode for events created with no resource yet" is the behavior before isCreating was added — the Timeline's creation placeholder does carry a resource and the prop decides anyway. The trailing clause has the same problem: on creation, a placeholder whose resource is a string does not keep that shape in multiple mode. This is the type consumers see in their editor, and it now contradicts getResourceSelectionMode's own JSDoc three files away.

Fix: Mirror the wording already in getResourceSelectionMode — decides the picker for every newly created event, and for existing events only when their resource is null or not set.

4. 🟡 The Event Calendar demo doesn't tell the two resources apart

Location: docs/data/scheduler/event-calendar/resources/MultipleResourcesPerEvent.tsx:5

const resources: SchedulerResource[] = [
  { id: 'team-a', title: 'Team A' },
  { id: 'team-b', title: 'Team B' },
];

Without eventColor, both events render in the component's default color, so "Cross-team sync" and "Team A standup" look identical. The Timeline demo gives its resources blue and pink, and that's what makes the point land there at a glance. The Calendar doesn't vary color per row, but the color still carries which team an event belongs to — and it's what makes visible that a multi-resource event takes the first resource in the array, which is exactly what step 2 of the color-resolution list further down the same page claims.

Fix: Same eventColor: 'blue' / 'pink' as the Timeline demo, so both pages teach from the same dataset.

5. 🟡 The Event Timeline demo uses a preset where the events can't be read

Location: docs/data/scheduler/event-timeline/resources/MultipleResourcesPerEvent.tsx:12

start: '2025-07-07T10:00:00',
end: '2025-07-07T11:00:00',
// …
start: '2025-07-08T09:00:00',
end: '2025-07-08T09:30:00',

With defaultPreset="dayAndWeek" and events of 1 hour and 30 minutes, each chip is a few pixels wide: the title isn't legible and the blue/pink contrast between rows — the headline of this demo, and what the paragraph above it asks the reader to compare — is reduced to two specks. The two events are also on different days, so they don't appear together in one glance. The neighbouring demos (NestedResources, ResourceColumnLabel) do use dayAndWeek, but there what matters is the resource column, not the chips.

Fix: defaultPreset="dayAndHour" and both events on the same day (July 7th), at nearby but non-overlapping times — so "Cross-team sync" shows up in the Team A row in blue and the Team B row in pink at the same time, with a readable title, and "Team A standup" sits next to it as the single-resource contrast.

6. 🟡 The ## Changelog section is still empty

Location: PR description

Third time asked, and listed as done in the comment above ("Updated the changelog"). The section still holds only the contributing-guide checkbox. #18613 asks explicitly for the user-visible behavior changes to be documented and communicated on release, and there are two: the creation default (resource: [] when the inference resolves to multiple) and the new canHaveMultipleResources prop.

Fix: One entry covering both, plus a note that an event with no resource comes back as [] after being edited.

Verdict

Request changes — only for the Timeline docs paragraph, which now says the opposite of what the code does; the implementation itself is done.


🤖 Review generated with Claude Code

@mustafajw07

Copy link
Copy Markdown
Contributor Author

Thanks for the follow-up review. I've addressed the remaining feedback:

  • Added the single-resource clearing payload assertion for undefined.
  • Derived the resource selection mode once in FormContent and passed it down to keep the UI and submit path in sync.
  • Updated the Event Timeline documentation to reflect the corrected creation behavior.
  • Clarified that resource-shape preservation applies to existing events, while creation follows canHaveMultipleResources.
  • Updated the canHaveMultipleResources JSDoc to match the final behavior.
  • Updated the Event Calendar demo with resource colors.
  • Adjusted the Event Timeline demo timing/preset so the multi-resource behavior is clearly visible.
  • Added the requested changelog entry.

Ready for another review.

@rita-codes

rita-codes commented Aug 17, 2026

Copy link
Copy Markdown
Member

PR review

Round three's feedback is applied almost in full: the payload assertion, the single derivation of the resource selection mode, both docs pages, the JSDoc, and the demo colors all check out against the code and in the browser. The post-merge data-palette follow-up also correctly adapts the dependency terminals from #23200 to the row-qualified color selector — every call site passes the row's resource id now. Verified on the branch: tsgo clean across the four scheduler packages, pnpm test:unit --project "x-scheduler*" 2420 passed / 41 skipped, eslint clean on the touched folders, pnpm proptypes + pnpm docs:api + pnpm docs:typescript:formatted produce no diff, prettier and vale clean.

Nothing is merge-blocking in the code or docs text — the two leftovers below are the Timeline demo and the changelog.

Docs (2)

1. 🟡 The Timeline demo's titles still truncate

Location: docs/data/scheduler/event-timeline/resources/MultipleResourcesPerEvent.tsx:12

The move to dayAndHour and a single day fixed the color contrast, but at roughly 40px per hour a 30-minute standup renders as "T…" and the 1-hour sync as "Cross…", so the reader still can't tell which chip is the multi-resource event without opening the code.

Longer events fix it: with a 3-hour block per event (9:00–12:00 and 13:00–16:00; the first one renamed to something that plausibly lasts that long, since a 3-hour standup doesn't), both titles render in full and the blue/pink duplication is readable in one glance. The .md paragraph only references "Cross-team sync", so it stays valid whatever you rename the first event to.

Screenshot 2026-08-17 at 11 51 46

2. 🟡 The ## Changelog section is empty

Location: PR description

It's listed as done in your last comment, but the section still only holds the contributing-guide checkbox.

Screenshot 2026-08-17 at 11 52 06

The content asked for in the previous round still applies: one entry covering the creation default and the new canHaveMultipleResources prop, plus a note that an event cleared of all its resources in multi-resource mode is saved back with resource: [].

Verdict

Approve after nits — the implementation and docs text are done; only the demo tweak and the changelog entry remain.

@mustafajw07

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback. Both changes are now addressed:

  • Updated the Timeline demo events to 3-hour blocks so the titles are fully visible.
  • Added the missing ## Changelog entry covering the creation default, canHaveMultipleResources, and saving resource: [] when all resources are cleared in multi-resource mode.

Ready for another review.

@mustafajw07 mustafajw07 reopened this Aug 17, 2026

@rita-codes rita-codes left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All feedback addressed — the Timeline demo now renders both titles in full and the changelog entry covers the creation default, canHaveMultipleResources, and the resource: [] clearing behavior.

And with that, this closes the last piece of multi-resource events — the whole feature ships end to end now: rendering, per-row colors, the dialog, creation and editing. 🎉🚀

Huge thanks for driving this over the finish line, @mustafajw07 — a feature this size landing as a community contribution, through four review rounds and a mid-review design change, is genuinely impressive. Congrats! 👏🥳

@mustafajw07

Copy link
Copy Markdown
Contributor Author

Thank you so much! I really appreciate all the guidance and feedback throughout the process. It was a great learning experience, especially working through the design changes and multiple rounds of review.
Glad we could get the feature across the finish line! 🚀

@rita-codes
rita-codes merged commit 44fc31f into mui:master Aug 18, 2026
22 checks passed
@mustafajw07
mustafajw07 deleted the feat/23016-multi-resource-event-dialog branch August 18, 2026 07:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: scheduler Changes related to the scheduler. type: new feature Expand the scope of the product to solve a new problem.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[scheduler] Event dialog: edit multi-resource events

2 participants