strapi-plugin-meilisearch-plus 0.0.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.
Files changed (70) hide show
  1. package/README.md +266 -0
  2. package/dist/_chunks/CollectionsTable-DhFoYbHi.mjs +376 -0
  3. package/dist/_chunks/CollectionsTable-QsBP3lXJ.js +376 -0
  4. package/dist/_chunks/CredentialsTab-CNnDxmdX.js +223 -0
  5. package/dist/_chunks/CredentialsTab-UuDtCixJ.mjs +223 -0
  6. package/dist/_chunks/HomePage-CwtBGxKu.js +36 -0
  7. package/dist/_chunks/HomePage-VgGeNynA.mjs +36 -0
  8. package/dist/_chunks/IndexSettingsTab-EvPHd16e.js +270 -0
  9. package/dist/_chunks/IndexSettingsTab-P4Oz-QWd.mjs +270 -0
  10. package/dist/_chunks/en-BijiR88Y.js +42 -0
  11. package/dist/_chunks/en-KhozGU-M.mjs +42 -0
  12. package/dist/_chunks/es-B2Fg_gLt.js +42 -0
  13. package/dist/_chunks/es-DAneCWM5.mjs +42 -0
  14. package/dist/_chunks/index-DgpT8G3z.mjs +134 -0
  15. package/dist/_chunks/index-eoiIjp_f.js +133 -0
  16. package/dist/_chunks/useI18n-B9yJCqkO.js +18 -0
  17. package/dist/_chunks/useI18n-RYsv52c9.mjs +19 -0
  18. package/dist/admin/index.js +3 -0
  19. package/dist/admin/index.mjs +4 -0
  20. package/dist/admin/src/components/Icons/DangerIcon.d.ts +7 -0
  21. package/dist/admin/src/components/Icons/RefreshIcon.d.ts +7 -0
  22. package/dist/admin/src/components/Icons/XIcon.d.ts +7 -0
  23. package/dist/admin/src/components/Icons/index.d.ts +3 -0
  24. package/dist/admin/src/components/Initializer.d.ts +10 -0
  25. package/dist/admin/src/components/PluginIcon.d.ts +10 -0
  26. package/dist/admin/src/containers/CollectionsTab.d.ts +6 -0
  27. package/dist/admin/src/containers/CollectionsTable.d.ts +6 -0
  28. package/dist/admin/src/containers/ContentTypesToggle.d.ts +6 -0
  29. package/dist/admin/src/containers/CredentialsTab.d.ts +6 -0
  30. package/dist/admin/src/containers/IndexSettingsTab.d.ts +6 -0
  31. package/dist/admin/src/containers/PluginTabs.d.ts +6 -0
  32. package/dist/admin/src/hooks/index.d.ts +3 -0
  33. package/dist/admin/src/hooks/useCredentials.d.ts +18 -0
  34. package/dist/admin/src/hooks/useI18n.d.ts +7 -0
  35. package/dist/admin/src/hooks/useIndexedContentTypes.d.ts +15 -0
  36. package/dist/admin/src/index.d.ts +14 -0
  37. package/dist/admin/src/pages/App.d.ts +2 -0
  38. package/dist/admin/src/pages/HomePage.d.ts +5 -0
  39. package/dist/admin/src/pages/SettingsPage.d.ts +5 -0
  40. package/dist/admin/src/pluginId.d.ts +1 -0
  41. package/dist/admin/src/utils/getTranslation.d.ts +2 -0
  42. package/dist/admin/src/utils/pluginId.d.ts +1 -0
  43. package/dist/server/index.js +1346 -0
  44. package/dist/server/index.mjs +1347 -0
  45. package/dist/server/src/bootstrap.d.ts +5 -0
  46. package/dist/server/src/config/index.d.ts +5 -0
  47. package/dist/server/src/content-types/index.d.ts +2 -0
  48. package/dist/server/src/controllers/content-types.d.ts +14 -0
  49. package/dist/server/src/controllers/controller.d.ts +7 -0
  50. package/dist/server/src/controllers/credentials.d.ts +12 -0
  51. package/dist/server/src/controllers/index-settings.d.ts +17 -0
  52. package/dist/server/src/controllers/index.d.ts +40 -0
  53. package/dist/server/src/controllers/indexing.d.ts +10 -0
  54. package/dist/server/src/controllers/search.d.ts +10 -0
  55. package/dist/server/src/destroy.d.ts +5 -0
  56. package/dist/server/src/index.d.ts +216 -0
  57. package/dist/server/src/middlewares/index.d.ts +2 -0
  58. package/dist/server/src/policies/index.d.ts +2 -0
  59. package/dist/server/src/register.d.ts +5 -0
  60. package/dist/server/src/routes/admin/index.d.ts +21 -0
  61. package/dist/server/src/routes/content-api/index.d.ts +12 -0
  62. package/dist/server/src/routes/index.d.ts +34 -0
  63. package/dist/server/src/services/content-types.d.ts +59 -0
  64. package/dist/server/src/services/index.d.ts +123 -0
  65. package/dist/server/src/services/lifecycle.d.ts +15 -0
  66. package/dist/server/src/services/meilisearch-client.d.ts +18 -0
  67. package/dist/server/src/services/meilisearch.d.ts +42 -0
  68. package/dist/server/src/services/service.d.ts +7 -0
  69. package/dist/server/src/services/store.d.ts +42 -0
  70. package/package.json +78 -0
