@dcrackel/hematournamentui 1.0.713 → 1.0.714

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (20) hide show
  1. package/README.md +12 -0
  2. package/dist/HemaTournamentUI-lib.es.js +6856 -6243
  3. package/dist/HemaTournamentUI-lib.umd.js +27 -27
  4. package/dist/hematournamentui-tailwind.css +79 -9
  5. package/package.json +1 -1
  6. package/src/index.js +43 -42
  7. package/src/stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.stories.js +49 -0
  8. package/src/stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.test.js +129 -0
  9. package/src/stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.vue +165 -0
  10. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.stories.js +10 -1
  11. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.test.js +31 -0
  12. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue +20 -1
  13. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.test.js +22 -0
  14. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue +27 -10
  15. package/src/stories/Templates/EventManagement/Bracket/Bracket.stories.js +33 -4
  16. package/src/stories/Templates/EventManagement/Bracket/Bracket.test.js +218 -178
  17. package/src/stories/Templates/EventManagement/Bracket/Bracket.vue +42 -1
  18. package/src/stories/Templates/EventManagement/Bracket/BracketPlannedSuggestions.js +8 -0
  19. package/src/stories/Templates/EventManagement/Bracket/BracketStories.test.js +121 -0
  20. package/src/stories/Util/bracketDirectors.js +86 -0
@@ -21,6 +21,7 @@
21
21
  --color-white: #fff;
22
22
  --spacing: 0.25rem;
23
23
  --container-md: 28rem;
24
+ --container-xl: 36rem;
24
25
  --text-xs: 0.75rem;
25
26
  --text-xs--line-height: calc(1 / 0.75);
26
27
  --text-sm: 0.875rem;
@@ -242,6 +243,9 @@
242
243
  .inset-0 {
243
244
  inset: calc(var(--spacing) * 0);
244
245
  }
246
+ .-top-1 {
247
+ top: calc(var(--spacing) * -1);
248
+ }
245
249
  .-top-4 {
246
250
  top: calc(var(--spacing) * -4);
247
251
  }
@@ -290,6 +294,9 @@
290
294
  .left-1 {
291
295
  left: calc(var(--spacing) * 1);
292
296
  }
297
+ .left-1\/2 {
298
+ left: calc(1 / 2 * 100%);
299
+ }
293
300
  .left-11 {
294
301
  left: calc(var(--spacing) * 11);
295
302
  }
@@ -311,6 +318,9 @@
311
318
  .z-50 {
312
319
  z-index: 50;
313
320
  }
321
+ .z-\[100\] {
322
+ z-index: 100;
323
+ }
314
324
  .order-first {
315
325
  order: -9999;
316
326
  }
@@ -359,14 +369,17 @@
359
369
  .m-4 {
360
370
  margin: calc(var(--spacing) * 4);
361
371
  }
372
+ .m-auto {
373
+ margin: auto;
374
+ }
362
375
  .mx-1 {
363
376
  margin-inline: calc(var(--spacing) * 1);
364
377
  }
365
378
  .mx-2 {
366
379
  margin-inline: calc(var(--spacing) * 2);
367
380
  }
