@authhero/react-admin 0.14.0 → 0.15.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @authhero/react-admin
2
2
 
3
+ ## 0.15.0
4
+
5
+ ### Minor Changes
6
+
7
+ - aaf0aa0: Fix paging issue for scopes
8
+
3
9
  ## 0.14.0
4
10
 
5
11
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@authhero/react-admin",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,6 +26,7 @@
26
26
  "react-admin": "^5.8.0",
27
27
  "react-admin-color-picker": "^1.0.3",
28
28
  "react-dom": "^19.1.0",
29
+ "react-hook-form": "^7.69.0",
29
30
  "react-router-dom": "^7.6.0",
30
31
  "recharts": "^2.15.0"
31
32
  },
@@ -2,8 +2,6 @@ import {
2
2
  Edit,
3
3
  TextInput,
4
4
  BooleanInput,
5
- ArrayInput,
6
- SimpleFormIterator,
7
5
  TextField,
8
6
  TabbedForm,
9
7
  required,
@@ -11,7 +9,11 @@ import {
11
9
  FormDataConsumer,
12
10
  useRecordContext,
13
11
  } from "react-admin";
14
- import { Stack, Alert } from "@mui/material";
12
+ import { Stack, Alert, Pagination, Box, Typography, Button, IconButton, TextField as MuiTextField } from "@mui/material";
13
+ import DeleteIcon from "@mui/icons-material/Delete";
14
+ import AddIcon from "@mui/icons-material/Add";
15
+ import { useState, useMemo, useCallback } from "react";
16
+ import { useFormContext, useWatch } from "react-hook-form";
15
17
 
16
18
  function SystemEntityAlert() {
17
19
  const record = useRecordContext();
@@ -26,6 +28,131 @@ function SystemEntityAlert() {
26
28
  );
27
29
  }
28
30
 
31
+ const SCOPES_PER_PAGE = 10;
32
+
33
+ interface Scope {
34
+ value: string;
35
+ description?: string;
36
+ }
37
+
38
+ function PaginatedScopesInput({ disabled }: { disabled?: boolean }) {
39
+ const { setValue, getValues } = useFormContext();
40
+ const scopes: Scope[] = useWatch({ name: "scopes" }) || [];
41
+ const [page, setPage] = useState(1);
42
+
43
+ const totalPages = Math.max(1, Math.ceil(scopes.length / SCOPES_PER_PAGE));
44
+
45
+ const paginatedScopes = useMemo(() => {
46
+ const start = (page - 1) * SCOPES_PER_PAGE;
47
+ const end = start + SCOPES_PER_PAGE;
48
+ return scopes.slice(start, end).map((scope, index) => ({
49
+ ...scope,
50
+ actualIndex: start + index,
51
+ }));
52
+ }, [scopes, page]);
53
+
54
+ const handlePageChange = (_event: React.ChangeEvent<unknown>, value: number) => {
55
+ setPage(value);
56
+ };
57
+
58
+ const handleAdd = useCallback(() => {
59
+ const currentScopes = getValues("scopes") || [];
60
+ setValue("scopes", [...currentScopes, { value: "", description: "" }], { shouldDirty: true });
61
+ // Navigate to the last page where the new item will be
62
+ const newTotalPages = Math.ceil((currentScopes.length + 1) / SCOPES_PER_PAGE);
63
+ setPage(newTotalPages);
64
+ }, [getValues, setValue]);
65
+
66
+ const handleRemove = useCallback((actualIndex: number) => {
67
+ const currentScopes = getValues("scopes") || [];
68
+ const newScopes = currentScopes.filter((_: Scope, i: number) => i !== actualIndex);
69
+ setValue("scopes", newScopes, { shouldDirty: true });
70
+ // Adjust page if we removed the last item on current page
71
+ const newTotalPages = Math.ceil(newScopes.length / SCOPES_PER_PAGE);
72
+ if (page > newTotalPages && newTotalPages > 0) {
73
+ setPage(newTotalPages);
74
+ }
75
+ }, [getValues, setValue, page]);
76
+
77
+ const handleScopeChange = useCallback((actualIndex: number, field: "value" | "description", newValue: string) => {
78
+ const currentScopes = getValues("scopes") || [];
79
+ const newScopes = [...currentScopes];
80
+ newScopes[actualIndex] = { ...newScopes[actualIndex], [field]: newValue };
81
+ setValue("scopes", newScopes, { shouldDirty: true });
82
+ }, [getValues, setValue]);
83
+
84
+ return (
85
+ <Box>
86
+ <Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "center", mb: 2 }}>
87
+ <Typography variant="body2" color="text.secondary">
88
+ {scopes.length} scope{scopes.length !== 1 ? "s" : ""} total
89
+ </Typography>
90
+ {!disabled && (
91
+ <Button
92
+ startIcon={<AddIcon />}
93
+ onClick={handleAdd}
94
+ size="small"
95
+ variant="outlined"
96
+ >
97
+ Add Scope
98
+ </Button>
99
+ )}
100
+ </Box>
101
+
102
+ {paginatedScopes.map((scope) => (
103
+ <Stack
104
+ key={scope.actualIndex}
105
+ spacing={2}
106
+ direction="row"
107
+ sx={{ width: "100%", alignItems: "flex-start", mb: 2 }}
108
+ >
109
+ <MuiTextField
110
+ value={scope.value || ""}
111
+ onChange={(e) => handleScopeChange(scope.actualIndex, "value", e.target.value)}
112
+ label="Scope Name"
113
+ helperText="e.g., read:users, write:posts"
114
+ sx={{ flex: 1 }}
115
+ disabled={disabled}
116
+ size="small"
117
+ required
118
+ />
119
+ <MuiTextField
120
+ value={scope.description || ""}
121
+ onChange={(e) => handleScopeChange(scope.actualIndex, "description", e.target.value)}
122
+ label="Description"
123
+ helperText="What this scope allows"
124
+ sx={{ flex: 2 }}
125
+ disabled={disabled}
126
+ size="small"
127
+ />
128
+ {!disabled && (
129
+ <IconButton
130
+ onClick={() => handleRemove(scope.actualIndex)}
131
+ color="error"
132
+ sx={{ mt: 1 }}
133
+ >
134
+ <DeleteIcon />
135
+ </IconButton>
136
+ )}
137
+ </Stack>
138
+ ))}
139
+
140
+ {totalPages > 1 && (
141
+ <Box sx={{ display: "flex", justifyContent: "center", mt: 2 }}>
142
+ <Pagination
143
+ count={totalPages}
144
+ page={page}
145
+ onChange={handlePageChange}
146
+ color="primary"
147
+ showFirstButton
148
+ showLastButton
149
+ />
150
+ </Box>
151
+ )}
152
+ </Box>
153
+ );
154
+ }
155
+
29
156
  function ResourceServerForm() {
30
157
  const record = useRecordContext();
31
158
  const isSystem = record?.is_system;
@@ -115,31 +242,7 @@ function ResourceServerForm() {
115
242
  </TabbedForm.Tab>
116
243
 
117
244
  <TabbedForm.Tab label="Scopes">
118
- <ArrayInput source="scopes" label="" disabled={isSystem}>
119
- <SimpleFormIterator disableAdd={isSystem} disableRemove={isSystem} disableReordering={isSystem}>
120
- <Stack
121
- spacing={2}
122
- direction="row"
123
- sx={{ width: "100%", alignItems: "flex-start" }}
124
- >
125
- <TextInput
126
- source="value"
127
- validate={[required()]}
128
- label="Scope Name"
129
- helperText="e.g., read:users, write:posts"
130
- sx={{ flex: 1 }}
131
- disabled={isSystem}
132
- />
133
- <TextInput
134
- source="description"
135
- label="Description"
136
- helperText="What this scope allows"
137
- sx={{ flex: 2 }}
138
- disabled={isSystem}
139
- />
140
- </Stack>
141
- </SimpleFormIterator>
142
- </ArrayInput>
245
+ <PaginatedScopesInput disabled={isSystem} />
143
246
  </TabbedForm.Tab>
144
247
  </TabbedForm>
145
248
  );