@tunghtml/strapi-plugin-filtered-relation 1.0.0 → 1.1.1
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 +72 -0
- package/PUBLISH.md +94 -0
- package/README.md +63 -65
- package/admin/src/components/FilteredRelationInput.jsx +355 -123
- package/package.json +3 -17
- package/push-to-github.sh +52 -0
- 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,72 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.1.1] - 2024-12-02
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **State Refresh Issue**: Fixed "Investor not found in current record" error that occurred after status changes
|
|
8
|
+
- Now refreshes local state immediately after disconnect/connect operations
|
|
9
|
+
- Prevents stale data issues when performing multiple status changes without page reload
|
|
10
|
+
- Reordered refresh logic: local field first, then dispatch event to other fields
|
|
11
|
+
- **User Experience**: Replaced browser `alert()` with Strapi's native notification system
|
|
12
|
+
- Success notifications for successful status changes
|
|
13
|
+
- Warning notifications for missing records or data issues
|
|
14
|
+
- Danger notifications for errors
|
|
15
|
+
- All notifications use `useNotification` hook from `@strapi/strapi/admin`
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Improved notification messages to be more user-friendly
|
|
20
|
+
- Enhanced error handling with proper notification types
|
|
21
|
+
|
|
22
|
+
## [1.1.0] - 2024-12-02
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- **Status Change Dropdown**: Added dropdown at the end of each item to change status
|
|
27
|
+
- Dropdown shows current status and all available enum options
|
|
28
|
+
- Options are dynamically fetched from target model schema (tries multiple endpoints)
|
|
29
|
+
- Clicking a different status moves the item between records using connect/disconnect API
|
|
30
|
+
- Dispatches custom events to refresh all filtered relation fields without page reload
|
|
31
|
+
- **New Configuration Option**: `statusField`
|
|
32
|
+
- Optional field to enable status change feature
|
|
33
|
+
- When configured, dropdown appears for each item
|
|
34
|
+
- Must be an enum field in target model
|
|
35
|
+
- Example: `"statusField": "participantStatus"`
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- Updated UI to include status dropdown alongside item label
|
|
40
|
+
- Improved item box layout to accommodate dropdown (space-between)
|
|
41
|
+
- Enhanced click handling to prevent navigation when interacting with dropdown
|
|
42
|
+
- Uses proper Strapi 5 connect/disconnect API format with both id and documentId
|
|
43
|
+
- Sends full payload matching UI admin format for reliable updates
|
|
44
|
+
|
|
45
|
+
### Technical Details
|
|
46
|
+
|
|
47
|
+
- Fetches enum values from schema via content-type-builder or content-manager endpoints
|
|
48
|
+
- Uses `SingleSelect` component from Strapi Design System
|
|
49
|
+
- Implements loading state during status update
|
|
50
|
+
- Uses connect/disconnect API with format: `{ id, documentId }`
|
|
51
|
+
- Dispatches `filtered-relation-updated` custom event for cross-field refresh
|
|
52
|
+
- All fields listen to event and refresh when same documentId
|
|
53
|
+
- Maintains backward compatibility with version 1.0.0
|
|
54
|
+
|
|
55
|
+
### Use Case
|
|
56
|
+
|
|
57
|
+
Perfect for scenarios like Meeting Participation Status where:
|
|
58
|
+
|
|
59
|
+
- Items are filtered by status (Pending, Accepted, Rejected, etc.)
|
|
60
|
+
- Users need to quickly change status without navigating to detail page
|
|
61
|
+
- Status change should automatically move items between lists
|
|
62
|
+
|
|
63
|
+
## [1.0.0] - 2024-11-XX
|
|
64
|
+
|
|
65
|
+
### Initial Release
|
|
66
|
+
|
|
67
|
+
- Display filtered relations based on field values
|
|
68
|
+
- Support for dynamic placeholders (`{{documentId}}`)
|
|
69
|
+
- Configurable filter fields (filterField1, filterField2)
|
|
70
|
+
- Auto-save functionality
|
|
71
|
+
- Click to navigate to related record
|
|
72
|
+
- 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,16 +1,26 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { useIntl } from "react-intl";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
Field,
|
|
5
|
+
Flex,
|
|
6
|
+
Box,
|
|
7
|
+
Typography,
|
|
8
|
+
SingleSelect,
|
|
9
|
+
SingleSelectOption,
|
|
10
|
+
} from "@strapi/design-system";
|
|
11
|
+
import { useFetchClient, useNotification } from "@strapi/strapi/admin";
|
|
5
12
|
import { useLocation, useNavigate } from "react-router-dom";
|
|
6
13
|
|
|
7
14
|
const FilteredRelationInput = (props) => {
|
|
8
15
|
const { formatMessage } = useIntl();
|
|
9
16
|
const { get, put } = useFetchClient();
|
|
17
|
+
const { toggleNotification } = useNotification();
|
|
10
18
|
const location = useLocation();
|
|
11
19
|
const navigate = useNavigate();
|
|
12
20
|
const [items, setItems] = React.useState([]);
|
|
13
21
|
const [loading, setLoading] = React.useState(false);
|
|
22
|
+
const [statusOptions, setStatusOptions] = React.useState([]);
|
|
23
|
+
const [updatingStatus, setUpdatingStatus] = React.useState({});
|
|
14
24
|
|
|
15
25
|
if (!props) {
|
|
16
26
|
return null;
|
|
@@ -19,12 +29,9 @@ const FilteredRelationInput = (props) => {
|
|
|
19
29
|
const { attribute, name, error, value } = props;
|
|
20
30
|
|
|
21
31
|
// Extract documentId from URL path
|
|
22
|
-
// URL format: /admin/content-manager/collection-types/api::meeting.meeting/bn0n8em49y7vljg5lrfh2aij
|
|
23
32
|
const getDocumentIdFromUrl = () => {
|
|
24
33
|
const pathParts = location.pathname.split("/");
|
|
25
|
-
// Get the last part of the URL which should be the documentId
|
|
26
34
|
const lastPart = pathParts[pathParts.length - 1];
|
|
27
|
-
// Check if it's not 'create' (for new documents)
|
|
28
35
|
return lastPart !== "create" ? lastPart : null;
|
|
29
36
|
};
|
|
30
37
|
|
|
@@ -37,16 +44,13 @@ const FilteredRelationInput = (props) => {
|
|
|
37
44
|
filterField2,
|
|
38
45
|
filterValue2,
|
|
39
46
|
displayField = "id",
|
|
47
|
+
statusField,
|
|
40
48
|
} = attribute?.options || {};
|
|
41
49
|
|
|
42
50
|
// Parse collection name to API ID
|
|
43
|
-
// "Meeting Participation Status" -> "api::meeting-participation-status.meeting-participation-status"
|
|
44
51
|
const parseCollectionName = (name) => {
|
|
45
52
|
if (!name) return null;
|
|
46
|
-
// If already API ID format, return as is
|
|
47
53
|
if (name.startsWith("api::")) return name;
|
|
48
|
-
|
|
49
|
-
// Convert display name to API ID
|
|
50
54
|
const slug = name
|
|
51
55
|
.toLowerCase()
|
|
52
56
|
.replace(/\s+/g, "-")
|
|
@@ -57,10 +61,8 @@ const FilteredRelationInput = (props) => {
|
|
|
57
61
|
const targetModel = parseCollectionName(targetModelInput);
|
|
58
62
|
|
|
59
63
|
// Parse displayField to get relation model
|
|
60
|
-
// displayField is like "investors" -> "api::investor.investor"
|
|
61
64
|
const getRelationModel = (fieldName) => {
|
|
62
65
|
if (!fieldName) return null;
|
|
63
|
-
// Remove 's' at the end for plural -> singular
|
|
64
66
|
const singular = fieldName.endsWith("s")
|
|
65
67
|
? fieldName.slice(0, -1)
|
|
66
68
|
: fieldName;
|
|
@@ -73,19 +75,16 @@ const FilteredRelationInput = (props) => {
|
|
|
73
75
|
// Build filter params with dynamic values
|
|
74
76
|
const filterParams = {};
|
|
75
77
|
if (filterField1 && filterValue1) {
|
|
76
|
-
// Support {{documentId}} placeholder for current entity
|
|
77
78
|
const value1 =
|
|
78
79
|
filterValue1 === "{{documentId}}" ? documentId : filterValue1;
|
|
79
80
|
filterParams[filterField1] = value1;
|
|
80
81
|
}
|
|
81
82
|
if (filterField2 && filterValue2) {
|
|
82
|
-
// Support {{documentId}} placeholder for current entity
|
|
83
83
|
const value2 =
|
|
84
84
|
filterValue2 === "{{documentId}}" ? documentId : filterValue2;
|
|
85
85
|
filterParams[filterField2] = value2;
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
// Get the content type from props
|
|
89
88
|
const contentType = props.contentType || props.model;
|
|
90
89
|
|
|
91
90
|
const saveFieldValue = async (newValue) => {
|
|
@@ -98,135 +97,331 @@ const FilteredRelationInput = (props) => {
|
|
|
98
97
|
data: {
|
|
99
98
|
[name]: newValue,
|
|
100
99
|
},
|
|
101
|
-
}
|
|
100
|
+
}
|
|
102
101
|
);
|
|
103
102
|
} catch (err) {
|
|
104
103
|
console.error("Error auto-saving filtered relation:", err);
|
|
105
104
|
}
|
|
106
105
|
};
|
|
107
106
|
|
|
107
|
+
// Fetch status options from target model schema
|
|
108
108
|
React.useEffect(() => {
|
|
109
|
-
const
|
|
110
|
-
if (!targetModel ||
|
|
109
|
+
const fetchStatusOptions = async () => {
|
|
110
|
+
if (!targetModel || !statusField) {
|
|
111
111
|
return;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
-
setLoading(true);
|
|
115
114
|
try {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
115
|
+
let enumValues = null;
|
|
116
|
+
|
|
117
|
+
// Try 1: Get from content-type-builder (most reliable for schema)
|
|
118
|
+
try {
|
|
119
|
+
const builderResponse = await get(
|
|
120
|
+
`/content-type-builder/content-types/${targetModel}`
|
|
121
|
+
);
|
|
122
|
+
const builderSchema = builderResponse?.data?.data?.schema;
|
|
123
|
+
enumValues = builderSchema?.attributes?.[statusField]?.enum;
|
|
124
|
+
} catch (e) {
|
|
125
|
+
// Endpoint might not be accessible, try next
|
|
119
126
|
}
|
|
120
127
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
if (enumValues && Array.isArray(enumValues)) {
|
|
129
|
+
setStatusOptions(enumValues);
|
|
130
|
+
}
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error("[FilteredRelation] Error fetching status options:", err);
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
fetchStatusOptions();
|
|
137
|
+
}, [targetModel, statusField]);
|
|
138
|
+
|
|
139
|
+
const handleStatusChange = async (itemId, newStatus, e) => {
|
|
140
|
+
// Prevent navigation when clicking dropdown
|
|
141
|
+
e?.stopPropagation();
|
|
142
|
+
|
|
143
|
+
if (!statusField || !targetModel || !displayField) {
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const item = items.find((i) => i.originalId === itemId);
|
|
148
|
+
if (!item) {
|
|
149
|
+
console.error("[FilteredRelation] Item not found:", itemId);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
setUpdatingStatus((prev) => ({ ...prev, [itemId]: true }));
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
// Step 1: Get current Meeting Participation Status record with ALL fields
|
|
157
|
+
const currentRecordUrl = `/content-manager/collection-types/${targetModel}/${itemId}?populate=*`;
|
|
158
|
+
const currentRecord = await get(currentRecordUrl);
|
|
159
|
+
const currentData = currentRecord?.data?.data || {};
|
|
160
|
+
const currentInvestors = currentData?.[displayField] || [];
|
|
161
|
+
|
|
162
|
+
// Step 2: Find the investor to disconnect (need full object)
|
|
163
|
+
const investorToRemove = Array.isArray(currentInvestors)
|
|
164
|
+
? currentInvestors.find((inv) => {
|
|
165
|
+
const invDocId = inv?.documentId || inv?.id;
|
|
166
|
+
return invDocId === item.relatedDocumentId;
|
|
132
167
|
})
|
|
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
|
-
|
|
168
|
+
: null;
|
|
169
|
+
|
|
170
|
+
if (!investorToRemove) {
|
|
171
|
+
console.error(
|
|
172
|
+
"[FilteredRelation] Investor not found in current record"
|
|
173
|
+
);
|
|
174
|
+
toggleNotification({
|
|
175
|
+
type: "warning",
|
|
176
|
+
message:
|
|
177
|
+
"Investor not found in current record. Please refresh the page.",
|
|
178
|
+
});
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Build full payload like UI admin does
|
|
183
|
+
const updatePayload = {
|
|
184
|
+
documentId: currentData.documentId,
|
|
185
|
+
[displayField]: {
|
|
186
|
+
connect: [],
|
|
187
|
+
disconnect: [
|
|
188
|
+
{
|
|
189
|
+
id: investorToRemove.id,
|
|
190
|
+
documentId: investorToRemove.documentId,
|
|
191
|
+
},
|
|
192
|
+
],
|
|
193
|
+
},
|
|
194
|
+
participantStatus: currentData.participantStatus,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
// Use disconnect with full payload
|
|
198
|
+
const removeResponse = await put(
|
|
199
|
+
`/content-manager/collection-types/${targetModel}/${itemId}`,
|
|
200
|
+
updatePayload
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Step 3: Find Meeting Participation Status with new status (same meeting)
|
|
204
|
+
const newFilterParams = {};
|
|
205
|
+
if (filterField1) {
|
|
206
|
+
newFilterParams[filterField1] = newStatus;
|
|
207
|
+
}
|
|
208
|
+
if (filterField2 && filterValue2) {
|
|
209
|
+
const value2 =
|
|
210
|
+
filterValue2 === "{{documentId}}" ? documentId : filterValue2;
|
|
211
|
+
newFilterParams[filterField2] = value2;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const filterQuery = Object.entries(newFilterParams)
|
|
215
|
+
.map(([key, value], index) => {
|
|
216
|
+
if (key.includes(".")) {
|
|
217
|
+
const [relationField, nestedField] = key.split(".");
|
|
218
|
+
return `filters[$and][${index}][${relationField}][${nestedField}][$eq]=${encodeURIComponent(value)}`;
|
|
219
|
+
}
|
|
220
|
+
return `filters[$and][${index}][${key}][$eq]=${encodeURIComponent(value)}`;
|
|
221
|
+
})
|
|
222
|
+
.join("&");
|
|
223
|
+
|
|
224
|
+
const searchUrl = `/content-manager/collection-types/${targetModel}?${filterQuery}&populate[${displayField}]=*`;
|
|
225
|
+
const searchResponse = await get(searchUrl);
|
|
226
|
+
const targetRecords =
|
|
227
|
+
searchResponse?.data?.results ||
|
|
228
|
+
searchResponse?.results ||
|
|
229
|
+
searchResponse?.data ||
|
|
230
|
+
[];
|
|
231
|
+
|
|
232
|
+
let targetRecordId;
|
|
233
|
+
let targetInvestors = [];
|
|
234
|
+
|
|
235
|
+
if (targetRecords.length === 0) {
|
|
236
|
+
console.error(
|
|
237
|
+
"[FilteredRelation] No target record found with new status for this meeting."
|
|
238
|
+
);
|
|
239
|
+
const collectionDisplayName = targetModelInput || "record";
|
|
240
|
+
toggleNotification({
|
|
241
|
+
type: "warning",
|
|
242
|
+
message: `No ${collectionDisplayName} found with status "${newStatus}" for this meeting. Please create one first.`,
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Get full target record data
|
|
248
|
+
const targetRecord = targetRecords[0];
|
|
249
|
+
targetRecordId = targetRecord.documentId || targetRecord.id;
|
|
250
|
+
|
|
251
|
+
// Get target record with all fields
|
|
252
|
+
const targetRecordUrl = `/content-manager/collection-types/${targetModel}/${targetRecordId}?populate=*`;
|
|
253
|
+
const targetRecordFull = await get(targetRecordUrl);
|
|
254
|
+
const targetData = targetRecordFull?.data?.data || {};
|
|
255
|
+
|
|
256
|
+
// Step 4: Add investor to target record
|
|
257
|
+
// Build full payload like UI admin does
|
|
258
|
+
const addPayload = {
|
|
259
|
+
documentId: targetData.documentId,
|
|
260
|
+
[displayField]: {
|
|
261
|
+
connect: [
|
|
262
|
+
{
|
|
263
|
+
id: investorToRemove.id,
|
|
264
|
+
documentId: investorToRemove.documentId,
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
disconnect: [],
|
|
268
|
+
},
|
|
269
|
+
participantStatus: targetData.participantStatus,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
// Use connect with full payload
|
|
273
|
+
const addResponse = await put(
|
|
274
|
+
`/content-manager/collection-types/${targetModel}/${targetRecordId}`,
|
|
275
|
+
addPayload
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
// Refresh current field first to update local state
|
|
279
|
+
await fetchFilteredData();
|
|
280
|
+
|
|
281
|
+
// Then dispatch custom event to trigger refresh of all other filtered relation fields
|
|
282
|
+
window.dispatchEvent(
|
|
283
|
+
new CustomEvent("filtered-relation-updated", {
|
|
284
|
+
detail: { documentId, targetModel },
|
|
285
|
+
})
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
toggleNotification({
|
|
289
|
+
type: "success",
|
|
290
|
+
message: "Status changed successfully",
|
|
291
|
+
});
|
|
292
|
+
} catch (err) {
|
|
293
|
+
console.error("[FilteredRelation] Error moving investor:", err);
|
|
294
|
+
toggleNotification({
|
|
295
|
+
type: "danger",
|
|
296
|
+
message: "Failed to change status. Please try again.",
|
|
297
|
+
});
|
|
298
|
+
} finally {
|
|
299
|
+
setUpdatingStatus((prev) => ({ ...prev, [itemId]: false }));
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const fetchFilteredData = async () => {
|
|
304
|
+
if (!targetModel || Object.keys(filterParams).length === 0) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
setLoading(true);
|
|
309
|
+
try {
|
|
310
|
+
const modelParts = targetModel.split("::");
|
|
311
|
+
if (modelParts.length !== 2) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Build filter query string
|
|
316
|
+
const filterQuery = Object.entries(filterParams)
|
|
317
|
+
.map(([key, value], index) => {
|
|
318
|
+
if (key.includes(".")) {
|
|
319
|
+
const [relationField, nestedField] = key.split(".");
|
|
320
|
+
return `filters[$and][${index}][${relationField}][${nestedField}][$eq]=${encodeURIComponent(value)}`;
|
|
321
|
+
}
|
|
322
|
+
return `filters[$and][${index}][${key}][$eq]=${encodeURIComponent(value)}`;
|
|
323
|
+
})
|
|
324
|
+
.join("&");
|
|
325
|
+
|
|
326
|
+
const populateFields = [displayField];
|
|
327
|
+
if (statusField) {
|
|
328
|
+
populateFields.push(statusField);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const apiUrl = `/content-manager/collection-types/${targetModel}?${filterQuery}&populate[${displayField}][populate]=*`;
|
|
332
|
+
const response = await get(apiUrl);
|
|
333
|
+
const items =
|
|
334
|
+
response?.data?.results || response?.results || response?.data || [];
|
|
335
|
+
|
|
336
|
+
if (items.length > 0) {
|
|
337
|
+
const formattedItems = [];
|
|
338
|
+
|
|
339
|
+
items.forEach((item) => {
|
|
340
|
+
if (displayField && item[displayField]) {
|
|
341
|
+
if (Array.isArray(item[displayField])) {
|
|
342
|
+
item[displayField].forEach((rel) => {
|
|
343
|
+
const displayValue =
|
|
344
|
+
rel.fullName ||
|
|
345
|
+
rel.name ||
|
|
346
|
+
rel.displayName ||
|
|
347
|
+
rel.firstName ||
|
|
348
|
+
`ID: ${rel.documentId || rel.id}`;
|
|
349
|
+
|
|
350
|
+
formattedItems.push({
|
|
351
|
+
id: `${item.documentId || item.id}-${rel.documentId || rel.id}`,
|
|
352
|
+
label: displayValue,
|
|
353
|
+
originalId: item.documentId || item.id,
|
|
354
|
+
relatedDocumentId: rel.documentId || rel.id,
|
|
355
|
+
relationModel: relationModel,
|
|
356
|
+
currentStatus: item[statusField],
|
|
162
357
|
});
|
|
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
358
|
});
|
|
359
|
+
} else if (
|
|
360
|
+
typeof item[displayField] === "object" &&
|
|
361
|
+
item[displayField] !== null
|
|
362
|
+
) {
|
|
363
|
+
if (item[displayField].count !== undefined) {
|
|
364
|
+
formattedItems.push({
|
|
365
|
+
id: item.id,
|
|
366
|
+
label: `${item[displayField].count} items`,
|
|
367
|
+
currentStatus: item[statusField],
|
|
368
|
+
});
|
|
369
|
+
} else {
|
|
370
|
+
const displayValue =
|
|
371
|
+
item[displayField].fullName ||
|
|
372
|
+
item[displayField].name ||
|
|
373
|
+
item[displayField].displayName ||
|
|
374
|
+
item[displayField].firstName ||
|
|
375
|
+
`ID: ${item[displayField].id}`;
|
|
376
|
+
|
|
377
|
+
formattedItems.push({
|
|
378
|
+
id: item.id,
|
|
379
|
+
label: displayValue,
|
|
380
|
+
originalId: item.documentId || item.id,
|
|
381
|
+
relatedDocumentId:
|
|
382
|
+
item[displayField].documentId || item[displayField].id,
|
|
383
|
+
relationModel: relationModel,
|
|
384
|
+
currentStatus: item[statusField],
|
|
385
|
+
});
|
|
386
|
+
}
|
|
201
387
|
}
|
|
202
|
-
}
|
|
388
|
+
} else {
|
|
389
|
+
formattedItems.push({
|
|
390
|
+
id: item.id,
|
|
391
|
+
label: `ID: ${item.id}`,
|
|
392
|
+
originalId: item.documentId || item.id,
|
|
393
|
+
relatedDocumentId: item.documentId || item.id,
|
|
394
|
+
relationModel: targetModel,
|
|
395
|
+
currentStatus: item[statusField],
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
});
|
|
203
399
|
|
|
204
|
-
|
|
400
|
+
setItems(formattedItems);
|
|
205
401
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
402
|
+
// Auto-save to database
|
|
403
|
+
const documentIds = formattedItems.map(
|
|
404
|
+
(item) => item.relatedDocumentId || item.id
|
|
405
|
+
);
|
|
406
|
+
const newValue = JSON.stringify(documentIds);
|
|
211
407
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
if (documentId && value) {
|
|
220
|
-
await saveFieldValue(null);
|
|
221
|
-
}
|
|
408
|
+
if (documentId && value !== newValue) {
|
|
409
|
+
await saveFieldValue(newValue);
|
|
410
|
+
}
|
|
411
|
+
} else {
|
|
412
|
+
setItems([]);
|
|
413
|
+
if (documentId && value) {
|
|
414
|
+
await saveFieldValue(null);
|
|
222
415
|
}
|
|
223
|
-
} catch (err) {
|
|
224
|
-
console.error("Error fetching filtered relations:", err);
|
|
225
|
-
} finally {
|
|
226
|
-
setLoading(false);
|
|
227
416
|
}
|
|
228
|
-
}
|
|
417
|
+
} catch (err) {
|
|
418
|
+
console.error("Error fetching filtered relations:", err);
|
|
419
|
+
} finally {
|
|
420
|
+
setLoading(false);
|
|
421
|
+
}
|
|
422
|
+
};
|
|
229
423
|
|
|
424
|
+
React.useEffect(() => {
|
|
230
425
|
fetchFilteredData();
|
|
231
426
|
}, [
|
|
232
427
|
targetModel,
|
|
@@ -239,6 +434,21 @@ const FilteredRelationInput = (props) => {
|
|
|
239
434
|
value,
|
|
240
435
|
]);
|
|
241
436
|
|
|
437
|
+
// Listen for updates from other filtered relation fields
|
|
438
|
+
React.useEffect(() => {
|
|
439
|
+
const handleUpdate = (event) => {
|
|
440
|
+
// Refresh if same document
|
|
441
|
+
if (event.detail?.documentId === documentId) {
|
|
442
|
+
fetchFilteredData();
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
window.addEventListener("filtered-relation-updated", handleUpdate);
|
|
447
|
+
return () => {
|
|
448
|
+
window.removeEventListener("filtered-relation-updated", handleUpdate);
|
|
449
|
+
};
|
|
450
|
+
}, [documentId]);
|
|
451
|
+
|
|
242
452
|
const fieldLabel = props.label || name;
|
|
243
453
|
const itemCount = items.length;
|
|
244
454
|
|
|
@@ -258,8 +468,8 @@ const FilteredRelationInput = (props) => {
|
|
|
258
468
|
) : items.length > 0 ? (
|
|
259
469
|
<Flex direction="column" alignItems="stretch" gap={1}>
|
|
260
470
|
{items.map((item) => {
|
|
261
|
-
// Use relation model (e.g., api::investor.investor) with relatedDocumentId
|
|
262
471
|
const editUrl = `/content-manager/collection-types/${item.relationModel}/${item.relatedDocumentId}`;
|
|
472
|
+
const isUpdating = updatingStatus[item.originalId];
|
|
263
473
|
|
|
264
474
|
return (
|
|
265
475
|
<Box
|
|
@@ -275,15 +485,38 @@ const FilteredRelationInput = (props) => {
|
|
|
275
485
|
paddingRight={4}
|
|
276
486
|
style={{
|
|
277
487
|
cursor: "pointer",
|
|
278
|
-
|
|
488
|
+
minHeight: "50px",
|
|
279
489
|
display: "flex",
|
|
280
490
|
alignItems: "center",
|
|
491
|
+
justifyContent: "space-between",
|
|
281
492
|
}}
|
|
282
493
|
onClick={() => navigate(editUrl)}
|
|
283
494
|
>
|
|
284
495
|
<Typography variant="omega" textColor="primary600">
|
|
285
496
|
{item.label}
|
|
286
497
|
</Typography>
|
|
498
|
+
|
|
499
|
+
{statusField && statusOptions.length > 0 && (
|
|
500
|
+
<Box
|
|
501
|
+
onClick={(e) => e.stopPropagation()}
|
|
502
|
+
style={{ minWidth: "150px" }}
|
|
503
|
+
>
|
|
504
|
+
<SingleSelect
|
|
505
|
+
size="S"
|
|
506
|
+
value={item.currentStatus}
|
|
507
|
+
onChange={(value) =>
|
|
508
|
+
handleStatusChange(item.originalId, value, null)
|
|
509
|
+
}
|
|
510
|
+
disabled={isUpdating}
|
|
511
|
+
>
|
|
512
|
+
{statusOptions.map((status) => (
|
|
513
|
+
<SingleSelectOption key={status} value={status}>
|
|
514
|
+
{status}
|
|
515
|
+
</SingleSelectOption>
|
|
516
|
+
))}
|
|
517
|
+
</SingleSelect>
|
|
518
|
+
</Box>
|
|
519
|
+
)}
|
|
287
520
|
</Box>
|
|
288
521
|
);
|
|
289
522
|
})}
|
|
@@ -301,7 +534,6 @@ const FilteredRelationInput = (props) => {
|
|
|
301
534
|
{Object.keys(filterParams).length > 0
|
|
302
535
|
? `Filters: ${Object.entries(filterParams)
|
|
303
536
|
.map(([k, v]) => {
|
|
304
|
-
// Show placeholder name if it's a dynamic value
|
|
305
537
|
const displayValue =
|
|
306
538
|
v === documentId ? "{{current documentId}}" : v;
|
|
307
539
|
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.1",
|
|
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"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Script to push plugin to GitHub
|
|
4
|
+
# Usage: ./push-to-github.sh
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
echo "🚀 Pushing strapi-plugin-filtered-relation to GitHub..."
|
|
9
|
+
|
|
10
|
+
# Check if we're in the right directory
|
|
11
|
+
if [ ! -f "package.json" ]; then
|
|
12
|
+
echo "❌ Error: package.json not found. Are you in the plugin directory?"
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# Check if git is initialized
|
|
17
|
+
if [ ! -d ".git" ]; then
|
|
18
|
+
echo "📦 Initializing git repository..."
|
|
19
|
+
git init
|
|
20
|
+
git remote add origin https://github.com/finnwasabi/strapi-plugin-filtered-relation.git
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# Add all files
|
|
24
|
+
echo "📝 Adding files..."
|
|
25
|
+
git add .
|
|
26
|
+
|
|
27
|
+
# Commit
|
|
28
|
+
echo "💾 Committing changes..."
|
|
29
|
+
git commit -m "Release v1.1.0 - Add status change dropdown feature
|
|
30
|
+
|
|
31
|
+
- Add status change dropdown at end of each item
|
|
32
|
+
- Dynamically fetch enum options from target model schema
|
|
33
|
+
- Move items between records using connect/disconnect API
|
|
34
|
+
- Dispatch custom events to refresh all fields without page reload
|
|
35
|
+
- Remove all console.log statements
|
|
36
|
+
- Update README with detailed options table and descriptions
|
|
37
|
+
- Update CHANGELOG with technical details
|
|
38
|
+
- Fully dynamic and reusable for any use case"
|
|
39
|
+
|
|
40
|
+
# Tag the release
|
|
41
|
+
echo "🏷️ Creating tag v1.1.0..."
|
|
42
|
+
git tag -a v1.1.0 -m "Version 1.1.0"
|
|
43
|
+
|
|
44
|
+
# Push to GitHub
|
|
45
|
+
echo "⬆️ Pushing to GitHub..."
|
|
46
|
+
git push origin main
|
|
47
|
+
git push origin v1.1.0
|
|
48
|
+
|
|
49
|
+
echo "✅ Successfully pushed to GitHub!"
|
|
50
|
+
echo "📦 Package: @tunghtml/strapi-plugin-filtered-relation@1.1.0"
|
|
51
|
+
echo "🔗 GitHub: https://github.com/finnwasabi/strapi-plugin-filtered-relation"
|
|
52
|
+
echo "🔗 NPM: https://www.npmjs.com/package/@tunghtml/strapi-plugin-filtered-relation"
|
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 = {};
|