368
- .mx-2\.5 {
369
- margin-inline: calc(var(--spacing) * 2.5);
381
+ .mx-3 {
382
+ margin-inline: calc(var(--spacing) * 3);
370
383
  }
371
384
  .mx-4 {
372
385
  margin-inline: calc(var(--spacing) * 4);
@@ -656,6 +669,9 @@
656
669
  .max-h-60 {
657
670
  max-height: calc(var(--spacing) * 60);
658
671
  }
672
+ .max-h-\[calc\(100dvh-2rem\)\] {
673
+ max-height: calc(100dvh - 2rem);
674
+ }
659
675
  .max-h-screen {
660
676
  max-height: 100vh;
661
677
  }
@@ -665,6 +681,9 @@
665
681
  .min-h-5 {
666
682
  min-height: calc(var(--spacing) * 5);
667
683
  }
684
+ .min-h-11 {
685
+ min-height: calc(var(--spacing) * 11);
686
+ }
668
687
  .min-h-16 {
669
688
  min-height: calc(var(--spacing) * 16);
670
689
  }
@@ -821,6 +840,9 @@
821
840
  .w-\[900px\] {
822
841
  width: 900px;
823
842
  }
843
+ .w-\[calc\(100\%-2rem\)\] {
844
+ width: calc(100% - 2rem);
845
+ }
824
846
  .w-full {
825
847
  width: 100%;
826
848
  }
@@ -839,12 +861,18 @@
839
861
  .max-w-md {
840
862
  max-width: var(--container-md);
841
863
  }
864
+ .max-w-xl {
865
+ max-width: var(--container-xl);
866
+ }
842
867
  .min-w-0 {
843
868
  min-width: calc(var(--spacing) * 0);
844
869
  }
845
870
  .min-w-20 {
846
871
  min-width: calc(var(--spacing) * 20);
847
872
  }
873
+ .min-w-36 {
874
+ min-width: calc(var(--spacing) * 36);
875
+ }
848
876
  .min-w-40 {
849
877
  min-width: calc(var(--spacing) * 40);
850
878
  }
@@ -886,6 +914,10 @@
886
914
  --tw-border-spacing-y: calc(var(--spacing) * 1);
887
915
  border-spacing: var(--tw-border-spacing-x) var(--tw-border-spacing-y);
888
916
  }
917
+ .-translate-x-1\/2 {
918
+ --tw-translate-x: calc(calc(1 / 2 * 100%) * -1);
919
+ translate: var(--tw-translate-x) var(--tw-translate-y);
920
+ }
889
921
  .translate-x-full {
890
922
  --tw-translate-x: 100%;
891
923
  translate: var(--tw-translate-x) var(--tw-translate-y);
@@ -1594,6 +1626,9 @@
1594
1626
  font-size: var(--text-xs);
1595
1627
  line-height: var(--tw-leading, var(--text-xs--line-height));
1596
1628
  }
1629
+ .text-\[9px\] {
1630
+ font-size: 9px;
1631
+ }
1597
1632
  .text-\[12rem\] {
1598
1633
  font-size: 12rem;
1599
1634
  }
@@ -1635,6 +1670,12 @@
1635
1670
  .text-nowrap {
1636
1671
  text-wrap: nowrap;
1637
1672
  }
1673
+ .break-words {
1674
+ overflow-wrap: break-word;
1675
+ }
1676
+ .wrap-break-word {
1677
+ overflow-wrap: break-word;
1678
+ }
1638
1679
  .whitespace-nowrap {
1639
1680
  white-space: nowrap;
1640
1681
  }
@@ -1737,6 +1778,9 @@
1737
1778
  .text-toggleBackground {
1738
1779
  color: #A9C4D6;
1739
1780
  }
1781
+ .text-white {
1782
+ color: var(--color-white);
1783
+ }
1740
1784
  .uppercase {
1741
1785
  text-transform: uppercase;
1742
1786
  }
@@ -1746,6 +1790,9 @@
1746
1790
  .underline {
1747
1791
  text-decoration-line: underline;
1748
1792
  }
1793
+ .accent-bright {
1794
+ accent-color: #6190f2;
1795
+ }
1749
1796
  .accent-primary {
1750
1797
  accent-color: #141B33;
1751
1798
  }
@@ -1873,6 +1920,19 @@
1873
1920
  -webkit-user-select: none;
1874
1921
  user-select: none;
1875
1922
  }
