@torq-north/react-tools 1.0.0 → 1.1.1

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/README.md CHANGED
@@ -1,6 +1,6 @@
1
- # Torq Tools
1
+ # Torq North Tools
2
2
 
3
- A collection of react components, hooks and other utilities used in various Crew 2 projects
3
+ A collection of react components, hooks and other utilities used in various projects
4
4
 
5
5
  ## Getting it running
6
6
 
@@ -8,6 +8,32 @@ Upon pulling down the repo, in order to get the component browser running, all y
8
8
 
9
9
  ![image](https://user-images.githubusercontent.com/68865874/229501155-90c89bfb-e29b-4664-9eab-bd6313b53c14.png)
10
10
 
11
+ ## Session expiry redirects
12
+
13
+ The API helpers (`sendGet`, `sendPost`, etc.) can automatically detect when a user's session has expired and redirect them to the login page. This is triggered when any API call receives a 401 response, or when the server issues a redirect to `/auth/login`, which is the typical behaviour of a Symfony OAuth firewall that redirects rather than returning 401 directly.
14
+
15
+ This behaviour is opt-in. Call `enableAuthRedirect` once at app startup with the login URL to activate it:
16
+
17
+ ```typescript
18
+ import { enableAuthRedirect } from "@torq-north/react-tools";
19
+
20
+ enableAuthRedirect("/auth/login");
21
+ ```
22
+
23
+ If `enableAuthRedirect` is never called, session expiry responses are left for the call site to handle as normal errors.
24
+
25
+ ### Dev environments
26
+
27
+ In local development the frontend and backend often run on different ports. If the backend is on a different port than the Vite dev server, navigating to `/auth/login` will hit Vite instead of the backend and the login page won't render correctly. Pass the full backend URL for dev:
28
+
29
+ ```typescript
30
+ enableAuthRedirect(
31
+ import.meta.env.DEV ? "http://localhost:8103/auth/login" : "/auth/login"
32
+ );
33
+ ```
34
+
35
+ Replace `8103` with whichever port your backend is exposed on locally. In production the relative URL is used and no special configuration is needed.
36
+
11
37
  ## Adding your own component
12
38
 
13
39
  1. Copy/paste in component from IRC project
package/dist/index.cjs CHANGED
@@ -146,8 +146,12 @@ var sendGet = function (url, params) { return __awaiter(void 0, void 0, void 0,
146
146
  switch (_a.label) {
147
147
  case 0:
148
148
  params &&
149
- Object.keys(params).forEach(function (key) { return params[key] === undefined && delete params[key]; });
150
- fetchUrl = params ? "".concat(url, "?").concat(new URLSearchParams(params).toString()) : url;
149
+ Object.keys(params).forEach(function (key) {
150
+ return params[key] === undefined && delete params[key];
151
+ });
152
+ fetchUrl = params
153
+ ? "".concat(url, "?").concat(new URLSearchParams(params).toString())
154
+ : url;
151
155
  return [4 /*yield*/, fetch(fetchUrl)];
152
156
  case 1:
153
157
  response = _a.sent();
@@ -248,11 +252,29 @@ function isJson(response) {
248
252
  }
249
253
  return contentType.indexOf("application/json") >= 0;
250
254
  }
255
+ var authLoginUrl = "/auth/login";
256
+ var authRedirectEnabled = false;
257
+ function enableAuthRedirect(loginUrl) {
258
+ if (loginUrl === void 0) { loginUrl = "/auth/login"; }
259
+ authLoginUrl = loginUrl;
260
+ authRedirectEnabled = true;
261
+ }
262
+ function isSessionExpired(response) {
263
+ return (response.status === 401 ||
264
+ (response.redirected && response.url.includes("/auth/login")));
265
+ }
251
266
  function process$1(response) {
252
267
  return __awaiter(this, void 0, void 0, function () {
253
268
  return __generator(this, function (_a) {
254
269
  switch (_a.label) {
255
- case 0: return [4 /*yield*/, checkResponse(response)];
270
+ case 0:
271
+ if (authRedirectEnabled && isSessionExpired(response)) {
272
+ window.location.href = authLoginUrl;
273
+ // Return a never-resolving promise to suspend execution while the browser navigates away,
274
+ // preventing any error handling or UI updates from firing before the page unloads.
275
+ return [2 /*return*/, new Promise(function () { })];
276
+ }
277
+ return [4 /*yield*/, checkResponse(response)];
256
278
  case 1:
257
279
  _a.sent();
258
280
  if (isJson(response)) {
@@ -770,7 +792,11 @@ var ErrorNotification = function (_a) {
770
792
  return (jsxRuntime.jsx(material.Snackbar, { open: open, autoHideDuration: 6000, onClose: onClose, children: jsxRuntime.jsx(material.Alert, { onClose: onClose, severity: "error", sx: { width: "100%" }, elevation: 6, variant: "filled", children: message !== null && message !== void 0 ? message : "An error has occured" }) }));
771
793
  };
772
794
 
773
- var DropIndicator = material.styled(material.Box)(function (_a) {
795
+ var DropIndicator = material.styled(material.Box, {
796
+ shouldForwardProp: function (propName) {
797
+ return propName != "dragOver" && propName != "multiple" && propName != "disabled";
798
+ },
799
+ })(function (_a) {
774
800
  var theme = _a.theme, dragOver = _a.dragOver, multiple = _a.multiple, disabled = _a.disabled;
775
801
  return ({
776
802
  padding: dragOver ? "1px" : "4px",
@@ -779,7 +805,7 @@ var DropIndicator = material.styled(material.Box)(function (_a) {
779
805
  borderRadius: 1,
780
806
  cursor: multiple ? undefined : "pointer",
781
807
  transition: "0.2s opacity",
782
- "&:hover": disabled
808
+ "&:hover": disabled || multiple
783
809
  ? undefined
784
810
  : {
785
811
  opacity: 0.8,
@@ -4780,7 +4806,7 @@ function useFileEditorField(name, options) {
4780
4806
  var _c = React.useState(""), errorMessage = _c[0], setErrorMessage = _c[1];
4781
4807
  var _d = React.useState(false), dragOverDropzone = _d[0], setDragOverDropzone = _d[1];
4782
4808
  var multiple = options.multiple, disabled = options.disabled, otherOptions = __rest(options, ["multiple", "disabled"]);
4783
- var _e = useDropzone(__assign({ multiple: multiple, noClick: multiple, noKeyboard: multiple, disabled: disabled, onDragEnter: function () { return setDragOverDropzone(true); }, onDragOver: function (e) {
4809
+ var _e = useDropzone(__assign({ multiple: multiple, noClick: multiple || values.length == 0, noKeyboard: multiple, disabled: disabled, onDragEnter: function () { return setDragOverDropzone(true); }, onDragOver: function (e) {
4784
4810
  e.preventDefault();
4785
4811
  e.stopPropagation();
4786
4812
  }, onDragLeave: function () { return setDragOverDropzone(false); }, onDrop: function () { return setDragOverDropzone(false); }, onDropAccepted: function (acceptedFiles) {
@@ -4883,9 +4909,9 @@ function IndividualFile(_a) {
4883
4909
  function FileDisplay(_a) {
4884
4910
  var files = _a.files, individualFileProps = __rest(_a, ["files"]);
4885
4911
  if (!Array.isArray(files)) {
4886
- return (jsxRuntime.jsx(material.Box, { p: 1, children: jsxRuntime.jsx(IndividualFile, __assign({ file: files }, individualFileProps)) }));
4912
+ return (jsxRuntime.jsx(material.Box, { children: jsxRuntime.jsx(IndividualFile, __assign({ file: files }, individualFileProps)) }));
4887
4913
  }
4888
- return (jsxRuntime.jsx(material.Stack, { p: 1, children: files.map(function (f) { return (jsxRuntime.jsx(IndividualFile, __assign({ file: f }, individualFileProps), isAsset(f) ? f.id : f.name)); }) }));
4914
+ return (jsxRuntime.jsx(material.Stack, { children: files.map(function (f) { return (jsxRuntime.jsx(IndividualFile, __assign({ file: f }, individualFileProps), isAsset(f) ? f.id : f.name)); }) }));
4889
4915
  }
4890
4916
 
4891
4917
  function FileEditorField(_a) {
@@ -4897,7 +4923,7 @@ function FileEditorField(_a) {
4897
4923
  required: required,
4898
4924
  fieldProps: slotProps === null || slotProps === void 0 ? void 0 : slotProps.field,
4899
4925
  }), getInputProps = _b.getInputProps, getRootProps = _b.getRootProps, dragOverDropzone = _b.dragOverDropzone, values = _b.values, removeFile = _b.removeFile, open = _b.open, showError = _b.showError, setShowError = _b.setShowError, errorMessage = _b.errorMessage, meta = _b.meta;
4900
- return (jsxRuntime.jsxs(FieldWrapper, { icon: icon, showSpace: showSpace, children: [jsxRuntime.jsxs(material.FormControl, { fullWidth: true, sx: { position: "relative" }, children: [label && (jsxRuntime.jsx(material.FormLabel, { disabled: disabled, error: meta.touched && meta.error, children: label })), jsxRuntime.jsx("input", __assign({}, getInputProps())), jsxRuntime.jsxs(DropIndicator, __assign({ multiple: multiple, dragOver: dragOverDropzone, disabled: disabled }, getRootProps(), { children: [jsxRuntime.jsx(material.Box, { sx: { opacity: disabled ? 0.4 : 1 }, children: jsxRuntime.jsx(FileDisplay, { files: values !== null && values !== void 0 ? values : [], disableLink: true, allowDelete: !disabled, onDelete: removeFile }) }), (multiple || (values === null || values === void 0 ? void 0 : values.length) == 0) && (jsxRuntime.jsx(material.Button, { onClick: open, startIcon: jsxRuntime.jsx(AddIcon, {}), fullWidth: true, disabled: disabled, children: placeholder !== null && placeholder !== void 0 ? placeholder : "Add File".concat(multiple ? "s" : "") }))] })), dragOverDropzone && (jsxRuntime.jsx(material.Stack, { position: "absolute", top: 0, bottom: 0, left: 0, right: 0, justifyContent: "center", alignItems: "center", sx: { pointerEvents: "none" }, children: jsxRuntime.jsx(NoteAddIcon, { sx: { fontSize: 50 } }) })), jsxRuntime.jsx(material.FormHelperText, { error: meta.touched && meta.error, children: (meta.touched && meta.error) || helperText })] }), jsxRuntime.jsx(ErrorNotification, { open: showError, onClose: function () { return setShowError(false); }, message: errorMessage })] }));
4926
+ return (jsxRuntime.jsxs(FieldWrapper, { icon: icon, showSpace: showSpace, children: [jsxRuntime.jsxs(material.FormControl, { fullWidth: true, sx: { position: "relative" }, children: [label && (jsxRuntime.jsx(material.FormLabel, { disabled: disabled, error: meta.touched && meta.error, children: label })), jsxRuntime.jsx("input", __assign({}, getInputProps())), jsxRuntime.jsx(DropIndicator, __assign({ multiple: multiple, dragOver: dragOverDropzone, disabled: disabled }, getRootProps(), { children: jsxRuntime.jsxs(material.Stack, { gap: 1, children: [values.length > 0 && (jsxRuntime.jsx(material.Box, { sx: { opacity: disabled ? 0.4 : 1 }, children: jsxRuntime.jsx(FileDisplay, { files: values !== null && values !== void 0 ? values : [], disableLink: true, allowDelete: !disabled, onDelete: removeFile }) })), (multiple || (values === null || values === void 0 ? void 0 : values.length) == 0) && (jsxRuntime.jsx(material.Button, { onClick: open, startIcon: jsxRuntime.jsx(AddIcon, {}), fullWidth: true, disabled: disabled, children: placeholder !== null && placeholder !== void 0 ? placeholder : "Add File".concat(multiple ? "s" : "") }))] }) })), dragOverDropzone && (jsxRuntime.jsx(material.Stack, { position: "absolute", top: 0, bottom: 0, left: 0, right: 0, justifyContent: "center", alignItems: "center", sx: { pointerEvents: "none" }, children: jsxRuntime.jsx(NoteAddIcon, { sx: { fontSize: 50 } }) })), jsxRuntime.jsx(material.FormHelperText, { error: meta.touched && meta.error, children: (meta.touched && meta.error) || helperText })] }), jsxRuntime.jsx(ErrorNotification, { open: showError, onClose: function () { return setShowError(false); }, message: errorMessage })] }));
4901
4927
  }
4902
4928
 
4903
4929
  function asInteger(value) {
@@ -5205,6 +5231,7 @@ exports.TabSet = TabSet;
5205
5231
  exports.TextFormField = TextFormField;
5206
5232
  exports.asInteger = asInteger;
5207
5233
  exports.asPhoneNumber = asPhoneNumber;
5234
+ exports.enableAuthRedirect = enableAuthRedirect;
5208
5235
  exports.isEmail = isEmail;
5209
5236
  exports.isPostalCode = isPostalCode;
5210
5237
  exports.sendDelete = sendDelete;