@orange-soft/strapi-deployment-trigger 1.0.1 → 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.
@@ -8,9 +8,17 @@ import {
8
8
  Alert,
9
9
  Loader,
10
10
  Field,
11
+ Table,
12
+ Thead,
13
+ Tbody,
14
+ Tr,
15
+ Th,
16
+ Td,
17
+ IconButton,
18
+ Dialog,
11
19
  Grid,
12
20
  } from '@strapi/design-system';
13
- import { Check } from '@strapi/icons';
21
+ import { Check, Plus, Pencil, Trash } from '@strapi/icons';
14
22
 
15
23
  import { PLUGIN_ID } from '../pluginId';
16
24
 
@@ -20,7 +28,7 @@ const REPO_URL_PATTERN = /^https:\/\/github\.com\/[a-zA-Z0-9_.-]+\/[a-zA-Z0-9_.-
20
28
  const WORKFLOW_PATTERN = /^[a-zA-Z0-9_.-]+\.ya?ml$/;
21
29
 
22
30
  const validateToken = (value) => {
23
- if (!value) return null; // Optional if already configured
31
+ if (!value) return null;
24
32
  if (!TOKEN_PATTERN.test(value)) {
25
33
  return 'Token must start with "github_pat_" followed by alphanumeric characters';
26
34
  }
@@ -36,7 +44,7 @@ const validateRepoUrl = (value) => {
36
44
  };
37
45
 
38
46
  const validateWorkflow = (value) => {
39
- if (!value) return null; // Optional, has default
47
+ if (!value) return 'Workflow file is required';
40
48
  if (!WORKFLOW_PATTERN.test(value)) {
41
49
  return 'Workflow file must end with .yml or .yaml';
42
50
  }
@@ -44,12 +52,11 @@ const validateWorkflow = (value) => {
44
52
  };
45
53
 
46
54
  const SettingsPage = () => {
47
- const { get, put } = useFetchClient();
55
+ const { get, put, post, del } = useFetchClient();
48
56
  const [settings, setSettings] = useState({
49
57
  githubToken: '',
50
58
  repoUrl: '',
51
- workflow: '',
52
- branch: '',
59
+ targets: [],
53
60
  });
54
61
  const [errors, setErrors] = useState({});
55
62
  const [hasExistingToken, setHasExistingToken] = useState(false);
@@ -58,6 +65,14 @@ const SettingsPage = () => {
58
65
  const [saving, setSaving] = useState(false);
59
66
  const [notification, setNotification] = useState(null);
60
67
 
68
+ // Target form state
69
+ const [editingTarget, setEditingTarget] = useState(null);
70
+ const [targetForm, setTargetForm] = useState({ name: '', workflow: '', branch: '' });
71
+ const [targetErrors, setTargetErrors] = useState({});
72
+ const [showAddForm, setShowAddForm] = useState(false);
73
+ const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
74
+ const [targetToDelete, setTargetToDelete] = useState(null);
75
+
61
76
  useEffect(() => {
62
77
  fetchSettings();
63
78
  }, []);
@@ -70,70 +85,52 @@ const SettingsPage = () => {
70
85
  setSettings({
71
86
  githubToken: '',
72
87
  repoUrl: fetchedSettings.repoUrl || '',
73
- workflow: fetchedSettings.workflow || '',
74
- branch: fetchedSettings.branch || '',
88
+ targets: fetchedSettings.targets || [],
75
89
  });
76
90
  setHasExistingToken(fetchedSettings.hasToken || false);
77
91
  setMaskedToken(fetchedSettings.maskedToken || null);
78
92
  } catch (error) {
79
93
  console.error('Error fetching settings:', error);
80
- setNotification({
81
- type: 'danger',
82
- message: 'Failed to load settings',
83
- });
94
+ setNotification({ type: 'danger', message: 'Failed to load settings' });
84
95
  }
85
96
  setLoading(false);
86
97
  };
87
98
 
88
- const validateAll = () => {
99
+ const handleSaveSettings = async () => {
89
100
  const newErrors = {};
90
-
91
- const tokenError = validateToken(settings.githubToken);
92
- if (tokenError && settings.githubToken) newErrors.githubToken = tokenError;
93
-
94
101
  const repoError = validateRepoUrl(settings.repoUrl);
95
102
  if (repoError) newErrors.repoUrl = repoError;
96
103
 
97
- const workflowError = validateWorkflow(settings.workflow);
98
- if (workflowError) newErrors.workflow = workflowError;
104
+ const tokenError = validateToken(settings.githubToken);
105
+ if (tokenError && settings.githubToken) newErrors.githubToken = tokenError;
99
106
 
100
107
  setErrors(newErrors);
101
- return Object.keys(newErrors).length === 0;
102
- };
103
-
104
- const handleSave = async () => {
105
- if (!validateAll()) {
106
- setNotification({
107
- type: 'warning',
108
- message: 'Please fix the validation errors before saving',
109
- });
108
+ if (Object.keys(newErrors).length > 0) {
109
+ setNotification({ type: 'warning', message: 'Please fix the validation errors' });
110
110
  return;
111
111
  }
112
112
 
113
113
  setSaving(true);
114
114
  try {
115
- const dataToSave = { ...settings };
116
- if (!dataToSave.githubToken) {
117
- delete dataToSave.githubToken;
115
+ const dataToSave = {
116
+ repoUrl: settings.repoUrl,
117
+ targets: settings.targets,
118
+ };
119
+ if (settings.githubToken) {
120
+ dataToSave.githubToken = settings.githubToken;
118
121
  }
119
122
 
120
- await put(`/${PLUGIN_ID}/settings`, { data: dataToSave });
123
+ const { data } = await put(`/${PLUGIN_ID}/settings`, { data: dataToSave });
121
124
 
122
125
  if (settings.githubToken) {
123
126
  setHasExistingToken(true);
127
+ setMaskedToken(data.data?.maskedToken);
124
128
  }
125
129
  setSettings(prev => ({ ...prev, githubToken: '' }));
126
-
127
- setNotification({
128
- type: 'success',
129
- message: 'Settings saved successfully',
130
- });
130
+ setNotification({ type: 'success', message: 'Settings saved successfully' });
131
131
  } catch (error) {
132
132
  console.error('Error saving settings:', error);
133
- setNotification({
134
- type: 'danger',
135
- message: 'Failed to save settings',
136
- });
133
+ setNotification({ type: 'danger', message: 'Failed to save settings' });
137
134
  }
138
135
  setSaving(false);
139
136
  };
@@ -141,17 +138,92 @@ const SettingsPage = () => {
141
138
  const handleChange = (field) => (e) => {
142
139
  const value = e.target.value;
143
140
  setSettings(prev => ({ ...prev, [field]: value }));
144
-
145
- // Clear error when user starts typing
146
141
  if (errors[field]) {
147
142
  setErrors(prev => ({ ...prev, [field]: null }));
148
143
  }
149
144
  };
150
145
 
151
- const handleBlur = (field, validator) => () => {
152
- const error = validator(settings[field]);
153
- if (error) {
154
- setErrors(prev => ({ ...prev, [field]: error }));
146
+ // Target management
147
+ const resetTargetForm = () => {
148
+ setTargetForm({ name: '', workflow: 'deploy.yml', branch: 'master' });
149
+ setTargetErrors({});
150
+ setEditingTarget(null);
151
+ setShowAddForm(false);
152
+ };
153
+
154
+ const validateTargetForm = () => {
155
+ const newErrors = {};
156
+ if (!targetForm.name.trim()) newErrors.name = 'Name is required';
157
+ const workflowError = validateWorkflow(targetForm.workflow);
158
+ if (workflowError) newErrors.workflow = workflowError;
159
+ if (!targetForm.branch.trim()) newErrors.branch = 'Branch is required';
160
+ setTargetErrors(newErrors);
161
+ return Object.keys(newErrors).length === 0;
162
+ };
163
+
164
+ const handleAddTarget = async () => {
165
+ if (!validateTargetForm()) return;
166
+
167
+ try {
168
+ const { data } = await post(`/${PLUGIN_ID}/targets`, { data: targetForm });
169
+ setSettings(prev => ({
170
+ ...prev,
171
+ targets: [...prev.targets, data.data],
172
+ }));
173
+ resetTargetForm();
174
+ setNotification({ type: 'success', message: 'Target added successfully' });
175
+ } catch (error) {
176
+ console.error('Error adding target:', error);
177
+ setNotification({ type: 'danger', message: 'Failed to add target' });
178
+ }
179
+ };
180
+
181
+ const handleEditTarget = (target) => {
182
+ setEditingTarget(target.id);
183
+ setTargetForm({ name: target.name, workflow: target.workflow, branch: target.branch });
184
+ setShowAddForm(false);
185
+ };
186
+
187
+ const handleUpdateTarget = async () => {
188
+ if (!validateTargetForm()) return;
189
+
190
+ try {
191
+ const { data } = await put(`/${PLUGIN_ID}/targets/${editingTarget}`, { data: targetForm });
192
+ setSettings(prev => ({
193
+ ...prev,
194
+ targets: prev.targets.map(t => t.id === editingTarget ? data.data : t),
195
+ }));
196
+ resetTargetForm();
197
+ setNotification({ type: 'success', message: 'Target updated successfully' });
198
+ } catch (error) {
199
+ console.error('Error updating target:', error);
200
+ setNotification({ type: 'danger', message: 'Failed to update target' });
201
+ }
202
+ };
203
+
204
+ const handleDeleteTarget = async () => {
205
+ if (!targetToDelete) return;
206
+
207
+ try {
208
+ await del(`/${PLUGIN_ID}/targets/${targetToDelete}`);
209
+ setSettings(prev => ({
210
+ ...prev,
211
+ targets: prev.targets.filter(t => t.id !== targetToDelete),
212
+ }));
213
+ setDeleteDialogOpen(false);
214
+ setTargetToDelete(null);
215
+ setNotification({ type: 'success', message: 'Target deleted successfully' });
216
+ } catch (error) {
217
+ console.error('Error deleting target:', error);
218
+ setNotification({ type: 'danger', message: 'Failed to delete target' });
219
+ }
220
+ };
221
+
222
+ const handleTargetFormChange = (field) => (e) => {
223
+ const value = e.target.value;
224
+ setTargetForm(prev => ({ ...prev, [field]: value }));
225
+ if (targetErrors[field]) {
226
+ setTargetErrors(prev => ({ ...prev, [field]: null }));
155
227
  }
156
228
  };
157
229
 
@@ -172,7 +244,7 @@ const SettingsPage = () => {
172
244
  );
173
245
  }
174
246
 
175
- const isValid = settings.repoUrl && !errors.repoUrl && !errors.githubToken && !errors.workflow;
247
+ const isValid = settings.repoUrl && !errors.repoUrl && !errors.githubToken;
176
248
 
177
249
  return (
178
250
  <Layouts.Root>
@@ -182,7 +254,7 @@ const SettingsPage = () => {
182
254
  navigationAction={<BackButton fallback={`/plugins/${PLUGIN_ID}`} />}
183
255
  primaryAction={
184
256
  <Button
185
- onClick={handleSave}
257
+ onClick={handleSaveSettings}
186
258
  loading={saving}
187
259
  disabled={loading || !isValid}
188
260
  startIcon={<Check />}
@@ -231,7 +303,6 @@ const SettingsPage = () => {
231
303
  placeholder="https://github.com/{owner}/{repo}"
232
304
  value={settings.repoUrl}
233
305
  onChange={handleChange('repoUrl')}
234
- onBlur={handleBlur('repoUrl', validateRepoUrl)}
235
306
  />
236
307
  <Field.Hint />
237
308
  <Field.Error />
@@ -239,7 +310,7 @@ const SettingsPage = () => {
239
310
  </Flex>
240
311
  </Box>
241
312
 
242
- {/* Workflow Section */}
313
+ {/* Deployment Targets Section */}
243
314
  <Box
244
315
  background="neutral0"
245
316
  hasRadius
@@ -250,46 +321,134 @@ const SettingsPage = () => {
250
321
  paddingRight={7}
251
322
  >
252
323
  <Flex direction="column" alignItems="stretch" gap={4}>
253
- <Typography variant="delta" tag="h2">
254
- Workflow Configuration
255
- </Typography>
256
-
257
- <Grid.Root gap={4}>
258
- <Grid.Item col={6} s={12}>
259
- <Field.Root
260
- name="workflow"
261
- required
262
- error={errors.workflow}
263
- hint="Filename in .github/workflows/"
264
- >
265
- <Field.Label>Workflow File</Field.Label>
266
- <Field.Input
267
- placeholder="deploy.yml"
268
- value={settings.workflow}
269
- onChange={handleChange('workflow')}
270
- onBlur={handleBlur('workflow', validateWorkflow)}
271
- />
272
- <Field.Hint />
273
- <Field.Error />
274
- </Field.Root>
275
- </Grid.Item>
276
-
277
- <Grid.Item col={6} s={12}>
278
- <Field.Root
279
- name="branch"
280
- required
281
- hint="Branch to trigger the workflow on"
324
+ <Flex justifyContent="space-between" alignItems="center">
325
+ <Typography variant="delta" tag="h2">
326
+ Deployment Targets
327
+ </Typography>
328
+ {!showAddForm && !editingTarget && (
329
+ <Button
330
+ startIcon={<Plus />}
331
+ onClick={() => {
332
+ setShowAddForm(true);
333
+ setTargetForm({ name: '', workflow: 'deploy.yml', branch: 'master' });
334
+ }}
335
+ size="S"
282
336
  >
283
- <Field.Label>Branch</Field.Label>
284
- <Field.Input
285
- placeholder="main"
286
- value={settings.branch}
287
- onChange={handleChange('branch')}
288
- />
289
- <Field.Hint />
290
- </Field.Root>
291
- </Grid.Item>
292
- </Grid.Root>
337
+ Add Target
338
+ </Button>
339
+ )}
340
+ </Flex>
341
+
342
+ {/* Add/Edit Form */}
343
+ {(showAddForm || editingTarget) && (
344
+ <Box
345
+ background="neutral100"
346
+ padding={4}
347
+ hasRadius
348
+ >
349
+ <Flex direction="column" gap={4}>
350
+ <Typography variant="omega" fontWeight="bold">
351
+ {editingTarget ? 'Edit Target' : 'Add New Target'}
352
+ </Typography>
353
+ <Grid.Root gap={4}>
354
+ <Grid.Item col={4} s={12}>
355
+ <Field.Root name="targetName" required error={targetErrors.name}>
356
+ <Field.Label>Name</Field.Label>
357
+ <Field.Input
358
+ placeholder="e.g., Production"
359
+ value={targetForm.name}
360
+ onChange={handleTargetFormChange('name')}
361
+ />
362
+ <Field.Error />
363
+ </Field.Root>
364
+ </Grid.Item>
365
+ <Grid.Item col={4} s={12}>
366
+ <Field.Root name="targetWorkflow" required error={targetErrors.workflow}>
367
+ <Field.Label>Workflow File</Field.Label>
368
+ <Field.Input
369
+ placeholder="deploy.yml"
370
+ value={targetForm.workflow}
371
+ onChange={handleTargetFormChange('workflow')}
372
+ />
373
+ <Field.Error />
374
+ </Field.Root>
375
+ </Grid.Item>
376
+ <Grid.Item col={4} s={12}>
377
+ <Field.Root name="targetBranch" required error={targetErrors.branch}>
378
+ <Field.Label>Branch</Field.Label>
379
+ <Field.Input
380
+ placeholder="main"
381
+ value={targetForm.branch}
382
+ onChange={handleTargetFormChange('branch')}
383
+ />
384
+ <Field.Error />
385
+ </Field.Root>
386
+ </Grid.Item>
387
+ </Grid.Root>
388
+ <Flex gap={2} justifyContent="flex-end">
389
+ <Button variant="tertiary" onClick={resetTargetForm}>
390
+ Cancel
391
+ </Button>
392
+ <Button
393
+ onClick={editingTarget ? handleUpdateTarget : handleAddTarget}
394
+ startIcon={<Check />}
395
+ >
396
+ {editingTarget ? 'Update' : 'Add'}
397
+ </Button>
398
+ </Flex>
399
+ </Flex>
400
+ </Box>
401
+ )}
402
+
403
+ {/* Targets Table */}
404
+ {settings.targets.length > 0 ? (
405
+ <Table>
406
+ <Thead>
407
+ <Tr>
408
+ <Th><Typography variant="sigma">Name</Typography></Th>
409
+ <Th><Typography variant="sigma">Workflow</Typography></Th>
410
+ <Th><Typography variant="sigma">Branch</Typography></Th>
411
+ <Th><Typography variant="sigma">Actions</Typography></Th>
412
+ </Tr>
413
+ </Thead>
414
+ <Tbody>
415
+ {settings.targets.map((target) => (
416
+ <Tr key={target.id}>
417
+ <Td><Typography variant="omega">{target.name}</Typography></Td>
418
+ <Td><Typography variant="omega">{target.workflow}</Typography></Td>
419
+ <Td><Typography variant="omega">{target.branch}</Typography></Td>
420
+ <Td>
421
+ <Flex gap={1}>
422
+ <IconButton
423
+ onClick={() => handleEditTarget(target)}
424
+ label="Edit"
425
+ variant="ghost"
426
+ >
427
+ <Pencil />
428
+ </IconButton>
429
+ <IconButton
430
+ onClick={() => {
431
+ setTargetToDelete(target.id);
432
+ setDeleteDialogOpen(true);
433
+ }}
434
+ label="Delete"
435
+ variant="ghost"
436
+ >
437
+ <Trash />
438
+ </IconButton>
439
+ </Flex>
440
+ </Td>
441
+ </Tr>
442
+ ))}
443
+ </Tbody>
444
+ </Table>
445
+ ) : (
446
+ !showAddForm && (
447
+ <Typography variant="pi" textColor="neutral600">
448
+ No deployment targets configured. Click "Add Target" to create one.
449
+ </Typography>
450
+ )
451
+ )}
293
452
  </Flex>
294
453
  </Box>
295
454
 
@@ -324,17 +483,11 @@ const SettingsPage = () => {
324
483
  placeholder={hasExistingToken ? "••••••••••••••••" : "github_pat_xxxxxxxxxxxx"}
325
484
  value={settings.githubToken}
326
485
  onChange={handleChange('githubToken')}
327
- onBlur={handleBlur('githubToken', validateToken)}
328
486
  />
329
487
  {hasExistingToken && maskedToken ? (
330
488
  <Typography variant="pi" textColor="neutral600">
331
489
  Existing token:{' '}
332
- <Typography
333
- variant="pi"
334
- fontWeight="bold"
335
- textColor="success600"
336
- tag="span"
337
- >
490
+ <Typography variant="pi" fontWeight="bold" textColor="success600" tag="span">
338
491
  {maskedToken}
339
492
  </Typography>
340
493
  . Leave empty to keep existing, or enter new to replace.
@@ -362,6 +515,26 @@ const SettingsPage = () => {
362
515
  </Flex>
363
516
  </Box>
364
517
  </Flex>
518
+
519
+ {/* Delete Confirmation Dialog */}
520
+ <Dialog.Root open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
521
+ <Dialog.Content>
522
+ <Dialog.Header>Delete Target</Dialog.Header>
523
+ <Dialog.Body>
524
+ Are you sure you want to delete this deployment target? This action cannot be undone.
525
+ </Dialog.Body>
526
+ <Dialog.Footer>
527
+ <Dialog.Cancel>
528
+ <Button variant="tertiary">Cancel</Button>
529
+ </Dialog.Cancel>
530
+ <Dialog.Action>
531
+ <Button variant="danger-light" onClick={handleDeleteTarget}>
532
+ Delete
533
+ </Button>
534
+ </Dialog.Action>
535
+ </Dialog.Footer>
536
+ </Dialog.Content>
537
+ </Dialog.Root>
365
538
  </Layouts.Content>
366
539
  </Layouts.Root>
367
540
  );