1923
+ .backdrop\:bg-black\/20 {
1924
+ &::backdrop {
1925
+ background-color: color-mix(in srgb, #000 20%, transparent);
1926
+ @supports (color: color-mix(in lab, red, red)) {
1927
+ background-color: color-mix(in oklab, var(--color-black) 20%, transparent);
1928
+ }
1929
+ }
1930
+ }
1931
+ .backdrop\:bg-secondary\/60 {
1932
+ &::backdrop {
1933
+ background-color: color-mix(in oklab, #2B324B 60%, transparent);
1934
+ }
1935
+ }
1876
1936
  .last\:border-b-0 {
1877
1937
  &:last-child {
1878
1938
  border-bottom-style: var(--tw-border-style);
@@ -2115,6 +2175,11 @@
2115
2175
  border-color: #6190f2;
2116
2176
  }
2117
2177
  }
2178
+ .focus-visible\:bg-dropdownSelect {
2179
+ &:focus-visible {
2180
+ background-color: #D5E4EE;
2181
+ }
2182
+ }
2118
2183
  .focus-visible\:ring-2 {
2119
2184
  &:focus-visible {
2120
2185
  --tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
@@ -2152,6 +2217,11 @@
2152
2217
  opacity: 40%;
2153
2218
  }
2154
2219
  }
2220
+ .disabled\:opacity-50 {
2221
+ &:disabled {
2222
+ opacity: 50%;
2223
+ }
2224
+ }
2155
2225
  .md\:m-9 {
2156
2226
  @media (width >= 48rem) {
2157
2227
  margin: calc(var(--spacing) * 9);
@@ -2272,6 +2342,11 @@
2272
2342
  display: none;
2273
2343
  }
2274
2344
  }
2345
+ .md\:inline-block {
2346
+ @media (width >= 48rem) {
2347
+ display: inline-block;
2348
+ }
2349
+ }
2275
2350
  .md\:h-7 {
2276
2351
  @media (width >= 48rem) {
2277
2352
  height: calc(var(--spacing) * 7);
@@ -2397,9 +2472,9 @@
2397
2472
  flex-direction: row;
2398
2473
  }
2399
2474
  }
2400
- .md\:flex-nowrap {
2475
+ .md\:flex-wrap {
2401
2476
  @media (width >= 48rem) {
2402
- flex-wrap: nowrap;
2477
+ flex-wrap: wrap;
2403
2478
  }
2404
2479
  }
2405
2480
  .md\:items-stretch {
@@ -2407,11 +2482,6 @@
2407
2482
  align-items: stretch;
2408
2483
  }
2409
2484
  }
2410
- .md\:justify-start {
2411
- @media (width >= 48rem) {
2412
- justify-content: flex-start;
2413
- }
2414
- }
2415
2485
  .md\:rounded-lg {
2416
2486
  @media (width >= 48rem) {
2417
2487
  border-radius: var(--radius-lg);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@dcrackel/hematournamentui",
3
3
  "private": false,
4
- "version": "1.0.713",
4
+ "version": "1.0.714",
5
5
  "type": "module",
6
6
  "main": "dist/HemaTournamentUI-lib.umd.js",
7
7
  "module": "dist/HemaTournamentUI-lib.es.js",
package/src/index.js CHANGED
@@ -1,42 +1,43 @@
1
- export { default as BaseIcon } from './stories/Atoms/Icon/BaseIcon.vue';
2
- export { default as BaseInput } from './stories/Atoms/Input/BaseInput.vue';
3
- export { default as BaseRadioGroup } from './stories/Atoms/RadioGroup/BaseRadioGroup.vue';
4
- export { default as BaseTag } from './stories/Atoms/Tag/BaseTag.vue';
5
- export { default as BaseText } from './stories/Atoms/Text/BaseText.vue';
6
-
7
- export { default as Breadcrumb } from './stories/Molecules/Breadcrumb/Admin/Breadcrumb.vue';
8
- export { default as BaseButton } from './stories/Molecules/Buttons/BaseButton/BaseButton.vue';
9
- export { default as FilterAndSortBar } from './stories/Molecules/Filters/FilterAndSortBar/FilterAndSortBar.vue';
10
- export { default as FilterUpcomingPast } from './stories/Molecules/Filters/FilterUpcomingPast/FilterUpcomingPast.vue';
11
- export { default as BaseModal } from './stories/Molecules/Modals/BaseModal/BaseModal.vue';
12
- export { default as LoadingModal } from './stories/Molecules/Modals/LoadingModal/LoadingModal.vue';
13
- export { default as EventWizardModal } from './stories/Molecules/Modals/EventWizardModal/EventWizardModal.vue';
14
-
15
- export { default as AddressAutocomplete } from './stories/Organisms/ComplexInputs/AddressAutocomplete/AddressAutocomplete.vue';
16
- export { default as DatePicker } from './stories/Organisms/ComplexInputs/DatePicker/DatePicker.vue';
17
- export { default as DropDownMenu } from './stories/Organisms/ComplexInputs/DropDown/DropDownMenu.vue';
18
- export { default as GridContainer } from './stories/Organisms/Grids/GridContainer.vue';
19
- export { default as PoolGrid } from './stories/Organisms/Containers/PoolGrid/PoolGrid.vue';
20
- export { default as BracketDirectorAssignmentCard } from './stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue';
21
- export { default as BracketDirectorAssignmentPanel } from './stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue';
22
- export { default as ImageCropper } from './stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.vue';
23
- export { default as TextBoxEditor } from './stories/Organisms/ComplexInputs/TextBoxEditor/TextBoxEditor.vue';
24
- export { default as EditEventWizard } from './stories/Organisms/Wizards/EditEventWizard/EditEventWizard.vue';
25
-
26
- export { default as TournamentManagement } from './stories/Templates/TournamentManagement/TournamentManagement.vue';
27
- export { default as ListEvents } from './stories/Templates/TournamentManagement/ListEvents/ListEvents.vue';
28
-
29
- export { default as AdminLeftMenu } from './stories/Templates/Menus/Admin/AdminLeftMenu.vue';
30
- export { default as EventAttendance } from './stories/Templates/EventManagement/EventAttendance/EventAttendance.vue';
31
- export { default as PoolManagement } from './stories/Templates/EventManagement/PoolManagement/PoolManagement.vue';
32
- export { default as StaffList } from './stories/Templates/EventManagement/StaffList/StaffList.vue';
33
- export { default as PoolLive } from './stories/Templates/EventManagement/PoolLive/PoolLive.vue';
34
- export { default as PoolResults } from './stories/Templates/EventManagement/PoolResults/PoolResults.vue';
35
- export { default as Bracket } from './stories/Templates/EventManagement/Bracket/Bracket.vue';
36
- export { default as EditEventInfo } from './stories/Templates/EventManagement/EditEventInfo/EditEventInfo.vue';
37
- export { default as EventFinalResults } from './stories/Templates/EventManagement/EventFinalResults/EventFinalResults.vue';
38
- export { default as KioskDisplay } from './stories/Templates/EventManagement/KioskDisplay/KioskDisplay.vue';
39
- export { default as PersonManagement } from './stories/Templates/PersonManagement/PersonManagement.vue';
40
- export { default as ClubManagement } from './stories/Templates/ClubManagement/ClubManagement.vue';
41
- export { default as Assignment } from './stories/Templates/Assignment/Assignment.vue';
42
- export { default as PenaltyReport } from './stories/Templates/EventManagement/Penalties/PenaltyReport.vue';
1
+ export { default as BaseIcon } from './stories/Atoms/Icon/BaseIcon.vue';
2
+ export { default as BaseInput } from './stories/Atoms/Input/BaseInput.vue';
3
+ export { default as BaseRadioGroup } from './stories/Atoms/RadioGroup/BaseRadioGroup.vue';
4
+ export { default as BaseTag } from './stories/Atoms/Tag/BaseTag.vue';
5
+ export { default as BaseText } from './stories/Atoms/Text/BaseText.vue';
6
+
7
+ export { default as Breadcrumb } from './stories/Molecules/Breadcrumb/Admin/Breadcrumb.vue';
8
+ export { default as BaseButton } from './stories/Molecules/Buttons/BaseButton/BaseButton.vue';
9
+ export { default as FilterAndSortBar } from './stories/Molecules/Filters/FilterAndSortBar/FilterAndSortBar.vue';
10
+ export { default as FilterUpcomingPast } from './stories/Molecules/Filters/FilterUpcomingPast/FilterUpcomingPast.vue';
11
+ export { default as BaseModal } from './stories/Molecules/Modals/BaseModal/BaseModal.vue';
12
+ export { default as BracketDirectorsModal } from './stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.vue';
13
+ export { default as LoadingModal } from './stories/Molecules/Modals/LoadingModal/LoadingModal.vue';
14
+ export { default as EventWizardModal } from './stories/Molecules/Modals/EventWizardModal/EventWizardModal.vue';
15
+
16
+ export { default as AddressAutocomplete } from './stories/Organisms/ComplexInputs/AddressAutocomplete/AddressAutocomplete.vue';
17
+ export { default as DatePicker } from './stories/Organisms/ComplexInputs/DatePicker/DatePicker.vue';
18
+ export { default as DropDownMenu } from './stories/Organisms/ComplexInputs/DropDown/DropDownMenu.vue';
19
+ export { default as GridContainer } from './stories/Organisms/Grids/GridContainer.vue';
20
+ export { default as PoolGrid } from './stories/Organisms/Containers/PoolGrid/PoolGrid.vue';
21
+ export { default as BracketDirectorAssignmentCard } from './stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue';
22
+ export { default as BracketDirectorAssignmentPanel } from './stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue';
23
+ export { default as ImageCropper } from './stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.vue';
24
+ export { default as TextBoxEditor } from './stories/Organisms/ComplexInputs/TextBoxEditor/TextBoxEditor.vue';
25
+ export { default as EditEventWizard } from './stories/Organisms/Wizards/EditEventWizard/EditEventWizard.vue';
26
+
27
+ export { default as TournamentManagement } from './stories/Templates/TournamentManagement/TournamentManagement.vue';
28
+ export { default as ListEvents } from './stories/Templates/TournamentManagement/ListEvents/ListEvents.vue';
29
+
30
+ export { default as AdminLeftMenu } from './stories/Templates/Menus/Admin/AdminLeftMenu.vue';
31
+ export { default as EventAttendance } from './stories/Templates/EventManagement/EventAttendance/EventAttendance.vue';
32
+ export { default as PoolManagement } from './stories/Templates/EventManagement/PoolManagement/PoolManagement.vue';
33
+ export { default as StaffList } from './stories/Templates/EventManagement/StaffList/StaffList.vue';
34
+ export { default as PoolLive } from './stories/Templates/EventManagement/PoolLive/PoolLive.vue';
35
+ export { default as PoolResults } from './stories/Templates/EventManagement/PoolResults/PoolResults.vue';
36
+ export { default as Bracket } from './stories/Templates/EventManagement/Bracket/Bracket.vue';
37
+ export { default as EditEventInfo } from './stories/Templates/EventManagement/EditEventInfo/EditEventInfo.vue';
38
+ export { default as EventFinalResults } from './stories/Templates/EventManagement/EventFinalResults/EventFinalResults.vue';
39
+ export { default as KioskDisplay } from './stories/Templates/EventManagement/KioskDisplay/KioskDisplay.vue';
40
+ export { default as PersonManagement } from './stories/Templates/PersonManagement/PersonManagement.vue';
41
+ export { default as ClubManagement } from './stories/Templates/ClubManagement/ClubManagement.vue';
42
+ export { default as Assignment } from './stories/Templates/Assignment/Assignment.vue';
43
+ export { default as PenaltyReport } from './stories/Templates/EventManagement/Penalties/PenaltyReport.vue';
@@ -0,0 +1,49 @@
1
+ import { ref, watch } from 'vue';
2
+ import BracketDirectorsModal from './BracketDirectorsModal.vue';
3
+ import {
4
+ availableDirectors,
5
+ availableRings,
6
+ plannedDirectingStations
7
+ } from '../../../Templates/EventManagement/Bracket/BracketPlannedSuggestions.js';
8
+
9
+ export default {
10
+ title: 'Molecules/Modals/BracketDirectorsModal',
11
+ component: BracketDirectorsModal,
12
+ args: { show: true, availableDirectors, rings: availableRings, stations: plannedDirectingStations.slice(0, 2) },
13
+ argTypes: {
14
+ show: { control: 'boolean' },
15
+ availableDirectors: { control: 'object' },
16
+ rings: { control: 'object' },
17
+ stations: { control: 'object' }
18
+ },
19
+ render: args => ({
20
+ components: { BracketDirectorsModal },
21
+ setup() {
22
+ const show = ref(args.show);
23
+ const stations = ref(args.stations);
24
+ watch(
25
+ () => args.show,
26
+ value => {
27
+ show.value = value;
28
+ }
29
+ );
30
+ watch(
31
+ () => args.stations,
32
+ value => {
33
+ stations.value = value;
34
+ },
35
+ { deep: true }
36
+ );
37
+ function saveStations(value) {
38
+ stations.value = value;
39
+ }
40
+ return { args, show, stations, saveStations };
41
+ },
42
+ template:
43
+ '<div><button type="button" class="rounded bg-primary px-4 py-2 text-white" @click="show = true">Manage directors</button><BracketDirectorsModal v-bind="args" v-model:show="show" :stations="stations" @submit:stations="saveStations" /></div>'
44
+ })
45
+ };
46
+ export const StaffAndPoolDefaults = {};
47
+ export const NoSelectedDirectors = { args: { stations: [] } };
48
+ export const NoAvailableStaff = { args: { stations: [], availableDirectors: [] } };
49
+ export const NoRings = { args: { rings: [] } };
@@ -0,0 +1,129 @@
1
+ import { mount } from '@vue/test-utils';
2
+ import { nextTick } from 'vue';
3
+ import { afterEach, describe, expect, it } from 'vitest';
4
+ import BracketDirectorsModal from './BracketDirectorsModal.vue';
5
+
6
+ const directors = [
7
+ { PersonId: 1, DisplayName: 'Dave Smith', Color: '#2563eb', DefaultRingId: 1 },
8
+ { PersonId: 2, DisplayName: 'Terry Jones', Color: '#dc2626' }
9
+ ];
10
+ const rings = [
11
+ { RingId: 1, RingName: 'Ring 1' },
12
+ { RingId: 2, RingName: 'Ring 2' },
13
+ { RingId: 3, RingName: 'Ring 3' }
14
+ ];
15
+ const station = {
16
+ RingId: 1,
17
+ RingName: 'Ring 1',
18
+ Color: '#2563eb',
19
+ Director: { PersonId: 1, DisplayName: 'Dave Smith' }
20
+ };
21
+ const wrappers = [];
22
+ const mountModal = async (props = {}) => {
23
+ const wrapper = mount(BracketDirectorsModal, {
24
+ attachTo: document.body,
25
+ global: { stubs: { Teleport: true } },
26
+ props: {
27
+ show: true,
28
+ availableDirectors: structuredClone(directors),
29
+ rings: structuredClone(rings),
30
+ stations: [structuredClone(station)],
31
+ ...props
32
+ }
33
+ });
34
+ wrappers.push(wrapper);
35
+ await nextTick();
36
+ return wrapper;
37
+ };
38
+ const dialog = () => document.querySelector('[data-testid="bracket-directors-modal"]');
39
+ const checkbox = (wrapper, id) => wrapper.get('[data-director-id="' + id + '"]');
40
+ const ringSelect = (wrapper, id) => wrapper.get('[data-ring-director-id="' + id + '"]');
41
+ afterEach(() => wrappers.splice(0).forEach(wrapper => wrapper.unmount()));
42
+
43
+ describe('BracketDirectorsModal', () => {
44
+ it('starts from supplied DE assignments and adds a director who has no pool assignment', async () => {
45
+ const wrapper = await mountModal();
46
+ expect(dialog().open).toBe(true);
47
+ expect(checkbox(wrapper, 1).element.checked).toBe(true);
48
+ expect(checkbox(wrapper, 2).element.checked).toBe(false);
49
+ const before = JSON.parse(JSON.stringify(wrapper.props()));
50
+ await checkbox(wrapper, 2).setValue(true);
51
+ await ringSelect(wrapper, 2).setValue('2');
52
+ await wrapper.get('form').trigger('submit');
53
+ const payload = wrapper.emitted('submit:stations')[0][0];
54
+ expect(payload.map(item => [item.Director.PersonId, item.RingId, item.Color])).toEqual([
55
+ [1, 1, '#2563eb'],
56
+ [2, 2, '#dc2626']
57
+ ]);
58
+ expect(wrapper.props()).toEqual(before);
59
+ payload[0].Director.DisplayName = 'Changed';
60
+ expect(wrapper.props('stations')[0].Director.DisplayName).toBe('Dave Smith');
61
+ expect(wrapper.emitted('update:show')).toEqual([[false]]);
62
+ });
63
+
64
+ it('removes directors by unchecking them and can save an empty roster', async () => {
65
+ const wrapper = await mountModal();
66
+ await checkbox(wrapper, 1).setValue(false);
67
+ await wrapper.get('form').trigger('submit');
68
+ expect(wrapper.emitted('submit:stations')).toEqual([[[]]]);
69
+ expect(wrapper.props('stations')).toHaveLength(1);
70
+ });
71
+
72
+ it('allows a draft ring swap and requires unique rings before saving', async () => {
73
+ const second = {
74
+ RingId: 2,
75
+ RingName: 'Ring 2',
76
+ Color: '#dc2626',
77
+ Director: { PersonId: 2, DisplayName: 'Terry Jones' }
78
+ };
79
+ const wrapper = await mountModal({ stations: [station, second] });
80
+ await ringSelect(wrapper, 1).setValue('2');
81
+ expect(wrapper.get('[role="alert"]').text()).toContain('different ring');
82
+ expect(wrapper.get('button[type="submit"]').attributes('disabled')).toBeDefined();
83
+ await wrapper.get('form').trigger('submit');
84
+ expect(wrapper.emitted('submit:stations')).toBeUndefined();
85
+ await ringSelect(wrapper, 2).setValue('1');
86
+ await wrapper.get('form').trigger('submit');
87
+ expect(
88
+ wrapper.emitted('submit:stations')[0][0].map(item => [item.Director.PersonId, item.RingId, item.Color])
89
+ ).toEqual([
90
+ [1, 2, '#2563eb'],
91
+ [2, 1, '#dc2626']
92
+ ]);
93
+ });
94
+
95
+ it('cancels without emitting changes and discards the draft when reopened', async () => {
96
+ const wrapper = await mountModal();
97
+ await checkbox(wrapper, 1).setValue(false);
98
+ await wrapper.get('[aria-label="Close director assignments"]').trigger('click');
99
+ expect(wrapper.emitted('submit:stations')).toBeUndefined();
100
+ await wrapper.setProps({ show: false });
101
+ expect(dialog().open).toBe(false);
102
+ await wrapper.setProps({ show: true });
103
+ expect(checkbox(wrapper, 1).element.checked).toBe(true);
104
+ await wrapper.get('dialog').trigger('cancel');
105
+ expect(wrapper.emitted('update:show')).toEqual([[false], [false]]);
106
+ });
107
+
108
+ it('accepts staff added while open without losing input drafts and reconciles parent-owned station changes', async () => {
109
+ const wrapper = await mountModal();
110
+ await checkbox(wrapper, 2).setValue(true);
111
+ await ringSelect(wrapper, 2).setValue('2');
112
+ await wrapper.setProps({ availableDirectors: [...directors, { PersonId: 3, DisplayName: 'Michael Cord' }] });
113
+ expect(checkbox(wrapper, 2).element.checked).toBe(true);
114
+ expect(ringSelect(wrapper, 2).element.value).toBe('2');
115
+ expect(checkbox(wrapper, 3).element.checked).toBe(false);
116
+ await wrapper.setProps({ stations: [] });
117
+ expect(checkbox(wrapper, 1).element.checked).toBe(false);
118
+ expect(checkbox(wrapper, 2).element.checked).toBe(false);
119
+ });
120
+
121
+ it('degrades gracefully for missing staff or rings and includes previously selected directors', async () => {
122
+ const wrapper = await mountModal({ availableDirectors: [null, {}, { PersonId: 0 }], rings: [] });
123
+ expect(checkbox(wrapper, 1).element.checked).toBe(true);
124
+ expect(wrapper.get('[role="alert"]').text()).toContain('Choose a ring');
125
+ expect(wrapper.get('button[type="submit"]').attributes('disabled')).toBeDefined();
126
+ await wrapper.setProps({ stations: [] });
127
+ expect(wrapper.text()).toContain('No directors available.');
128
+ });
129
+ });
@@ -0,0 +1,165 @@
1
+ <template>
2
+ <Teleport to="body">
3
+ <dialog
4
+ ref="dialog"
5
+ aria-label="DE directors"
6
+ data-testid="bracket-directors-modal"
7
+ class="fixed inset-0 m-auto max-h-[calc(100dvh-2rem)] w-[calc(100%-2rem)] max-w-xl overflow-y-auto rounded-xl border border-dropdownSelect bg-neutral p-0 text-primaryHighlight shadow-xl backdrop:bg-secondary/60"
8
+ @cancel.prevent="close"
9
+ @click="close"
10
+ >
11
+ <form class="p-5" @submit.prevent="submit" @click.stop>
12
+ <div class="mb-4 flex items-start justify-between gap-3">
13
+ <div>
14
+ <h2 class="text-xl font-bold">Directors</h2>
15
+ <p class="mt-1 text-sm text-quaternary">Select DE directors and choose their rings.</p>
16
+ </div>
17
+ <button
18
+ type="button"
19
+ aria-label="Close director assignments"
20
+ class="flex h-8 w-8 flex-none items-center justify-center rounded text-xl text-quaternary hover:bg-dropdownSelect focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bright"
21
+ @click="close"
22
+ >
23
+ &times;
24
+ </button>
25
+ </div>
26
+ <p v-if="!draft.length" class="rounded border border-dropdownSelect p-4 text-sm text-quaternary">
27
+ No directors available.
28
+ </p>
29
+ <div class="space-y-3">
30
+ <div
31
+ v-for="row in draft"
32
+ :key="row.Director.PersonId"
33
+ class="flex items-center gap-3 rounded-lg border border-dropdownSelect px-3 py-3"
34
+ >
35
+ <label class="flex min-w-0 flex-1 cursor-pointer items-center gap-3">
36
+ <input
37
+ v-model="row.Selected"
38
+ type="checkbox"
39
+ :data-director-id="row.Director.PersonId"
40
+ :aria-label="'Include ' + row.Director.DisplayName + ' in DE'"
41
+ class="h-5 w-5 flex-none accent-bright"
42
+ />
43
+ <span
44
+ v-if="row.Color"
45
+ class="h-2.5 w-2.5 flex-none rounded-full"
46
+ :style="{ backgroundColor: row.Color }"
47
+ aria-hidden="true"
48
+ ></span>
49
+ <span class="min-w-0 wrap-break-word text-sm font-semibold">{{ row.Director.DisplayName }}</span>
50
+ </label>
51
+ <select
52
+ v-model="row.RingId"
53
+ :disabled="!row.Selected"
54
+ :data-ring-director-id="row.Director.PersonId"
55
+ :aria-label="'Ring for ' + row.Director.DisplayName"
56
+ class="w-28 flex-none rounded border border-dropdownSelect bg-white px-2 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bright disabled:opacity-50"
57
+ >
58
+ <option value="">Choose ring</option>
59
+ <option v-for="ring in ringOptions" :key="ring.RingId" :value="ring.RingId" :disabled="ring.Disabled">
60
+ {{ ring.RingName }}
61
+ </option>
62
+ </select>
63
+ </div>
64
+ </div>
65
+ <p v-if="error" role="alert" class="mt-3 text-sm text-alarm">{{ error }}</p>
66
+ <div class="mt-5 flex justify-end gap-2">
67
+ <button
68
+ type="button"
69
+ class="rounded border border-dropdownSelect px-4 py-2 text-sm hover:bg-dropdownSelect focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bright"
70
+ @click="close"
71
+ >
72
+ Cancel
73
+ </button>
74
+ <button
75
+ type="submit"
76
+ :disabled="Boolean(error)"
77
+ class="rounded bg-primary px-4 py-2 text-sm font-semibold text-neutral hover:bg-primaryHighlight focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-bright disabled:cursor-not-allowed disabled:opacity-50"
78
+ >
79
+ Save assignments
80
+ </button>
81
+ </div>
82
+ </form>
83
+ </dialog>
84
+ </Teleport>
85
+ </template>
86
+
87
+ <script>
88
+ import {
89
+ createDirectorDraft,
90
+ reconcileDirectorDraft,
91
+ createDirectorStations,
92
+ getDirectorDraftError,
93
+ getRingOptions
94
+ } from '../../../Util/bracketDirectors.js';
95
+
96
+ export default {
97
+ name: 'BracketDirectorsModal',
98
+ emits: ['update:show', 'submit:stations'],
99
+ props: {
100
+ show: { type: Boolean, default: false },
101
+ availableDirectors: { type: Array, default: () => [] },
102
+ rings: { type: Array, default: () => [] },
103
+ stations: { type: Array, default: () => [] }
104
+ },
105
+ data() {
106
+ return { draft: [] };
107
+ },
108
+ computed: {
109
+ ringOptions() {
110
+ return getRingOptions(this.rings);
111
+ },
112
+ error() {
113
+ return getDirectorDraftError(this.draft, this.rings);
114
+ }
115
+ },
116
+ watch: {
117
+ show: {
118
+ immediate: true,
119
+ handler(value) {
120
+ if (value) this.resetDraft();
121
+ this.$nextTick(this.syncDialog);
122
+ }
123
+ },
124
+ stations: {
125
+ deep: true,
126
+ handler() {
127
+ if (this.show) this.resetDraft();
128
+ }
129
+ },
130
+ availableDirectors: {
131
+ deep: true,
132
+ handler() {
133
+ if (!this.show) return;
134
+ this.draft = reconcileDirectorDraft(this.availableDirectors, this.stations, this.draft);
135
+ }
136
+ }
137
+ },
138
+ mounted() {
139
+ this.syncDialog();
140
+ },
141
+ methods: {
142
+ resetDraft() {
143
+ this.draft = createDirectorDraft(this.availableDirectors, this.stations);
144
+ },
145
+ syncDialog() {
146
+ const dialog = this.$refs.dialog;
147
+ if (!dialog) return;
148
+ if (this.show && !dialog.open) {
149
+ dialog.showModal();
150
+ return;
151
+ }
152
+ if (!this.show && dialog.open) dialog.close();
153
+ },
154
+ close() {
155
+ this.$emit('update:show', false);
156
+ },
157
+ submit() {
158
+ const stations = createDirectorStations(this.draft, this.rings);
159
+ if (!stations) return;
160
+ this.$emit('submit:stations', stations);
161
+ this.close();
162
+ }
163
+ }
164
+ };
165
+ </script>