@canmingir/link 1.2.22 → 1.2.24

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 (60) hide show
  1. package/package.json +3 -1
  2. package/src/Platform.jsx +10 -14
  3. package/src/config/schemas.js +1 -0
  4. package/src/context/Context.js +98 -0
  5. package/src/context/reducer.js +590 -10
  6. package/src/layouts/auth/modern.jsx +2 -0
  7. package/src/layouts/common/account-popover.jsx +3 -7
  8. package/src/lib/APIDialogAction/APIDialogAction.jsx +109 -0
  9. package/src/lib/APIDialogAction/index.js +1 -0
  10. package/src/lib/APIDialogAction/styles.js +6 -0
  11. package/src/lib/APIParams/APIParams.jsx +57 -0
  12. package/src/lib/APIParams/index.js +1 -0
  13. package/src/lib/APIPath/APIPath.jsx +82 -0
  14. package/src/lib/APIPath/index.js +1 -0
  15. package/src/lib/APIPath/styles.js +19 -0
  16. package/src/lib/APITree/APITree.jsx +409 -0
  17. package/src/lib/APITree/Arrow.jsx +21 -0
  18. package/src/lib/APITree/DeleteMethodDialog.jsx +41 -0
  19. package/src/lib/APITree/index.js +1 -0
  20. package/src/lib/APITree/styles.js +19 -0
  21. package/src/lib/APITypes/APITypes.jsx +141 -0
  22. package/src/lib/APITypes/TypeEditor.jsx +46 -0
  23. package/src/lib/APITypes/TypeList.jsx +180 -0
  24. package/src/lib/APITypes/index.js +1 -0
  25. package/src/lib/BlankTreeMessage/BlankTreeMessage.jsx +39 -0
  26. package/src/lib/BlankTreeMessage/index.js +1 -0
  27. package/src/lib/DialogTootip/DialogTooltip.jsx +67 -0
  28. package/src/lib/DialogTootip/index.js +1 -0
  29. package/src/lib/DialogTootip/styles.js +9 -0
  30. package/src/lib/NewApiBody/NewAPIBody.jsx +97 -0
  31. package/src/lib/NewApiBody/ParamView.jsx +38 -0
  32. package/src/lib/NucDialog/NucDialog.jsx +108 -0
  33. package/src/lib/NucDialog/index.js +1 -0
  34. package/src/lib/ParamTable/ParamTable.jsx +133 -0
  35. package/src/lib/ParamTable/TypeMenu.jsx +102 -0
  36. package/src/lib/ParamTable/defaults.js +47 -0
  37. package/src/lib/ParamTable/index.js +1 -0
  38. package/src/lib/ParamTable/styles.js +12 -0
  39. package/src/lib/ResourceMenu/AlertMassage.jsx +28 -0
  40. package/src/lib/ResourceMenu/DeleteResourceDialog.jsx +60 -0
  41. package/src/lib/ResourceMenu/ResourceMenu.jsx +156 -0
  42. package/src/lib/ResourceMenu/index.js +1 -0
  43. package/src/lib/ResourceMenu/styles.js +5 -0
  44. package/src/lib/Schema/Schema.jsx +204 -0
  45. package/src/lib/Schema/index.js +1 -0
  46. package/src/lib/SchemaEditor/SchemaEditor.jsx +258 -0
  47. package/src/lib/SchemaEditor/SchemaEditor.test.js +193 -0
  48. package/src/lib/SchemaEditor/SchemaPropertyEditor.jsx +135 -0
  49. package/src/lib/SchemaEditor/SchemaUtils.js +152 -0
  50. package/src/lib/SchemaEditor/index.js +1 -0
  51. package/src/lib/ToggleableMenu/ToggleableMenu.jsx +35 -0
  52. package/src/lib/ToggleableMenu/index.js +1 -0
  53. package/src/lib/index.js +14 -0
  54. package/src/pages/Callback.jsx +6 -8
  55. package/src/stories/APITree.stories.jsx +429 -0
  56. package/src/stories/FlowChart.stories.jsx +1 -1
  57. package/src/templates/ActionTemplate.js +24 -0
  58. package/src/widgets/Login/CognitoLogin.jsx +4 -2
  59. package/src/widgets/Login/DemoLogin.jsx +8 -6
  60. package/src/widgets/SettingsDialog.jsx +38 -16
