@planningcenter/tapestry 4.12.0 → 4.13.0

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 (150) hide show
  1. package/dist/componentRegistrars.d.ts +1 -0
  2. package/dist/componentRegistrars.d.ts.map +1 -1
  3. package/dist/componentRegistrars.js +6 -2
  4. package/dist/componentRegistrars.js.map +1 -1
  5. package/dist/components/DataTable/ActiveFilters.d.ts +23 -1
  6. package/dist/components/DataTable/ActiveFilters.d.ts.map +1 -1
  7. package/dist/components/DataTable/ActiveFilters.js +49 -4
  8. package/dist/components/DataTable/ActiveFilters.js.map +1 -1
  9. package/dist/components/DataTable/CollectionEditorsLazy.d.ts.map +1 -1
  10. package/dist/components/DataTable/CollectionEditorsLazy.js +20 -18
  11. package/dist/components/DataTable/CollectionEditorsLazy.js.map +1 -1
  12. package/dist/components/DataTable/DataTable.d.ts +66 -50
  13. package/dist/components/DataTable/DataTable.d.ts.map +1 -1
  14. package/dist/components/DataTable/DataTable.js +78 -17
  15. package/dist/components/DataTable/DataTable.js.map +1 -1
  16. package/dist/components/DataTable/ManageSavedViewsModal.d.ts +18 -7
  17. package/dist/components/DataTable/ManageSavedViewsModal.d.ts.map +1 -1
  18. package/dist/components/DataTable/ManageSavedViewsModal.js +120 -104
  19. package/dist/components/DataTable/ManageSavedViewsModal.js.map +1 -1
  20. package/dist/components/DataTable/ManageSavedViewsModal.module.css.js +1 -1
  21. package/dist/components/DataTable/NewSavedViewButton.d.ts.map +1 -1
  22. package/dist/components/DataTable/NewSavedViewButton.js +1 -1
  23. package/dist/components/DataTable/NewSavedViewButton.js.map +1 -1
  24. package/dist/components/DataTable/SavedViewActions.d.ts +19 -0
  25. package/dist/components/DataTable/SavedViewActions.d.ts.map +1 -0
  26. package/dist/components/DataTable/SavedViewActions.js +34 -0
  27. package/dist/components/DataTable/SavedViewActions.js.map +1 -0
  28. package/dist/components/DataTable/SavedViewMenu.d.ts +18 -0
  29. package/dist/components/DataTable/SavedViewMenu.d.ts.map +1 -0
  30. package/dist/components/DataTable/SavedViewMenu.js +25 -0
  31. package/dist/components/DataTable/SavedViewMenu.js.map +1 -0
  32. package/dist/components/DataTable/SavedViewsBar.d.ts +25 -13
  33. package/dist/components/DataTable/SavedViewsBar.d.ts.map +1 -1
  34. package/dist/components/DataTable/SavedViewsBar.js +81 -93
  35. package/dist/components/DataTable/SavedViewsBar.js.map +1 -1
  36. package/dist/components/DataTable/TableActions.d.ts +4 -2
  37. package/dist/components/DataTable/TableActions.d.ts.map +1 -1
  38. package/dist/components/DataTable/TableActions.js +11 -14
  39. package/dist/components/DataTable/TableActions.js.map +1 -1
  40. package/dist/components/DataTable/index.d.ts +2 -0
  41. package/dist/components/DataTable/index.d.ts.map +1 -1
  42. package/dist/components/DataTable/saveErrorMessage.d.ts +3 -0
  43. package/dist/components/DataTable/saveErrorMessage.d.ts.map +1 -0
  44. package/dist/components/DataTable/saveErrorMessage.js +9 -0
  45. package/dist/components/DataTable/saveErrorMessage.js.map +1 -0
  46. package/dist/components/DataTable/savedViews.d.ts +15 -30
  47. package/dist/components/DataTable/savedViews.d.ts.map +1 -1
  48. package/dist/components/DataTable/savedViews.js +51 -98
  49. package/dist/components/DataTable/savedViews.js.map +1 -1
  50. package/dist/components/DataTable/useActiveSavedView.d.ts +1 -1
  51. package/dist/components/DataTable/useActiveSavedView.d.ts.map +1 -1
  52. package/dist/components/DataTable/useActiveSavedView.js +5 -4
  53. package/dist/components/DataTable/useActiveSavedView.js.map +1 -1
  54. package/dist/components/DataTable/useColumnVisibility.d.ts +7 -7
  55. package/dist/components/DataTable/useColumnVisibility.d.ts.map +1 -1
  56. package/dist/components/DataTable/useColumnVisibility.js +14 -14
  57. package/dist/components/DataTable/useColumnVisibility.js.map +1 -1
  58. package/dist/components/DataTable/useResultsAnnouncement.d.ts +26 -0
  59. package/dist/components/DataTable/useResultsAnnouncement.d.ts.map +1 -0
  60. package/dist/components/DataTable/{useFilterAnnouncement.js → useResultsAnnouncement.js} +16 -12
  61. package/dist/components/DataTable/useResultsAnnouncement.js.map +1 -0
  62. package/dist/components/DataTable/useSaveStatus.d.ts +32 -0
  63. package/dist/components/DataTable/useSaveStatus.d.ts.map +1 -0
  64. package/dist/components/DataTable/useSaveStatus.js +80 -0
  65. package/dist/components/DataTable/useSaveStatus.js.map +1 -0
  66. package/dist/components/DataTable/useSavedViewNameModal.d.ts +14 -0
  67. package/dist/components/DataTable/useSavedViewNameModal.d.ts.map +1 -0
  68. package/dist/components/DataTable/useSavedViewNameModal.js +43 -0
  69. package/dist/components/DataTable/useSavedViewNameModal.js.map +1 -0
  70. package/dist/components/DataTable/useSavedViewsClient.d.ts +58 -0
  71. package/dist/components/DataTable/useSavedViewsClient.d.ts.map +1 -0
  72. package/dist/components/DataTable/useSavedViewsClient.js +87 -0
  73. package/dist/components/DataTable/useSavedViewsClient.js.map +1 -0
  74. package/dist/components/DataTable/useSavedViewsState.d.ts +35 -0
  75. package/dist/components/DataTable/useSavedViewsState.d.ts.map +1 -0
  76. package/dist/components/DataTable/useSavedViewsState.js +192 -0
  77. package/dist/components/DataTable/useSavedViewsState.js.map +1 -0
  78. package/dist/components/DataTable/useUpdateSavedViewName.d.ts +2 -3
  79. package/dist/components/DataTable/useUpdateSavedViewName.d.ts.map +1 -1
  80. package/dist/components/DataTable/useUpdateSavedViewName.js +1 -2
  81. package/dist/components/DataTable/useUpdateSavedViewName.js.map +1 -1
  82. package/dist/components/DataTable/useVisibleSavedViews.d.ts +31 -0
  83. package/dist/components/DataTable/useVisibleSavedViews.d.ts.map +1 -0
  84. package/dist/components/DataTable/useVisibleSavedViews.js +178 -0
  85. package/dist/components/DataTable/useVisibleSavedViews.js.map +1 -0
  86. package/dist/components/button/BaseButton.d.ts +2 -0
  87. package/dist/components/button/BaseButton.d.ts.map +1 -1
  88. package/dist/components/button/BaseButton.js +8 -17
  89. package/dist/components/button/BaseButton.js.map +1 -1
  90. package/dist/components/internal/LoadingAnnouncement.d.ts +6 -0
  91. package/dist/components/internal/LoadingAnnouncement.d.ts.map +1 -1
  92. package/dist/components/internal/LoadingAnnouncement.js +12 -3
  93. package/dist/components/internal/LoadingAnnouncement.js.map +1 -1
  94. package/dist/components/internal/index.d.ts +1 -1
  95. package/dist/components/internal/index.d.ts.map +1 -1
  96. package/dist/components/internal/reorderable-list-box/ReorderableListBox.d.ts +6 -0
  97. package/dist/components/internal/reorderable-list-box/ReorderableListBox.d.ts.map +1 -1
  98. package/dist/components/internal/reorderable-list-box/ReorderableListBox.js +9 -2
  99. package/dist/components/internal/reorderable-list-box/ReorderableListBox.js.map +1 -1
  100. package/dist/components/internal/tooltip/useTooltipTriggerProps.d.ts +13 -0
  101. package/dist/components/internal/tooltip/useTooltipTriggerProps.d.ts.map +1 -0
  102. package/dist/components/internal/tooltip/useTooltipTriggerProps.js +24 -0
  103. package/dist/components/internal/tooltip/useTooltipTriggerProps.js.map +1 -0
  104. package/dist/components/link/BaseLink.d.ts.map +1 -1
  105. package/dist/components/link/BaseLink.js +3 -13
  106. package/dist/components/link/BaseLink.js.map +1 -1
  107. package/dist/components/segmented-control/IconSegment.d.ts +30 -8
  108. package/dist/components/segmented-control/IconSegment.d.ts.map +1 -1
  109. package/dist/components/segmented-control/IconSegment.js +10 -3
  110. package/dist/components/segmented-control/IconSegment.js.map +1 -1
  111. package/dist/components/segmented-control/SegmentBase.d.ts.map +1 -1
  112. package/dist/components/segmented-control/SegmentBase.js +4 -2
  113. package/dist/components/segmented-control/SegmentBase.js.map +1 -1
  114. package/dist/components/segmented-control/SegmentedControl.d.ts.map +1 -1
  115. package/dist/components/segmented-control/SegmentedControl.js +10 -3
  116. package/dist/components/segmented-control/SegmentedControl.js.map +1 -1
  117. package/dist/index.css +28 -12
  118. package/dist/index.css.map +1 -1
  119. package/dist/packages/tapestry-render/dist/{buildComponent-C5PfOgH9.js → buildComponent-DzY2PDMT.js} +23 -23
  120. package/dist/packages/tapestry-render/dist/buildComponent-DzY2PDMT.js.map +1 -0
  121. package/dist/packages/tapestry-render/dist/index.js +2 -2
  122. package/dist/packages/tapestry-render/dist/index.js.map +1 -1
  123. package/dist/packages/tapestry-render/dist/legacy.js +2 -2
  124. package/dist/packages/tapestry-render/dist/legacy.js.map +1 -1
  125. package/dist/product-tokens/giving.css +1 -0
  126. package/dist/reactRender.css +773 -658
  127. package/dist/reactRender.css.map +1 -1
  128. package/dist/reactRender.js +1 -1
  129. package/dist/reactRenderLegacy.css +773 -658
  130. package/dist/reactRenderLegacy.css.map +1 -1
  131. package/dist/reactRenderLegacy.js +1 -1
  132. package/dist/tapestryMount.css +773 -658
  133. package/dist/tapestryMount.css.map +1 -1
  134. package/dist/tapestryMount.js +1 -1
  135. package/dist/tapestryMountLegacy.css +773 -658
  136. package/dist/tapestryMountLegacy.css.map +1 -1
  137. package/dist/tapestryMountLegacy.js +1 -1
  138. package/dist/tokens-dark.css +2 -2
  139. package/dist/tokens-dark.css.map +1 -1
  140. package/dist/tokens.css +11 -10
  141. package/dist/tokens.css.map +1 -1
  142. package/dist/unstable.css +418 -303
  143. package/dist/unstable.css.map +1 -1
  144. package/dist/unstable.js +1 -0
  145. package/dist/unstable.js.map +1 -1
  146. package/package.json +8 -9
  147. package/dist/components/DataTable/useFilterAnnouncement.d.ts +0 -24
  148. package/dist/components/DataTable/useFilterAnnouncement.d.ts.map +0 -1
  149. package/dist/components/DataTable/useFilterAnnouncement.js.map +0 -1
  150. package/dist/packages/tapestry-render/dist/buildComponent-C5PfOgH9.js.map +0 -1
