dce-reactkit 5.0.7 → 5.0.9

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.
package/dist/cjs/index.js CHANGED
@@ -826,6 +826,8 @@ const _setStubResponse = (opts) => {
826
826
  * @param opts.path - the path of the server endpoint
827
827
  * @param [opts.method=GET] - the method of the endpoint
828
828
  * @param [opts.params] - query/body parameters to include
829
+ * @param [opts.headers] - custom headers to include; values must already be
830
+ * string to avoid issue with header serialization
829
831
  * @returns response from server
830
832
  */
831
833
  const visitServerEndpoint = (opts) => __awaiter(void 0, void 0, void 0, function* () {
@@ -875,6 +877,7 @@ const visitServerEndpoint = (opts) => __awaiter(void 0, void 0, void 0, function
875
877
  path: opts.path,
876
878
  method: (_c = opts.method) !== null && _c !== void 0 ? _c : 'GET',
877
879
  params,
880
+ headers: opts.headers,
878
881
  });
879
882
  // Check for failure
880
883
  if (!response || !response.body) {
@@ -2703,6 +2706,13 @@ const Tooltip = (props) => {
2703
2706
  (() => __awaiter(void 0, void 0, void 0, function* () {
2704
2707
  // Import bootstrap tooltip
2705
2708
  const BSTooltip = (yield Promise.resolve().then(function () { return /*#__PURE__*/_interopNamespace(require('bootstrap')); })).Tooltip;
2709
+ // The child may have unmounted while the dynamic import above was
2710
+ // resolving (React nulls the ref on unmount). Constructing a
2711
+ // Bootstrap tooltip with a null element causes an error,
2712
+ // so bail out instead
2713
+ if (!childRef.current) {
2714
+ return;
2715
+ }
2706
2716
  // Initialize
2707
2717
  t = new BSTooltip(childRef.current, {
2708
2718
  title: text,
@@ -2714,6 +2724,15 @@ const Tooltip = (props) => {
2714
2724
  return () => {
2715
2725
  if (t) {
2716
2726
  t.dispose();
2727
+ // Bootstrap's hide() queues a transition-end callback that can
2728
+ // fire AFTER dispose and touch the (now null) trigger map and
2729
+ // element (twbs/bootstrap issue #37474). Point those private
2730
+ // fields at harmless stand-ins so the late callback is a no-op
2731
+ // instead of a crash.
2732
+ /* eslint-disable no-underscore-dangle */
2733
+ t._activeTrigger = {};
2734
+ t._element = document.createElement('noscript');
2735
+ /* eslint-enable no-underscore-dangle */
2717
2736
  }
2718
2737
  };
2719
2738
  }, [text]);
@@ -2867,22 +2886,23 @@ const NestableItemList = (props) => {
2867
2886
  onChanged(changeChecked(item.id, checked, items));
2868
2887
  }, ariaLabel: `Select ${accessibleName}`, checkedVariant: Variant$1.Light, uncheckedVariant: Variant$1.Light }));
2869
2888
  return (React__default["default"].createElement("div", { key: item.id },
2870
- React__default["default"].createElement("span", { className: "NestableItemList-dropdown-button-container d-inline-block", style: {
2871
- minWidth: '2rem',
2872
- } }, item.isGroup && (React__default["default"].createElement("button", { className: `NestableItemList-dropdown-button NestableItemList-dropdown-button-${item.id}`, style: {
2873
- border: 0,
2874
- backgroundColor: 'transparent',
2875
- }, type: "button", onClick: () => {
2876
- dispatch({
2877
- type: ActionType$9.ToggleChild,
2878
- id: item.id,
2879
- });
2880
- }, "aria-label": `${childExpanded[item.id] ? 'Hide' : 'Show'} items in ${accessibleName}` },
2881
- React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: childExpanded[item.id] ? freeSolidSvgIcons.faChevronDown : freeSolidSvgIcons.faChevronRight })))),
2882
- item.tooltip
2883
- ? (React__default["default"].createElement(Tooltip, { text: item.tooltip },
2884
- React__default["default"].createElement("span", { className: "d-inline-block" }, checkbox)))
2885
- : checkbox,
2889
+ React__default["default"].createElement("div", { className: "NestableItemList-item d-flex align-items-center" },
2890
+ React__default["default"].createElement("span", { className: "NestableItemList-dropdown-button-container", style: {
2891
+ minWidth: '2rem',
2892
+ } }, item.isGroup && (React__default["default"].createElement("button", { className: `NestableItemList-dropdown-button NestableItemList-dropdown-button-${item.id}`, style: {
2893
+ border: 0,
2894
+ backgroundColor: 'transparent',
2895
+ }, type: "button", onClick: () => {
2896
+ dispatch({
2897
+ type: ActionType$9.ToggleChild,
2898
+ id: item.id,
2899
+ });
2900
+ }, "aria-label": `${childExpanded[item.id] ? 'Hide' : 'Show'} items in ${accessibleName}` },
2901
+ React__default["default"].createElement(reactFontawesome.FontAwesomeIcon, { icon: childExpanded[item.id] ? freeSolidSvgIcons.faChevronDown : freeSolidSvgIcons.faChevronRight })))),
2902
+ item.tooltip
2903
+ ? (React__default["default"].createElement(Tooltip, { text: item.tooltip },
2904
+ React__default["default"].createElement("span", { className: "d-inline-block" }, checkbox)))
2905
+ : checkbox),
2886
2906
  (item.isGroup && childExpanded[item.id]) && (React__default["default"].createElement("div", { className: "NestableItemList-children-container", style: {
2887
2907
  paddingLeft: '2.2rem',
2888
2908
  } },
@@ -14758,6 +14778,286 @@ const useForceRender = (useReducer) => {
14758
14778
  };
14759
14779
  };
14760
14780
 
14781
+ /**
14782
+ * State of the current subpanel, determining what happens when the user tries
14783
+ * to go back to the home screen
14784
+ * @author Yuen Ler Chow
14785
+ */
14786
+ var BackState;
14787
+ (function (BackState) {
14788
+ // The user can go back immediately, no confirmation required
14789
+ BackState["Normal"] = "Normal";
14790
+ // The user must confirm before going back because progress will be lost
14791
+ BackState["UnsavedChanges"] = "UnsavedChanges";
14792
+ // The user cannot go back right now because work is in progress
14793
+ BackState["Blocked"] = "Blocked";
14794
+ })(BackState || (BackState = {}));
14795
+ var BackState$1 = BackState;
14796
+
14797
+ /*------------------------------------------------------------------------*/
14798
+ /* ------------------------------ Constants ----------------------------- */
14799
+ /*------------------------------------------------------------------------*/
14800
+ // Marker stored on history entries owned by this hook
14801
+ const HISTORY_STATE_MARKER = { dceReactKitBackButton: true };
14802
+ // Default message shown when the user tries to go back while blocked
14803
+ const BLOCKED_TITLE = 'Cannot Go Back';
14804
+ const BLOCKED_MESSAGE = 'A task is currently in progress. Please try again once it finishes.';
14805
+ // Default message shown when the user tries to go back with unsaved changes
14806
+ const UNSAVED_CHANGES_TITLE = 'Abandon Changes?';
14807
+ const UNSAVED_CHANGES_MESSAGE = 'Any unsaved changes may be lost.';
14808
+ /*------------------------------------------------------------------------*/
14809
+ /* --------------------------- Static State ----------------------------- */
14810
+ /*------------------------------------------------------------------------*/
14811
+ // Current state, stored statically so that any subpanel can drive the back
14812
+ // button without prop drilling or context
14813
+ let state = {
14814
+ promptVisible: false,
14815
+ bypassNextPop: false,
14816
+ };
14817
+ /*------------------------------------------------------------------------*/
14818
+ /* ------------------------- Helper Functions --------------------------- */
14819
+ /*------------------------------------------------------------------------*/
14820
+ /**
14821
+ * Get the app's go-home handler, throwing if the hook has not been set up yet
14822
+ * @author Yuen Ler Chow
14823
+ * @returns handler that returns the app to its home screen
14824
+ */
14825
+ const getHandleGoHome = () => {
14826
+ if (!state.handleGoHome) {
14827
+ throw new Error('Cannot use the back button: call useBackButton in your top-level app before using backButtonController.');
14828
+ }
14829
+ return state.handleGoHome;
14830
+ };
14831
+ /**
14832
+ * Return to the home screen: clear the current subpanel and run the app's
14833
+ * go-home handler
14834
+ * @author Yuen Ler Chow
14835
+ * @param consumeHistoryEntry if true, also step back over the history entry
14836
+ * that was added when the subpanel was entered
14837
+ */
14838
+ const returnHome = (consumeHistoryEntry) => {
14839
+ const handleGoHome = getHandleGoHome();
14840
+ const wasInSubpanel = !!state.subpanel;
14841
+ // Dropping the subpanel clears its back state and custom messages at once
14842
+ state.subpanel = undefined;
14843
+ // Update the app
14844
+ handleGoHome();
14845
+ // Entering a subpanel added a history entry. When the user leaves via an
14846
+ // in-app control, that entry is still on the stack, so we step over it to keep
14847
+ // the browser history in sync with the app. When the browser's back button is
14848
+ // what brought us here, that entry has already been consumed by the browser,
14849
+ // and stepping back again would take the user out of the app entirely
14850
+ if (consumeHistoryEntry && wasInSubpanel) {
14851
+ state.bypassNextPop = true;
14852
+ window.history.back();
14853
+ }
14854
+ };
14855
+ /**
14856
+ * Keep the user in place after a back navigation that should not be allowed.
14857
+ * The browser's popstate event is not cancelable, so the only way to stay put
14858
+ * is to immediately push a new entry to replace the one that was just popped.
14859
+ * This has to happen synchronously while handling the pop, before awaiting
14860
+ * anything, otherwise the navigation has already taken effect
14861
+ * @author Yuen Ler Chow
14862
+ */
14863
+ const undoPop = () => {
14864
+ window.history.pushState(HISTORY_STATE_MARKER, '');
14865
+ };
14866
+ /**
14867
+ * Tell the user that they cannot go back right now
14868
+ * @author Yuen Ler Chow
14869
+ */
14870
+ const showBlockedMessage = () => __awaiter(void 0, void 0, void 0, function* () {
14871
+ var _a, _b;
14872
+ state.promptVisible = true;
14873
+ yield alert(BLOCKED_TITLE, (_b = (_a = state.subpanel) === null || _a === void 0 ? void 0 : _a.customBlockedMessage) !== null && _b !== void 0 ? _b : BLOCKED_MESSAGE);
14874
+ state.promptVisible = false;
14875
+ });
14876
+ /**
14877
+ * Ask the user whether they want to leave despite having unsaved changes
14878
+ * @author Yuen Ler Chow
14879
+ * @returns true if the user wants to leave
14880
+ */
14881
+ const askToAbandonChanges = () => __awaiter(void 0, void 0, void 0, function* () {
14882
+ var _c, _d;
14883
+ state.promptVisible = true;
14884
+ const confirmed = yield confirm(UNSAVED_CHANGES_TITLE, (_d = (_c = state.subpanel) === null || _c === void 0 ? void 0 : _c.customUnsavedChangesMessage) !== null && _d !== void 0 ? _d : UNSAVED_CHANGES_MESSAGE, {
14885
+ confirmButtonText: 'Abandon Changes',
14886
+ cancelButtonText: 'Stay Here',
14887
+ });
14888
+ state.promptVisible = false;
14889
+ return confirmed;
14890
+ });
14891
+ /**
14892
+ * Handle a back navigation performed by the browser
14893
+ * @author Yuen Ler Chow
14894
+ */
14895
+ const handlePopState = () => {
14896
+ // Back navigation that we triggered ourselves: let it through
14897
+ if (state.bypassNextPop) {
14898
+ state.bypassNextPop = false;
14899
+ return;
14900
+ }
14901
+ // Already on the home screen: nothing for us to intercept
14902
+ if (!state.subpanel) {
14903
+ return;
14904
+ }
14905
+ // A prompt is already on screen: stay put instead of stacking another one
14906
+ if (state.promptVisible) {
14907
+ undoPop();
14908
+ return;
14909
+ }
14910
+ // Blocked: stay put and explain why
14911
+ if (state.subpanel.backState === BackState$1.Blocked) {
14912
+ undoPop();
14913
+ showBlockedMessage();
14914
+ return;
14915
+ }
14916
+ // Unsaved changes: stay put until the user confirms. The pop is undone
14917
+ // asynchronously here, before awaiting the confirmation, so that the user
14918
+ // remains in the subpanel while they decide
14919
+ if (state.subpanel.backState === BackState$1.UnsavedChanges) {
14920
+ undoPop();
14921
+ (() => __awaiter(void 0, void 0, void 0, function* () {
14922
+ if (yield askToAbandonChanges()) {
14923
+ returnHome(true);
14924
+ }
14925
+ }))();
14926
+ return;
14927
+ }
14928
+ // Normal: allow it. The browser already consumed the history entry, so there
14929
+ // is nothing left for us to step over
14930
+ returnHome(false);
14931
+ };
14932
+ /*------------------------------------------------------------------------*/
14933
+ /* ------------------------------ Controller ---------------------------- */
14934
+ /*------------------------------------------------------------------------*/
14935
+ /**
14936
+ * Controller for driving back navigation from anywhere in the app. Requires
14937
+ * useBackButton to have been called in the top-level app
14938
+ * @author Yuen Ler Chow
14939
+ */
14940
+ const backButtonController = {
14941
+ /**
14942
+ * Call this when the user navigates to a child of the home screen (something
14943
+ * they can come back from)
14944
+ * @author Yuen Ler Chow
14945
+ */
14946
+ onSubpanelEntered: () => {
14947
+ getHandleGoHome();
14948
+ state.subpanel = {
14949
+ backState: BackState$1.Normal,
14950
+ };
14951
+ // Add a history entry to come back to, so that the next back navigation is
14952
+ // intercepted instead of leaving the app
14953
+ window.history.pushState(HISTORY_STATE_MARKER, '');
14954
+ },
14955
+ /**
14956
+ * Send the user back to the home screen
14957
+ * @author Yuen Ler Chow
14958
+ * @param [force] if true, go home immediately without checking the subpanel
14959
+ * state. If falsy, nothing happens while blocked and confirmation is
14960
+ * required when there are unsaved changes
14961
+ */
14962
+ goHome: (force) => __awaiter(void 0, void 0, void 0, function* () {
14963
+ var _e, _f;
14964
+ getHandleGoHome();
14965
+ // Unless the caller is forcing the navigation, the subpanel's state decides
14966
+ // whether the user may leave: a blocked subpanel refuses and explains why,
14967
+ // and one with unsaved changes leaves only if the user confirms
14968
+ if (!force) {
14969
+ // A prompt is already asking the user this same question
14970
+ if (state.promptVisible) {
14971
+ return;
14972
+ }
14973
+ if (((_e = state.subpanel) === null || _e === void 0 ? void 0 : _e.backState) === BackState$1.Blocked) {
14974
+ yield showBlockedMessage();
14975
+ return;
14976
+ }
14977
+ if (((_f = state.subpanel) === null || _f === void 0 ? void 0 : _f.backState) === BackState$1.UnsavedChanges) {
14978
+ const confirmed = yield askToAbandonChanges();
14979
+ if (!confirmed) {
14980
+ return;
14981
+ }
14982
+ }
14983
+ }
14984
+ returnHome(true);
14985
+ }),
14986
+ /**
14987
+ * Set the state of the current subpanel, which determines what happens when
14988
+ * the user tries to go back. Ignored while on the home screen, where there
14989
+ * is no back navigation to describe
14990
+ * @author Yuen Ler Chow
14991
+ * @param newSubpanelState the new state of the subpanel
14992
+ */
14993
+ setSubpanelState: (newSubpanelState) => {
14994
+ if (!state.subpanel) {
14995
+ return;
14996
+ }
14997
+ state.subpanel.backState = newSubpanelState;
14998
+ },
14999
+ /**
15000
+ * Set the confirmation message shown if the user tries to go back while there
15001
+ * are unsaved changes. Cleared upon returning to the home screen
15002
+ * @author Yuen Ler Chow
15003
+ * @param message the message to show
15004
+ */
15005
+ setCustomUnsavedChangesMessage: (message) => {
15006
+ if (!state.subpanel) {
15007
+ return;
15008
+ }
15009
+ state.subpanel.customUnsavedChangesMessage = message;
15010
+ },
15011
+ /**
15012
+ * Set the message shown if the user tries to go back while blocked. Cleared
15013
+ * upon returning to the home screen
15014
+ * @author Yuen Ler Chow
15015
+ * @param message the message to show
15016
+ */
15017
+ setCustomBlockedMessage: (message) => {
15018
+ if (!state.subpanel) {
15019
+ return;
15020
+ }
15021
+ state.subpanel.customBlockedMessage = message;
15022
+ },
15023
+ };
15024
+ /*------------------------------------------------------------------------*/
15025
+ /* --------------------------------- Hook ------------------------------- */
15026
+ /*------------------------------------------------------------------------*/
15027
+ /**
15028
+ * Hook that makes the browser's back button navigate within the app instead of
15029
+ * leaving it. Call this once in your top-level app, then use
15030
+ * backButtonController to enter subpanels and describe their state.
15031
+ *
15032
+ * Assumes a single level of navigation: one home screen plus subpanels that the
15033
+ * user returns home from.
15034
+ * @author Yuen Ler Chow
15035
+ * @param handleGoHomeFunc handler that performs the app state changes required
15036
+ * to return to the home screen
15037
+ */
15038
+ const useBackButton = (handleGoHomeFunc) => {
15039
+ // Store the handler on every render so that the listener below, which is only
15040
+ // registered once, always calls the app's current version of it
15041
+ state.handleGoHome = handleGoHomeFunc;
15042
+ // The popstate listener has to be attached to the window, which is outside of
15043
+ // React, so an effect is used to add it when the app mounts and remove it if
15044
+ // the app ever unmounts. The empty dependency array keeps this to a single
15045
+ // listener for the lifetime of the app instead of one per render
15046
+ React.useEffect(() => {
15047
+ // Mark the current entry as the home entry
15048
+ window.history.replaceState(HISTORY_STATE_MARKER, '');
15049
+ window.addEventListener('popstate', handlePopState);
15050
+ return () => {
15051
+ window.removeEventListener('popstate', handlePopState);
15052
+ // Clear the static state so that a remount starts from scratch
15053
+ state = {
15054
+ promptVisible: false,
15055
+ bypassNextPop: false,
15056
+ };
15057
+ };
15058
+ }, []);
15059
+ };
15060
+
14761
15061
  /*------------------------------------------------------------------------*/
14762
15062
  /* ------------------------------- Caching ------------------------------ */
14763
15063
  /*------------------------------------------------------------------------*/
@@ -15028,6 +15328,7 @@ Object.defineProperty(exports, 'waitMs', {
15028
15328
  });
15029
15329
  exports.AppWrapper = AppWrapper;
15030
15330
  exports.AutoscrollToBottomContainer = AutoscrollToBottomContainer;
15331
+ exports.BackState = BackState$1;
15031
15332
  exports.ButtonInputGroup = ButtonInputGroup;
15032
15333
  exports.CSVDownloadButton = CSVDownloadButton;
15033
15334
  exports.CheckboxButton = CheckboxButton;
@@ -15064,6 +15365,7 @@ exports.Tooltip = Tooltip;
15064
15365
  exports.Variant = Variant$1;
15065
15366
  exports.addFatalErrorHandler = addFatalErrorHandler;
15066
15367
  exports.alert = alert;
15368
+ exports.backButtonController = backButtonController;
15067
15369
  exports.canReviewLogs = canReviewLogs;
15068
15370
  exports.combineClassNames = combineClassNames;
15069
15371
  exports.confirm = confirm;
@@ -15077,6 +15379,7 @@ exports.prompt = prompt;
15077
15379
  exports.setClientEventMetadataPopulator = setClientEventMetadataPopulator;
15078
15380
  exports.showFatalError = showFatalError;
15079
15381
  exports.stubServerEndpoint = stubServerEndpoint;
15382
+ exports.useBackButton = useBackButton;
15080
15383
  exports.useForceRender = useForceRender;
15081
15384
  exports.visitServerEndpoint = visitServerEndpoint;
15082
15385
  //# sourceMappingURL=index.js.map