@tunghtml/strapi-plugin-filtered-relation 1.1.6 → 1.1.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/CHANGELOG.md +12 -0
- package/admin/src/components/FilteredRelationInput.jsx +84 -116
- package/package.json +1 -1
- package/server/register.js +82 -82
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.1.7] - 2026-10-19
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Removed the auto-save that could never run in Strapi 5 (it needed `contentType`/`model` props the Content Manager does not pass) and sent a wrongly shaped payload
|
|
8
|
+
- Hooks no longer run after an early return
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- The lifecycle hook collects filtered-relation fields once instead of scanning every content type on each database write
|
|
13
|
+
- Split the hook into small functions, removed unused variables and the nested ternary in the field render
|
|
14
|
+
|
|
3
15
|
## [1.1.3] - 2024-12-02
|
|
4
16
|
|
|
5
17
|
### Changed
|
|
@@ -22,10 +22,6 @@ const FilteredRelationInput = (props) => {
|
|
|
22
22
|
const [statusOptions, setStatusOptions] = React.useState([]);
|
|
23
23
|
const [updatingStatus, setUpdatingStatus] = React.useState({});
|
|
24
24
|
|
|
25
|
-
if (!props) {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
25
|
const { attribute, name, error, value } = props;
|
|
30
26
|
|
|
31
27
|
// Extract documentId from URL path
|
|
@@ -85,25 +81,6 @@ const FilteredRelationInput = (props) => {
|
|
|
85
81
|
filterParams[filterField2] = value2;
|
|
86
82
|
}
|
|
87
83
|
|
|
88
|
-
const contentType = props.contentType || props.model;
|
|
89
|
-
|
|
90
|
-
const saveFieldValue = async (newValue) => {
|
|
91
|
-
if (!documentId || !contentType) return;
|
|
92
|
-
|
|
93
|
-
try {
|
|
94
|
-
await put(
|
|
95
|
-
`/content-manager/collection-types/${contentType}/${documentId}`,
|
|
96
|
-
{
|
|
97
|
-
data: {
|
|
98
|
-
[name]: newValue,
|
|
99
|
-
},
|
|
100
|
-
}
|
|
101
|
-
);
|
|
102
|
-
} catch (err) {
|
|
103
|
-
console.error("Error auto-saving filtered relation:", err);
|
|
104
|
-
}
|
|
105
|
-
};
|
|
106
|
-
|
|
107
84
|
// Fetch status options from target model schema
|
|
108
85
|
React.useEffect(() => {
|
|
109
86
|
const fetchStatusOptions = async () => {
|
|
@@ -121,8 +98,8 @@ const FilteredRelationInput = (props) => {
|
|
|
121
98
|
);
|
|
122
99
|
const builderSchema = builderResponse?.data?.data?.schema;
|
|
123
100
|
enumValues = builderSchema?.attributes?.[statusField]?.enum;
|
|
124
|
-
} catch
|
|
125
|
-
// Endpoint might not be accessible
|
|
101
|
+
} catch {
|
|
102
|
+
// Endpoint might not be accessible: no status options then
|
|
126
103
|
}
|
|
127
104
|
|
|
128
105
|
if (enumValues && Array.isArray(enumValues)) {
|
|
@@ -254,7 +231,7 @@ const FilteredRelationInput = (props) => {
|
|
|
254
231
|
};
|
|
255
232
|
|
|
256
233
|
// Use disconnect with full payload
|
|
257
|
-
|
|
234
|
+
await put(
|
|
258
235
|
`/content-manager/collection-types/${targetModel}/${itemId}`,
|
|
259
236
|
updatePayload
|
|
260
237
|
);
|
|
@@ -276,7 +253,7 @@ const FilteredRelationInput = (props) => {
|
|
|
276
253
|
};
|
|
277
254
|
|
|
278
255
|
// Use connect with full payload
|
|
279
|
-
|
|
256
|
+
await put(
|
|
280
257
|
`/content-manager/collection-types/${targetModel}/${targetRecordId}`,
|
|
281
258
|
addPayload
|
|
282
259
|
);
|
|
@@ -329,11 +306,6 @@ const FilteredRelationInput = (props) => {
|
|
|
329
306
|
})
|
|
330
307
|
.join("&");
|
|
331
308
|
|
|
332
|
-
const populateFields = [displayField];
|
|
333
|
-
if (statusField) {
|
|
334
|
-
populateFields.push(statusField);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
309
|
const apiUrl = `/content-manager/collection-types/${targetModel}?${filterQuery}&populate[${displayField}][populate]=*`;
|
|
338
310
|
const response = await get(apiUrl);
|
|
339
311
|
const items =
|
|
@@ -404,21 +376,8 @@ const FilteredRelationInput = (props) => {
|
|
|
404
376
|
});
|
|
405
377
|
|
|
406
378
|
setItems(formattedItems);
|
|
407
|
-
|
|
408
|
-
// Auto-save to database
|
|
409
|
-
const documentIds = formattedItems.map(
|
|
410
|
-
(item) => item.relatedDocumentId || item.id
|
|
411
|
-
);
|
|
412
|
-
const newValue = JSON.stringify(documentIds);
|
|
413
|
-
|
|
414
|
-
if (documentId && value !== newValue) {
|
|
415
|
-
await saveFieldValue(newValue);
|
|
416
|
-
}
|
|
417
379
|
} else {
|
|
418
380
|
setItems([]);
|
|
419
|
-
if (documentId && value) {
|
|
420
|
-
await saveFieldValue(null);
|
|
421
|
-
}
|
|
422
381
|
}
|
|
423
382
|
} catch (err) {
|
|
424
383
|
console.error("Error fetching filtered relations:", err);
|
|
@@ -458,83 +417,92 @@ const FilteredRelationInput = (props) => {
|
|
|
458
417
|
const fieldLabel = props.label || name;
|
|
459
418
|
const itemCount = items.length;
|
|
460
419
|
|
|
420
|
+
let content;
|
|
421
|
+
if (loading) {
|
|
422
|
+
content = (
|
|
423
|
+
<Typography variant="omega" textColor="neutral600">
|
|
424
|
+
{formatMessage({
|
|
425
|
+
id: "filtered-relation.loading",
|
|
426
|
+
defaultMessage: "Loading...",
|
|
427
|
+
})}
|
|
428
|
+
</Typography>
|
|
429
|
+
);
|
|
430
|
+
} else if (items.length > 0) {
|
|
431
|
+
content = (
|
|
432
|
+
<Flex direction="column" alignItems="stretch" gap={1}>
|
|
433
|
+
{items.map((item) => {
|
|
434
|
+
const editUrl = `/content-manager/collection-types/${item.relationModel}/${item.relatedDocumentId}`;
|
|
435
|
+
const isUpdating = updatingStatus[item.originalId];
|
|
436
|
+
|
|
437
|
+
return (
|
|
438
|
+
<Box
|
|
439
|
+
key={item.id}
|
|
440
|
+
background="neutral0"
|
|
441
|
+
borderColor="neutral200"
|
|
442
|
+
borderStyle="solid"
|
|
443
|
+
borderWidth="1px"
|
|
444
|
+
hasRadius
|
|
445
|
+
paddingTop={2}
|
|
446
|
+
paddingBottom={2}
|
|
447
|
+
paddingLeft={4}
|
|
448
|
+
paddingRight={4}
|
|
449
|
+
style={{
|
|
450
|
+
cursor: "pointer",
|
|
451
|
+
minHeight: "50px",
|
|
452
|
+
display: "flex",
|
|
453
|
+
alignItems: "center",
|
|
454
|
+
justifyContent: "space-between",
|
|
455
|
+
}}
|
|
456
|
+
onClick={() => navigate(editUrl)}
|
|
457
|
+
>
|
|
458
|
+
<Typography variant="omega" textColor="primary600">
|
|
459
|
+
{item.label}
|
|
460
|
+
</Typography>
|
|
461
|
+
|
|
462
|
+
{statusField && statusOptions.length > 0 && (
|
|
463
|
+
<Box
|
|
464
|
+
onClick={(e) => e.stopPropagation()}
|
|
465
|
+
style={{ minWidth: "150px" }}
|
|
466
|
+
>
|
|
467
|
+
<SingleSelect
|
|
468
|
+
size="S"
|
|
469
|
+
value={item.currentStatus}
|
|
470
|
+
onChange={(value) =>
|
|
471
|
+
handleStatusChange(item.originalId, value, null)
|
|
472
|
+
}
|
|
473
|
+
disabled={isUpdating}
|
|
474
|
+
>
|
|
475
|
+
{statusOptions.map((status) => (
|
|
476
|
+
<SingleSelectOption key={status} value={status}>
|
|
477
|
+
{status}
|
|
478
|
+
</SingleSelectOption>
|
|
479
|
+
))}
|
|
480
|
+
</SingleSelect>
|
|
481
|
+
</Box>
|
|
482
|
+
)}
|
|
483
|
+
</Box>
|
|
484
|
+
);
|
|
485
|
+
})}
|
|
486
|
+
</Flex>
|
|
487
|
+
);
|
|
488
|
+
} else {
|
|
489
|
+
content = (
|
|
490
|
+
<Typography variant="omega" textColor="neutral600">
|
|
491
|
+
{formatMessage({
|
|
492
|
+
id: "filtered-relation.no-results",
|
|
493
|
+
defaultMessage: "No matching records found",
|
|
494
|
+
})}
|
|
495
|
+
</Typography>
|
|
496
|
+
);
|
|
497
|
+
}
|
|
498
|
+
|
|
461
499
|
return (
|
|
462
500
|
<Field.Root name={name} error={error}>
|
|
463
501
|
<Flex direction="column" alignItems="stretch" gap={1}>
|
|
464
502
|
<Field.Label>
|
|
465
503
|
{fieldLabel} {itemCount > 0 && `(${itemCount})`}
|
|
466
504
|
</Field.Label>
|
|
467
|
-
{
|
|
468
|
-
<Typography variant="omega" textColor="neutral600">
|
|
469
|
-
{formatMessage({
|
|
470
|
-
id: "filtered-relation.loading",
|
|
471
|
-
defaultMessage: "Loading...",
|
|
472
|
-
})}
|
|
473
|
-
</Typography>
|
|
474
|
-
) : items.length > 0 ? (
|
|
475
|
-
<Flex direction="column" alignItems="stretch" gap={1}>
|
|
476
|
-
{items.map((item) => {
|
|
477
|
-
const editUrl = `/content-manager/collection-types/${item.relationModel}/${item.relatedDocumentId}`;
|
|
478
|
-
const isUpdating = updatingStatus[item.originalId];
|
|
479
|
-
|
|
480
|
-
return (
|
|
481
|
-
<Box
|
|
482
|
-
key={item.id}
|
|
483
|
-
background="neutral0"
|
|
484
|
-
borderColor="neutral200"
|
|
485
|
-
borderStyle="solid"
|
|
486
|
-
borderWidth="1px"
|
|
487
|
-
hasRadius
|
|
488
|
-
paddingTop={2}
|
|
489
|
-
paddingBottom={2}
|
|
490
|
-
paddingLeft={4}
|
|
491
|
-
paddingRight={4}
|
|
492
|
-
style={{
|
|
493
|
-
cursor: "pointer",
|
|
494
|
-
minHeight: "50px",
|
|
495
|
-
display: "flex",
|
|
496
|
-
alignItems: "center",
|
|
497
|
-
justifyContent: "space-between",
|
|
498
|
-
}}
|
|
499
|
-
onClick={() => navigate(editUrl)}
|
|
500
|
-
>
|
|
501
|
-
<Typography variant="omega" textColor="primary600">
|
|
502
|
-
{item.label}
|
|
503
|
-
</Typography>
|
|
504
|
-
|
|
505
|
-
{statusField && statusOptions.length > 0 && (
|
|
506
|
-
<Box
|
|
507
|
-
onClick={(e) => e.stopPropagation()}
|
|
508
|
-
style={{ minWidth: "150px" }}
|
|
509
|
-
>
|
|
510
|
-
<SingleSelect
|
|
511
|
-
size="S"
|
|
512
|
-
value={item.currentStatus}
|
|
513
|
-
onChange={(value) =>
|
|
514
|
-
handleStatusChange(item.originalId, value, null)
|
|
515
|
-
}
|
|
516
|
-
disabled={isUpdating}
|
|
517
|
-
>
|
|
518
|
-
{statusOptions.map((status) => (
|
|
519
|
-
<SingleSelectOption key={status} value={status}>
|
|
520
|
-
{status}
|
|
521
|
-
</SingleSelectOption>
|
|
522
|
-
))}
|
|
523
|
-
</SingleSelect>
|
|
524
|
-
</Box>
|
|
525
|
-
)}
|
|
526
|
-
</Box>
|
|
527
|
-
);
|
|
528
|
-
})}
|
|
529
|
-
</Flex>
|
|
530
|
-
) : (
|
|
531
|
-
<Typography variant="omega" textColor="neutral600">
|
|
532
|
-
{formatMessage({
|
|
533
|
-
id: "filtered-relation.no-results",
|
|
534
|
-
defaultMessage: "No matching records found",
|
|
535
|
-
})}
|
|
536
|
-
</Typography>
|
|
537
|
-
)}
|
|
505
|
+
{content}
|
|
538
506
|
<Field.Hint>
|
|
539
507
|
<Typography variant="pi" textColor="neutral600">
|
|
540
508
|
{Object.keys(filterParams).length > 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tunghtml/strapi-plugin-filtered-relation",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.7",
|
|
4
4
|
"description": "A Strapi custom field plugin for displaying filtered relations with dynamic placeholder support and status change capability",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"strapi",
|
package/server/register.js
CHANGED
|
@@ -23,93 +23,93 @@ module.exports = ({ strapi }) => {
|
|
|
23
23
|
});
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
const FILTERED_RELATION_UID = "plugin::filtered-relation.filtered-relation";
|
|
27
|
+
|
|
28
|
+
// Parse collection name to API ID
|
|
29
|
+
const parseCollectionName = (name) => {
|
|
30
|
+
if (!name) return null;
|
|
31
|
+
if (name.startsWith("api::")) return name;
|
|
32
|
+
const slug = name
|
|
33
|
+
.toLowerCase()
|
|
34
|
+
.replace(/\s+/g, "-")
|
|
35
|
+
.replace(/[^a-z0-9-]/g, "");
|
|
36
|
+
return `api::${slug}.${slug}`;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// Static filters only: values bound to the current document cannot be resolved here
|
|
40
|
+
const buildStaticFilters = ({
|
|
41
|
+
filterField1,
|
|
42
|
+
filterValue1,
|
|
43
|
+
filterField2,
|
|
44
|
+
filterValue2,
|
|
45
|
+
}) => {
|
|
46
|
+
const filterParams = {};
|
|
47
|
+
if (filterField1 && filterValue1 && filterValue1 !== "{{documentId}}") {
|
|
48
|
+
filterParams[filterField1] = filterValue1;
|
|
49
|
+
}
|
|
50
|
+
if (filterField2 && filterValue2 && filterValue2 !== "{{documentId}}") {
|
|
51
|
+
filterParams[filterField2] = filterValue2;
|
|
52
|
+
}
|
|
53
|
+
return filterParams;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Schemas are fixed once Strapi is running, so the filtered-relation fields are collected once
|
|
57
|
+
let filteredRelationFields = null;
|
|
58
|
+
|
|
59
|
+
function getFilteredRelationFields(strapi) {
|
|
60
|
+
if (filteredRelationFields) return filteredRelationFields;
|
|
61
|
+
|
|
62
|
+
filteredRelationFields = [];
|
|
63
|
+
for (const uid of Object.keys(strapi.contentTypes)) {
|
|
64
|
+
if (!uid.startsWith("api::")) continue;
|
|
65
|
+
|
|
66
|
+
const schema = strapi.getModel(uid);
|
|
67
|
+
for (const [fieldName, fieldConfig] of Object.entries(
|
|
68
|
+
schema?.attributes || {},
|
|
69
|
+
)) {
|
|
70
|
+
if (fieldConfig.customField !== FILTERED_RELATION_UID) continue;
|
|
71
|
+
|
|
72
|
+
const options = fieldConfig.options || {};
|
|
73
|
+
filteredRelationFields.push({
|
|
74
|
+
uid,
|
|
75
|
+
schema,
|
|
76
|
+
fieldName,
|
|
77
|
+
targetModel: parseCollectionName(options.targetModel),
|
|
78
|
+
filterParams: buildStaticFilters(options),
|
|
79
|
+
displayField: options.displayField,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return filteredRelationFields;
|
|
84
|
+
}
|
|
85
|
+
|
|
26
86
|
async function updateRelatedFilteredFields(event, strapi) {
|
|
27
87
|
const { model } = event;
|
|
28
88
|
if (!model) return;
|
|
29
89
|
|
|
30
|
-
const currentModelUid = model.uid;
|
|
31
|
-
|
|
32
90
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
filterField2,
|
|
56
|
-
filterValue2,
|
|
57
|
-
displayField,
|
|
58
|
-
} = fieldConfig.options || {};
|
|
59
|
-
|
|
60
|
-
// Parse collection name to API ID
|
|
61
|
-
const parseCollectionName = (name) => {
|
|
62
|
-
if (!name) return null;
|
|
63
|
-
if (name.startsWith("api::")) return name;
|
|
64
|
-
const slug = name
|
|
65
|
-
.toLowerCase()
|
|
66
|
-
.replace(/\s+/g, "-")
|
|
67
|
-
.replace(/[^a-z0-9-]/g, "");
|
|
68
|
-
return `api::${slug}.${slug}`;
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
const targetModel = parseCollectionName(targetModelInput);
|
|
72
|
-
|
|
73
|
-
// Build filter params
|
|
74
|
-
const filterParams = {};
|
|
75
|
-
if (
|
|
76
|
-
filterField1 &&
|
|
77
|
-
filterValue1 &&
|
|
78
|
-
filterValue1 !== "{{documentId}}"
|
|
79
|
-
) {
|
|
80
|
-
filterParams[filterField1] = filterValue1;
|
|
81
|
-
}
|
|
82
|
-
if (
|
|
83
|
-
filterField2 &&
|
|
84
|
-
filterValue2 &&
|
|
85
|
-
filterValue2 !== "{{documentId}}"
|
|
86
|
-
) {
|
|
87
|
-
filterParams[filterField2] = filterValue2;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Check if this field targets the model that was just modified
|
|
91
|
-
if (targetModel === currentModelUid) {
|
|
92
|
-
// Find all related entities that need updating
|
|
93
|
-
const relatedEntities = await findRelatedEntities(
|
|
94
|
-
event,
|
|
95
|
-
schema,
|
|
96
|
-
uid,
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
// Update each related entity
|
|
100
|
-
for (const entityId of relatedEntities) {
|
|
101
|
-
await updateFilteredField(
|
|
102
|
-
strapi,
|
|
103
|
-
uid,
|
|
104
|
-
entityId,
|
|
105
|
-
fieldName,
|
|
106
|
-
targetModel,
|
|
107
|
-
filterParams,
|
|
108
|
-
displayField,
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
}
|
|
91
|
+
const fields = getFilteredRelationFields(strapi).filter(
|
|
92
|
+
(field) => field.targetModel === model.uid,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
for (const field of fields) {
|
|
96
|
+
// Find all related entities that need updating
|
|
97
|
+
const relatedEntities = await findRelatedEntities(
|
|
98
|
+
event,
|
|
99
|
+
field.schema,
|
|
100
|
+
field.uid,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
for (const entityId of relatedEntities) {
|
|
104
|
+
await updateFilteredField(
|
|
105
|
+
strapi,
|
|
106
|
+
field.uid,
|
|
107
|
+
entityId,
|
|
108
|
+
field.fieldName,
|
|
109
|
+
field.targetModel,
|
|
110
|
+
field.filterParams,
|
|
111
|
+
field.displayField,
|
|
112
|
+
);
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
} catch (error) {
|