@@ -2,19 +2,21 @@ import { useAnnounce } from '../../utilities/useAnnounce.js';
2
2
  import { useRef, useState, useEffect } from 'react';
3
3
  import { getResultsCountLabel } from './ResultsCount.js';
4
4
 
5
- const FILTER_ANNOUNCEMENT_DELAY_MS = 500;
5
+ const RESULTS_ANNOUNCEMENT_DELAY_MS = 500;
6
6
  /**
7
7
  * Announces the results count to screen reader users after each filter
8
- * change, once the results settle: `loadingState` is not `"loading"`, and
9
- * neither it nor the count has changed for
10
- * {@link FILTER_ANNOUNCEMENT_DELAY_MS}. A change to `"error"` cancels the
11
- * message, because the error is announced on its own. An `"error"` left over
12
- * from an earlier request only delays it.
8
+ * change and each load, once the results settle: `loadingState` is
9
+ * `"idle"`, and neither it nor the count has changed for
10
+ * {@link RESULTS_ANNOUNCEMENT_DELAY_MS}. A load covers the first load and
11
+ * page changes too. A change to `"error"` cancels the message, because the
12
+ * error is announced on its own. An `"error"` left over from an earlier
13
+ * request only delays it.
13
14
  *
14
- * Returns an `onActiveFiltersChange` to use in place of the caller's, so only
15
- * changes the user makes are announced.
15
+ * Returns an `onActiveFiltersChange` to use in place of the caller's, so
16
+ * filter changes the user makes are announced without a load. Filter changes
17
+ * from outside are announced only when they load.
16
18
  */