@@ -0,0 +1,429 @@
1
+ import APIDialogAction from "../lib/APIDialogAction/APIDialogAction";
2
+ import APIParams from "../lib/APIParams/APIParams";
3
+ import APIPath from "../lib/APIPath/APIPath";
4
+ import APITree from "../lib/APITree/APITree";
5
+ import APITypes from "../lib/APITypes/APITypes";
6
+ import CssBaseline from "@mui/material/CssBaseline";
7
+ import NewAPIBody from "../lib/NewApiBody/NewAPIBody";
8
+ import NucDialog from "../lib/NucDialog/NucDialog";
9
+
10
+ import { Box, Divider } from "@mui/material";
11
+ import React, { useEffect, useRef, useState } from "react";
12
+ import { ThemeProvider, createTheme } from "@mui/material/styles";
13
+ import { publish, useEvent } from "@nucleoidai/react-event";
14
+
15
+ const theme = createTheme({
16
+ palette: {
17
+ mode: "dark",
18
+ custom: {
19
+ apiTreeRightClick: "rgba(255, 255, 255, 0.08)",
20
+ },
21
+ },
22
+ custom: {
23
+ apiTreeItem: {
24
+ fontSize: 12,
25
+ color: "#666",
26
+ fontWeight: "bold",
27
+ backgroundColor: "#fdfdfd",
28
+ border: "1px solid #c3c5c8",
29
+ width: 44,
30
+ borderRadius: 8,
31
+ mt: 1 / 4,
32
+ mb: 1 / 4,
33
+ boxShadow: "1px 1px #b8b8b8",
34
+ },
35
+ },
36
+ });
37
+
38
+ const sampleApi = [
39
+ {
40
+ path: "/",
41
+ method: "GET",
42
+ summary: "Hello World",
43
+ description: "Hello World",
44
+ params: [],
45
+ action: 'function action(req) {\n return { message: "Hello World" };\n}',
46
+ },
47
+ {
48
+ path: "/items",
49
+ method: "GET",
50
+ summary: "List items",
51
+ description: "List all items",
52
+ params: [{ in: "query", type: "string", required: true, name: "name" }],
53
+ action:
54
+ "function action(req) {\n return Item.filter(i => i.name === req.query.name);\n}",
55
+ },
56
+ {
57
+ path: "/items",
58
+ method: "POST",
59
+ summary: "Create item",
60
+ description: "Create a new item",
61
+ params: [],
62
+ action:
63
+ "function action(req) {\n return new Item(req.body.name, req.body.barcode);\n}",
64
+ },
65
+ {
66
+ path: "/items",
67
+ method: "DELETE",
68
+ summary: "Delete item",
69
+ description: "Delete an item",
70
+ params: [],
71
+ action: "function action(req) {\n // delete logic\n}",
72
+ },
73
+ ];
74
+
75
+ export default {
76
+ title: "Components/APITree",
77
+ component: APITree,
78
+ parameters: {
79
+ docs: {
80
+ description: {
81
+ component:
82
+ "APITree displays API endpoints in a tree structure grouped by path segments. " +
83
+ "It supports context menus for editing/deleting methods and a resource menu for adding new endpoints.",
84
+ },
85
+ },
86
+ layout: "centered",
87
+ },
88
+ };
89
+
90
+ const ALL_METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH"];
91
+
92
+ const APIWorkspace = () => {
93
+ const [view, setView] = useState("PARAMS");
94
+ const [dialogKey, setDialogKey] = useState(0);
95
+
96
+ const methodRef = useRef("GET");
97
+ const pathRef = useRef("/");
98
+ const paramsRef = useRef([]);
99
+ const addParams = useRef(null);
100
+ const requestSchemaRef = useRef();
101
+ const responseSchemaRef = useRef();
102
+ const typesRef = useRef({});
103
+
104
+ const [dialog, setDialog] = useState({
105
+ type: null,
106
+ action: null,
107
+ open: false,
108
+ });
109
+
110
+ const [api, setApi] = useState(sampleApi);
111
+
112
+ const [selected] = useEvent("SELECTED_API_CHANGED", {
113
+ path: "/",
114
+ method: "GET",
115
+ });
116
+
117
+ const [types, setTypes] = useState([]);
118
+
119
+ const [typeAdd] = useEvent("API_TYPE_ADD", null);
120
+ const [typeDelete] = useEvent("API_TYPE_DELETE", null);
121
+ const [typeRename] = useEvent("API_TYPE_RENAME", null);
122
+
123
+ const [methodDelete] = useEvent("API_METHOD_DELETE", null);
124
+ const [resourceDelete] = useEvent("API_RESOURCE_DELETE", null);
125
+ const [apiDialogOpen] = useEvent("API_DIALOG_OPEN", null);
126
+
127
+ const isEdit = dialog.type === "method" && dialog.action === "edit";
128
+ const isAddMethod = dialog.type === "method" && dialog.action === "add";
129
+
130
+ const selectedEndpoint = api.find(
131
+ (ep) =>
132
+ ep.path === selected?.path &&
133
+ ep.method?.toLowerCase() === selected?.method?.toLowerCase()
134
+ );
135
+
136
+ useEffect(() => {
137
+ if (!typeAdd) return;
138
+
139
+ setTypes((prev) => {
140
+ const { typeName } = typeAdd;
141
+ if (!typeName) return prev;
142
+
143
+ const exists = prev.some((t) => t.name === typeName);
144
+ if (exists) return prev;
145
+
146
+ return [
147
+ ...prev,
148
+ {
149
+ name: typeName,
150
+ schema: {
151
+ name: typeName,
152
+ type: "object",
153
+ properties: [
154
+ { type: "string", name: "id" },
155
+ { type: "string", name: "name" },
156
+ ],
157
+ },
158
+ },
159
+ ];
160
+ });
161
+ }, [typeAdd]);
162
+
163
+ useEffect(() => {
164
+ if (!typeDelete) return;
165
+
166
+ setTypes((prev) => prev.filter((t) => t.name !== typeDelete.typeName));
167
+ }, [typeDelete]);
168
+
169
+ useEffect(() => {
170
+ if (!typeRename) return;
171
+
172
+ setTypes((prev) =>
173
+ prev.map((t) =>
174
+ t.name === typeRename.oldTypeName
175
+ ? {
176
+ ...t,
177
+ name: typeRename.newTypeName,
178
+ schema: {
179
+ ...t.schema,
180
+ name: typeRename.newTypeName,
181
+ },
182
+ }
183
+ : t
184
+ )
185
+ );
186
+ }, [typeRename]);
187
+
188
+ useEffect(() => {
189
+ publish("API_TYPES_CHANGED", { types });
190
+ }, [types]);
191
+
192
+ useEffect(() => {
193
+ publish("API_DATA_CHANGED", api);
194
+ }, [api]);
195
+
196
+ useEffect(() => {
197
+ if (!apiDialogOpen) return;
198
+
199
+ setDialog((prev) => ({
200
+ ...prev,
201
+ type: apiDialogOpen.type,
202
+ action: apiDialogOpen.action,
203
+ open: true,
204
+ }));
205
+ }, [apiDialogOpen]);
206
+
207
+ useEffect(() => {
208
+ if (!methodDelete || !selected) return;
209
+
210
+ setApi((prev) =>
211
+ prev.filter(
212
+ (route) =>
213
+ !(
214
+ route.path === selected.path &&
215
+ route.method?.toLowerCase() === selected.method?.toLowerCase()
216
+ )
217
+ )
218
+ );
219
+ }, [methodDelete, selected]);
220
+
221
+ useEffect(() => {
222
+ if (!resourceDelete || !resourceDelete.path) return;
223
+
224
+ const pathToDelete = resourceDelete.path;
225
+ setApi((prev) => prev.filter((ep) => !ep.path.startsWith(pathToDelete)));
226
+ }, [resourceDelete]);
227
+
228
+ useEffect(() => {
229
+ if (!dialog.open) return;
230
+
231
+ setView("PARAMS");
232
+ setDialogKey((k) => k + 1);
233
+
234
+ if (isEdit && selectedEndpoint) {
235
+ methodRef.current = selectedEndpoint.method?.toUpperCase() || "GET";
236
+ pathRef.current = selectedEndpoint.path || "/";
237
+ paramsRef.current = Array.isArray(selectedEndpoint.params)
238
+ ? [...selectedEndpoint.params]
239
+ : [];
240
+ } else {
241
+ methodRef.current = "GET";
242
+ pathRef.current = selected?.path || "/";
243
+ paramsRef.current = [];
244
+ }
245
+ // eslint-disable-next-line react-hooks/exhaustive-deps
246
+ }, [dialog.open]);
247
+
248
+ const usedMethods = api
249
+ .filter((ep) => ep.path === selected?.path)
250
+ .map((ep) => ep.method?.toUpperCase());
251
+
252
+ const allowedMethods = isEdit
253
+ ? [selectedEndpoint?.method?.toUpperCase() || "GET"]
254
+ : isAddMethod
255
+ ? ALL_METHODS.filter((m) => !usedMethods.includes(m))
256
+ : ALL_METHODS;
257
+
258
+ const basePath = isEdit
259
+ ? selectedEndpoint?.path || selected?.path || "/"
260
+ : selected?.path || "/";
261
+
262
+ const isPathDisabled = isAddMethod || isEdit;
263
+ const isMethodDisabled = isEdit;
264
+
265
+ const dialogTitle = isEdit
266
+ ? "Edit API Endpoint"
267
+ : isAddMethod
268
+ ? "Add Method"
269
+ : "Add Resource";
270
+
271
+ const handleCloseDialog = () =>
272
+ setDialog((prev) => ({
273
+ ...prev,
274
+ open: false,
275
+ }));
276
+
277
+ const handleSave = () => {
278
+ if (isEdit) {
279
+ setApi((prev) => {
280
+ const idx = prev.findIndex(
281
+ (ep) =>
282
+ ep.path === selected.path &&
283
+ ep.method?.toLowerCase() === selected.method?.toLowerCase()
284
+ );
285
+ if (idx === -1) return prev;
286
+
287
+ const next = [...prev];
288
+ const current = next[idx];
289
+
290
+ next[idx] = {
291
+ ...current,
292
+ request: {
293
+ ...(current.request || {}),
294
+ schema: requestSchemaRef.current?.schemaOutput?.() || [],
295
+ },
296
+ response: {
297
+ ...(current.response || {}),
298
+ schema: responseSchemaRef.current?.schemaOutput?.() || [],
299
+ },
300
+ params: paramsRef.current,
301
+ };
302
+
303
+ return next;
304
+ });
305
+
306
+ setDialog((prev) => ({ ...prev, open: false }));
307
+ } else {
308
+ const newEndpoint = {
309
+ path: pathRef.current,
310
+ method: methodRef.current,
311
+ request: requestSchemaRef.current?.schemaOutput?.() || {},
312
+ response: responseSchemaRef.current?.schemaOutput?.() || {},
313
+ params: paramsRef.current,
314
+ summary: `${methodRef.current} ${pathRef.current}`,
315
+ description: `API endpoint for ${pathRef.current}`,
316
+ };
317
+
318
+ setApi((prev) => [...prev, newEndpoint]);
319
+ setDialog((prev) => ({ ...prev, open: false }));
320
+ }
321
+ };
322
+
323
+ const handleDelete = () => {
324
+ if (!selected) return;
325
+ setApi((prev) =>
326
+ prev.filter(
327
+ (ep) =>
328
+ !(
329
+ ep.path === selected.path &&
330
+ ep.method?.toLowerCase() === selected.method?.toLowerCase()
331
+ )
332
+ )
333
+ );
334
+ setDialog((prev) => ({ ...prev, open: false }));
335
+ };
336
+
337
+ return (
338
+ <Box sx={{ display: "flex", height: "100%", gap: 2 }}>
339
+ <Box sx={{ width: 280, flexShrink: 0, height: "100%" }}>
340
+ <APITree />
341
+ </Box>
342
+
343
+ <NucDialog
344
+ title={dialogTitle}
345
+ open={dialog.open}
346
+ handleClose={handleCloseDialog}
347
+ maximizedDimensions={{ width: "75rem", height: "60rem" }}
348
+ minimizedDimensions={{ width: "65rem", height: "50rem" }}
349
+ action={
350
+ <APIDialogAction
351
+ view={view}
352
+ setApiDialogView={setView}
353
+ saveApiDialog={handleSave}
354
+ saveDisable={false}
355
+ deleteDisable={!isEdit}
356
+ deleteMethod={handleDelete}
357
+ />
358
+ }
359
+ >
360
+ <Box
361
+ key={dialogKey}
362
+ sx={{ display: "flex", flexDirection: "column", height: "100%" }}
363
+ >
364
+ <APIPath
365
+ method={allowedMethods[0] || "GET"}
366
+ path={basePath}
367
+ methodRef={methodRef}
368
+ pathRef={pathRef}
369
+ onTypesButtonClick={() => setView("TYPES")}
370
+ allowedMethods={allowedMethods.length ? allowedMethods : ["GET"]}
371
+ isMethodDisabled={isMethodDisabled}
372
+ isPathDisabled={isPathDisabled}
373
+ validatePath={() => {}}
374
+ />
375
+ <Divider sx={{ my: 2 }} />
376
+ {view === "PARAMS" && (
377
+ <APIParams
378
+ types={types}
379
+ paramsRef={paramsRef}
380
+ addParams={addParams}
381
+ />
382
+ )}
383
+ {view === "BODY" && (
384
+ <NewAPIBody
385
+ types={types}
386
+ api={{
387
+ path: basePath,
388
+ method: methodRef.current,
389
+ params: paramsRef.current,
390
+ request: selectedEndpoint?.request || { schema: [] },
391
+ response: selectedEndpoint?.response || { schema: [] },
392
+ }}
393
+ requestSchemaRef={requestSchemaRef}
394
+ responseSchemaRef={responseSchemaRef}
395
+ />
396
+ )}
397
+ {view === "TYPES" && (
398
+ <APITypes tstypes={[]} nuctypes={types} typesRef={typesRef} />
399
+ )}
400
+ </Box>
401
+ </NucDialog>
402
+ </Box>
403
+ );
404
+ };
405
+
406
+ export const APITreeWorkspace = {
407
+ render: () => <APIWorkspace />,
408
+ decorators: [
409
+ (Story) => (
410
+ <ThemeProvider theme={theme}>
411
+ <CssBaseline />
412
+ <div style={{ width: 900, height: 600 }}>
413
+ <Story />
414
+ </div>
415
+ </ThemeProvider>
416
+ ),
417
+ ],
418
+ parameters: {
419
+ layout: "centered",
420
+ docs: {
421
+ description: {
422
+ story:
423
+ "Full workspace: right-click a path node or click the + FAB to open the ResourceMenu, " +
424
+ "then choose Resource, Method, or Delete. Adding a resource or method opens the API dialog " +
425
+ "where you can configure the path, method, params, and body before saving.",
426
+ },
427
+ },
428
+ },
429
+ };
@@ -1,4 +1,4 @@
1
- import { FlowChart } from "../lib/FlowChart";
1
+ import { FlowChart } from "../lib/Flow";
2
2
  import React from "react";
