@nestledjs/data-browser 1.0.14 → 1.0.16
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 +43 -39
- package/index.js +6 -1
- package/lib/components/ExportButton.d.ts +14 -0
- package/lib/components/ExportButton.js +281 -0
- package/lib/components/FilterField.js +1 -2
- package/lib/components/RelationFieldWrapper.d.ts +1 -1
- package/lib/components/RelationFieldWrapper.js +2 -2
- package/lib/components/filters/DateRangeFilter.d.ts +1 -1
- package/lib/components/filters/DateRangeFilter.js +2 -2
- package/lib/components/filters/EnumFilter.d.ts +1 -1
- package/lib/components/filters/NumberRangeFilter.d.ts +1 -1
- package/lib/components/filters/NumberRangeFilter.js +2 -2
- package/lib/components/filters/RelationComponents.d.ts +5 -5
- package/lib/components/filters/RelationComponents.js +40 -13
- package/lib/components/filters/RelationFilterField.d.ts +1 -1
- package/lib/components/filters/RelationFilterField.js +18 -10
- package/lib/components/index.d.ts +1 -0
- package/lib/components/shared/AdminBreadcrumbs.js +2 -17
- package/lib/components/shared/AdminErrorStates.js +6 -1
- package/lib/components/shared/AdminStatusDisplay.js +7 -11
- package/lib/context/AdminDataContext.d.ts +2 -2
- package/lib/hooks/useAdminList.js +1 -1
- package/lib/hooks/useClickOutside.js +1 -1
- package/lib/hooks/useRelationData.js +2 -1
- package/lib/layouts/AdminDataLayout.js +87 -14
- package/lib/pages/AdminDataCreatePage.d.ts +1 -1
- package/lib/pages/AdminDataCreatePage.js +68 -45
- package/lib/pages/AdminDataEditPage.js +71 -38
- package/lib/pages/AdminDataListPage.js +116 -85
- package/lib/utils/graphql-utils.d.ts +7 -1
- package/lib/utils/graphql-utils.js +343 -331
- package/lib/utils/secure-storage.js +26 -20
- package/lib/utils/string-utils.js +31 -8
- package/package.json +2 -2
|
@@ -3,7 +3,7 @@ const ADMIN_CONFIG_VERSION = "1.0";
|
|
|
3
3
|
const MAX_CONFIG_SIZE = 5e4;
|
|
4
4
|
const sanitizeString = (value) => {
|
|
5
5
|
if (typeof value !== "string") return "";
|
|
6
|
-
return value.
|
|
6
|
+
return value.replaceAll(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, "").replaceAll(/javascript:/gi, "").replaceAll(/on\w+\s*=/gi, "").trim().substring(0, 1e3);
|
|
7
7
|
};
|
|
8
8
|
const sanitizeArray = (value) => {
|
|
9
9
|
if (!Array.isArray(value)) return [];
|
|
@@ -15,9 +15,21 @@ const sanitizeSortPreference = (value) => {
|
|
|
15
15
|
const orderBy = sanitizeString(obj.orderBy);
|
|
16
16
|
const orderDirection = sanitizeString(obj.orderDirection);
|
|
17
17
|
if (!["asc", "desc"].includes(orderDirection)) return null;
|
|
18
|
-
if (!/^[a-zA-Z_]
|
|
18
|
+
if (!/^[a-zA-Z_]\w*$/.test(orderBy)) return null;
|
|
19
19
|
return { orderBy, orderDirection };
|
|
20
20
|
};
|
|
21
|
+
const isValidModelName = (name) => /^[a-zA-Z_]\w*$/.test(name);
|
|
22
|
+
const isValidModelConfig = (modelConfig) => {
|
|
23
|
+
if (!modelConfig || typeof modelConfig !== "object") return true;
|
|
24
|
+
const cfg = modelConfig;
|
|
25
|
+
if (cfg.visibleColumns !== void 0 && !Array.isArray(cfg.visibleColumns)) return false;
|
|
26
|
+
if (cfg.sortPreference !== void 0) {
|
|
27
|
+
const sortPref = sanitizeSortPreference(cfg.sortPreference);
|
|
28
|
+
if (cfg.sortPreference !== null && sortPref === null) return false;
|
|
29
|
+
}
|
|
30
|
+
if (cfg.searchFields !== void 0 && !Array.isArray(cfg.searchFields)) return false;
|
|
31
|
+
return true;
|
|
32
|
+
};
|
|
21
33
|
const validateAdminConfig = (config) => {
|
|
22
34
|
if (!config || typeof config !== "object") return false;
|
|
23
35
|
const configObj = config;
|
|
@@ -25,16 +37,8 @@ const validateAdminConfig = (config) => {
|
|
|
25
37
|
if (!configObj.models || typeof configObj.models !== "object") return false;
|
|
26
38
|
const models = configObj.models;
|
|
27
39
|
for (const [modelName, modelConfig] of Object.entries(models)) {
|
|
28
|
-
if (
|
|
29
|
-
if (modelConfig
|
|
30
|
-
const config2 = modelConfig;
|
|
31
|
-
if (config2.visibleColumns !== void 0 && !Array.isArray(config2.visibleColumns)) return false;
|
|
32
|
-
if (config2.sortPreference !== void 0) {
|
|
33
|
-
const sortPref = sanitizeSortPreference(config2.sortPreference);
|
|
34
|
-
if (config2.sortPreference !== null && sortPref === null) return false;
|
|
35
|
-
}
|
|
36
|
-
if (config2.searchFields !== void 0 && !Array.isArray(config2.searchFields)) return false;
|
|
37
|
-
}
|
|
40
|
+
if (!isValidModelName(modelName)) return false;
|
|
41
|
+
if (!isValidModelConfig(modelConfig)) return false;
|
|
38
42
|
}
|
|
39
43
|
return true;
|
|
40
44
|
};
|
|
@@ -56,10 +60,11 @@ const SecureAdminLocalStorage = {
|
|
|
56
60
|
return { version: ADMIN_CONFIG_VERSION, models: {} };
|
|
57
61
|
}
|
|
58
62
|
return parsed;
|
|
59
|
-
} catch
|
|
63
|
+
} catch {
|
|
60
64
|
try {
|
|
61
65
|
localStorage.removeItem(ADMIN_CONFIG_KEY);
|
|
62
|
-
} catch {
|
|
66
|
+
} catch (cleanupError) {
|
|
67
|
+
console.error("Unexpected error:", cleanupError);
|
|
63
68
|
}
|
|
64
69
|
return { version: ADMIN_CONFIG_VERSION, models: {} };
|
|
65
70
|
}
|
|
@@ -77,16 +82,16 @@ const SecureAdminLocalStorage = {
|
|
|
77
82
|
localStorage.setItem(ADMIN_CONFIG_KEY, serialized);
|
|
78
83
|
return true;
|
|
79
84
|
} catch (error) {
|
|
85
|
+
console.error("Unexpected error:", error);
|
|
80
86
|
return false;
|
|
81
87
|
}
|
|
82
88
|
},
|
|
83
89
|
// Get visible columns for a specific model with sanitization
|
|
84
90
|
getColumnVisibility: (modelName) => {
|
|
85
|
-
var _a;
|
|
86
91
|
const sanitizedModelName = sanitizeString(modelName);
|
|
87
92
|
if (!sanitizedModelName) return null;
|
|
88
93
|
const config = SecureAdminLocalStorage.getConfig();
|
|
89
|
-
const columns =
|
|
94
|
+
const columns = config.models[sanitizedModelName]?.visibleColumns;
|
|
90
95
|
return columns ? sanitizeArray(columns) : null;
|
|
91
96
|
},
|
|
92
97
|
// Set visible columns for a specific model with validation
|
|
@@ -103,11 +108,10 @@ const SecureAdminLocalStorage = {
|
|
|
103
108
|
},
|
|
104
109
|
// Get sort preference for a specific model with validation
|
|
105
110
|
getSortPreference: (modelName) => {
|
|
106
|
-
var _a;
|
|
107
111
|
const sanitizedModelName = sanitizeString(modelName);
|
|
108
112
|
if (!sanitizedModelName) return null;
|
|
109
113
|
const config = SecureAdminLocalStorage.getConfig();
|
|
110
|
-
const sortPref =
|
|
114
|
+
const sortPref = config.models[sanitizedModelName]?.sortPreference;
|
|
111
115
|
return sortPref ? sanitizeSortPreference(sortPref) : null;
|
|
112
116
|
},
|
|
113
117
|
// Set sort preference for a specific model with validation
|
|
@@ -124,11 +128,10 @@ const SecureAdminLocalStorage = {
|
|
|
124
128
|
},
|
|
125
129
|
// Get search fields for a specific model with sanitization
|
|
126
130
|
getSearchFields: (modelName) => {
|
|
127
|
-
var _a;
|
|
128
131
|
const sanitizedModelName = sanitizeString(modelName);
|
|
129
132
|
if (!sanitizedModelName) return null;
|
|
130
133
|
const config = SecureAdminLocalStorage.getConfig();
|
|
131
|
-
const searchFields =
|
|
134
|
+
const searchFields = config.models[sanitizedModelName]?.searchFields;
|
|
132
135
|
return searchFields ? sanitizeArray(searchFields) : null;
|
|
133
136
|
},
|
|
134
137
|
// Set search fields for a specific model with validation
|
|
@@ -152,6 +155,7 @@ const SecureAdminLocalStorage = {
|
|
|
152
155
|
}
|
|
153
156
|
return JSON.stringify(config, null, 2);
|
|
154
157
|
} catch (error) {
|
|
158
|
+
console.error("Unexpected error:", error);
|
|
155
159
|
return null;
|
|
156
160
|
}
|
|
157
161
|
},
|
|
@@ -181,6 +185,7 @@ const SecureAdminLocalStorage = {
|
|
|
181
185
|
}
|
|
182
186
|
return SecureAdminLocalStorage.setConfig(config);
|
|
183
187
|
} catch (error) {
|
|
188
|
+
console.error("Unexpected error:", error);
|
|
184
189
|
return false;
|
|
185
190
|
}
|
|
186
191
|
},
|
|
@@ -190,6 +195,7 @@ const SecureAdminLocalStorage = {
|
|
|
190
195
|
localStorage.removeItem(ADMIN_CONFIG_KEY);
|
|
191
196
|
return true;
|
|
192
197
|
} catch (error) {
|
|
198
|
+
console.error("Unexpected error:", error);
|
|
193
199
|
return false;
|
|
194
200
|
}
|
|
195
201
|
}
|
|
@@ -4,6 +4,15 @@ function kebabCase(name) {
|
|
|
4
4
|
function spacedWords(name) {
|
|
5
5
|
return name.replaceAll(/([a-z])([A-Z])/g, "$1 $2").replaceAll(/([A-Z])([A-Z][a-z])/g, "$1 $2");
|
|
6
6
|
}
|
|
7
|
+
function isUppercaseLetter(char) {
|
|
8
|
+
return char >= "A" && char <= "Z";
|
|
9
|
+
}
|
|
10
|
+
function isLowercaseLetter(char) {
|
|
11
|
+
return char !== void 0 && char >= "a" && char <= "z";
|
|
12
|
+
}
|
|
13
|
+
function titleCaseAcronym(acronym) {
|
|
14
|
+
return acronym.charAt(0).toUpperCase() + acronym.slice(1).toLowerCase();
|
|
15
|
+
}
|
|
7
16
|
function formatFieldName(fieldName) {
|
|
8
17
|
if (fieldName.includes(".")) {
|
|
9
18
|
return fieldName.split(".").map((part) => {
|
|
@@ -22,15 +31,29 @@ function formatFieldName(fieldName) {
|
|
|
22
31
|
return fieldName.replaceAll(/([a-z])([A-Z])/g, "$1 $2").replace(/^./, (str) => str.toUpperCase());
|
|
23
32
|
}
|
|
24
33
|
function normalizeModelNameForDocument(modelName) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
let normalized = "";
|
|
35
|
+
let index = 0;
|
|
36
|
+
while (index < modelName.length) {
|
|
37
|
+
if (!isUppercaseLetter(modelName[index])) {
|
|
38
|
+
normalized += modelName[index];
|
|
39
|
+
index += 1;
|
|
40
|
+
continue;
|
|
28
41
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
42
|
+
const runStart = index;
|
|
43
|
+
while (index < modelName.length && isUppercaseLetter(modelName[index])) {
|
|
44
|
+
index += 1;
|
|
45
|
+
}
|
|
46
|
+
const run = modelName.slice(runStart, index);
|
|
47
|
+
const nextIsLowercase = isLowercaseLetter(modelName[index]);
|
|
48
|
+
if (run.length >= 4 && nextIsLowercase) {
|
|
49
|
+
normalized += titleCaseAcronym(run.slice(0, -1)) + run[run.length - 1];
|
|
50
|
+
} else if (run.length >= 3 && index === modelName.length) {
|
|
51
|
+
normalized += titleCaseAcronym(run);
|
|
52
|
+
} else {
|
|
53
|
+
normalized += run;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return normalized;
|
|
34
57
|
}
|
|
35
58
|
function getItemDisplayName(item) {
|
|
36
59
|
if (item.name) return item.name;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestledjs/data-browser",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.16",
|
|
4
4
|
"description": "Universal admin data browser for Nestled framework projects with full CRUD operations",
|
|
5
5
|
"main": "./index.js",
|
|
6
6
|
"module": "./index.js",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"peerDependencies": {
|
|
38
38
|
"@apollo/client": "^4.0.0",
|
|
39
39
|
"@heroicons/react": "^2.0.0",
|
|
40
|
-
"@nestledjs/forms": "
|
|
40
|
+
"@nestledjs/forms": "0.7.7",
|
|
41
41
|
"@nestledjs/shared-components": "^1.0.14",
|
|
42
42
|
"react": "^19.0.0",
|
|
43
43
|
"react-router": "^7.0.0"
|