17
- function useFilterAnnouncement({ isPageCount, loadingState, onActiveFiltersChange, resultNoun, totalCount, }) {
19
+ function useResultsAnnouncement({ isPageCount, loadingState, onActiveFiltersChange, resultNoun, totalCount, }) {
18
20
  const announce = useAnnounce();
19
21
  const isDue = useRef(false);
20
22
  const previousLoadingState = useRef(loadingState);
@@ -24,6 +26,8 @@ function useFilterAnnouncement({ isPageCount, loadingState, onActiveFiltersChang
24
26
  useEffect(() => {
25
27
  const failed = loadingState === "error" && previousLoadingState.current !== "error";
26
28
  previousLoadingState.current = loadingState;
29
+ if (loadingState === "loading")
30
+ isDue.current = true;
27
31
  if (!isDue.current)
28
32
  return;
29
33
  if (failed) {
@@ -35,7 +39,7 @@ function useFilterAnnouncement({ isPageCount, loadingState, onActiveFiltersChang
35
39
  const timeout = setTimeout(() => {
36
40
  isDue.current = false;
37
41
  announce(message);
38
- }, FILTER_ANNOUNCEMENT_DELAY_MS);
42
+ }, RESULTS_ANNOUNCEMENT_DELAY_MS);
39
43
  return () => clearTimeout(timeout);
40
44
  }, [announce, changeCount, loadingState, message]);
41
45
  return (activeFilters) => {
@@ -45,5 +49,5 @@ function useFilterAnnouncement({ isPageCount, loadingState, onActiveFiltersChang
45
49
  };
46
50
  }
47
51
 
48
- export { FILTER_ANNOUNCEMENT_DELAY_MS, useFilterAnnouncement };
49
- //# sourceMappingURL=useFilterAnnouncement.js.map
52
+ export { RESULTS_ANNOUNCEMENT_DELAY_MS, useResultsAnnouncement };
53
+ //# sourceMappingURL=useResultsAnnouncement.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useResultsAnnouncement.js","sources":["../../../src/components/DataTable/useResultsAnnouncement.ts"],"sourcesContent":["import { useAnnounce } from \"@utilities/useAnnounce\"\nimport { useEffect, useRef, useState } from \"react\"\n\nimport type { DataTableLoadingState } from \"./DataTable\"\nimport type { ActiveFilter } from \"./FilterTypes\"\nimport { getResultsCountLabel, type ResultNoun } from \"./ResultsCount\"\n\nexport const RESULTS_ANNOUNCEMENT_DELAY_MS = 500\n\n/**\n * Announces the results count to screen reader users after each filter\n * change and each load, once the results settle: `loadingState` is\n * `\"idle\"`, and neither it nor the count has changed for\n * {@link RESULTS_ANNOUNCEMENT_DELAY_MS}. A load covers the first load and\n * page changes too. A change to `\"error\"` cancels the message, because the\n * error is announced on its own. An `\"error\"` left over from an earlier\n * request only delays it.\n *\n * Returns an `onActiveFiltersChange` to use in place of the caller's, so\n * filter changes the user makes are announced without a load. Filter changes\n * from outside are announced only when they load.\n */\nexport function useResultsAnnouncement({\n isPageCount,\n loadingState,\n onActiveFiltersChange,\n resultNoun,\n totalCount,\n}: {\n /** Whether `totalCount` covers only the current page, not the whole set. */\n isPageCount: boolean\n loadingState: DataTableLoadingState\n onActiveFiltersChange: (activeFilters: ActiveFilter[]) => void\n resultNoun: ResultNoun | undefined\n totalCount: number\n}): (activeFilters: ActiveFilter[]) => void {\n const announce = useAnnounce()\n const isDue = useRef(false)\n const previousLoadingState = useRef(loadingState)\n const [changeCount, setChangeCount] = useState(0)\n const countLabel = getResultsCountLabel(totalCount, resultNoun || true)\n const message = isPageCount ? `${countLabel} on this page` : countLabel\n\n useEffect(() => {\n const failed =\n loadingState === \"error\" && previousLoadingState.current !== \"error\"\n previousLoadingState.current = loadingState\n if (loadingState === \"loading\") isDue.current = true\n if (!isDue.current) return\n if (failed) {\n isDue.current = false\n return\n }\n if (loadingState !== \"idle\") return\n const timeout = setTimeout(() => {\n isDue.current = false\n announce(message)\n }, RESULTS_ANNOUNCEMENT_DELAY_MS)\n return () => clearTimeout(timeout)\n }, [announce, changeCount, loadingState, message])\n\n return (activeFilters) => {\n isDue.current = true\n setChangeCount((count) => count + 1)\n onActiveFiltersChange(activeFilters)\n }\n}\n"],"names":[],"mappings":";;;;AAOO,MAAM,6BAA6B,GAAG;AAE7C;;;;;;;;;;;;AAYG;AACG,SAAU,sBAAsB,CAAC,EACrC,WAAW,EACX,YAAY,EACZ,qBAAqB,EACrB,UAAU,EACV,UAAU,GAQX,EAAA;AACC,IAAA,MAAM,QAAQ,GAAG,WAAW,EAAE;AAC9B,IAAA,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;AAC3B,IAAA,MAAM,oBAAoB,GAAG,MAAM,CAAC,YAAY,CAAC;IACjD,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,CAAC,CAAC;IACjD,MAAM,UAAU,GAAG,oBAAoB,CAAC,UAAU,EAAE,UAAU,IAAI,IAAI,CAAC;AACvE,IAAA,MAAM,OAAO,GAAG,WAAW,GAAG,CAAA,EAAG,UAAU,CAAA,aAAA,CAAe,GAAG,UAAU;IAEvE,SAAS,CAAC,MAAK;QACb,MAAM,MAAM,GACV,YAAY,KAAK,OAAO,IAAI,oBAAoB,CAAC,OAAO,KAAK,OAAO;AACtE,QAAA,oBAAoB,CAAC,OAAO,GAAG,YAAY;QAC3C,IAAI,YAAY,KAAK,SAAS;AAAE,YAAA,KAAK,CAAC,OAAO,GAAG,IAAI;QACpD,IAAI,CAAC,KAAK,CAAC,OAAO;YAAE;QACpB,IAAI,MAAM,EAAE;AACV,YAAA,KAAK,CAAC,OAAO,GAAG,KAAK;YACrB;QACF;QACA,IAAI,YAAY,KAAK,MAAM;YAAE;AAC7B,QAAA,MAAM,OAAO,GAAG,UAAU,CAAC,MAAK;AAC9B,YAAA,KAAK,CAAC,OAAO,GAAG,KAAK;YACrB,QAAQ,CAAC,OAAO,CAAC;QACnB,CAAC,EAAE,6BAA6B,CAAC;AACjC,QAAA,OAAO,MAAM,YAAY,CAAC,OAAO,CAAC;IACpC,CAAC,EAAE,CAAC,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,OAAO,CAAC,CAAC;IAElD,OAAO,CAAC,aAAa,KAAI;AACvB,QAAA,KAAK,CAAC,OAAO,GAAG,IAAI;QACpB,cAAc,CAAC,CAAC,KAAK,KAAK,KAAK,GAAG,CAAC,CAAC;QACpC,qBAAqB,CAAC,aAAa,CAAC;AACtC,IAAA,CAAC;AACH;;;;"}
@@ -0,0 +1,32 @@
1
+ import React from "react";
2
+ import type { SavedView } from "./savedViews";
3
+ /** Tracks saves of views not in the list yet, which have no id to go by. */
4
+ export declare const NEW_VIEW: unique symbol;
5
+ type SaveStatusKey = SavedView["id"] | typeof NEW_VIEW;
6
+ type SaveStatus = {
7
+ message: string;
8
+ status: "failed";
9
+ } | {
10
+ status: "saving";
11
+ };
12
+ /**
13
+ * Tracks saved view saves, by view. A save shows a spinner while it's
14
+ * pending, which fades in after 500ms so quick saves show nothing. A failed
15
+ * one shows a "not saved" icon for 3s and is announced to screen readers by
16
+ * the view's `name`. A newer save of the same view replaces the older one's.
17
+ */
18
+ export declare function useSaveStatus(): {
19
+ statusFor: (id: SaveStatusKey) => SaveStatus | undefined;
20
+ track: (id: SaveStatusKey, saving: unknown, name: string) => Promise<void>;
21
+ };
22
+ /**
23
+ * Hidden from screen readers, which hear failures announced instead, so it
24
+ * stays out of the view's name. With a `label`, a failure shows it next to
25
+ * the icon.
26
+ */
27
+ export declare function SaveStatusIcon({ label, status, }: {
28
+ label?: string;
29
+ status?: SaveStatus;
30
+ }): React.JSX.Element | null;
31
+ export {};
32
+ //# sourceMappingURL=useSaveStatus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSaveStatus.d.ts","sourceRoot":"","sources":["../../../src/components/DataTable/useSaveStatus.tsx"],"names":[],"mappings":"AAGA,OAAO,KAA2B,MAAM,OAAO,CAAA;AAE/C,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAK7C,4EAA4E;AAC5E,eAAO,MAAM,QAAQ,eAAqB,CAAA;AAE1C,KAAK,aAAa,GAAG,SAAS,CAAC,IAAI,CAAC,GAAG,OAAO,QAAQ,CAAA;AAEtD,KAAK,UAAU,GAAG;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAA;CAAE,GAAG;IAAE,MAAM,EAAE,QAAQ,CAAA;CAAE,CAAA;AAI9E;;;;;GAKG;AACH,wBAAgB,aAAa;oBAyCF,aAAa;gBA1Bb,aAAa,UAAU,OAAO,QAAQ,MAAM;EA2BtE;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAC7B,KAAK,EACL,MAAM,GACP,EAAE;IACD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,MAAM,CAAC,EAAE,UAAU,CAAA;CACpB,4BAgCA"}
@@ -0,0 +1,80 @@
1
+ import LoadingSpinner from '../internal/LoadingSpinner.js';
2
+ import '../internal/LoadingAnnouncement.js';
3
+ import '../internal/tooltip/Tooltip.js';
4
+ import Icon from '../../utilities/Icon.js';
5
+ import { useAnnounce } from '../../utilities/useAnnounce.js';
6
+ import React__default, { useState, useRef } from 'react';
7
+ import { saveErrorMessage } from './saveErrorMessage.js';
8
+
9
+ const FAILED_DURATION = 3000;
10
+ /** Tracks saves of views not in the list yet, which have no id to go by. */
11
+ const NEW_VIEW = Symbol("new view");
12
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
13
+ /**
14
+ * Tracks saved view saves, by view. A save shows a spinner while it's
15
+ * pending, which fades in after 500ms so quick saves show nothing. A failed
16
+ * one shows a "not saved" icon for 3s and is announced to screen readers by
17
+ * the view's `name`. A newer save of the same view replaces the older one's.
18
+ */
19
+ function useSaveStatus() {
20
+ const announce = useAnnounce();
21
+ const [statuses, setStatuses] = useState(() => new Map());
22
+ const latestSave = useRef(new Map());
23
+ const setStatus = (id, status) => setStatuses((current) => {
24
+ const next = new Map(current);
25
+ if (status)
26
+ next.set(id, status);
27
+ else
28
+ next.delete(id);
29
+ return next;
30
+ });
31
+ const track = async (id, saving, name) => {
32
+ const save = (latestSave.current.get(id) ?? 0) + 1;
33
+ latestSave.current.set(id, save);
34
+ const isLatest = () => latestSave.current.get(id) === save;
35
+ setStatus(id, { status: "saving" });
36
+ let failure;
37
+ try {
38
+ await saving;
39
+ }
40
+ catch (error) {
41
+ failure = { error };
42
+ }
43
+ if (!isLatest())
44
+ return;
45
+ if (!failure) {
46
+ setStatus(id);
47
+ return;
48
+ }
49
+ const message = saveErrorMessage(failure.error);
50
+ setStatus(id, { message, status: "failed" });
51
+ announce(`${name} not saved. ${message}`);
52
+ await wait(FAILED_DURATION);
53
+ if (isLatest())
54
+ setStatus(id);
55
+ };
56
+ return { statusFor: (id) => statuses.get(id), track };
57
+ }
58
+ /**
59
+ * Hidden from screen readers, which hear failures announced instead, so it
60
+ * stays out of the view's name. With a `label`, a failure shows it next to
61
+ * the icon.
62
+ */
63
+ function SaveStatusIcon({ label, status, }) {
64
+ if (!status)
65
+ return null;
66
+ if (status.status === "saving") {
67
+ return (React__default.createElement("span", { "aria-hidden": true, className: "tds-data-table-saved-view-save-status tds-data-table-saved-view-save-status--saving" },
68
+ React__default.createElement(LoadingSpinner, null)));
69
+ }
70
+ const icon = (React__default.createElement("span", { "aria-hidden": true, className: "tds-data-table-saved-view-save-status tds-data-table-saved-view-save-status--failed", title: `Not saved. ${status.message}` },
71
+ React__default.createElement(Icon, { "aria-hidden": true, symbol: "general#exclamation-triangle" })));
72
+ if (!label)
73
+ return icon;
74
+ return (React__default.createElement("span", { "aria-hidden": true, className: "tds-data-table-saved-view-save-error", title: status.message },
75
+ icon,
76
+ label));
77
+ }
78
+
79
+ export { NEW_VIEW, SaveStatusIcon, useSaveStatus };
80
+ //# sourceMappingURL=useSaveStatus.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSaveStatus.js","sources":["../../../src/components/DataTable/useSaveStatus.tsx"],"sourcesContent":["import { LoadingSpinner } from \"@components/internal\"\nimport Icon from \"@utilities/Icon\"\nimport { useAnnounce } from \"@utilities/useAnnounce\"\nimport React, { useRef, useState } from \"react\"\n\nimport type { SavedView } from \"./savedViews\"\nimport { saveErrorMessage } from \"./saveErrorMessage\"\n\nconst FAILED_DURATION = 3000\n\n/** Tracks saves of views not in the list yet, which have no id to go by. */\nexport const NEW_VIEW = Symbol(\"new view\")\n\ntype SaveStatusKey = SavedView[\"id\"] | typeof NEW_VIEW\n\ntype SaveStatus = { message: string; status: \"failed\" } | { status: \"saving\" }\n\nconst wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))\n\n/**\n * Tracks saved view saves, by view. A save shows a spinner while it's\n * pending, which fades in after 500ms so quick saves show nothing. A failed\n * one shows a \"not saved\" icon for 3s and is announced to screen readers by\n * the view's `name`. A newer save of the same view replaces the older one's.\n */\nexport function useSaveStatus() {\n const announce = useAnnounce()\n const [statuses, setStatuses] = useState(\n () => new Map<SaveStatusKey, SaveStatus>()\n )\n const latestSave = useRef(new Map<SaveStatusKey, number>())\n\n const setStatus = (id: SaveStatusKey, status?: SaveStatus) =>\n setStatuses((current) => {\n const next = new Map(current)\n if (status) next.set(id, status)\n else next.delete(id)\n return next\n })\n\n const track = async (id: SaveStatusKey, saving: unknown, name: string) => {\n const save = (latestSave.current.get(id) ?? 0) + 1\n latestSave.current.set(id, save)\n const isLatest = () => latestSave.current.get(id) === save\n\n setStatus(id, { status: \"saving\" })\n\n let failure: { error: unknown } | undefined\n try {\n await saving\n } catch (error) {\n failure = { error }\n }\n if (!isLatest()) return\n if (!failure) {\n setStatus(id)\n return\n }\n\n const message = saveErrorMessage(failure.error)\n setStatus(id, { message, status: \"failed\" })\n announce(`${name} not saved. ${message}`)\n await wait(FAILED_DURATION)\n if (isLatest()) setStatus(id)\n }\n\n return { statusFor: (id: SaveStatusKey) => statuses.get(id), track }\n}\n\n/**\n * Hidden from screen readers, which hear failures announced instead, so it\n * stays out of the view's name. With a `label`, a failure shows it next to\n * the icon.\n */\nexport function SaveStatusIcon({\n label,\n status,\n}: {\n label?: string\n status?: SaveStatus\n}) {\n if (!status) return null\n if (status.status === \"saving\") {\n return (\n <span\n aria-hidden\n className=\"tds-data-table-saved-view-save-status tds-data-table-saved-view-save-status--saving\"\n >\n <LoadingSpinner />\n </span>\n )\n }\n const icon = (\n <span\n aria-hidden\n className=\"tds-data-table-saved-view-save-status tds-data-table-saved-view-save-status--failed\"\n title={`Not saved. ${status.message}`}\n >\n <Icon aria-hidden symbol=\"general#exclamation-triangle\" />\n </span>\n )\n if (!label) return icon\n return (\n <span\n aria-hidden\n className=\"tds-data-table-saved-view-save-error\"\n title={status.message}\n >\n {icon}\n {label}\n </span>\n )\n}\n"],"names":["React"],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { NewSavedViewModalProps } from "./NewSavedViewModal";
2
+ import { type SavedView } from "./savedViews";
3
+ /**
4
+ * The name modal's state, for a new view, a duplicate, or a rename. Spread
5
+ * `modalProps` onto a `NewSavedViewModal`. What it saves outlives the modal's
6
+ * open state, so the title holds while the modal fades out.
7
+ */
8
+ export declare function useSavedViewNameModal(onUpdate?: (savedView: SavedView) => Promise<SavedView | undefined> | SavedView): {
9
+ modalProps: NewSavedViewModalProps;
10
+ open: (title: string, defaultName: string, save: (name: string) => void) => void;
11
+ openDuplicate: (view: SavedView, create: (duplicate: Omit<SavedView, "id">) => void) => void;
12
+ openRename: (view: SavedView, track: (saving: Promise<void>) => void) => void;
13
+ };
14
+ //# sourceMappingURL=useSavedViewNameModal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSavedViewNameModal.d.ts","sourceRoot":"","sources":["../../../src/components/DataTable/useSavedViewNameModal.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,qBAAqB,CAAA;AACjE,OAAO,EAAE,KAAK,SAAS,EAAsB,MAAM,cAAc,CAAA;AAGjE;;;;GAIG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,CAAC,EAAE,CACT,SAAS,EAAE,SAAS,KACjB,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,GAAG,SAAS;;kBAWtC,MAAM,eACA,MAAM,QACb,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI;0BAQtB,SAAS,UACP,CAAC,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,KAAK,IAAI;uBAS5C,SAAS,SACR,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI;EAmBzC"}
@@ -0,0 +1,43 @@
1
+ import { useState } from 'react';
2
+ import { savedViewDuplicate } from './savedViews.js';
3
+ import { useUpdateSavedViewName } from './useUpdateSavedViewName.js';
4
+
5
+ /**
6
+ * The name modal's state, for a new view, a duplicate, or a rename. Spread
7
+ * `modalProps` onto a `NewSavedViewModal`. What it saves outlives the modal's
8
+ * open state, so the title holds while the modal fades out.
9
+ */
10
+ function useSavedViewNameModal(onUpdate) {
11
+ const [draft, setDraft] = useState();
12
+ const [isOpen, setIsOpen] = useState(false);
13
+ const updateSavedViewName = useUpdateSavedViewName(onUpdate);
14
+ const open = (title, defaultName, save) => {
15
+ setDraft({ defaultName, save, title });
16
+ setIsOpen(true);
17
+ };
18
+ /** `create` gets the duplicate under the entered name, without an id. */
19
+ const openDuplicate = (view, create) => {
20
+ open("Duplicate view", `${view.name} copy`, (name) => create(savedViewDuplicate(view, name)));
21
+ };
22
+ /** `track` gets the rename's save. */
23
+ const openRename = (view, track) => {
24
+ open("Rename view", view.name, (name) => {
25
+ if (name !== view.name)
26
+ track(updateSavedViewName(view, name));
27
+ });
28
+ };
29
+ const modalProps = {
30
+ defaultName: draft?.defaultName,
31
+ onClose: () => setIsOpen(false),
32
+ onSave: (name) => {
33
+ setIsOpen(false);
34
+ draft?.save(name);
35
+ },
36
+ open: isOpen,
37
+ title: draft?.title,
38
+ };
39
+ return { modalProps, open, openDuplicate, openRename };
40
+ }
41
+
42
+ export { useSavedViewNameModal };
43
+ //# sourceMappingURL=useSavedViewNameModal.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSavedViewNameModal.js","sources":["../../../src/components/DataTable/useSavedViewNameModal.ts"],"sourcesContent":["import { useState } from \"react\"\n\nimport type { NewSavedViewModalProps } from \"./NewSavedViewModal\"\nimport { type SavedView, savedViewDuplicate } from \"./savedViews\"\nimport { useUpdateSavedViewName } from \"./useUpdateSavedViewName\"\n\n/**\n * The name modal's state, for a new view, a duplicate, or a rename. Spread\n * `modalProps` onto a `NewSavedViewModal`. What it saves outlives the modal's\n * open state, so the title holds while the modal fades out.\n */\nexport function useSavedViewNameModal(\n onUpdate?: (\n savedView: SavedView\n ) => Promise<SavedView | undefined> | SavedView\n) {\n const [draft, setDraft] = useState<{\n defaultName: string\n save: (name: string) => void\n title: string\n }>()\n const [isOpen, setIsOpen] = useState(false)\n const updateSavedViewName = useUpdateSavedViewName(onUpdate)\n\n const open = (\n title: string,\n defaultName: string,\n save: (name: string) => void\n ) => {\n setDraft({ defaultName, save, title })\n setIsOpen(true)\n }\n\n /** `create` gets the duplicate under the entered name, without an id. */\n const openDuplicate = (\n view: SavedView,\n create: (duplicate: Omit<SavedView, \"id\">) => void\n ) => {\n open(\"Duplicate view\", `${view.name} copy`, (name) =>\n create(savedViewDuplicate(view, name))\n )\n }\n\n /** `track` gets the rename's save. */\n const openRename = (\n view: SavedView,\n track: (saving: Promise<void>) => void\n ) => {\n open(\"Rename view\", view.name, (name) => {\n if (name !== view.name) track(updateSavedViewName(view, name))\n })\n }\n\n const modalProps: NewSavedViewModalProps = {\n defaultName: draft?.defaultName,\n onClose: () => setIsOpen(false),\n onSave: (name) => {\n setIsOpen(false)\n draft?.save(name)\n },\n open: isOpen,\n title: draft?.title,\n }\n\n return { modalProps, open, openDuplicate, openRename }\n}\n"],"names":[],"mappings":";;;;AAMA;;;;AAIG;AACG,SAAU,qBAAqB,CACnC,QAE+C,EAAA;IAE/C,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,EAI9B;IACJ,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC;AAC3C,IAAA,MAAM,mBAAmB,GAAG,sBAAsB,CAAC,QAAQ,CAAC;IAE5D,MAAM,IAAI,GAAG,CACX,KAAa,EACb,WAAmB,EACnB,IAA4B,KAC1B;QACF,QAAQ,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC;QACtC,SAAS,CAAC,IAAI,CAAC;AACjB,IAAA,CAAC;;AAGD,IAAA,MAAM,aAAa,GAAG,CACpB,IAAe,EACf,MAAkD,KAChD;QACF,IAAI,CAAC,gBAAgB,EAAE,CAAA,EAAG,IAAI,CAAC,IAAI,CAAA,KAAA,CAAO,EAAE,CAAC,IAAI,KAC/C,MAAM,CAAC,kBAAkB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CACvC;AACH,IAAA,CAAC;;AAGD,IAAA,MAAM,UAAU,GAAG,CACjB,IAAe,EACf,KAAsC,KACpC;QACF,IAAI,CAAC,aAAa,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,IAAI,KAAI;AACtC,YAAA,IAAI,IAAI,KAAK,IAAI,CAAC,IAAI;gBAAE,KAAK,CAAC,mBAAmB,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AAChE,QAAA,CAAC,CAAC;AACJ,IAAA,CAAC;AAED,IAAA,MAAM,UAAU,GAA2B;QACzC,WAAW,EAAE,KAAK,EAAE,WAAW;AAC/B,QAAA,OAAO,EAAE,MAAM,SAAS,CAAC,KAAK,CAAC;AAC/B,QAAA,MAAM,EAAE,CAAC,IAAI,KAAI;YACf,SAAS,CAAC,KAAK,CAAC;AAChB,YAAA,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC;QACnB,CAAC;AACD,QAAA,IAAI,EAAE,MAAM;QACZ,KAAK,EAAE,KAAK,EAAE,KAAK;KACpB;IAED,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,aAAa,EAAE,UAAU,EAAE;AACxD;;;;"}
@@ -0,0 +1,58 @@
1
+ import { type SavedView } from "./savedViews";
2
+ /**
3
+ * Sends requests to the saved views API. Matches
4
+ * `@planningcenter/api-client`'s `Client`, so auth, CSRF, and the API root stay
5
+ * in the app; pass one whose root serves the private graph. Each method
6
+ * resolves to the parsed response and rejects on failure.
7
+ */
8
+ export interface ApiClient {
9
+ delete(request: {
10
+ url: string;
11
+ }): Promise<unknown>;
12
+ patch(request: {
13
+ data: object;
14
+ url: string;
15
+ }): Promise<unknown>;
16
+ post(request: {
17
+ data: object;
18
+ url: string;
19
+ }): Promise<unknown>;
20
+ }
21
+ export interface UseSavedViewsClientOptions {
22
+ client: ApiClient;
23
+ tableKey: string;
24
+ }
25
+ /**
26
+ * Returns DataTable's saved view callbacks, sending each change to the saved
27
+ * views endpoints that `tapestry:saved_views` adds to pco-api's private graph:
28
+ *
29
+ * - `POST /data_tables/:table_key/saved_views`
30
+ * - `PATCH /data_tables/:table_key/saved_views/:id` (also takes `after_id` to
31
+ * reorder)
32
+ * - `DELETE /data_tables/:table_key/saved_views/:id`
33
+ * - `POST` and `DELETE /data_tables/:table_key/saved_view_default`
34
+ *
35
+ * Each callback resolves once its request succeeds and rejects with the
36
+ * client's error. Default changes are sent one at a time, so the server keeps
37
+ * the last one chosen. DataTable shows the change right away and undoes it on
38
+ * rejection. Doesn't load views or hold state, so pass `data_table_props` to
39
+ * DataTable too:
40
+ *
41
+ * ```tsx
42
+ * <DataTable {...dataTableProps} {...useSavedViewsClient({ client, tableKey })} />
43
+ * ```
44
+ */
45
+ export declare function useSavedViewsClient({ client, tableKey, }: UseSavedViewsClientOptions): {
46
+ onDefaultViewIdChange: (id: number | string) => Promise<void>;
47
+ onSavedViewCreate: (view: Omit<SavedView, "id">) => Promise<{
48
+ id: string | number;
49
+ sort?: import("./DataTable").Sort | undefined;
50
+ name: string;
51
+ activeFilters: import("./FilterTypes").ActiveFilter[];
52
+ hiddenColumnNames?: string[] | undefined;
53
+ }>;
54
+ onSavedViewDelete: (view: SavedView) => Promise<void>;
55
+ onSavedViewReorder: (view: SavedView, after: SavedView | null) => Promise<void>;
56
+ onSavedViewUpdate: (view: SavedView) => Promise<SavedView>;
57
+ };
58
+ //# sourceMappingURL=useSavedViewsClient.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSavedViewsClient.d.ts","sourceRoot":"","sources":["../../../src/components/DataTable/useSavedViewsClient.ts"],"names":[],"mappings":"AAGA,OAAO,EAAe,KAAK,SAAS,EAAE,MAAM,cAAc,CAAA;AAE1D;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACxB,MAAM,CAAC,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAClD,KAAK,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;IAC/D,IAAI,CAAC,OAAO,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;CAC/D;AAED,MAAM,WAAW,0BAA0B;IACzC,MAAM,EAAE,SAAS,CAAA;IACjB,QAAQ,EAAE,MAAM,CAAA;CACjB;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,QAAQ,GACT,EAAE,0BAA0B;gCA0Cc,MAAM,GAAG,MAAM;8BAnCjB,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC;;;;;;;8BA8BrB,SAAS;+BAdxC,SAAS,SACR,SAAS,GAAG,IAAI;8BARc,SAAS;EA0DjD"}
@@ -0,0 +1,87 @@
1
+ import { useRef } from 'react';
2
+ import { initialView } from './savedViews.js';
3
+
4
+ /**
5
+ * Returns DataTable's saved view callbacks, sending each change to the saved
6
+ * views endpoints that `tapestry:saved_views` adds to pco-api's private graph:
7
+ *
8
+ * - `POST /data_tables/:table_key/saved_views`
9
+ * - `PATCH /data_tables/:table_key/saved_views/:id` (also takes `after_id` to
10
+ * reorder)
11
+ * - `DELETE /data_tables/:table_key/saved_views/:id`
12
+ * - `POST` and `DELETE /data_tables/:table_key/saved_view_default`
13
+ *
14
+ * Each callback resolves once its request succeeds and rejects with the
15
+ * client's error. Default changes are sent one at a time, so the server keeps
16
+ * the last one chosen. DataTable shows the change right away and undoes it on
17
+ * rejection. Doesn't load views or hold state, so pass `data_table_props` to
18
+ * DataTable too:
19
+ *
20
+ * ```tsx
21
+ * <DataTable {...dataTableProps} {...useSavedViewsClient({ client, tableKey })} />
22
+ * ```
23
+ */
24
+ function useSavedViewsClient({ client, tableKey, }) {
25
+ const tableUrl = `/data_tables/${encodeURIComponent(tableKey)}`;
26
+ const viewUrl = (id) => `${tableUrl}/saved_views/${encodeURIComponent(String(id))}`;
27
+ const defaultUrl = `${tableUrl}/saved_view_default`;
28
+ const lastDefaultChange = useRef(Promise.resolve());
29
+ const onSavedViewCreate = async (view) => {
30
+ // Only the id is read, since clients may reshape the response
31
+ const { data } = (await client.post({
32
+ data: savedViewDocument(view),
33
+ url: `${tableUrl}/saved_views`,
34
+ }));
35
+ return { ...view, id: data.id };
36
+ };
37
+ const onSavedViewUpdate = async (view) => {
38
+ await client.patch({ data: savedViewDocument(view), url: viewUrl(view.id) });
39
+ return view;
40
+ };
41
+ /** Places the view after `after`, or first when it's `null`. */
42
+ const onSavedViewReorder = async (view, after) => {
43
+ await client.patch({
44
+ data: {
45
+ data: {
46
+ attributes: { after_id: after?.id ?? null },
47
+ type: "SavedView",
48
+ },
49
+ },
50
+ url: viewUrl(view.id),
51
+ });
52
+ };
53
+ const onSavedViewDelete = async (view) => {
54
+ await client.delete({ url: viewUrl(view.id) });
55
+ };
56
+ /** Pass the initial view's id to reset the default. */
57
+ const onDefaultViewIdChange = async (id) => {
58
+ const send = () => id === initialView.id
59
+ ? client.delete({ url: defaultUrl })
60
+ : client.post({
61
+ data: {
62
+ data: {
63
+ attributes: { view: String(id) },
64
+ type: "SavedViewDefault",
65
+ },
66
+ },
67
+ url: defaultUrl,
68
+ });
69
+ const change = lastDefaultChange.current.then(send, send);
70
+ lastDefaultChange.current = change;
71
+ await change;
72
+ };
73
+ return {
74
+ onDefaultViewIdChange,
75
+ onSavedViewCreate,
76
+ onSavedViewDelete,
77
+ onSavedViewReorder,
78
+ onSavedViewUpdate,
79
+ };
80
+ }
81
+ function savedViewDocument(view) {
82
+ const { id: _id, name, ...state } = view;
83
+ return { data: { attributes: { name, state }, type: "SavedView" } };
84
+ }
85
+
86
+ export { useSavedViewsClient };
87
+ //# sourceMappingURL=useSavedViewsClient.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSavedViewsClient.js","sources":["../../../src/components/DataTable/useSavedViewsClient.ts"],"sourcesContent":["import { useRef } from \"react\"\n\nimport type { DataTableProps } from \"./DataTable\"\nimport { initialView, type SavedView } from \"./savedViews\"\n\n/**\n * Sends requests to the saved views API. Matches\n * `@planningcenter/api-client`'s `Client`, so auth, CSRF, and the API root stay\n * in the app; pass one whose root serves the private graph. Each method\n * resolves to the parsed response and rejects on failure.\n */\nexport interface ApiClient {\n delete(request: { url: string }): Promise<unknown>\n patch(request: { data: object; url: string }): Promise<unknown>\n post(request: { data: object; url: string }): Promise<unknown>\n}\n\nexport interface UseSavedViewsClientOptions {\n client: ApiClient\n tableKey: string\n}\n\n/**\n * Returns DataTable's saved view callbacks, sending each change to the saved\n * views endpoints that `tapestry:saved_views` adds to pco-api's private graph:\n *\n * - `POST /data_tables/:table_key/saved_views`\n * - `PATCH /data_tables/:table_key/saved_views/:id` (also takes `after_id` to\n * reorder)\n * - `DELETE /data_tables/:table_key/saved_views/:id`\n * - `POST` and `DELETE /data_tables/:table_key/saved_view_default`\n *\n * Each callback resolves once its request succeeds and rejects with the\n * client's error. Default changes are sent one at a time, so the server keeps\n * the last one chosen. DataTable shows the change right away and undoes it on\n * rejection. Doesn't load views or hold state, so pass `data_table_props` to\n * DataTable too:\n *\n * ```tsx\n * <DataTable {...dataTableProps} {...useSavedViewsClient({ client, tableKey })} />\n * ```\n */\nexport function useSavedViewsClient({\n client,\n tableKey,\n}: UseSavedViewsClientOptions) {\n const tableUrl = `/data_tables/${encodeURIComponent(tableKey)}`\n const viewUrl = (id: SavedView[\"id\"]) =>\n `${tableUrl}/saved_views/${encodeURIComponent(String(id))}`\n const defaultUrl = `${tableUrl}/saved_view_default`\n const lastDefaultChange = useRef<Promise<unknown>>(Promise.resolve())\n\n const onSavedViewCreate = async (view: Omit<SavedView, \"id\">) => {\n // Only the id is read, since clients may reshape the response\n const { data } = (await client.post({\n data: savedViewDocument(view),\n url: `${tableUrl}/saved_views`,\n })) as { data: { id: number | string } }\n return { ...view, id: data.id }\n }\n\n const onSavedViewUpdate = async (view: SavedView) => {\n await client.patch({ data: savedViewDocument(view), url: viewUrl(view.id) })\n return view\n }\n\n /** Places the view after `after`, or first when it's `null`. */\n const onSavedViewReorder = async (\n view: SavedView,\n after: SavedView | null\n ) => {\n await client.patch({\n data: {\n data: {\n attributes: { after_id: after?.id ?? null },\n type: \"SavedView\",\n },\n },\n url: viewUrl(view.id),\n })\n }\n\n const onSavedViewDelete = async (view: SavedView) => {\n await client.delete({ url: viewUrl(view.id) })\n }\n\n /** Pass the initial view's id to reset the default. */\n const onDefaultViewIdChange = async (id: number | string) => {\n const send = () =>\n id === initialView.id\n ? client.delete({ url: defaultUrl })\n : client.post({\n data: {\n data: {\n attributes: { view: String(id) },\n type: \"SavedViewDefault\",\n },\n },\n url: defaultUrl,\n })\n const change = lastDefaultChange.current.then(send, send)\n lastDefaultChange.current = change\n await change\n }\n\n return {\n onDefaultViewIdChange,\n onSavedViewCreate,\n onSavedViewDelete,\n onSavedViewReorder,\n onSavedViewUpdate,\n } satisfies Pick<\n DataTableProps,\n | \"onDefaultViewIdChange\"\n | \"onSavedViewCreate\"\n | \"onSavedViewDelete\"\n | \"onSavedViewReorder\"\n | \"onSavedViewUpdate\"\n >\n}\n\nfunction savedViewDocument(view: Omit<SavedView, \"id\">) {\n const { id: _id, name, ...state } = view as SavedView\n return { data: { attributes: { name, state }, type: \"SavedView\" } }\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,35 @@
1
+ import { type SavedView } from "./savedViews";
2
+ interface SavedViewsStateOptions {
3
+ initialDefaultViewId: NonNullable<SavedView["id"]>;
4
+ initialSavedViews: SavedView[] | undefined;
5
+ onCreate?: (savedView: Omit<SavedView, "id">) => Promise<SavedView> | SavedView;
6
+ onDelete?: (savedView: SavedView) => Promise<void> | void;
7
+ onReorder?: (savedView: SavedView, after: SavedView | null) => Promise<void> | void;
8
+ onSetDefault?: (id: NonNullable<SavedView["id"]>) => Promise<void> | void;
9
+ onUpdate?: (savedView: SavedView) => Promise<SavedView> | SavedView;
10
+ }
11
+ /**
12
+ * Keeps the saved views and default view id after mount. Each callback is
13
+ * wrapped to apply its change here, and stays undefined when the consumer's is,
14
+ * since that hides the matching action. Updates, deletes, and default changes
15
+ * show right away and are undone if the callback rejects, unless a newer
16
+ * change has replaced them. Reorders save one at a time, so a failed one puts
17
+ * back the order from before it.
18
+ */
19
+ export declare function useSavedViewsState({ initialDefaultViewId, initialSavedViews, onCreate, onDelete, onReorder, onSetDefault, onUpdate, }: SavedViewsStateOptions): {
20
+ defaultViewId: string | number;
21
+ isReordering: boolean;
22
+ onCreate: ((view: Omit<SavedView, "id">) => Promise<SavedView>) | undefined;
23
+ onDelete: ((view: SavedView) => Promise<void>) | undefined;
24
+ onReorder: ((view: SavedView, target: ReorderTarget) => Promise<void>) | undefined;
25
+ onSetDefault: ((id: NonNullable<SavedView["id"]>) => Promise<void>) | undefined;
26
+ onUpdate: ((view: SavedView) => Promise<SavedView>) | undefined;
27
+ savedViews: SavedView[] | undefined;
28
+ };
29
+ /** Where a view was dropped, as React Aria reports it. */
30
+ export interface ReorderTarget {
31
+ dropPosition: "after" | "before";
32
+ view: SavedView;
33
+ }
34
+ export {};
35
+ //# sourceMappingURL=useSavedViewsState.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSavedViewsState.d.ts","sourceRoot":"","sources":["../../../src/components/DataTable/useSavedViewsState.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAiB,MAAM,cAAc,CAAA;AAE5D,UAAU,sBAAsB;IAC9B,oBAAoB,EAAE,WAAW,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAA;IAClD,iBAAiB,EAAE,SAAS,EAAE,GAAG,SAAS,CAAA;IAC1C,QAAQ,CAAC,EAAE,CACT,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,KAC7B,OAAO,CAAC,SAAS,CAAC,GAAG,SAAS,CAAA;IACnC,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACzD,SAAS,CAAC,EAAE,CACV,SAAS,EAAE,SAAS,EACpB,KAAK,EAAE,SAAS,GAAG,IAAI,KACpB,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACzB,YAAY,CAAC,EAAE,CAAC,EAAE,EAAE,WAAW,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IACzE,QAAQ,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,KAAK,OAAO,CAAC,SAAS,CAAC,GAAG,SAAS,CAAA;CACpE;AAgCD;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,EACjC,oBAAoB,EACpB,iBAAiB,EACjB,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,YAAY,EACZ,QAAQ,GACT,EAAE,sBAAsB;;;sBAwDL,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC;sBASrB,SAAS;uBA6BT,SAAS,UAAU,aAAa;wBA6BlC,WAAW,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;sBAa1B,SAAS;;EAoB5B;AAED,0DAA0D;AAC1D,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,OAAO,GAAG,QAAQ,CAAA;IAChC,IAAI,EAAE,SAAS,CAAA;CAChB"}