3
3
 
4
4
  const treeToLinked = (tree) => {
@@ -0,0 +1,24 @@
1
+ const ActionTemplate = {
2
+ GET: `function action(req) {
3
+ // Implement your GET logic here
4
+ return {};
5
+ }`,
6
+ POST: `function action(req) {
7
+ // Implement your POST logic here
8
+ return {};
9
+ }`,
10
+ PUT: `function action(req) {
11
+ // Implement your PUT logic here
12
+ return {};
13
+ }`,
14
+ DELETE: `function action(req) {
15
+ // Implement your DELETE logic here
16
+ return {};
17
+ }`,
18
+ PATCH: `function action(req) {
19
+ // Implement your PATCH logic here
20
+ return {};
21
+ }`,
22
+ };
23
+
24
+ export default ActionTemplate;
@@ -53,7 +53,7 @@ export default function CognitoLogin() {
53
53
 
54
54
  const navigate = useNavigate();
55
55
 
56
- const { appId } = config();
56
+ const { appId, credentials } = config();
57
57
 
58
58
  const handleLogin = async () => {
59
59
  try {
@@ -63,7 +63,9 @@ export default function CognitoLogin() {
63
63
  if (!tokens?.accessToken)
64
64
  throw new Error("No Cognito access token received");
65
65
 
66
- const res = await fetch("/api/oauth", {
66
+ const requestUrl = credentials.requestUrl || "/api/oauth";
67
+
68
+ const res = await fetch(requestUrl, {
67
69
  method: "POST",
68
70
  headers: { "Content-Type": "application/json" },
69
71
  body: JSON.stringify({
@@ -1,3 +1,7 @@
1
+ import config from "../../config/config";
2
+ import { storage } from "@nucleoidjs/webstorage";
3
+ import { useNavigate } from "react-router-dom";
4
+
1
5
  import {
2
6
  Box,
3
7
  Button,
@@ -16,20 +20,18 @@ import {
16
20
  } from "@mui/icons-material";
17
21
  import React, { useState } from "react";
18
22
 
19
- import config from "../../config/config";
20
- import { storage } from "@nucleoidjs/webstorage";
21
- import { useNavigate } from "react-router-dom";
22
-
23
23
  export default function DemoLogin() {
24
24
  const [username, setUsername] = useState("");
25
25
  const [password, setPassword] = useState("");
26
26
  const [showPassword, setShowPassword] = useState(false);
27
27
  const navigate = useNavigate();
28
28
 
29
- const { appId } = config();
29
+ const { appId, credentials } = config();
30
30
 
31
31
  async function handleLogin() {
32
- const res = await fetch("/api/oauth", {
32
+ const requestUrl = credentials.requestUrl || "/api/oauth";
33
+
34
+ const res = await fetch(requestUrl, {
33
35
  method: "POST",
34
36
  headers: { "Content-Type": "application/json" },
35
37
  body: JSON.stringify({
@@ -1,3 +1,9 @@
1
+ import Iconify from "../components/Iconify";
2
+ import config from "../config/config";
3
+ import { useEvent } from "@nucleoidai/react-event";
4
+ import useSettings from "../hooks/useSettings";
5
+ import { useUser } from "../hooks/use-user";
6
+
1
7
  import {
2
8
  Avatar,
3
9
  Box,
@@ -22,12 +28,17 @@ import {
22
28
  import { Button, Dialog, DialogActions, DialogContent } from "@mui/material";
23
29
  import React, { useEffect, useState } from "react";
24
30
 
25
- import Iconify from "../components/Iconify";
26
- import config from "../config/config";
27
- import pkg from "../../../../../../package.json";
28
- import { useEvent } from "@nucleoidai/react-event";
29
- import useSettings from "../hooks/useSettings";
30
- import { useUser } from "../hooks/use-user";
31
+ let pkg = {
32
+ name: "",
33
+ version: "",
34
+ description: "",
35
+ };
36
+
37
+ try {
38
+ pkg = require("../../../../../../package.json");
39
+ } catch (error) {
40
+ console.error("Failed to load package.json for About tab:", error);
41
+ }
31
42
 
32
43
  function a11yProps(index) {
33
44
  return {
@@ -45,6 +56,9 @@ const TabPanel = (props) => {
45
56
  const SettingsDialogTabs = ({ tabs }) => {
46
57
  const [value, setValue] = useState(0);
47
58
 
59
+ const hasPkgInfo =
60
+ pkg && (pkg.name || pkg.version || pkg.description) ? true : false;
61
+
48
62
  const handleChange = (event, newValue) => {
49
63
  setValue(newValue);
50
64
  };
@@ -98,18 +112,20 @@ const SettingsDialogTabs = ({ tabs }) => {
98
112
  sx={{ "& label": { color: "custom.grey" } }}
99
113
  {...a11yProps(1)}
100
114
  />
101
- <Tab
102
- label={"About"}
103
- sx={{ "& label": { color: "custom.grey" } }}
104
- {...a11yProps(2)}
105
- />
115
+ {hasPkgInfo && (
116
+ <Tab
117
+ label={"About"}
118
+ sx={{ "& label": { color: "custom.grey" } }}
119
+ {...a11yProps(2)}
120
+ />
121
+ )}
106
122
  {tabs?.map((tab, index) => (
107
123
  <Tab
108
124
  key={tab.label}
109
125
  iconPosition="start"
110
126
  label={tab.label}
111
127
  sx={{ "& label": { color: "custom.grey" } }}
112
- {...a11yProps(index + 3)}
128
+ {...a11yProps(index + (hasPkgInfo ? 3 : 2))}
113
129
  />
114
130
  ))}
115
131
  </Tabs>
@@ -120,11 +136,17 @@ const SettingsDialogTabs = ({ tabs }) => {
120
136
  <TabPanel value={value} index={1}>
121
137
  <Settings />
122
138
  </TabPanel>
123
- <TabPanel value={value} index={2}>
124
- <About />
125
- </TabPanel>
139
+ {hasPkgInfo && (
140
+ <TabPanel value={value} index={2}>
141
+ <About />
142
+ </TabPanel>
143
+ )}
126
144
  {tabs?.map((tab, index) => (
127
- <TabPanel key={tab.label} value={value} index={index + 3}>
145
+ <TabPanel
146
+ key={tab.label}
147
+ value={value}
148
+ index={index + (hasPkgInfo ? 3 : 2)}
149
+ >
128
150
  <tab.panel />
129
151
  </TabPanel>
130
152
  ))}