package/README.md ADDED
@@ -0,0 +1,266 @@
1
+ # MeiliSearch Plus Plugin
2
+
3
+ A comprehensive MeiliSearch integration plugin for Strapi v5 that enables full-text search capabilities for your content.
4
+
5
+ ### Features
6
+
7
+ #### Server-Side
8
+
9
+ - **Credential Management**: Securely store MeiliSearch host and API key
10
+ - **Automatic Indexing**: Automatically index documents on create, update, and delete
11
+ - **Published Documents Only**: Only published documents are indexed (draft documents are excluded)
12
+ - **Content Type Management**: Choose which content types to index
13
+ - **Manual Indexing**: Trigger reindexing of specific content types without affecting others
14
+ - **Safe Reindex**: Reindex operations only affect the specified content type
15
+ - **Search API**: Full-text search with filters, pagination, and sorting
16
+ - **Connection Testing**: Verify MeiliSearch connection before operations
17
+ - **Index Status**: Monitor index health and sync status with display names
18
+ - **Content Type Display Names**: Full content type metadata with human-readable names
19
+
20
+ ### Admin API Routes
21
+
22
+ #### Credentials Management
23
+
24
+ - `GET /meilisearch-plus/credentials` - Get current credentials
25
+ - `POST /meilisearch-plus/credentials` - Set credentials (host, apiKey, indexName)
26
+ - `GET /meilisearch-plus/test-connection` - Test MeiliSearch connection
27
+
28
+ #### Search Operations
29
+
30
+ - `POST /meilisearch-plus/search` - Execute search query
31
+ - Body: `{ query, filters?, limit?, offset? }`
32
+ - `GET /meilisearch-plus/index-status` - Get index status and statistics
33
+
34
+ #### Content Type Management
35
+
36
+ - `GET /meilisearch-plus/sync-status/all` - Get all configured content types with sync status
37
+ - Returns: `{ contentType, displayName, isIndexed, total, indexed, syncPercentage, isSynced }`
38
+ - `GET /meilisearch-plus/indexed-content-types` - List indexed content types
39
+ - `POST /meilisearch-plus/indexed-content-types` - Add content type to index
40
+ - Body: `{ contentType }`
41
+ - `DELETE /meilisearch-plus/indexed-content-types` - Remove content type from index and MeiliSearch
42
+
43
+ #### Manual Indexing
44
+
45
+ - `POST /meilisearch-plus/reindex` - Reindex documents for a specific content type
46
+ - Body: `{ contentType }` - Only reindexes the specified content type
47
+ - `POST /meilisearch-plus/clear-index` - Clear indexed documents for a content type
48
+ - Body: `{ contentType }` - Only clears the specified content type
49
+
50
+ ### Content API Routes (Public)
51
+
52
+ - `POST /api/plugins/meilisearch-plus/search` - Public search endpoint
53
+ - Body: `{ query, filters?, limit?, offset? }`
54
+
55
+ ## Architecture
56
+
57
+ ### Services
58
+
59
+ #### `meilisearch-client.ts`
60
+
61
+ Factory service for creating and managing MeiliSearch client instances.
62
+
63
+ - `createClient(credentials)` - Create authenticated client
64
+ - `testConnection(client)` - Verify connection health
65
+ - `getIndexUids(client)` - List all indexes
66
+ - `createIndex(client, indexName)` - Create new index
67
+ - `deleteIndex(client, indexName)` - Delete index
68
+
69
+ #### `store.ts`
70
+
71
+ Persistent storage for credentials and configuration using Strapi's store API.
72
+
73
+ - `getCredentials()` / `setCredentials()`
74
+ - `getApiKey()` / `setApiKey()`
75
+ - `getHost()` / `setHost()`
76
+ - `getIndexName()` / `setIndexName()`
77
+ - `getIndexedContentTypes()` / `setIndexedContentTypes()`
78
+ - `addIndexedContentType()` / `removeIndexedContentType()`
79
+
80
+ #### `meilisearch.ts`
81
+
82
+ Core indexing and search service.
83
+
84
+ - `indexDocument()` - Index single document
85
+ - `indexDocuments()` - Bulk index documents
86
+ - `deleteDocument()` - Remove document from index
87
+ - `deleteAllDocuments()` - Clear all documents
88
+ - `search()` - Search with filters and pagination
89
+ - `getIndexSettings()` / `updateIndexSettings()`
90
+
91
+ #### `lifecycle.ts`
92
+
93
+ Lifecycle hook subscriptions for automatic indexing.
94
+
95
+ - `subscribeContentType(contentType)` - Listen to create/update/delete events
96
+ - `unsubscribeContentType(contentType)` - Stop listening (metadata only)
97
+
98
+ ### Controllers
99
+
100
+ #### `credentials.ts`
101
+
102
+ Admin endpoints for managing MeiliSearch credentials and connections.
103
+
104
+ #### `search.ts`
105
+
106
+ Search functionality with index status monitoring.
107
+
108
+ #### `content-types.ts`
109
+
110
+ Manage which content types are indexed.
111
+
112
+ #### `indexing.ts`
113
+
114
+ Manual indexing operations (reindex, clear).
115
+
116
+ ## Admin Panel
117
+
118
+ The plugin provides a dedicated admin interface in the Strapi Settings menu with two sections:
119
+
120
+ ### Settings Page
121
+
122
+ Access the plugin settings at **Settings > MeiliSearch Plus**:
123
+
124
+ #### Credentials Tab
125
+
126
+ Configure MeiliSearch connection details:
127
+
128
+ - **MeiliSearch URL** - The URL where MeiliSearch is running (e.g., `http://localhost:7700`)
129
+ - **API Key** - API key with permission to create indexes (master key recommended)
130
+ - **Index Name** - Name of the MeiliSearch index to use for this installation
131
+
132
+ Features:
133
+
134
+ - Test connection button to verify credentials
135
+ - Security warning about API key exposure
136
+ - Save configuration
137
+
138
+ #### Index Settings Tab
139
+
140
+ Configure which fields are searchable and sortable per content type:
141
+
142
+ - **Max Total Hits** - Maximum number of documents returned by MeiliSearch (default: 1000)
143
+ - **Filterable Attributes** - Select which fields can be used as filters in search queries
144
+ - **Sortable Attributes** - Select which fields can be sorted in search results
145
+ - **Mandatory Field**: `_contentType` is always included (required for document categorization)
146
+ - Organize by content type using tabbed interface
147
+ - Human-readable field names (converted from snake_case)
148
+
149
+ ### Collections Tab
150
+
151
+ Monitor indexed content types in the main plugin view:
152
+
153
+ - View all configured content types with display names
154
+ - Check indexing status (complete/in-progress)
155
+ - See database count vs indexed documents
156
+ - Toggle content types on/off for indexing
157
+ - Update index (sync without full rebuild)
158
+ - Reindex individual content types (only affects the selected type)
159
+ - See hooks status (automatic sync enabled/disabled)
160
+
161
+ ## Configuration
162
+
163
+ The plugin is enabled by default in `apps/cms/config/plugins.ts`:
164
+
165
+ ```typescript
166
+ 'meilisearch-plus': {
167
+ enabled: true,
168
+ resolve: './src/plugins/meilisearch-plus',
169
+ },
170
+ ```
171
+
172
+ ## Environment Variables
173
+
174
+ Set these in your `.env` file:
175
+
176
+ ```
177
+ MEILISEARCH_HOST=http://localhost:7700
178
+ MEILISEARCH_API_KEY=your-api-key
179
+ MEILISEARCH_INDEX_NAME=your-index-name
180
+ ```
181
+
182
+ ## UID Mapping and Content Type Handling
183
+
184
+ Content types are configured using short names in the plugin config (e.g., `page`, `job`) and are mapped to full UIDs:
185
+
186
+ ```typescript
187
+ // Config uses short names
188
+ 'meilisearch-plus': {
189
+ page: { /* config */},
190
+ job: { /* config */},
191
+ }
192
+
193
+ // Internally mapped to full UIDs
194
+ api::page.page
195
+ api::job.job
196
+ ```
197
+
198
+ The `/sync-status/all` endpoint returns:
199
+
200
+ ```json
201
+ {
202
+ "contentType": "api::page.page",
203
+ "displayName": "Pagina",
204
+ "isIndexed": true,
205
+ "total": 18,
206
+ "indexed": 18,
207
+ "syncPercentage": 100,
208
+ "isSynced": true
209
+ }
210
+ ```
211
+
212
+ ## Key Implementation Details
213
+
214
+ ### Content Type Identification
215
+
216
+ The plugin uses full content type UIDs throughout the application (e.g., `api::page.page`):
217
+
218
+ - UIDs are mapped from plugin configuration short names
219
+ - Display names are fetched from Strapi content type schema
220
+ - All API responses include both UID and displayName for clarity
221
+
222
+ ### Document Indexing
223
+
224
+ - **Status Filter**: Only `published` documents are indexed (draft documents are excluded)
225
+ - **Content Type Field**: The `_contentType` field is automatically added to all documents for categorization
226
+ - **Batch Processing**: Documents are indexed in batches of 1000
227
+ - **Safe Deletion**: When toggling off a content type, all its documents are removed from MeiliSearch
228
+
229
+ ### Reindex Operations
230
+
231
+ - **Content Type Scoped**: Reindex and clear operations only affect the specified content type
232
+ - **No Cross-Type Interference**: Toggling or reindexing one type doesn't affect others
233
+ - **Full Rebuild**: Reindex performs a complete delete and re-add cycle for the specific type
234
+
235
+ ### Attribute Settings
236
+
237
+ - **Mandatory Fields**: `_contentType` is always included in filterable and sortable attributes (required for document lookup)
238
+ - **Field Name Formatting**: Field names are converted to human-readable format in the UI (e.g., `publishedAt` → "Published At")
239
+ - **Per-Content-Type Config**: Settings are merged across content types to create a unified index configuration
240
+
241
+ ### Database & Storage
242
+
243
+ The plugin uses Strapi's store API to persist:
244
+
245
+ - MeiliSearch credentials (host, API key, index name)
246
+ - Indexed content types list
247
+ - Index settings (filterable/sortable attributes, max total hits)
248
+ - Lifecycle subscriptions for automatic indexing
249
+
250
+ ## Error Handling
251
+
252
+ All services include comprehensive error handling with logging:
253
+
254
+ - Connection errors are logged but don't block startup
255
+ - Document indexing failures are logged but don't break transactions
256
+ - Missing credentials/index names are handled gracefully
257
+ - All operations have timeout protection
258
+ - Reindex and clear operations safely handle already-deleted documents
259
+
260
+ ## Type Safety
261
+
262
+ The plugin is fully written in TypeScript with:
263
+
264
+ - Strict type checking
265
+ - Comprehensive JSDoc comments
266
+ - Service interfaces for IDE autocompletion
@@ -0,0 +1,376 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { memo, useState, useEffect } from "react";
3
+ import { Flex, Loader, Box, Typography, Badge, Table, Tr, Td, Tbody, Checkbox, Button } from "@strapi/design-system";
4
+ import { useFetchClient, useNotification } from "@strapi/strapi/admin";
5
+ import { Check } from "@strapi/icons";
6
+ import { u as useI18n } from "./useI18n-RYsv52c9.mjs";
7
+ import { p as pluginId } from "./index-DgpT8G3z.mjs";
8
+ const RefreshIcon = ({ size = 16, color = "currentColor" }) => /* @__PURE__ */ jsxs(
9
+ "svg",
10
+ {
11
+ width: size,
12
+ height: size,
13
+ viewBox: "0 0 24 24",
14
+ fill: "none",
15
+ stroke: color,
16
+ strokeWidth: "2",
17
+ strokeLinecap: "round",
18
+ strokeLinejoin: "round",
19
+ children: [
20
+ /* @__PURE__ */ jsx("polyline", { points: "23 4 23 10 17 10" }),
21
+ /* @__PURE__ */ jsx("polyline", { points: "1 20 1 14 7 14" }),
22
+ /* @__PURE__ */ jsx("path", { d: "M3.51 9a9 9 0 0 1 14.85-3.36M20.49 15a9 9 0 0 1-14.85 3.36" })
23
+ ]
24
+ }
25
+ );
26
+ const XIcon = ({ size = 16, color = "currentColor" }) => /* @__PURE__ */ jsxs(
27
+ "svg",
28
+ {
29
+ width: size,
30
+ height: size,
31
+ viewBox: "0 0 24 24",
32
+ fill: "none",
33
+ stroke: color,
34
+ strokeWidth: "2",
35
+ strokeLinecap: "round",
36
+ strokeLinejoin: "round",
37
+ children: [
38
+ /* @__PURE__ */ jsx("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
39
+ /* @__PURE__ */ jsx("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
40
+ ]
41
+ }
42
+ );
43
+ const DangerIcon = ({ size = 16, color = "currentColor" }) => /* @__PURE__ */ jsx(
44
+ "svg",
45
+ {
46
+ width: size,
47
+ height: size,
48
+ viewBox: "0 0 24 24",
49
+ fill: "none",
50
+ stroke: color,
51
+ strokeWidth: "2",
52
+ strokeLinecap: "round",
53
+ strokeLinejoin: "round",
54
+ children: /* @__PURE__ */ jsx("polygon", { points: "12 2 22 20 2 20" })
55
+ }
56
+ );
57
+ const CollectionsTable = memo(() => {
58
+ const { i18n } = useI18n();
59
+ const { get, post, del } = useFetchClient();
60
+ const { toggleNotification } = useNotification();
61
+ const [contentTypes, setContentTypes] = useState([]);
62
+ const [isLoading, setIsLoading] = useState(true);
63
+ const [loadingState, setLoadingState] = useState({});
64
+ const [syncData, setSyncData] = useState({});
65
+ useEffect(() => {
66
+ loadContentTypes();
67
+ }, [get]);
68
+ const loadContentTypes = async () => {
69
+ setIsLoading(true);
70
+ try {
71
+ const syncResponse = await get(`/${pluginId}/sync-status/all`);
72
+ const syncStatuses = syncResponse.data?.data || [];
73
+ console.log("Sync statuses from API:", syncStatuses);
74
+ const configuredUids = syncStatuses.map((item) => item.contentType);
75
+ console.log("Configured content types from API:", configuredUids);
76
+ const allContentTypes = strapi.contentTypes;
77
+ const syncMap = {};
78
+ const types = syncStatuses.map((item) => {
79
+ syncMap[item.contentType] = {
80
+ total: item.total || 0,
81
+ indexed: item.indexed || 0,
82
+ syncPercentage: item.syncPercentage || 0,
83
+ isSynced: item.isSynced || false,
84
+ isIndexed: item.isIndexed || false
85
+ };
86
+ return {
87
+ name: item.displayName || item.contentType,
88
+ uid: item.contentType,
89
+ displayName: item.displayName || item.contentType,
90
+ isIndexed: item.isIndexed || false,
91
+ total: item.total || 0,
92
+ indexed: item.indexed || 0,
93
+ syncPercentage: item.syncPercentage || 0,
94
+ isSynced: item.isSynced || false
95
+ };
96
+ });
97
+ setSyncData(syncMap);
98
+ setContentTypes(types);
99
+ } catch (error) {
100
+ console.error("[meilisearch-plus] Failed to load content types:", error);
101
+ toggleNotification({
102
+ type: "warning",
103
+ message: "Failed to load sync status"
104
+ });
105
+ } finally {
106
+ setIsLoading(false);
107
+ }
108
+ };
109
+ const handleToggle = async (name) => {
110
+ console.log(contentTypes);
111
+ const currentContentType = contentTypes.find((ct) => ct.name === name || ct.uid === name);
112
+ if (!currentContentType) return;
113
+ const shouldAddIndex = !currentContentType.isIndexed;
114
+ console.log(`[meilisearch-plus] Toggling ${name} to ${shouldAddIndex}`);
115
+ setLoadingState((prev) => ({ ...prev, [name]: true }));
116
+ try {
117
+ const contentTypeUid = currentContentType?.uid || name;
118
+ if (shouldAddIndex) {
119
+ console.log(`[meilisearch-plus] POST to /${pluginId}/indexed-content-types`);
120
+ const response = await post(`/${pluginId}/indexed-content-types`, {
121
+ contentType: contentTypeUid
122
+ });
123
+ console.log("[meilisearch-plus] POST response:", response);
124
+ console.log(`[meilisearch-plus] Reindexing ${contentTypeUid} after adding to index`);
125
+ await post(`/${pluginId}/reindex`, { contentType: contentTypeUid });
126
+ console.log(`[meilisearch-plus] Reindex complete for ${contentTypeUid}`);
127
+ } else {
128
+ console.log(
129
+ `[meilisearch-plus] DELETE to /${pluginId}/indexed-content-types?contentType=${contentTypeUid}`
130
+ );
131
+ const response = await del(
132
+ `/${pluginId}/indexed-content-types?contentType=${contentTypeUid}`
133
+ );
134
+ console.log("[meilisearch-plus] DELETE response:", response);
135
+ }
136
+ setContentTypes(
137
+ (prev) => prev.map((ct) => ct.name === name ? { ...ct, isIndexed: shouldAddIndex } : ct)
138
+ );
139
+ toggleNotification({
140
+ type: "success",
141
+ message: shouldAddIndex ? `${name} added to index` : `${name} removed from index`
142
+ });
143
+ await loadContentTypes();
144
+ } catch (error) {
145
+ console.error(`[meilisearch-plus] Failed to toggle ${name}:`, error);
146
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
147
+ console.error("[meilisearch-plus] Error details:", errorMessage);
148
+ toggleNotification({
149
+ type: "warning",
150
+ message: `Failed to update ${name}: ${errorMessage}`
151
+ });
152
+ } finally {
153
+ setLoadingState((prev) => ({ ...prev, [name]: false }));
154
+ }
155
+ };
156
+ const handleUpdate = async (contentType) => {
157
+ console.log(`[meilisearch-plus] Updating ${contentType}`);
158
+ setLoadingState((prev) => ({ ...prev, [contentType]: true }));
159
+ try {
160
+ const ct = contentTypes.find((ct2) => ct2.name === contentType || ct2.uid === contentType);
161
+ const contentTypeUid = ct?.uid || contentType;
162
+ const response = await post(`/${pluginId}/reindex`, { contentType: contentTypeUid });
163
+ console.log("[meilisearch-plus] Update response:", response);
164
+ toggleNotification({
165
+ type: "success",
166
+ message: `${contentType} index updated`
167
+ });
168
+ await loadContentTypes();
169
+ } catch (error) {
170
+ console.error(`[meilisearch-plus] Failed to update ${contentType}:`, error);
171
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
172
+ toggleNotification({
173
+ type: "warning",
174
+ message: `Failed to update ${contentType}: ${errorMessage}`
175
+ });
176
+ } finally {
177
+ setLoadingState((prev) => ({ ...prev, [contentType]: false }));
178
+ }
179
+ };
180
+ const handleReindex = async (contentType) => {
181
+ console.log(`[meilisearch-plus] Reindexing ${contentType}`);
182
+ setLoadingState((prev) => ({ ...prev, [contentType]: true }));
183
+ try {
184
+ const ct = contentTypes.find((ct2) => ct2.name === contentType || ct2.uid === contentType);
185
+ const contentTypeUid = ct?.uid || contentType;
186
+ await post(`/${pluginId}/clear-index`, { contentType: contentTypeUid });
187
+ console.log("[meilisearch-plus] Index cleared for", contentTypeUid);
188
+ const response = await post(`/${pluginId}/reindex`, { contentType: contentTypeUid });
189
+ console.log("[meilisearch-plus] Reindex response:", response);
190
+ toggleNotification({
191
+ type: "success",
192
+ message: `${contentType} index reindexed`
193
+ });
194
+ await loadContentTypes();
195
+ } catch (error) {
196
+ console.error(`[meilisearch-plus] Failed to reindex ${contentType}:`, error);
197
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
198
+ toggleNotification({
199
+ type: "warning",
200
+ message: `Failed to reindex ${contentType}: ${errorMessage}`
201
+ });
202
+ } finally {
203
+ setLoadingState((prev) => ({ ...prev, [contentType]: false }));
204
+ }
205
+ };
206
+ if (isLoading) {
207
+ return /* @__PURE__ */ jsx(Flex, { justifyContent: "center", alignItems: "center", minHeight: "400px", children: /* @__PURE__ */ jsx(Loader, {}) });
208
+ }
209
+ const COL_COUNT = 8;
210
+ const ROW_COUNT = Math.max(6, contentTypes.length);
211
+ return /* @__PURE__ */ jsxs(Box, { background: "neutral100", padding: 4, children: [
212
+ /* @__PURE__ */ jsxs(Box, { padding: 6, marginBottom: 6, background: "neutral0", hasRadius: true, children: [
213
+ /* @__PURE__ */ jsx(Typography, { variant: "beta", marginBottom: 6, paddingBottom: 4, children: i18n("plugin.legend.title", "Legend") }),
214
+ /* @__PURE__ */ jsxs(
215
+ Flex,
216
+ {
217
+ direction: "column",
218
+ alignItems: "flex-start",
219
+ justifyContent: "between",
220
+ gap: 6,
221
+ style: { width: "100%" },
222
+ children: [
223
+ /* @__PURE__ */ jsxs(Flex, { alignItems: "flex-start", justifyContent: "between", gap: 6, style: { width: "100%" }, children: [
224
+ /* @__PURE__ */ jsxs(Box, { flex: 1, children: [
225
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", marginBottom: 3, children: i18n("plugin.legend.in-meilisearch", "IN MEILISEARCH") }),
226
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", alignItems: "flex-start", gap: 3, paddingTop: 2, children: [
227
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "flex-start", children: [
228
+ /* @__PURE__ */ jsx(Badge, { background: "success600", textColor: "neutral0", children: /* @__PURE__ */ jsx(Check, {}) }),
229
+ /* @__PURE__ */ jsx(Typography, { size: "sm", children: i18n("plugin.legend.indexed", "Content type is indexed") })
230
+ ] }),
231
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "flex-start", children: [
232
+ /* @__PURE__ */ jsx(Badge, { background: "neutral900", textColor: "neutral0", children: /* @__PURE__ */ jsx(XIcon, { size: 16, color: "white" }) }),
233
+ /* @__PURE__ */ jsx(Typography, { size: "sm", children: i18n("plugin.legend.not-indexed", "Content type is not indexed") })
234
+ ] })
235
+ ] })
236
+ ] }),
237
+ /* @__PURE__ */ jsxs(Box, { flex: 1, children: [
238
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", marginBottom: 3, children: i18n("plugin.legend.indexing-status", "INDEXING STATUS") }),
239
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, alignItems: "flex-start", paddingTop: 2, children: [
240
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "flex-start", children: [
241
+ /* @__PURE__ */ jsx(Badge, { background: "success600", textColor: "neutral0", children: /* @__PURE__ */ jsx(Check, {}) }),
242
+ /* @__PURE__ */ jsx(Typography, { size: "sm", children: i18n("plugin.legend.indexing-complete", "Indexing complete") })
243
+ ] }),
244
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "flex-start", children: [
245
+ /* @__PURE__ */ jsx(Badge, { background: "warning600", textColor: "neutral0", children: /* @__PURE__ */ jsx(RefreshIcon, { size: 16, color: "white" }) }),
246
+ /* @__PURE__ */ jsx(Typography, { size: "sm", children: i18n("plugin.legend.indexing-in-progress", "Indexing in progress") })
247
+ ] })
248
+ ] })
249
+ ] }),
250
+ /* @__PURE__ */ jsxs(Box, { flex: 1, children: [
251
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", marginBottom: 3, children: i18n("plugin.legend.hooks", "HOOKS") }),
252
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 3, paddingTop: 2, alignItems: "flex-start", children: [
253
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "flex-start", children: [
254
+ /* @__PURE__ */ jsx(Badge, { background: "success600", textColor: "neutral0", children: /* @__PURE__ */ jsx(Check, {}) }),
255
+ /* @__PURE__ */ jsx(Typography, { size: "sm", children: i18n("plugin.legend.hooks-active", "Hooks active - automatic sync enabled") })
256
+ ] }),
257
+ /* @__PURE__ */ jsxs(Flex, { gap: 3, alignItems: "flex-start", children: [
258
+ /* @__PURE__ */ jsx(Badge, { background: "neutral900", textColor: "neutral0", children: /* @__PURE__ */ jsx(DangerIcon, { size: 16, color: "white" }) }),
259
+ /* @__PURE__ */ jsx(Typography, { size: "sm", children: i18n(
260
+ "plugin.legend.hooks-inactive",
261
+ "Hooks not active - manual reload required"
262
+ ) })
263
+ ] })
264
+ ] })
265
+ ] })
266
+ ] }),
267
+ /* @__PURE__ */ jsxs(Box, { children: [
268
+ /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", marginBottom: 3, children: i18n("plugin.legend.actions", "ACTIONS") }),
269
+ /* @__PURE__ */ jsxs(Flex, { direction: "column", gap: 2, alignItems: "flex-start", children: [
270
+ /* @__PURE__ */ jsxs(Typography, { size: "sm", children: [
271
+ /* @__PURE__ */ jsx("strong", { children: i18n("plugin.button.update", "Update") }),
272
+ " —",
273
+ " ",
274
+ i18n(
275
+ "plugin.legend.update",
276
+ "Sync & refresh the index with the latest entries without deleting existing data."
277
+ )
278
+ ] }),
279
+ /* @__PURE__ */ jsxs(Typography, { size: "sm", children: [
280
+ /* @__PURE__ */ jsx("strong", { children: i18n("plugin.button.reindex", "Reindex") }),
281
+ " —",
282
+ " ",
283
+ i18n(
284
+ "plugin.legend.reindex",
285
+ "Full rebuild: delete all indexed documents and reindex everything from scratch."
286
+ )
287
+ ] })
288
+ ] })
289
+ ] })
290
+ ]
291
+ }
292
+ )
293
+ ] }),
294
+ /* @__PURE__ */ jsx(Box, { background: "neutral0", hasRadius: true, children: /* @__PURE__ */ jsxs(Table, { colCount: COL_COUNT, rowCount: ROW_COUNT, children: [
295
+ /* @__PURE__ */ jsx("thead", { children: /* @__PURE__ */ jsxs(Tr, { children: [
296
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: i18n("plugin.table.header.index", "Index") }) }),
297
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: i18n("plugin.table.header.name", "Name") }) }),
298
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: i18n("plugin.table.header.in-meilisearch", "In MeiliSearch") }) }),
299
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: i18n("plugin.table.header.database", "Database") }) }),
300
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: i18n("plugin.table.header.indexed", "Indexed") }) }),
301
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: i18n("plugin.table.header.sync", "Sync %") }) }),
302
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { variant: "sigma", textColor: "neutral600", children: i18n("plugin.table.header.actions", "Actions") }) }),
303
+ /* @__PURE__ */ jsx(Td, {})
304
+ ] }) }),
305
+ /* @__PURE__ */ jsx(Tbody, { children: contentTypes.length === 0 ? /* @__PURE__ */ jsx(Tr, { children: /* @__PURE__ */ jsx(Td, { colSpan: COL_COUNT, children: /* @__PURE__ */ jsx(Box, { padding: 4, children: /* @__PURE__ */ jsx(Typography, { children: i18n("plugin.noConfiguredTypes", "No content types configured") }) }) }) }) : contentTypes.map((contentType) => /* @__PURE__ */ jsxs(Tr, { children: [
306
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(
307
+ Checkbox,
308
+ {
309
+ checked: contentType.isIndexed,
310
+ onCheckedChange: () => {
311
+ handleToggle(contentType.name);
312
+ },
313
+ disabled: loadingState[contentType.name],
314
+ "aria-label": `Toggle indexing for ${contentType.name}`
315
+ }
316
+ ) }),
317
+ /* @__PURE__ */ jsxs(Td, { children: [
318
+ /* @__PURE__ */ jsx(Typography, { weight: "bold", children: contentType.displayName || contentType.name }),
319
+ /* @__PURE__ */ jsx(
320
+ Typography,
321
+ {
322
+ variant: "pi",
323
+ textColor: "neutral600",
324
+ fontSize: 12,
325
+ style: { display: "block" },
326
+ children: contentType.uid
327
+ }
328
+ )
329
+ ] }),
330
+ /* @__PURE__ */ jsx(Td, { children: contentType.isIndexed ? /* @__PURE__ */ jsx(Badge, { background: "success600", textColor: "neutral0", children: /* @__PURE__ */ jsx(Check, {}) }) : /* @__PURE__ */ jsx(Badge, { background: "neutral900", textColor: "neutral0", children: /* @__PURE__ */ jsx(XIcon, { size: 16, color: "white" }) }) }),
331
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { size: "sm", children: contentType.total || 0 }) }),
332
+ /* @__PURE__ */ jsx(Td, { children: /* @__PURE__ */ jsx(Typography, { size: "sm", children: contentType.indexed || 0 }) }),
333
+ /* @__PURE__ */ jsx(Td, { children: contentType.isSynced ? /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
334
+ /* @__PURE__ */ jsx(Badge, { background: "success600", textColor: "neutral0", children: /* @__PURE__ */ jsx(Check, {}) }),
335
+ /* @__PURE__ */ jsxs(Typography, { size: "sm", weight: "bold", children: [
336
+ contentType.syncPercentage || 0,
337
+ "%"
338
+ ] })
339
+ ] }) : /* @__PURE__ */ jsxs(Flex, { gap: 2, alignItems: "center", children: [
340
+ /* @__PURE__ */ jsx(Badge, { background: "warning600", textColor: "neutral0", children: /* @__PURE__ */ jsx(DangerIcon, { size: 16, color: "white" }) }),
341
+ /* @__PURE__ */ jsxs(Typography, { size: "sm", children: [
342
+ contentType.syncPercentage || 0,
343
+ "%"
344
+ ] })
345
+ ] }) }),
346
+ /* @__PURE__ */ jsx(Td, { children: contentType.isIndexed && /* @__PURE__ */ jsxs(Flex, { gap: 2, children: [
347
+ /* @__PURE__ */ jsx(
348
+ Button,
349
+ {
350
+ size: "S",
351
+ variant: "secondary",
352
+ disabled: loadingState[contentType.name],
353
+ onClick: () => handleUpdate(contentType.name),
354
+ children: i18n("plugin.button.update", "Update")
355
+ }
356
+ ),
357
+ /* @__PURE__ */ jsx(
358
+ Button,
359
+ {
360
+ size: "S",
361
+ variant: "tertiary",
362
+ disabled: loadingState[contentType.name],
363
+ onClick: () => handleReindex(contentType.name),
364
+ children: i18n("plugin.button.reindex", "Reindex")
365
+ }
366
+ )
367
+ ] }) }),
368
+ /* @__PURE__ */ jsx(Td, { children: loadingState[contentType.name] && /* @__PURE__ */ jsx(Loader, {}) })
369
+ ] }, contentType.uid || contentType.name)) })
370
+ ] }) })
371
+ ] });
372
+ });
373
+ CollectionsTable.displayName = "CollectionsTable";
374
+ export {
375
+ CollectionsTable as default
376
+ };