@tunghtml/strapi-plugin-filtered-relation 1.0.0 → 1.1.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 +53 -0
- package/PUBLISH.md +94 -0
- package/README.md +63 -65
- package/admin/src/components/FilteredRelationInput.jsx +339 -121
- package/package.json +3 -17
- package/server/register.js +2 -2
- package/strapi-admin.js +27 -1
- package/strapi-server.js +0 -6
- package/server/controllers/content-types.js +0 -26
- package/server/controllers/index.js +0 -5
- package/server/routes/index.js +0 -11
- package/server/services/index.js +0 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.1.0] - 2024-12-02
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Status Change Dropdown**: Added dropdown at the end of each item to change status
|
|
8
|
+
- Dropdown shows current status and all available enum options
|
|
9
|
+
- Options are dynamically fetched from target model schema (tries multiple endpoints)
|
|
10
|
+
- Clicking a different status moves the item between records using connect/disconnect API
|
|
11
|
+
- Dispatches custom events to refresh all filtered relation fields without page reload
|
|
12
|
+
- **New Configuration Option**: `statusField`
|
|
13
|
+
- Optional field to enable status change feature
|
|
14
|
+
- When configured, dropdown appears for each item
|
|
15
|
+
- Must be an enum field in target model
|
|
16
|
+
- Example: `"statusField": "participantStatus"`
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- Updated UI to include status dropdown alongside item label
|
|
21
|
+
- Improved item box layout to accommodate dropdown (space-between)
|
|
22
|
+
- Enhanced click handling to prevent navigation when interacting with dropdown
|
|
23
|
+
- Uses proper Strapi 5 connect/disconnect API format with both id and documentId
|
|
24
|
+
- Sends full payload matching UI admin format for reliable updates
|
|
25
|
+
|
|
26
|
+
### Technical Details
|
|
27
|
+
|
|
28
|
+
- Fetches enum values from schema via content-type-builder or content-manager endpoints
|
|
29
|
+
- Uses `SingleSelect` component from Strapi Design System
|
|
30
|
+
- Implements loading state during status update
|
|
31
|
+
- Uses connect/disconnect API with format: `{ id, documentId }`
|
|
32
|
+
- Dispatches `filtered-relation-updated` custom event for cross-field refresh
|
|
33
|
+
- All fields listen to event and refresh when same documentId
|
|
34
|
+
- Maintains backward compatibility with version 1.0.0
|
|
35
|
+
|
|
36
|
+
### Use Case
|
|
37
|
+
|
|
38
|
+
Perfect for scenarios like Meeting Participation Status where:
|
|
39
|
+
|
|
40
|
+
- Items are filtered by status (Pending, Accepted, Rejected, etc.)
|
|
41
|
+
- Users need to quickly change status without navigating to detail page
|
|
42
|
+
- Status change should automatically move items between lists
|
|
43
|
+
|
|
44
|
+
## [1.0.0] - 2024-11-XX
|
|
45
|
+
|
|
46
|
+
### Initial Release
|
|
47
|
+
|
|
48
|
+
- Display filtered relations based on field values
|
|
49
|
+
- Support for dynamic placeholders (`{{documentId}}`)
|
|
50
|
+
- Configurable filter fields (filterField1, filterField2)
|
|
51
|
+
- Auto-save functionality
|
|
52
|
+
- Click to navigate to related record
|
|
53
|
+
- Display count in field label
|
package/PUBLISH.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# Publishing Guide
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
1. Make sure you're logged in to npm:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm login
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
2. Verify your npm account has access to publish `@tunghtml` scoped packages
|
|
12
|
+
|
|
13
|
+
## Steps to Publish
|
|
14
|
+
|
|
15
|
+
### 1. Test Locally First
|
|
16
|
+
|
|
17
|
+
The plugin is already configured as a local plugin in `backend/config/plugins.js`:
|
|
18
|
+
|
|
19
|
+
```javascript
|
|
20
|
+
"filtered-relation": {
|
|
21
|
+
enabled: true,
|
|
22
|
+
resolve: "./src/plugins/filtered-relation",
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Test thoroughly in your local Strapi instance before publishing.
|
|
27
|
+
|
|
28
|
+
### 2. Update Version
|
|
29
|
+
|
|
30
|
+
Update version in `package.json`:
|
|
31
|
+
|
|
32
|
+
- Patch: 1.1.0 -> 1.1.1 (bug fixes)
|
|
33
|
+
- Minor: 1.1.0 -> 1.2.0 (new features, backward compatible)
|
|
34
|
+
- Major: 1.1.0 -> 2.0.0 (breaking changes)
|
|
35
|
+
|
|
36
|
+
### 3. Publish to NPM
|
|
37
|
+
|
|
38
|
+
From the plugin directory:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
cd backend/src/plugins/filtered-relation
|
|
42
|
+
npm publish --access public
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 4. Update Project to Use NPM Package
|
|
46
|
+
|
|
47
|
+
After publishing, you can switch back to using the npm package:
|
|
48
|
+
|
|
49
|
+
1. Update `backend/package.json`:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@tunghtml/strapi-plugin-filtered-relation": "^1.1.0"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
2. Update `backend/config/plugins.js`:
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
"filtered-relation": {
|
|
63
|
+
enabled: true,
|
|
64
|
+
// Remove the resolve line to use npm package
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
3. Install the package:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
cd backend
|
|
72
|
+
yarn install
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Current Version
|
|
76
|
+
|
|
77
|
+
Version 1.1.0 includes:
|
|
78
|
+
|
|
79
|
+
- Status change dropdown feature
|
|
80
|
+
- Dynamic status options from schema
|
|
81
|
+
- Auto-refresh on status change
|
|
82
|
+
- Backward compatible with 1.0.0
|
|
83
|
+
|
|
84
|
+
## Testing Checklist
|
|
85
|
+
|
|
86
|
+
Before publishing, verify:
|
|
87
|
+
|
|
88
|
+
- [ ] Plugin loads without errors
|
|
89
|
+
- [ ] Filtered relations display correctly
|
|
90
|
+
- [ ] Status dropdown shows correct options
|
|
91
|
+
- [ ] Status change updates the record
|
|
92
|
+
- [ ] Lists refresh automatically after status change
|
|
93
|
+
- [ ] Works with different content types (reusable)
|
|
94
|
+
- [ ] No console errors
|
package/README.md
CHANGED
|
@@ -1,113 +1,111 @@
|
|
|
1
1
|
# Strapi Plugin Filtered Relation
|
|
2
2
|
|
|
3
|
-
A Strapi
|
|
3
|
+
A Strapi 5 custom field plugin for displaying filtered relations with dynamic placeholder support and status change capability.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
7
|
+
- Display filtered relations based on field values
|
|
8
|
+
- Support dynamic placeholders (e.g., `{{documentId}}`)
|
|
9
|
+
- Change status directly from the relation list via dropdown
|
|
10
|
+
- Auto-refresh when status changes
|
|
11
|
+
- Fully configurable and reusable
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
npm install @tunghtml/strapi-plugin-filtered-relation
|
|
17
|
-
```
|
|
15
|
+
### Local Development
|
|
18
16
|
|
|
19
|
-
|
|
17
|
+
This plugin is currently being used as a local plugin. It's located in `backend/src/plugins/filtered-relation`.
|
|
18
|
+
|
|
19
|
+
### NPM Package (for publishing)
|
|
20
20
|
|
|
21
21
|
```bash
|
|
22
|
+
npm install @tunghtml/strapi-plugin-filtered-relation
|
|
23
|
+
# or
|
|
22
24
|
yarn add @tunghtml/strapi-plugin-filtered-relation
|
|
23
25
|
```
|
|
24
26
|
|
|
25
|
-
##
|
|
26
|
-
|
|
27
|
-
### 1. Enable the plugin
|
|
27
|
+
## Configuration
|
|
28
28
|
|
|
29
|
-
Add
|
|
29
|
+
Add to `config/plugins.js`:
|
|
30
30
|
|
|
31
31
|
```javascript
|
|
32
32
|
module.exports = {
|
|
33
33
|
"filtered-relation": {
|
|
34
34
|
enabled: true,
|
|
35
|
+
resolve: "./src/plugins/filtered-relation", // for local plugin
|
|
35
36
|
},
|
|
36
37
|
};
|
|
37
38
|
```
|
|
38
39
|
|
|
39
|
-
|
|
40
|
+
## Usage
|
|
40
41
|
|
|
41
|
-
|
|
42
|
+
### Schema Configuration
|
|
43
|
+
|
|
44
|
+
Add the custom field to your content type schema:
|
|
42
45
|
|
|
43
46
|
```json
|
|
44
47
|
{
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
48
|
+
"pendingInvestors": {
|
|
49
|
+
"type": "customField",
|
|
50
|
+
"customField": "plugin::filtered-relation.filtered-relation",
|
|
51
|
+
"options": {
|
|
52
|
+
"displayField": "investors",
|
|
53
|
+
"targetModel": "Meeting Participation Status",
|
|
54
|
+
"filterField1": "participantStatus",
|
|
55
|
+
"filterValue1": "Pending",
|
|
56
|
+
"filterField2": "meeting.documentId",
|
|
57
|
+
"filterValue2": "{{documentId}}",
|
|
58
|
+
"statusField": "participantStatus"
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
```
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
### Options
|
|
63
65
|
|
|
64
|
-
| Option | Type |
|
|
65
|
-
| -------------- | ------ |
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `filterField1` | string | First filter field (
|
|
69
|
-
| `filterValue1` | string |
|
|
70
|
-
| `filterField2` | string | Second filter field (
|
|
71
|
-
| `filterValue2` | string |
|
|
66
|
+
| Option | Type | Required | Description |
|
|
67
|
+
| -------------- | ------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
68
|
+
| `displayField` | string | Yes | The relation field name to display from target model (e.g., "investors") |
|
|
69
|
+
| `targetModel` | string | Yes | Target collection display name (e.g., "Meeting Participation Status"). Will auto-convert to API ID format. |
|
|
70
|
+
| `filterField1` | string | Yes | First filter field name (e.g., "participantStatus") |
|
|
71
|
+
| `filterValue1` | string | Yes | First filter value (e.g., "Pending"). Supports `{{documentId}}` placeholder for current entity. |
|
|
72
|
+
| `filterField2` | string | No | Second filter field name (e.g., "meeting.documentId") for additional filtering |
|
|
73
|
+
| `filterValue2` | string | No | Second filter value. Supports `{{documentId}}` placeholder. |
|
|
74
|
+
| `statusField` | string | No | Field name for status dropdown (e.g., "participantStatus"). When configured, enables status change feature with dropdown. Must be an enum field in target model. |
|
|
72
75
|
|
|
73
|
-
|
|
76
|
+
### Status Change Feature
|
|
74
77
|
|
|
75
|
-
|
|
78
|
+
When `statusField` is configured, a dropdown appears at the end of each item allowing quick status changes.
|
|
76
79
|
|
|
77
|
-
|
|
78
|
-
{
|
|
79
|
-
"filterField2": "meeting.documentId",
|
|
80
|
-
"filterValue2": "{{documentId}}"
|
|
81
|
-
}
|
|
82
|
-
```
|
|
80
|
+
**Requirements:**
|
|
83
81
|
|
|
84
|
-
|
|
82
|
+
- `statusField` must be an enum field in the target model
|
|
83
|
+
- Target model must have records for each status value
|
|
84
|
+
- All records must be linked to the same parent entity (via filterField2)
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
**How it works:**
|
|
87
|
+
|
|
88
|
+
1. Dropdown shows current status and all available enum options (fetched from schema)
|
|
89
|
+
2. User selects a new status
|
|
90
|
+
3. Plugin automatically:
|
|
91
|
+
- Removes the item from current status record (disconnect)
|
|
92
|
+
- Adds the item to target status record (connect)
|
|
93
|
+
- Refreshes all filtered relation fields on the page
|
|
94
|
+
4. No page reload needed - all lists update automatically via custom events
|
|
87
95
|
|
|
88
|
-
|
|
96
|
+
## Example Use Case
|
|
89
97
|
|
|
90
|
-
|
|
91
|
-
2. **Display Field**: investors (shows investor names)
|
|
92
|
-
3. **Filter 1**: `participantStatus = "Rejected"`
|
|
93
|
-
4. **Filter 2**: `meeting.documentId = {{documentId}}`
|
|
98
|
+
For a Meeting with multiple investor status lists (Pending, Accepted, Rejected, etc.):
|
|
94
99
|
|
|
95
|
-
|
|
100
|
+
1. Each list shows investors filtered by their participation status
|
|
101
|
+
2. Click the dropdown on any investor to change their status
|
|
102
|
+
3. The investor automatically moves to the appropriate list
|
|
103
|
+
4. All lists refresh to reflect the change
|
|
96
104
|
|
|
97
|
-
##
|
|
105
|
+
## Version
|
|
98
106
|
|
|
99
|
-
|
|
100
|
-
- Node.js >= 18.0.0
|
|
101
|
-
- React >= 18.0.0
|
|
107
|
+
Current version: 1.1.0
|
|
102
108
|
|
|
103
109
|
## License
|
|
104
110
|
|
|
105
111
|
MIT
|
|
106
|
-
|
|
107
|
-
## Author
|
|
108
|
-
|
|
109
|
-
Tung HTML
|
|
110
|
-
|
|
111
|
-
## Repository
|
|
112
|
-
|
|
113
|
-
[https://github.com/finnwasabi/strapi-plugin-filtered-relation](https://github.com/finnwasabi/strapi-plugin-filtered-relation)
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { useIntl } from "react-intl";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
Field,
|
|
5
|
+
Flex,
|
|
6
|
+
Box,
|
|
7
|
+
Typography,
|
|
8
|
+
SingleSelect,
|
|
9
|
+
SingleSelectOption,
|
|
10
|
+
} from "@strapi/design-system";
|
|
4
11
|
import { useFetchClient } from "@strapi/strapi/admin";
|
|
5
12
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
6
13
|
|
|
@@ -11,6 +18,8 @@ const FilteredRelationInput = (props) => {
|
|
|
11
18
|
const navigate = useNavigate();
|
|
12
19
|
const [items, setItems] = React.useState([]);
|
|
13
20
|
const [loading, setLoading] = React.useState(false);
|
|
21
|
+
const [statusOptions, setStatusOptions] = React.useState([]);
|
|
22
|
+
const [updatingStatus, setUpdatingStatus] = React.useState({});
|
|
14
23
|
|
|
15
24
|
if (!props) {
|
|
16
25
|
return null;
|
|
@@ -19,12 +28,9 @@ const FilteredRelationInput = (props) => {
|
|
|
19
28
|
const { attribute, name, error, value } = props;
|
|
20
29
|
|
|
21
30
|
// Extract documentId from URL path
|
|
22
|
-
// URL format: /admin/content-manager/collection-types/api::meeting.meeting/bn0n8em49y7vljg5lrfh2aij
|
|
23
31
|
const getDocumentIdFromUrl = () => {
|
|
24
32
|
const pathParts = location.pathname.split("/");
|
|
25
|
-
// Get the last part of the URL which should be the documentId
|
|
26
33
|
const lastPart = pathParts[pathParts.length - 1];
|
|
27
|
-
// Check if it's not 'create' (for new documents)
|
|
28
34
|
return lastPart !== "create" ? lastPart : null;
|
|
29
35
|
};
|
|
30
36
|
|
|
@@ -37,16 +43,13 @@ const FilteredRelationInput = (props) => {
|
|
|
37
43
|
filterField2,
|
|
38
44
|
filterValue2,
|
|
39
45
|
displayField = "id",
|
|
46
|
+
statusField,
|
|
40
47
|
} = attribute?.options || {};
|
|
41
48
|
|
|
42
49
|
// Parse collection name to API ID
|
|
43
|
-
// "Meeting Participation Status" -> "api::meeting-participation-status.meeting-participation-status"
|
|
44
50
|
const parseCollectionName = (name) => {
|
|
45
51
|
if (!name) return null;
|
|
46
|
-
// If already API ID format, return as is
|
|
47
52
|
if (name.startsWith("api::")) return name;
|
|
48
|
-
|
|
49
|
-
// Convert display name to API ID
|
|
50
53
|
const slug = name
|
|
51
54
|
.toLowerCase()
|
|
52
55
|
.replace(/\s+/g, "-")
|
|
@@ -57,10 +60,8 @@ const FilteredRelationInput = (props) => {
|
|
|
57
60
|
const targetModel = parseCollectionName(targetModelInput);
|
|
58
61
|
|
|
59
62
|
// Parse displayField to get relation model
|
|
60
|
-
// displayField is like "investors" -> "api::investor.investor"
|
|
61
63
|
const getRelationModel = (fieldName) => {
|
|
62
64
|
if (!fieldName) return null;
|
|
63
|
-
// Remove 's' at the end for plural -> singular
|
|
64
65
|
const singular = fieldName.endsWith("s")
|
|
65
66
|
? fieldName.slice(0, -1)
|
|
66
67
|
: fieldName;
|
|
@@ -73,19 +74,16 @@ const FilteredRelationInput = (props) => {
|
|
|
73
74
|
// Build filter params with dynamic values
|
|
74
75
|
const filterParams = {};
|
|
75
76
|
if (filterField1 && filterValue1) {
|
|
76
|
-
// Support {{documentId}} placeholder for current entity
|
|
77
77
|
const value1 =
|
|
78
78
|
filterValue1 === "{{documentId}}" ? documentId : filterValue1;
|
|
79
79
|
filterParams[filterField1] = value1;
|
|
80
80
|
}
|
|
81
81
|
if (filterField2 && filterValue2) {
|
|
82
|
-
// Support {{documentId}} placeholder for current entity
|
|
83
82
|
const value2 =
|
|
84
83
|
filterValue2 === "{{documentId}}" ? documentId : filterValue2;
|
|
85
84
|
filterParams[filterField2] = value2;
|
|
86
85
|
}
|
|
87
86
|
|
|
88
|
-
// Get the content type from props
|
|
89
87
|
const contentType = props.contentType || props.model;
|
|
90
88
|
|
|
91
89
|
const saveFieldValue = async (newValue) => {
|
|
@@ -105,128 +103,311 @@ const FilteredRelationInput = (props) => {
|
|
|
105
103
|
}
|
|
106
104
|
};
|
|
107
105
|
|
|
106
|
+
// Fetch status options from target model schema
|
|
108
107
|
React.useEffect(() => {
|
|
109
|
-
const
|
|
110
|
-
if (!targetModel ||
|
|
108
|
+
const fetchStatusOptions = async () => {
|
|
109
|
+
if (!targetModel || !statusField) {
|
|
111
110
|
return;
|
|
112
111
|
}
|
|
113
112
|
|
|
114
|
-
setLoading(true);
|
|
115
113
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
114
|
+
let enumValues = null;
|
|
115
|
+
|
|
116
|
+
// Try 1: Get from content-type-builder (most reliable for schema)
|
|
117
|
+
try {
|
|
118
|
+
const builderResponse = await get(
|
|
119
|
+
`/content-type-builder/content-types/${targetModel}`,
|
|
120
|
+
);
|
|
121
|
+
const builderSchema = builderResponse?.data?.data?.schema;
|
|
122
|
+
enumValues = builderSchema?.attributes?.[statusField]?.enum;
|
|
123
|
+
} catch (e) {
|
|
124
|
+
// Endpoint might not be accessible, try next
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
127
|
+
if (enumValues && Array.isArray(enumValues)) {
|
|
128
|
+
setStatusOptions(enumValues);
|
|
129
|
+
}
|
|
130
|
+
} catch (err) {
|
|
131
|
+
console.error("[FilteredRelation] Error fetching status options:", err);
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
fetchStatusOptions();
|
|
136
|
+
}, [targetModel, statusField]);
|
|
137
|
+
|
|
138
|
+
const handleStatusChange = async (itemId, newStatus, e) => {
|
|
139
|
+
// Prevent navigation when clicking dropdown
|
|
140
|
+
e?.stopPropagation();
|
|
141
|
+
|
|
142
|
+
if (!statusField || !targetModel || !displayField) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const item = items.find((i) => i.originalId === itemId);
|
|
147
|
+
if (!item) {
|
|
148
|
+
console.error("[FilteredRelation] Item not found:", itemId);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
setUpdatingStatus((prev) => ({ ...prev, [itemId]: true }));
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
// Step 1: Get current Meeting Participation Status record with ALL fields
|
|
156
|
+
const currentRecordUrl = `/content-manager/collection-types/${targetModel}/${itemId}?populate=*`;
|
|
157
|
+
const currentRecord = await get(currentRecordUrl);
|
|
158
|
+
const currentData = currentRecord?.data?.data || {};
|
|
159
|
+
const currentInvestors = currentData?.[displayField] || [];
|
|
160
|
+
|
|
161
|
+
// Step 2: Find the investor to disconnect (need full object)
|
|
162
|
+
const investorToRemove = Array.isArray(currentInvestors)
|
|
163
|
+
? currentInvestors.find((inv) => {
|
|
164
|
+
const invDocId = inv?.documentId || inv?.id;
|
|
165
|
+
return invDocId === item.relatedDocumentId;
|
|
132
166
|
})
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
167
|
+
: null;
|
|
168
|
+
|
|
169
|
+
if (!investorToRemove) {
|
|
170
|
+
console.error(
|
|
171
|
+
"[FilteredRelation] Investor not found in current record",
|
|
172
|
+
);
|
|
173
|
+
alert("Investor not found in current record");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Build full payload like UI admin does
|
|
178
|
+
const updatePayload = {
|
|
179
|
+
documentId: currentData.documentId,
|
|
180
|
+
[displayField]: {
|
|
181
|
+
connect: [],
|
|
182
|
+
disconnect: [
|
|
183
|
+
{
|
|
184
|
+
id: investorToRemove.id,
|
|
185
|
+
documentId: investorToRemove.documentId,
|
|
186
|
+
},
|
|
187
|
+
],
|
|
188
|
+
},
|
|
189
|
+
participantStatus: currentData.participantStatus,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Use disconnect with full payload
|
|
193
|
+
const removeResponse = await put(
|
|
194
|
+
`/content-manager/collection-types/${targetModel}/${itemId}`,
|
|
195
|
+
updatePayload,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
// Step 3: Find Meeting Participation Status with new status (same meeting)
|
|
199
|
+
const newFilterParams = {};
|
|
200
|
+
if (filterField1) {
|
|
201
|
+
newFilterParams[filterField1] = newStatus;
|
|
202
|
+
}
|
|
203
|
+
if (filterField2 && filterValue2) {
|
|
204
|
+
const value2 =
|
|
205
|
+
filterValue2 === "{{documentId}}" ? documentId : filterValue2;
|
|
206
|
+
newFilterParams[filterField2] = value2;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const filterQuery = Object.entries(newFilterParams)
|
|
210
|
+
.map(([key, value], index) => {
|
|
211
|
+
if (key.includes(".")) {
|
|
212
|
+
const [relationField, nestedField] = key.split(".");
|
|
213
|
+
return `filters[$and][${index}][${relationField}][${nestedField}][$eq]=${encodeURIComponent(value)}`;
|
|
214
|
+
}
|
|
215
|
+
return `filters[$and][${index}][${key}][$eq]=${encodeURIComponent(value)}`;
|
|
216
|
+
})
|
|
217
|
+
.join("&");
|
|
218
|
+
|
|
219
|
+
const searchUrl = `/content-manager/collection-types/${targetModel}?${filterQuery}&populate[${displayField}]=*`;
|
|
220
|
+
const searchResponse = await get(searchUrl);
|
|
221
|
+
const targetRecords =
|
|
222
|
+
searchResponse?.data?.results ||
|
|
223
|
+
searchResponse?.results ||
|
|
224
|
+
searchResponse?.data ||
|
|
225
|
+
[];
|
|
226
|
+
|
|
227
|
+
let targetRecordId;
|
|
228
|
+
let targetInvestors = [];
|
|
229
|
+
|
|
230
|
+
if (targetRecords.length === 0) {
|
|
231
|
+
console.error(
|
|
232
|
+
"[FilteredRelation] No target record found with new status for this meeting.",
|
|
233
|
+
);
|
|
234
|
+
const collectionDisplayName = targetModelInput || "record";
|
|
235
|
+
alert(
|
|
236
|
+
`No ${collectionDisplayName} found with status "${newStatus}" for this meeting. Please create one first.`,
|
|
237
|
+
);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Get full target record data
|
|
242
|
+
const targetRecord = targetRecords[0];
|
|
243
|
+
targetRecordId = targetRecord.documentId || targetRecord.id;
|
|
244
|
+
|
|
245
|
+
// Get target record with all fields
|
|
246
|
+
const targetRecordUrl = `/content-manager/collection-types/${targetModel}/${targetRecordId}?populate=*`;
|
|
247
|
+
const targetRecordFull = await get(targetRecordUrl);
|
|
248
|
+
const targetData = targetRecordFull?.data?.data || {};
|
|
249
|
+
|
|
250
|
+
// Step 4: Add investor to target record
|
|
251
|
+
// Build full payload like UI admin does
|
|
252
|
+
const addPayload = {
|
|
253
|
+
documentId: targetData.documentId,
|
|
254
|
+
[displayField]: {
|
|
255
|
+
connect: [
|
|
256
|
+
{
|
|
257
|
+
id: investorToRemove.id,
|
|
258
|
+
documentId: investorToRemove.documentId,
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
disconnect: [],
|
|
262
|
+
},
|
|
263
|
+
participantStatus: targetData.participantStatus,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Use connect with full payload
|
|
267
|
+
const addResponse = await put(
|
|
268
|
+
`/content-manager/collection-types/${targetModel}/${targetRecordId}`,
|
|
269
|
+
addPayload,
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// Dispatch custom event to trigger refresh of all filtered relation fields
|
|
273
|
+
window.dispatchEvent(
|
|
274
|
+
new CustomEvent("filtered-relation-updated", {
|
|
275
|
+
detail: { documentId, targetModel },
|
|
276
|
+
}),
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Refresh current field
|
|
280
|
+
await fetchFilteredData();
|
|
281
|
+
} catch (err) {
|
|
282
|
+
console.error("[FilteredRelation] Error moving investor:", err);
|
|
283
|
+
alert("Failed to change status. Check console for details.");
|
|
284
|
+
} finally {
|
|
285
|
+
setUpdatingStatus((prev) => ({ ...prev, [itemId]: false }));
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
const fetchFilteredData = async () => {
|
|
290
|
+
if (!targetModel || Object.keys(filterParams).length === 0) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
setLoading(true);
|
|
295
|
+
try {
|
|
296
|
+
const modelParts = targetModel.split("::");
|
|
297
|
+
if (modelParts.length !== 2) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Build filter query string
|
|
302
|
+
const filterQuery = Object.entries(filterParams)
|
|
303
|
+
.map(([key, value], index) => {
|
|
304
|
+
if (key.includes(".")) {
|
|
305
|
+
const [relationField, nestedField] = key.split(".");
|
|
306
|
+
return `filters[$and][${index}][${relationField}][${nestedField}][$eq]=${encodeURIComponent(value)}`;
|
|
307
|
+
}
|
|
308
|
+
return `filters[$and][${index}][${key}][$eq]=${encodeURIComponent(value)}`;
|
|
309
|
+
})
|
|
310
|
+
.join("&");
|
|
311
|
+
|
|
312
|
+
const populateFields = [displayField];
|
|
313
|
+
if (statusField) {
|
|
314
|
+
populateFields.push(statusField);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const apiUrl = `/content-manager/collection-types/${targetModel}?${filterQuery}&populate[${displayField}][populate]=*`;
|
|
318
|
+
const response = await get(apiUrl);
|
|
319
|
+
const items =
|
|
320
|
+
response?.data?.results || response?.results || response?.data || [];
|
|
321
|
+
|
|
322
|
+
if (items.length > 0) {
|
|
323
|
+
const formattedItems = [];
|
|
324
|
+
|
|
325
|
+
items.forEach((item) => {
|
|
326
|
+
if (displayField && item[displayField]) {
|
|
327
|
+
if (Array.isArray(item[displayField])) {
|
|
328
|
+
item[displayField].forEach((rel) => {
|
|
329
|
+
const displayValue =
|
|
330
|
+
rel.fullName ||
|
|
331
|
+
rel.name ||
|
|
332
|
+
rel.displayName ||
|
|
333
|
+
rel.firstName ||
|
|
334
|
+
`ID: ${rel.documentId || rel.id}`;
|
|
335
|
+
|
|
336
|
+
formattedItems.push({
|
|
337
|
+
id: `${item.documentId || item.id}-${rel.documentId || rel.id}`,
|
|
338
|
+
label: displayValue,
|
|
339
|
+
originalId: item.documentId || item.id,
|
|
340
|
+
relatedDocumentId: rel.documentId || rel.id,
|
|
341
|
+
relationModel: relationModel,
|
|
342
|
+
currentStatus: item[statusField],
|
|
162
343
|
});
|
|
163
|
-
}
|
|
164
|
-
// Handle single relation object
|
|
165
|
-
else if (
|
|
166
|
-
typeof item[displayField] === "object" &&
|
|
167
|
-
item[displayField] !== null
|
|
168
|
-
) {
|
|
169
|
-
if (item[displayField].count !== undefined) {
|
|
170
|
-
formattedItems.push({
|
|
171
|
-
id: item.id,
|
|
172
|
-
label: `${item[displayField].count} items`,
|
|
173
|
-
});
|
|
174
|
-
} else {
|
|
175
|
-
const displayValue =
|
|
176
|
-
item[displayField].fullName ||
|
|
177
|
-
item[displayField].name ||
|
|
178
|
-
item[displayField].displayName ||
|
|
179
|
-
item[displayField].firstName ||
|
|
180
|
-
`ID: ${item[displayField].id}`;
|
|
181
|
-
|
|
182
|
-
formattedItems.push({
|
|
183
|
-
id: item.id,
|
|
184
|
-
label: displayValue,
|
|
185
|
-
originalId: item.documentId || item.id,
|
|
186
|
-
relatedDocumentId:
|
|
187
|
-
item[displayField].documentId || item[displayField].id,
|
|
188
|
-
relationModel: relationModel,
|
|
189
|
-
});
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
} else {
|
|
193
|
-
// Fallback if no displayField
|
|
194
|
-
formattedItems.push({
|
|
195
|
-
id: item.id,
|
|
196
|
-
label: `ID: ${item.id}`,
|
|
197
|
-
originalId: item.documentId || item.id,
|
|
198
|
-
relatedDocumentId: item.documentId || item.id,
|
|
199
|
-
relationModel: targetModel,
|
|
200
344
|
});
|
|
345
|
+
} else if (
|
|
346
|
+
typeof item[displayField] === "object" &&
|
|
347
|
+
item[displayField] !== null
|
|
348
|
+
) {
|
|
349
|
+
if (item[displayField].count !== undefined) {
|
|
350
|
+
formattedItems.push({
|
|
351
|
+
id: item.id,
|
|
352
|
+
label: `${item[displayField].count} items`,
|
|
353
|
+
currentStatus: item[statusField],
|
|
354
|
+
});
|
|
355
|
+
} else {
|
|
356
|
+
const displayValue =
|
|
357
|
+
item[displayField].fullName ||
|
|
358
|
+
item[displayField].name ||
|
|
359
|
+
item[displayField].displayName ||
|
|
360
|
+
item[displayField].firstName ||
|
|
361
|
+
`ID: ${item[displayField].id}`;
|
|
362
|
+
|
|
363
|
+
formattedItems.push({
|
|
364
|
+
id: item.id,
|
|
365
|
+
label: displayValue,
|
|
366
|
+
originalId: item.documentId || item.id,
|
|
367
|
+
relatedDocumentId:
|
|
368
|
+
item[displayField].documentId || item[displayField].id,
|
|
369
|
+
relationModel: relationModel,
|
|
370
|
+
currentStatus: item[statusField],
|
|
371
|
+
});
|
|
372
|
+
}
|
|
201
373
|
}
|
|
202
|
-
}
|
|
374
|
+
} else {
|
|
375
|
+
formattedItems.push({
|
|
376
|
+
id: item.id,
|
|
377
|
+
label: `ID: ${item.id}`,
|
|
378
|
+
originalId: item.documentId || item.id,
|
|
379
|
+
relatedDocumentId: item.documentId || item.id,
|
|
380
|
+
relationModel: targetModel,
|
|
381
|
+
currentStatus: item[statusField],
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
});
|
|
203
385
|
|
|
204
|
-
|
|
386
|
+
setItems(formattedItems);
|
|
205
387
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
388
|
+
// Auto-save to database
|
|
389
|
+
const documentIds = formattedItems.map(
|
|
390
|
+
(item) => item.relatedDocumentId || item.id,
|
|
391
|
+
);
|
|
392
|
+
const newValue = JSON.stringify(documentIds);
|
|
211
393
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (documentId && value) {
|
|
220
|
-
await saveFieldValue(null);
|
|
221
|
-
}
|
|
394
|
+
if (documentId && value !== newValue) {
|
|
395
|
+
await saveFieldValue(newValue);
|
|
396
|
+
}
|
|
397
|
+
} else {
|
|
398
|
+
setItems([]);
|
|
399
|
+
if (documentId && value) {
|
|
400
|
+
await saveFieldValue(null);
|
|
222
401
|
}
|
|
223
|
-
} catch (err) {
|
|
224
|
-
console.error("Error fetching filtered relations:", err);
|
|
225
|
-
} finally {
|
|
226
|
-
setLoading(false);
|
|
227
402
|
}
|
|
228
|
-
}
|
|
403
|
+
} catch (err) {
|
|
404
|
+
console.error("Error fetching filtered relations:", err);
|
|
405
|
+
} finally {
|
|
406
|
+
setLoading(false);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
229
409
|
|
|
410
|
+
React.useEffect(() => {
|
|
230
411
|
fetchFilteredData();
|
|
231
412
|
}, [
|
|
232
413
|
targetModel,
|
|
@@ -239,6 +420,21 @@ const FilteredRelationInput = (props) => {
|
|
|
239
420
|
value,
|
|
240
421
|
]);
|
|
241
422
|
|
|
423
|
+
// Listen for updates from other filtered relation fields
|
|
424
|
+
React.useEffect(() => {
|
|
425
|
+
const handleUpdate = (event) => {
|
|
426
|
+
// Refresh if same document
|
|
427
|
+
if (event.detail?.documentId === documentId) {
|
|
428
|
+
fetchFilteredData();
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
window.addEventListener("filtered-relation-updated", handleUpdate);
|
|
433
|
+
return () => {
|
|
434
|
+
window.removeEventListener("filtered-relation-updated", handleUpdate);
|
|
435
|
+
};
|
|
436
|
+
}, [documentId]);
|
|
437
|
+
|
|
242
438
|
const fieldLabel = props.label || name;
|
|
243
439
|
const itemCount = items.length;
|
|
244
440
|
|
|
@@ -258,8 +454,8 @@ const FilteredRelationInput = (props) => {
|
|
|
258
454
|
) : items.length > 0 ? (
|
|
259
455
|
<Flex direction="column" alignItems="stretch" gap={1}>
|
|
260
456
|
{items.map((item) => {
|
|
261
|
-
// Use relation model (e.g., api::investor.investor) with relatedDocumentId
|
|
262
457
|
const editUrl = `/content-manager/collection-types/${item.relationModel}/${item.relatedDocumentId}`;
|
|
458
|
+
const isUpdating = updatingStatus[item.originalId];
|
|
263
459
|
|
|
264
460
|
return (
|
|
265
461
|
<Box
|
|
@@ -275,15 +471,38 @@ const FilteredRelationInput = (props) => {
|
|
|
275
471
|
paddingRight={4}
|
|
276
472
|
style={{
|
|
277
473
|
cursor: "pointer",
|
|
278
|
-
|
|
474
|
+
minHeight: "50px",
|
|
279
475
|
display: "flex",
|
|
280
476
|
alignItems: "center",
|
|
477
|
+
justifyContent: "space-between",
|
|
281
478
|
}}
|
|
282
479
|
onClick={() => navigate(editUrl)}
|
|
283
480
|
>
|
|
284
481
|
<Typography variant="omega" textColor="primary600">
|
|
285
482
|
{item.label}
|
|
286
483
|
</Typography>
|
|
484
|
+
|
|
485
|
+
{statusField && statusOptions.length > 0 && (
|
|
486
|
+
<Box
|
|
487
|
+
onClick={(e) => e.stopPropagation()}
|
|
488
|
+
style={{ minWidth: "150px" }}
|
|
489
|
+
>
|
|
490
|
+
<SingleSelect
|
|
491
|
+
size="S"
|
|
492
|
+
value={item.currentStatus}
|
|
493
|
+
onChange={(value) =>
|
|
494
|
+
handleStatusChange(item.originalId, value, null)
|
|
495
|
+
}
|
|
496
|
+
disabled={isUpdating}
|
|
497
|
+
>
|
|
498
|
+
{statusOptions.map((status) => (
|
|
499
|
+
<SingleSelectOption key={status} value={status}>
|
|
500
|
+
{status}
|
|
501
|
+
</SingleSelectOption>
|
|
502
|
+
))}
|
|
503
|
+
</SingleSelect>
|
|
504
|
+
</Box>
|
|
505
|
+
)}
|
|
287
506
|
</Box>
|
|
288
507
|
);
|
|
289
508
|
})}
|
|
@@ -301,7 +520,6 @@ const FilteredRelationInput = (props) => {
|
|
|
301
520
|
{Object.keys(filterParams).length > 0
|
|
302
521
|
? `Filters: ${Object.entries(filterParams)
|
|
303
522
|
.map(([k, v]) => {
|
|
304
|
-
// Show placeholder name if it's a dynamic value
|
|
305
523
|
const displayValue =
|
|
306
524
|
v === documentId ? "{{current documentId}}" : v;
|
|
307
525
|
return `${k} = ${displayValue}`;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tunghtml/strapi-plugin-filtered-relation",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "A Strapi custom field plugin for displaying filtered relations with dynamic placeholder support",
|
|
3
|
+
"version": "1.1.0",
|
|
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",
|
|
7
7
|
"strapi-plugin",
|
|
@@ -10,18 +10,10 @@
|
|
|
10
10
|
"filter",
|
|
11
11
|
"strapi5"
|
|
12
12
|
],
|
|
13
|
-
"homepage": "https://github.com/finnwasabi/strapi-plugin-filtered-relation#readme",
|
|
14
|
-
"bugs": {
|
|
15
|
-
"url": "https://github.com/finnwasabi/strapi-plugin-filtered-relation/issues"
|
|
16
|
-
},
|
|
17
|
-
"repository": {
|
|
18
|
-
"type": "git",
|
|
19
|
-
"url": "git+https://github.com/finnwasabi/strapi-plugin-filtered-relation.git"
|
|
20
|
-
},
|
|
21
13
|
"strapi": {
|
|
22
14
|
"name": "filtered-relation",
|
|
23
15
|
"displayName": "Filtered Relation",
|
|
24
|
-
"description": "Relation field with filtering capability and
|
|
16
|
+
"description": "Relation field with filtering capability, dynamic placeholders, and status change",
|
|
25
17
|
"kind": "plugin"
|
|
26
18
|
},
|
|
27
19
|
"dependencies": {
|
|
@@ -39,12 +31,6 @@
|
|
|
39
31
|
"name": "Tung HTML",
|
|
40
32
|
"email": "tunghtml@example.com"
|
|
41
33
|
},
|
|
42
|
-
"maintainers": [
|
|
43
|
-
{
|
|
44
|
-
"name": "Tung HTML",
|
|
45
|
-
"email": "tunghtml@example.com"
|
|
46
|
-
}
|
|
47
|
-
],
|
|
48
34
|
"engines": {
|
|
49
35
|
"node": ">=18.0.0 <=22.x.x",
|
|
50
36
|
"npm": ">=6.0.0"
|
package/server/register.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
1
3
|
module.exports = ({ strapi }) => {
|
|
2
4
|
strapi.customFields.register({
|
|
3
5
|
name: "filtered-relation",
|
|
@@ -69,8 +71,6 @@ async function updateRelatedFilteredFields(event, strapi) {
|
|
|
69
71
|
const targetModel = parseCollectionName(targetModelInput);
|
|
70
72
|
|
|
71
73
|
// Build filter params
|
|
72
|
-
// Note: {{documentId}} placeholder cannot be resolved in lifecycle hooks
|
|
73
|
-
// as we don't have context of which entity is being viewed
|
|
74
74
|
const filterParams = {};
|
|
75
75
|
if (
|
|
76
76
|
filterField1 &&
|
package/strapi-admin.js
CHANGED
|
@@ -36,7 +36,8 @@ export default {
|
|
|
36
36
|
},
|
|
37
37
|
intlDescription: {
|
|
38
38
|
id: "filtered-relation.description",
|
|
39
|
-
defaultMessage:
|
|
39
|
+
defaultMessage:
|
|
40
|
+
"Relation field with filtering by field value and status change",
|
|
40
41
|
},
|
|
41
42
|
components: {
|
|
42
43
|
Input: async () => {
|
|
@@ -153,6 +154,31 @@ export default {
|
|
|
153
154
|
},
|
|
154
155
|
],
|
|
155
156
|
},
|
|
157
|
+
{
|
|
158
|
+
sectionTitle: {
|
|
159
|
+
id: "filtered-relation.section.statusChange",
|
|
160
|
+
defaultMessage: "Status Change Configuration",
|
|
161
|
+
},
|
|
162
|
+
items: [
|
|
163
|
+
{
|
|
164
|
+
name: "options.statusField",
|
|
165
|
+
type: "text",
|
|
166
|
+
intlLabel: {
|
|
167
|
+
id: "filtered-relation.statusField.label",
|
|
168
|
+
defaultMessage: "Status Field Name",
|
|
169
|
+
},
|
|
170
|
+
description: {
|
|
171
|
+
id: "filtered-relation.statusField.description",
|
|
172
|
+
defaultMessage:
|
|
173
|
+
"Field name in target model that contains status (e.g., 'participantStatus'). Leave empty to disable status change.",
|
|
174
|
+
},
|
|
175
|
+
placeholder: {
|
|
176
|
+
id: "filtered-relation.statusField.placeholder",
|
|
177
|
+
defaultMessage: "participantStatus",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
},
|
|
156
182
|
],
|
|
157
183
|
},
|
|
158
184
|
});
|
package/strapi-server.js
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
const register = require("./server/register");
|
|
2
|
-
const controllers = require("./server/controllers");
|
|
3
|
-
const routes = require("./server/routes");
|
|
4
|
-
const services = require("./server/services");
|
|
5
2
|
|
|
6
3
|
module.exports = {
|
|
7
4
|
register,
|
|
8
|
-
controllers,
|
|
9
|
-
routes,
|
|
10
|
-
services,
|
|
11
5
|
};
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
module.exports = {
|
|
2
|
-
async find(ctx) {
|
|
3
|
-
try {
|
|
4
|
-
const contentTypes = strapi.contentTypes;
|
|
5
|
-
const apiTypes = [];
|
|
6
|
-
|
|
7
|
-
for (const [uid, contentType] of Object.entries(contentTypes)) {
|
|
8
|
-
if (uid.startsWith("api::")) {
|
|
9
|
-
apiTypes.push({
|
|
10
|
-
uid,
|
|
11
|
-
displayName:
|
|
12
|
-
contentType.info?.displayName ||
|
|
13
|
-
contentType.schema?.displayName ||
|
|
14
|
-
uid.split("::")[1],
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
apiTypes.sort((a, b) => a.displayName.localeCompare(b.displayName));
|
|
20
|
-
|
|
21
|
-
ctx.body = { data: apiTypes };
|
|
22
|
-
} catch (error) {
|
|
23
|
-
ctx.throw(500, error);
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
};
|
package/server/routes/index.js
DELETED
package/server/services/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
module.exports = {};
|