@canmingir/link 1.2.5 → 1.2.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@canmingir/link",
3
- "version": "1.2.5",
3
+ "version": "1.2.7",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./index.js",
@@ -1,13 +1,10 @@
1
- import config from "../config/config";
2
1
  import { jwtDecode } from "jwt-decode";
3
2
  import { storage } from "@nucleoidjs/webstorage";
4
3
 
5
- const { name } = config();
6
-
7
4
  let login = true;
8
5
  const itemId = storage.get("itemId");
9
6
  try {
10
- const token = storage.get(name, "accessToken");
7
+ const token = storage.get("link", "accessToken");
11
8
  const decodedToken = jwtDecode(token);
12
9
 
13
10
  if (decodedToken.exp * 1000 < Date.now()) {
package/src/http/index.js CHANGED
@@ -15,8 +15,8 @@ const instance = axios.create({
15
15
  });
16
16
 
17
17
  instance.interceptors.request.use((request) => {
18
- const { name, base } = config();
19
- const accessToken = storage.get(name, "accessToken");
18
+ const { base } = config();
19
+ const accessToken = storage.get("link", "accessToken");
20
20
 
21
21
  if (!accessToken) {
22
22
  window.location.href = base === "/" ? "/login" : `${base}/login`;
@@ -85,14 +85,17 @@ export const fetcher = (url) => instance.get(url).then((res) => res.data);
85
85
 
86
86
  const refreshAuthLogic = async (failedRequest) => {
87
87
  try {
88
- const { name, appId } = config();
88
+ const { appId } = config();
89
89
 
90
90
  const projectId = storage.get("projectId");
91
91
 
92
+ const identityProvider = storage.get("link", "identityProvider");
93
+
92
94
  const { data } = await oauth.post("/oauth", {
93
- refreshToken: storage.get(name, "refreshToken"),
95
+ refreshToken: storage.get("link", "refreshToken"),
94
96
  appId,
95
97
  projectId,
98
+ identityProvider,
96
99
  });
97
100
 
98
101
  const accessToken = data.accessToken;
@@ -100,13 +103,13 @@ const refreshAuthLogic = async (failedRequest) => {
100
103
  failedRequest.response.config.headers["Authorization"] =
101
104
  "Bearer " + accessToken;
102
105
 
103
- storage.set(name, "accessToken", accessToken);
106
+ storage.set("link", "accessToken", accessToken);
104
107
  return Promise.resolve();
105
108
  } catch (error) {
106
109
  const { name, base } = config();
107
110
 
108
- storage.remove(name, "accessToken");
109
- storage.remove(name, "refreshToken");
111
+ storage.remove("link", "accessToken");
112
+ storage.remove("link", "refreshToken");
110
113
 
111
114
  window.location.href = `${window.location.origin}${base}/login`;
112
115
  return Promise.reject(error);
@@ -122,7 +125,7 @@ refreshInterceptor(instance, refreshAuthLogic, {
122
125
  statusCodes: [401, 403],
123
126
  shouldRefresh: () => {
124
127
  const { name, base } = config();
125
- const token = storage.get(name, "accessToken");
128
+ const token = storage.get("link", "accessToken");
126
129
 
127
130
  if (!token) {
128
131
  window.location.href = `${window.location.origin}${base}/login`;
package/src/http/user.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import axios from "axios";
2
2
  import config from "../config/config.js";
3
3
  import http from "./index";
4
+ import { jwtDecode } from "jwt-decode";
5
+ import oauth from "./oauth";
4
6
  import { storage } from "@nucleoidjs/webstorage";
5
7
 
6
8
  const instance = axios.create({
@@ -11,15 +13,8 @@ const instance = axios.create({
11
13
  },
12
14
  });
13
15
 
14
- function getProjectName() {
15
- const { name } = config();
16
- if (name) {
17
- return name;
18
- }
19
- }
20
-
21
16
  instance.interceptors.request.use(async (request) => {
22
- const refreshToken = await storage.get(getProjectName(), "refreshToken");
17
+ const refreshToken = await storage.get("link", "refreshToken");
23
18
  if (refreshToken) {
24
19
  request.headers["Authorization"] = `Bearer ${refreshToken}`;
25
20
  }
@@ -28,25 +23,62 @@ instance.interceptors.request.use(async (request) => {
28
23
 
29
24
  instance.getUserDetails = async () => {
30
25
  try {
31
- const refreshToken = await storage.get(getProjectName(), "refreshToken");
26
+ const refreshToken = await storage.get("link", "refreshToken");
27
+
32
28
  if (!refreshToken) {
33
29
  console.log("No refresh token found");
34
30
  return null;
35
31
  }
36
32
 
33
+ let accessToken = await storage.get("link", "accessToken");
34
+
35
+ if (!accessToken) {
36
+ console.log("No access token found");
37
+ return null;
38
+ }
39
+
40
+ try {
41
+ const decodedToken = jwtDecode(accessToken);
42
+ const isExpired = decodedToken.exp * 1000 < Date.now();
43
+
44
+ if (isExpired) {
45
+ console.log(
46
+ "Access token expired, refreshing before fetching user details..."
47
+ );
48
+ const { appId } = config();
49
+ const projectId = storage.get("projectId");
50
+ const identityProvider = storage.get("link", "identityProvider");
51
+
52
+ const { data } = await oauth.post("/oauth", {
53
+ refreshToken,
54
+ appId,
55
+ projectId,
56
+ identityProvider,
57
+ });
58
+
59
+ accessToken = data.accessToken;
60
+ storage.set("link", "accessToken", accessToken);
61
+ console.log(
62
+ "Access token refreshed successfully, now fetching user details"
63
+ );
64
+ }
65
+ } catch (error) {
66
+ console.error("Error checking or refreshing token:", error);
67
+ return null;
68
+ }
69
+
37
70
  const response = await http.get("/oauth/user", {
38
71
  headers: {
39
- 'X-Refresh-Token': refreshToken
40
- }
72
+ "X-Refresh-Token": refreshToken,
73
+ },
41
74
  });
42
-
75
+
43
76
  if (response.data && response.data.user) {
44
77
  return response.data.user;
45
78
  }
46
-
79
+
47
80
  console.log("No user data received from server");
48
81
  return null;
49
-
50
82
  } catch (error) {
51
83
  console.error("Error fetching user details from server:", error);
52
84
  return null;
@@ -55,7 +87,7 @@ instance.getUserDetails = async () => {
55
87
 
56
88
  instance.getPermittedUsers = async () => {
57
89
  const userIds = [];
58
- const refreshToken = await storage.get(getProjectName(), "refreshToken");
90
+ const refreshToken = await storage.get("link", "refreshToken");
59
91
  const response = await http.get("/permissions");
60
92
 
61
93
  response.data.forEach((permission) => {
@@ -75,4 +107,4 @@ instance.getPermittedUsers = async () => {
75
107
  return users;
76
108
  };
77
109
 
78
- export default instance;
110
+ export default instance;
@@ -9,15 +9,16 @@ import SettingsDialog from "../../widgets/SettingsDialog";
9
9
  import Stack from "@mui/material/Stack";
10
10
  import config from "../../config/config";
11
11
  import { hideScroll } from "../../theme/css";
12
+ import { useResponsive } from "../../hooks/use-responsive";
12
13
  import { useUser } from "../../hooks/use-user";
13
14
 
14
15
  import React, { useState } from "react";
15
16
 
16
17
  export default function NavMini({ only }) {
17
18
  const { user } = useUser();
18
- const { sideMenu } = config().menu;
19
+ const { sideMenu, actionButtons } = config().menu;
19
20
  const [openSettings, setOpenSettings] = useState(false);
20
-
21
+ const lgUp = useResponsive("up", "lg");
21
22
  const handleCloseSettings = () => {
22
23
  setOpenSettings(false);
23
24
  };
@@ -57,21 +58,40 @@ export default function NavMini({ only }) {
57
58
  }}
58
59
  />
59
60
  <Box sx={{ flexGrow: 1 }} />
60
- <Button
61
- data-cy="end-item"
62
- fullWidth={true}
63
- onClick={() => setOpenSettings(true)}
61
+ <Stack
62
+ direction={"column"}
63
+ alignItems={"center"}
64
+ justifyItems={"center"}
65
+ sx={{
66
+ marginBottom: lgUp ? 3 : 0,
67
+ position: lgUp ? "static" : "fixed",
68
+ bottom: lgUp ? "auto" : 66,
69
+ width: "100%",
70
+ }}
71
+ gap={2}
64
72
  >
65
- <Iconify
66
- icon={"ic:baseline-settings"}
73
+ {actionButtons &&
74
+ actionButtons.map((Action, index) => (
75
+ <Box key={index} component={Action}></Box>
76
+ ))}
77
+ <Button
78
+ onClick={() => setOpenSettings(true)}
67
79
  sx={{
68
- width: 32,
69
- height: 32,
70
- color: "text.secondary",
71
- mx: "auto",
80
+ position: lgUp ? "static" : "fixed",
81
+ bottom: lgUp ? "auto" : 16,
82
+ width: "100%",
72
83
  }}
73
- />
74
- </Button>
84
+ >
85
+ <Iconify
86
+ icon={"ic:baseline-settings"}
87
+ sx={{
88
+ width: 32,
89
+ height: 32,
90
+ color: "text.secondary",
91
+ }}
92
+ />
93
+ </Button>
94
+ </Stack>
75
95
  <SettingsDialog open={openSettings} handleClose={handleCloseSettings} />
76
96
  </Stack>
77
97
  </Box>
@@ -16,7 +16,7 @@ function NavHorizontal() {
16
16
 
17
17
  const { user } = useUser();
18
18
 
19
- const { sideMenu } = config().menu;
19
+ const { sideMenu, actionButtons } = config().menu;
20
20
  return (
21
21
  <AppBar component="div" data-cy="nav-horizontal">
22
22
  <Toolbar
@@ -35,6 +35,11 @@ function NavHorizontal() {
35
35
  ...theme.mixins.toolbar,
36
36
  }}
37
37
  />
38
+
39
+ {actionButtons &&
40
+ actionButtons.map((Action, index) => (
41
+ <Box key={index} component={Action}></Box>
42
+ ))}
38
43
  </Toolbar>
39
44
 
40
45
  <HeaderShadow />
@@ -1,10 +1,3 @@
1
- import { Button, DialogActions } from "@mui/material";
2
- import Dialog, { dialogClasses } from "@mui/material/Dialog";
3
- import React, { useCallback, useState } from "react";
4
- import { publish, useEvent } from "@nucleoidai/react-event";
5
- import { storage, useStorage } from "@nucleoidjs/webstorage";
6
- import { useMediaQuery, useTheme } from "@mui/material";
7
-
8
1
  import Box from "@mui/material/Box";
9
2
  import IconButton from "@mui/material/IconButton";
10
3
  import Iconify from "../../../components/Iconify";
@@ -29,9 +22,16 @@ import { useEventListener } from "../../../hooks/use-event-listener";
29
22
  import { useNavigate } from "react-router-dom";
30
23
  import useProjects from "../../../hooks/useProjects";
31
24
 
25
+ import { Button, DialogActions } from "@mui/material";
26
+ import Dialog, { dialogClasses } from "@mui/material/Dialog";
27
+ import React, { useCallback, useState } from "react";
28
+ import { publish, useEvent } from "@nucleoidai/react-event";
29
+ import { storage, useStorage } from "@nucleoidjs/webstorage";
30
+ import { useMediaQuery, useTheme } from "@mui/material";
31
+
32
32
  function ProjectBar() {
33
33
  const label = config().template?.projectBar?.label;
34
- const { appId, name } = config();
34
+ const { appId } = config();
35
35
  const theme = useTheme();
36
36
  const isSmallScreen = useMediaQuery(theme.breakpoints.down(435));
37
37
  const { loading, projects, getProjects } = useProjects();
@@ -91,15 +91,15 @@ function ProjectBar() {
91
91
  const handleSelect = (project) => {
92
92
  const { id: projectId } = project;
93
93
 
94
- const refreshToken = storage.get(name, "refreshToken");
95
- const provider = storage.get(name, "provider");
94
+ const refreshToken = storage.get("link", "refreshToken");
95
+ const identityProvider = storage.get("link", "identityProvider");
96
96
 
97
97
  oauth
98
- .post("/oauth", { appId, refreshToken, projectId, provider })
98
+ .post("/oauth", { appId, refreshToken, projectId, identityProvider })
99
99
  .then(({ data }) => {
100
100
  const { refreshToken, accessToken } = data;
101
- storage.set(name, "accessToken", accessToken);
102
- storage.set(name, "refreshToken", refreshToken);
101
+ storage.set("link", "accessToken", accessToken);
102
+ storage.set("link", "refreshToken", refreshToken);
103
103
  storage.set("projectId", projectId);
104
104
  })
105
105
  .finally(() => {
@@ -19,7 +19,6 @@ import CustomPopover, { usePopover } from "../../components/custom-popover";
19
19
  // ----------------------------------------------------------------------
20
20
 
21
21
  export default function AccountPopover() {
22
- const { name } = config();
23
22
  const { options } = config().menu;
24
23
  const router = useRouter();
25
24
  const { user } = useUser();
@@ -28,8 +27,8 @@ export default function AccountPopover() {
28
27
 
29
28
  const handleLogout = async () => {
30
29
  try {
31
- storage.remove(name, "accessToken");
32
- storage.remove(name, "refreshToken");
30
+ storage.remove("link", "accessToken");
31
+ storage.remove("link", "refreshToken");
33
32
  popover.onClose();
34
33
  router.replace("/login");
35
34
  } catch (error) {
@@ -30,6 +30,7 @@ const DynamicConnector = ({
30
30
  };
31
31
 
32
32
  const children = childEls.map((el) => {
33
+ if (!el) return { x: parent.x, y: parent.y };
33
34
  const r = el.getBoundingClientRect();
34
35
  return {
35
36
  x: r.left + r.width / 2 - cRect.left,
@@ -45,12 +46,13 @@ const DynamicConnector = ({
45
46
  setDims({ w: cRect.width, h: cRect.height });
46
47
  };
47
48
 
49
+ update();
50
+
48
51
  const ro = new ResizeObserver(update);
49
52
  ro.observe(containerEl);
50
53
  ro.observe(parentEl);
51
54
  childEls.forEach((el) => el && ro.observe(el));
52
55
 
53
- update();
54
56
  return () => ro.disconnect();
55
57
  }, [containerEl, parentEl, childEls, tick]);
56
58
 
@@ -65,6 +67,18 @@ const DynamicConnector = ({
65
67
 
66
68
  const onlyOne = points.children.length === 1;
67
69
 
70
+ const svgProps = {
71
+ style: {
72
+ position: "absolute",
73
+ inset: 0,
74
+ pointerEvents: "none",
75
+ overflow: "visible",
76
+ },
77
+ width: "100%",
78
+ height: "100%",
79
+ viewBox: `0 0 ${dims.w} ${dims.h}`,
80
+ };
81
+
68
82
  if (connectorType === "curved" || connectorType === "n8n") {
69
83
  const createN8nPath = (from, to) => {
70
84
  const v = Math.max(32, Math.abs(to.y - from.y) * 0.35);
@@ -83,12 +97,7 @@ const DynamicConnector = ({
83
97
  };
84
98
 
85
99
  return (
86
- <svg
87
- style={{ position: "absolute", inset: 0, pointerEvents: "none" }}
88
- width="100%"
89
- height="100%"
90
- viewBox={`0 0 ${dims.w} ${dims.h}`}
91
- >
100
+ <svg {...svgProps}>
92
101
  {points.children.map((child, i) => (
93
102
  <path
94
103
  key={i}
@@ -106,12 +115,7 @@ const DynamicConnector = ({
106
115
  }
107
116
 
108
117
  return (
109
- <svg
110
- style={{ position: "absolute", inset: 0, pointerEvents: "none" }}
111
- width="100%"
112
- height="100%"
113
- viewBox={`0 0 ${dims.w} ${dims.h}`}
114
- >
118
+ <svg {...svgProps}>
115
119
  {onlyOne ? (
116
120
  <line
117
121
  x1={points.parent.x}
@@ -136,8 +140,8 @@ const DynamicConnector = ({
136
140
 
137
141
  {(() => {
138
142
  const xs = points.children.map((c) => c.x);
139
- const xMin = Math.min(...xs);
140
- const xMax = Math.max(...xs);
143
+ const xMin = Math.min(...xs, points.parent.x);
144
+ const xMax = Math.max(...xs, points.parent.x);
141
145
  return (
142
146
  <line
143
147
  x1={xMin}
@@ -27,7 +27,13 @@ export const Flow = ({ data, variant = "simple", style, plugin }) => {
27
27
  }, [nodesById, roots]);
28
28
 
29
29
  return (
30
- <FlowNode node={treeData} variant={variant} style={style} plugin={plugin} />
30
+ <FlowNode
31
+ node={treeData}
32
+ variant={variant}
33
+ style={style}
34
+ plugin={plugin}
35
+ isRoot={true}
36
+ />
31
37
  );
32
38
  };
33
39
 
@@ -11,7 +11,15 @@ import {
11
11
  toPxNumber,
12
12
  } from "./styles";
13
13
 
14
- const FlowNode = ({ node, type, variant, style, plugin }) => {
14
+ const NodeContent = ({
15
+ node,
16
+ type,
17
+ variant,
18
+ style,
19
+ plugin,
20
+ registerRef,
21
+ onDrag,
22
+ }) => {
15
23
  const baseStyle = getBaseStyleForVariant(variant);
16
24
  const hasChildren = Array.isArray(node.children) && node.children.length > 0;
17
25
 
@@ -25,23 +33,19 @@ const FlowNode = ({ node, type, variant, style, plugin }) => {
25
33
  styleTokens = style;
26
34
  }
27
35
 
28
- let plugins = null;
36
+ let _plugin = null;
29
37
  if (plugin) {
30
38
  if (typeof plugin === "function") {
31
- plugin = plugin(type, node);
32
- } else if (
33
- typeof plugin === "object" &&
34
- (typeof plugin.renderNode === "function" ||
35
- typeof plugin.resolveStyle === "function")
36
- ) {
37
- plugins = plugin;
39
+ _plugin = plugin(type, node) || null;
40
+ } else if (typeof plugin === "object") {
41
+ _plugin = plugin;
38
42
  }
39
43
  }
40
44
 
41
45
  let pluginTokens = {};
42
- if (plugins && typeof plugins.resolveStyle === "function") {
46
+ if (_plugin && typeof _plugin.style === "function") {
43
47
  pluginTokens =
44
- plugins.resolveStyle({
48
+ _plugin.style({
45
49
  node,
46
50
  style: styleTokens,
47
51
  }) || {};
@@ -82,7 +86,11 @@ const FlowNode = ({ node, type, variant, style, plugin }) => {
82
86
  const [childElList, setChildElList] = useState([]);
83
87
 
84
88
  const [connectorTick, setConnectorTick] = useState(0);
85
- const notifyDrag = () => setConnectorTick((t) => t + 1);
89
+
90
+ const handleDrag = (newOffset) => {
91
+ setConnectorTick((t) => t + 1);
92
+ if (onDrag) onDrag(newOffset);
93
+ };
86
94
 
87
95
  useLayoutEffect(() => {
88
96
  const els = (node.children || [])
@@ -194,8 +202,8 @@ const FlowNode = ({ node, type, variant, style, plugin }) => {
194
202
  };
195
203
 
196
204
  const renderContent = () => {
197
- if (plugin && typeof plugin.renderNode === "function") {
198
- return plugin.renderNode({
205
+ if (_plugin && typeof _plugin.node === "function") {
206
+ return _plugin.node({
199
207
  node,
200
208
  title,
201
209
  subtitle,
@@ -217,9 +225,15 @@ const FlowNode = ({ node, type, variant, style, plugin }) => {
217
225
  position: "relative",
218
226
  }}
219
227
  >
220
- <Box ref={parentRef} sx={{ position: "relative" }}>
228
+ <DraggableNode
229
+ registerRef={(el) => {
230
+ parentRef.current = el;
231
+ if (registerRef) registerRef(el);
232
+ }}
233
+ onDrag={handleDrag}
234
+ >
221
235
  {renderContent()}
222
- </Box>
236
+ </DraggableNode>
223
237
 
224
238
  {hasChildren && (
225
239
  <>
@@ -246,19 +260,17 @@ const FlowNode = ({ node, type, variant, style, plugin }) => {
246
260
  }}
247
261
  >
248
262
  {node.children.map((child) => (
249
- <DraggableNode
263
+ <FlowNode
250
264
  key={child.id}
265
+ node={child}
266
+ type={type}
267
+ variant={variant}
268
+ style={style}
269
+ plugin={plugin}
251
270
  registerRef={(el) => (childRefs.current[child.id] = el)}
252
- onDrag={notifyDrag}
253
- >
254
- <FlowNode
255
- node={child}
256
- type={type}
257
- variant={variant}
258
- style={style}
259
- plugin={plugin}
260
- />
261
- </DraggableNode>
271
+ onDrag={() => setConnectorTick((t) => t + 1)}
272
+ isRoot={false}
273
+ />
262
274
  ))}
263
275
  </Box>
264
276
  </>
@@ -267,4 +279,93 @@ const FlowNode = ({ node, type, variant, style, plugin }) => {
267
279
  );
268
280
  };
269
281
 
282
+ const FlowNode = ({ isRoot = false, ...props }) => {
283
+ if (!isRoot) {
284
+ return <NodeContent {...props} />;
285
+ }
286
+
287
+ const [offset, setOffset] = useState({ x: 0, y: 0 });
288
+ const [isDragging, setIsDragging] = useState(false);
289
+
290
+ const [zoom, setZoom] = useState(1);
291
+
292
+ const clampZoom = (z) => Math.min(2.5, Math.max(0.25, z));
293
+
294
+ useEffect(() => {
295
+ const onWheel = (e) => {
296
+ const wantsZoom = e.ctrlKey || e.metaKey;
297
+ if (!wantsZoom) return;
298
+
299
+ e.preventDefault();
300
+
301
+ const direction = e.deltaY > 0 ? -1 : 1;
302
+ const factor = direction > 0 ? 1.1 : 1 / 1.1;
303
+
304
+ setZoom((z) => clampZoom(z * factor));
305
+ };
306
+
307
+ window.addEventListener("wheel", onWheel, { passive: false });
308
+ return () => window.removeEventListener("wheel", onWheel);
309
+ }, []);
310
+
311
+ const handleCanvasMouseDown = (e) => {
312
+ if (e.target?.closest?.('[data-flow-zoom="true"]')) return;
313
+
314
+ if (e.button !== 0) return;
315
+
316
+ setIsDragging(true);
317
+
318
+ const startX = e.clientX;
319
+ const startY = e.clientY;
320
+ const startOffset = { ...offset };
321
+
322
+ const onMove = (ev) => {
323
+ setOffset({
324
+ x: startOffset.x + (ev.clientX - startX),
325
+ y: startOffset.y + (ev.clientY - startY),
326
+ });
327
+ };
328
+
329
+ const onUp = () => {
330
+ setIsDragging(false);
331
+ window.removeEventListener("mousemove", onMove);
332
+ window.removeEventListener("mouseup", onUp);
333
+ };
334
+
335
+ window.addEventListener("mousemove", onMove);
336
+ window.addEventListener("mouseup", onUp);
337
+ };
338
+
339
+ return (
340
+ <Box
341
+ onMouseDown={handleCanvasMouseDown}
342
+ sx={{
343
+ width: "100vw",
344
+ height: "100vh",
345
+ overflow: "hidden",
346
+ bgcolor: "none",
347
+ cursor: isDragging ? "grabbing" : "default",
348
+ userSelect: "none",
349
+ position: "relative",
350
+ }}
351
+ >
352
+ <Box
353
+ sx={{
354
+ transform: `translate(${offset.x}px, ${offset.y}px) scale(${zoom})`,
355
+ transformOrigin: "center center",
356
+ width: "100%",
357
+ height: "100%",
358
+ display: "flex",
359
+ alignItems: "center",
360
+ justifyContent: "center",
361
+ transition: isDragging ? "none" : "transform 0.1s ease-out",
362
+ pointerEvents: "auto",
363
+ }}
364
+ >
365
+ <NodeContent {...props} />
366
+ </Box>
367
+ </Box>
368
+ );
369
+ };
370
+
270
371
  export default FlowNode;
@@ -1,12 +1,27 @@
1
1
  import React from "react";
2
2
 
3
- import { Box, Card, Stack } from "@mui/material";
3
+ import { Box, Card, Stack, Typography } from "@mui/material";
4
4
  import { alpha, useTheme } from "@mui/material/styles";
5
5
 
6
+ const STYLES = {
7
+ width: "100%",
8
+ display: "-webkit-box",
9
+ WebkitLineClamp: 2,
10
+ WebkitBoxOrient: "vertical",
11
+ overflow: "hidden",
12
+ textOverflow: "ellipsis",
13
+ wordBreak: "break-word",
14
+ lineHeight: 1.3,
15
+ opacity: 0.85,
16
+ };
17
+
6
18
  export function MediaAvatarCard({
7
19
  sx,
8
20
  leftContent,
9
21
  rightContent,
22
+ title,
23
+ description,
24
+ footer,
10
25
  background,
11
26
  overlayColor,
12
27
  backgroundClassName = "animated-background-image",
@@ -25,6 +40,9 @@ export function MediaAvatarCard({
25
40
  position: "relative",
26
41
  display: "flex",
27
42
  flexDirection: "row",
43
+ fontSize: 12,
44
+ color: "rgba(255,255,255,0.82)",
45
+ lineHeight: 1.3,
28
46
  boxShadow: (theme) => theme.shadows[3],
29
47
  "&:hover": {
30
48
  boxShadow: (theme) => theme.shadows[6],
@@ -82,75 +100,44 @@ export function MediaAvatarCard({
82
100
 
83
101
  <Stack
84
102
  direction="column"
85
- spacing={1}
86
103
  sx={{
87
104
  flex: 1,
88
105
  padding: 2,
89
106
  zIndex: 1,
90
107
  position: "relative",
108
+ height: "100%",
109
+ justifyContent: "space-between",
91
110
  }}
92
111
  >
93
- {rightContent}
94
- </Stack>
95
- </Card>
96
- );
97
- }
112
+ {rightContent ? (
113
+ rightContent
114
+ ) : (
115
+ <>
116
+ <Stack spacing={1} sx={{ width: "100%", minWidth: 0 }}>
117
+ <Box>{title}</Box>
98
118
 
99
- export function HeaderCard({
100
- sx,
101
- header,
102
- children,
103
- height = 140,
104
- padding = 2,
105
- }) {
106
- const theme = useTheme();
119
+ {description && (
120
+ <Typography variant="inherit" sx={STYLES}>
121
+ {description}
122
+ </Typography>
123
+ )}
124
+ </Stack>
107
125
 
108
- return (
109
- <Card
110
- sx={{
111
- p: padding,
112
- minWidth: 200,
113
- maxWidth: 420,
114
- height,
115
- borderRadius: 3,
116
- position: "relative",
117
- overflow: "hidden",
118
- backgroundColor: theme.palette.background.paper,
119
- transition: "all 0.3s ease-in-out",
120
- "&:hover": {
121
- transform: "translateY(-4px)",
122
- boxShadow: theme.shadows[8],
123
- "&:before": {
124
- height: "100%",
125
- },
126
- },
127
- "&:before": {
128
- content: '""',
129
- width: "100%",
130
- height: "40%",
131
- position: "absolute",
132
- top: 0,
133
- left: 0,
134
- background: `linear-gradient(135deg, ${alpha(
135
- theme.palette.secondary.light,
136
- 0.2
137
- )}, ${alpha(theme.palette.primary.main, 0.3)})`,
138
- borderRadius: "8px 8px 0 0",
139
- transition: "height 0.3s ease-in-out",
140
- },
141
- ...sx,
142
- }}
143
- >
144
- <Stack
145
- spacing={2}
146
- sx={{
147
- height: "100%",
148
- position: "relative",
149
- zIndex: 1,
150
- }}
151
- >
152
- {header}
153
- {children}
126
+ {footer && (
127
+ <Box
128
+ sx={{
129
+ display: "flex",
130
+ justifyContent: "flex-end",
131
+ alignItems: "center",
132
+ gap: 0.5,
133
+ mt: 0.5,
134
+ }}
135
+ >
136
+ {footer}
137
+ </Box>
138
+ )}
139
+ </>
140
+ )}
154
141
  </Stack>
155
142
  </Card>
156
143
  );
@@ -160,6 +147,9 @@ export function SideStripeCard({
160
147
  sx,
161
148
  stripeContent,
162
149
  mainContent,
150
+ title,
151
+ description,
152
+ footer,
163
153
  minWidth = 200,
164
154
  maxWidth = 290,
165
155
  height = 100,
@@ -168,11 +158,7 @@ export function SideStripeCard({
168
158
  const theme = useTheme();
169
159
 
170
160
  return (
171
- <Box
172
- sx={{
173
- cursor: "pointer",
174
- }}
175
- >
161
+ <Box sx={{ cursor: "pointer" }}>
176
162
  <Card
177
163
  sx={{
178
164
  display: "flex",
@@ -185,6 +171,9 @@ export function SideStripeCard({
185
171
  boxShadow: 3,
186
172
  position: "relative",
187
173
  overflow: "hidden",
174
+ fontSize: 12,
175
+ lineHeight: 1.3,
176
+ color: "rgba(255,255,255,0.82)",
188
177
  "&:hover": {
189
178
  boxShadow: 6,
190
179
  "& .animated-background-selector": {
@@ -233,20 +222,118 @@ export function SideStripeCard({
233
222
 
234
223
  <Stack
235
224
  direction="column"
236
- spacing={1}
237
225
  sx={{
238
226
  padding: 2,
239
227
  position: "relative",
240
228
  zIndex: 1,
229
+ flex: 1,
230
+ height: "100%",
231
+ justifyContent: "space-between",
241
232
  }}
242
233
  >
243
- {mainContent}
234
+ {mainContent ? (
235
+ mainContent
236
+ ) : (
237
+ <>
238
+ <Box sx={{ minWidth: 0 }}>
239
+ {typeof title === "string" ? (
240
+ <Typography
241
+ variant="inherit"
242
+ sx={{
243
+ color: "text.primary",
244
+ wordBreak: "break-word",
245
+ hyphens: "auto",
246
+ fontWeight: 600,
247
+ }}
248
+ >
249
+ {title}
250
+ </Typography>
251
+ ) : (
252
+ title
253
+ )}
254
+
255
+ {description && (
256
+ <Typography
257
+ variant="inherit"
258
+ sx={{
259
+ ...STYLES,
260
+ mt: 0.5,
261
+ color: "text.primary",
262
+ opacity: 0.8,
263
+ }}
264
+ >
265
+ {description}
266
+ </Typography>
267
+ )}
268
+ </Box>
269
+
270
+ {footer && (
271
+ <Box sx={{ alignSelf: "flex-end", mt: 0.5 }}>{footer}</Box>
272
+ )}
273
+ </>
274
+ )}
244
275
  </Stack>
245
276
  </Card>
246
277
  </Box>
247
278
  );
248
279
  }
249
280
 
281
+ export function HeaderCard({
282
+ sx,
283
+ header,
284
+ children,
285
+ height = 140,
286
+ padding = 2,
287
+ }) {
288
+ const theme = useTheme();
289
+ return (
290
+ <Card
291
+ sx={{
292
+ p: padding,
293
+ minWidth: 200,
294
+ maxWidth: 420,
295
+ height,
296
+ borderRadius: 3,
297
+ position: "relative",
298
+ overflow: "hidden",
299
+ backgroundColor: theme.palette.background.paper,
300
+ fontSize: 12,
301
+ lineHeight: 1.3,
302
+ color: "rgba(255,255,255,0.82)",
303
+ transition: "all 0.3s ease-in-out",
304
+ "&:hover": {
305
+ transform: "translateY(-4px)",
306
+ boxShadow: theme.shadows[8],
307
+ "&:before": { height: "100%" },
308
+ },
309
+ "&:before": {
310
+ content: '""',
311
+ width: "100%",
312
+ height: "40%",
313
+ position: "absolute",
314
+ top: 0,
315
+ left: 0,
316
+ background: `linear-gradient(135deg, ${alpha(
317
+ theme.palette.secondary.light,
318
+ 0.2
319
+ )}, ${alpha(theme.palette.primary.main, 0.3)})`,
320
+ borderRadius: "8px 8px 0 0",
321
+ transition: "height 0.3s ease-in-out",
322
+ },
323
+ ...sx,
324
+ }}
325
+ >
326
+ <Stack
327
+ spacing={2}
328
+ sx={{ height: "100%", position: "relative", zIndex: 1 }}
329
+ >
330
+ {header}
331
+ {children}
332
+ </Stack>
333
+ </Card>
334
+ );
335
+ }
336
+
250
337
  export function AvatarRoleCard({
251
338
  sx,
252
339
  leftContent,
@@ -264,13 +351,14 @@ export function AvatarRoleCard({
264
351
  borderRadius: 2,
265
352
  position: "relative",
266
353
  backgroundColor: theme.palette.background.paper,
354
+ fontSize: 12,
355
+ lineHeight: 1.3,
356
+ color: "rgba(255,255,255,0.82)",
267
357
  transition: "all 0.3s ease-in-out",
268
358
  "&:hover": {
269
359
  transform: "translateY(-4px)",
270
360
  boxShadow: theme.shadows[8],
271
- "&:before": {
272
- height: "100%",
273
- },
361
+ "&:before": { height: "100%" },
274
362
  },
275
363
  "&:before": {
276
364
  content: '""',
@@ -291,11 +379,7 @@ export function AvatarRoleCard({
291
379
  >
292
380
  <Stack
293
381
  spacing={1}
294
- sx={{
295
- height: "100%",
296
- position: "relative",
297
- zIndex: 1,
298
- }}
382
+ sx={{ height: "100%", position: "relative", zIndex: 1 }}
299
383
  >
300
384
  <Stack
301
385
  direction="row"
@@ -1,5 +1,3 @@
1
- import { useEffect, useRef } from "react";
2
-
3
1
  import Page from "../layouts/Page";
4
2
  import React from "react";
5
3
  import config from "../config/config";
@@ -10,6 +8,8 @@ import { useContext } from "../ContextProvider/ContextProvider";
10
8
  import { useLocation } from "react-router-dom";
11
9
  import { useNavigate } from "react-router-dom";
12
10
 
11
+ import { useEffect, useRef } from "react";
12
+
13
13
  function Callback() {
14
14
  const { project: appConfig, name, appId } = config();
15
15
  const projectBar = config().template?.projectBar;
@@ -28,12 +28,12 @@ function Callback() {
28
28
  const parsedQuery = qs.parse(location.search, { ignoreQueryPrefix: true });
29
29
  const { code, error, error_description, state } = parsedQuery;
30
30
 
31
- let provider;
31
+ let identityProvider;
32
32
  let stateData = {};
33
33
 
34
34
  if (state) {
35
35
  stateData = JSON.parse(decodeURIComponent(state));
36
- provider = stateData.provider;
36
+ identityProvider = stateData.identityProvider;
37
37
  }
38
38
 
39
39
  if (error) {
@@ -59,7 +59,7 @@ function Callback() {
59
59
  google,
60
60
  };
61
61
 
62
- const providerConfig = providerConfigs[provider];
62
+ const providerConfig = providerConfigs[identityProvider];
63
63
 
64
64
  if (!providerConfig) {
65
65
  console.error("Could not determine OAuth provider or redirect URI");
@@ -85,7 +85,7 @@ function Callback() {
85
85
  appId,
86
86
  code,
87
87
  redirectUri,
88
- provider: provider,
88
+ identityProvider: identityProvider,
89
89
  grant_type: "authorization_code",
90
90
  })
91
91
  .then(({ data }) => {
@@ -93,10 +93,10 @@ function Callback() {
93
93
  const refreshToken = data.refreshToken;
94
94
  const userInfo = data.user;
95
95
 
96
- storage.set(name, "accessToken", accessToken);
97
- storage.set(name, "refreshToken", refreshToken);
96
+ storage.set("link", "accessToken", accessToken);
97
+ storage.set("link", "refreshToken", refreshToken);
98
98
  // TODO - update provider info
99
- storage.set(name, "provider", provider);
99
+ storage.set("link", "identityProvider", identityProvider);
100
100
 
101
101
  dispatch({ type: "LOGIN", payload: { user: userInfo } });
102
102
 
@@ -11,7 +11,10 @@ function LoginPage() {
11
11
  const navigate = useNavigate();
12
12
 
13
13
  function token() {
14
- if (storage.get(name, "refreshToken") && storage.get(name, "accessToken")) {
14
+ if (
15
+ storage.get("link", "refreshToken") &&
16
+ storage.get("link", "accessToken")
17
+ ) {
15
18
  return true;
16
19
  } else {
17
20
  return false;
@@ -19,7 +19,10 @@ export default function Auth0LoginView() {
19
19
  const navigate = useNavigate();
20
20
 
21
21
  function token() {
22
- if (storage.get(name, "refreshToken") && storage.get(name, "accessToken")) {
22
+ if (
23
+ storage.get("link", "refreshToken") &&
24
+ storage.get("link", "accessToken")
25
+ ) {
23
26
  return true;
24
27
  } else {
25
28
  return false;
@@ -1,17 +1,17 @@
1
- import { Box, Divider, Link as MuiLink, Typography } from "@mui/material";
2
- import React, { useState } from "react";
3
-
4
1
  import NucleoidLoginForm from "../../components/NucleoidLoginForm";
5
2
  import SocialLoginButtons from "../../components/SocialLoginButtons";
6
3
  import Stack from "@mui/material/Stack";
7
4
  import config from "../../config/config";
8
5
 
6
+ import { Box, Divider, Link as MuiLink, Typography } from "@mui/material";
7
+ import React, { useState } from "react";
8
+
9
9
  const handleOAuthLogin = (
10
10
  { redirectUri, authUrl, clientId, scope },
11
- provider
11
+ identityProvider
12
12
  ) => {
13
13
  const state = JSON.stringify({
14
- provider: provider,
14
+ identityProvider: identityProvider,
15
15
  });
16
16
  const encodedState = encodeURIComponent(state);
17
17
  window.location.href = `${authUrl}?client_id=${clientId}&scope=${scope}&response_type=code&redirect_uri=${redirectUri}&state=${encodedState}`;