strapi-content-sync-pro 1.0.4 → 1.0.6
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/LICENSE +1 -1
- package/README.md +32 -14
- package/admin/src/components/BulkTransferTab.jsx +185 -20
- package/admin/src/components/ConfigTab.jsx +81 -3
- package/admin/src/components/ContentTypesTab.jsx +28 -1
- package/admin/src/components/HelpTab.jsx +34 -0
- package/admin/src/components/LogsTab.jsx +66 -8
- package/admin/src/components/MediaTab.jsx +253 -36
- package/admin/src/components/SyncProfilesTab.jsx +140 -4
- package/admin/src/components/SyncTab.jsx +161 -35
- package/docs/Screenshot 2026-04-22 183540.png +0 -0
- package/docs/Screenshot 2026-04-22 183552.png +0 -0
- package/docs/Screenshot 2026-04-23 114332.png +0 -0
- package/docs/Screenshot 2026-04-23 114644.png +0 -0
- package/docs/Screenshot 2026-04-23 114651.png +0 -0
- package/docs/Screenshot 2026-04-23 114737.png +0 -0
- package/docs/Screenshot 2026-04-23 114904.png +0 -0
- package/docs/Screenshot 2026-04-23 114940.png +0 -0
- package/docs/Screenshot 2026-04-23 115003.png +0 -0
- package/docs/Screenshot 2026-04-23 115024.png +0 -0
- package/docs/Screenshot 2026-04-23 115116.png +0 -0
- package/docs/Screenshot 2026-04-23 115141.png +0 -0
- package/docs/Screenshot 2026-04-23 115252.png +0 -0
- package/docs/Screenshot 2026-04-23 115448.png +0 -0
- package/docs/Screenshot 2026-04-23 120534.png +0 -0
- package/docs/Screenshot 2026-04-23 122544.png +0 -0
- package/docs/Screenshot 2026-04-23 122712.png +0 -0
- package/docs/Screenshot 2026-04-23 122730.png +0 -0
- package/docs/Screenshot 2026-04-23 122858.png +0 -0
- package/docs/Screenshot 2026-04-23 122924.png +0 -0
- package/docs/Screenshot 2026-04-23 122937.png +0 -0
- package/docs/sync-strategy-approach-review.md +127 -0
- package/package.json +1 -1
- package/server/src/controllers/config.js +76 -3
- package/server/src/controllers/sync-media.js +24 -0
- package/server/src/routes/index.js +3 -0
- package/server/src/services/bulk-transfer.js +45 -1
- package/server/src/services/dependency-resolver.js +37 -0
- package/server/src/services/sync-execution.js +21 -9
- package/server/src/services/sync-media.js +168 -32
- package/server/src/services/sync-profiles.js +36 -15
- package/server/src/services/sync.js +234 -134
- package/server/src/utils/fetcher.js +7 -0
- package/docs/Screenshot 2026-04-20 160506.png +0 -0
- package/docs/Screenshot 2026-04-20 160558.png +0 -0
- package/docs/Screenshot 2026-04-20 175903.png +0 -0
- package/docs/Screenshot 2026-04-20 175931.png +0 -0
- package/docs/Screenshot 2026-04-20 180001.png +0 -0
- package/docs/Screenshot 2026-04-20 180041.png +0 -0
- package/docs/Screenshot 2026-04-20 180116.png +0 -0
- package/docs/Screenshot 2026-04-20 180135.png +0 -0
- package/docs/Screenshot 2026-04-20 180202.png +0 -0
- package/docs/Screenshot 2026-04-20 180228.png +0 -0
- package/docs/Screenshot 2026-04-20 180251.png +0 -0
- package/docs/Screenshot 2026-04-20 180301.png +0 -0
- package/docs/clipchamp-screen-recording-script.md +0 -0
- package/docs/production-readiness-status.md +0 -34
- package/docs/production-readiness-test-matrix.md +0 -151
- package/docs/test-environments-setup-legacy.txt +0 -60
|
@@ -38,6 +38,11 @@ const CONFLICT_STRATEGY_OPTIONS = [
|
|
|
38
38
|
{ value: 'remote_wins', label: 'Remote Wins' },
|
|
39
39
|
];
|
|
40
40
|
|
|
41
|
+
const EXECUTION_STRATEGY_OPTIONS = [
|
|
42
|
+
{ value: 'hybrid_two_pass', label: 'Hybrid Two-Pass (Recommended)', hint: 'Pass 1: entities. Pass 2: relations from owner side. Most reliable.' },
|
|
43
|
+
{ value: 'one_pass', label: 'One-Pass (Advanced)', hint: 'Single pass. Depth fixed to 1. Only owner-side direct in-scope targets. Less reliable for relation-heavy content.' },
|
|
44
|
+
];
|
|
45
|
+
|
|
41
46
|
const FIELD_DIRECTION_OPTIONS = [
|
|
42
47
|
{ value: 'both', label: 'Both' },
|
|
43
48
|
{ value: 'push', label: 'Push' },
|
|
@@ -64,6 +69,11 @@ const SyncProfilesTab = () => {
|
|
|
64
69
|
const [sortField, setSortField] = useState('name');
|
|
65
70
|
const [sortDirection, setSortDirection] = useState('asc');
|
|
66
71
|
|
|
72
|
+
// Filter state
|
|
73
|
+
const [filterName, setFilterName] = useState('');
|
|
74
|
+
const [filterDirection, setFilterDirection] = useState('');
|
|
75
|
+
const [filterStatus, setFilterStatus] = useState('');
|
|
76
|
+
|
|
67
77
|
// Selection state for bulk operations
|
|
68
78
|
const [selectedProfiles, setSelectedProfiles] = useState([]);
|
|
69
79
|
|
|
@@ -77,6 +87,7 @@ const SyncProfilesTab = () => {
|
|
|
77
87
|
contentType: '',
|
|
78
88
|
direction: 'both',
|
|
79
89
|
conflictStrategy: 'latest',
|
|
90
|
+
executionStrategy: 'hybrid_two_pass',
|
|
80
91
|
syncDeletions: false,
|
|
81
92
|
isActive: false,
|
|
82
93
|
isSimple: true,
|
|
@@ -86,9 +97,26 @@ const SyncProfilesTab = () => {
|
|
|
86
97
|
const [loadingSchema, setLoadingSchema] = useState(false);
|
|
87
98
|
const [syncMode, setSyncMode] = useState('paired');
|
|
88
99
|
|
|
89
|
-
// Sorted profiles
|
|
100
|
+
// Sorted + filtered profiles
|
|
90
101
|
const sortedProfiles = useMemo(() => {
|
|
91
|
-
|
|
102
|
+
let filtered = [...profiles];
|
|
103
|
+
|
|
104
|
+
if (filterName.trim()) {
|
|
105
|
+
const q = filterName.trim().toLowerCase();
|
|
106
|
+
filtered = filtered.filter(
|
|
107
|
+
(p) => p.name.toLowerCase().includes(q) || p.contentType.toLowerCase().includes(q)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
if (filterDirection) {
|
|
111
|
+
filtered = filtered.filter((p) => p.direction === filterDirection);
|
|
112
|
+
}
|
|
113
|
+
if (filterStatus) {
|
|
114
|
+
filtered = filtered.filter((p) =>
|
|
115
|
+
filterStatus === 'active' ? p.isActive : !p.isActive
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
filtered.sort((a, b) => {
|
|
92
120
|
let aVal = a[sortField];
|
|
93
121
|
let bVal = b[sortField];
|
|
94
122
|
|
|
@@ -113,8 +141,8 @@ const SyncProfilesTab = () => {
|
|
|
113
141
|
}
|
|
114
142
|
return sortDirection === 'asc' ? aVal - bVal : bVal - aVal;
|
|
115
143
|
});
|
|
116
|
-
return
|
|
117
|
-
}, [profiles, sortField, sortDirection, contentTypes]);
|
|
144
|
+
return filtered;
|
|
145
|
+
}, [profiles, sortField, sortDirection, contentTypes, filterName, filterDirection, filterStatus]);
|
|
118
146
|
|
|
119
147
|
const handleSort = (field) => {
|
|
120
148
|
if (sortField === field) {
|
|
@@ -291,6 +319,7 @@ const SyncProfilesTab = () => {
|
|
|
291
319
|
contentType: '',
|
|
292
320
|
direction: 'both',
|
|
293
321
|
conflictStrategy: 'latest',
|
|
322
|
+
executionStrategy: 'hybrid_two_pass',
|
|
294
323
|
syncDeletions: false,
|
|
295
324
|
isActive: false,
|
|
296
325
|
isSimple: true,
|
|
@@ -308,6 +337,7 @@ const SyncProfilesTab = () => {
|
|
|
308
337
|
contentType: profile.contentType,
|
|
309
338
|
direction: profile.direction || 'both',
|
|
310
339
|
conflictStrategy: profile.conflictStrategy || 'latest',
|
|
340
|
+
executionStrategy: profile.executionStrategy || 'hybrid_two_pass',
|
|
311
341
|
syncDeletions: !!profile.syncDeletions,
|
|
312
342
|
isActive: profile.isActive,
|
|
313
343
|
isSimple: profile.isSimple !== false,
|
|
@@ -402,6 +432,16 @@ const SyncProfilesTab = () => {
|
|
|
402
432
|
}
|
|
403
433
|
};
|
|
404
434
|
|
|
435
|
+
const handleDeactivate = async (profile) => {
|
|
436
|
+
try {
|
|
437
|
+
await put(`/${PLUGIN_ID}/sync-profiles/${profile.id}`, { isActive: false });
|
|
438
|
+
setMessage({ type: 'success', text: `Deactivated: ${profile.name}` });
|
|
439
|
+
loadData();
|
|
440
|
+
} catch (err) {
|
|
441
|
+
setMessage({ type: 'danger', text: err?.response?.data?.error?.message || err.message || 'Failed to deactivate profile' });
|
|
442
|
+
}
|
|
443
|
+
};
|
|
444
|
+
|
|
405
445
|
const getContentTypeName = (uid) => {
|
|
406
446
|
const ct = contentTypes.find((c) => c.uid === uid);
|
|
407
447
|
return ct?.displayName || uid;
|
|
@@ -472,6 +512,55 @@ const SyncProfilesTab = () => {
|
|
|
472
512
|
)}
|
|
473
513
|
|
|
474
514
|
<Box paddingTop={4}>
|
|
515
|
+
{/* Filter Bar */}
|
|
516
|
+
<Flex gap={3} wrap="wrap" marginBottom={4} alignItems="flex-end">
|
|
517
|
+
<Box style={{ flex: '1 1 200px', minWidth: 160 }}>
|
|
518
|
+
<TextInput
|
|
519
|
+
placeholder="Search by name or type…"
|
|
520
|
+
value={filterName}
|
|
521
|
+
onChange={(e) => setFilterName(e.target.value)}
|
|
522
|
+
label="Search"
|
|
523
|
+
size="S"
|
|
524
|
+
/>
|
|
525
|
+
</Box>
|
|
526
|
+
<Box style={{ minWidth: 160 }}>
|
|
527
|
+
<SingleSelect
|
|
528
|
+
placeholder="All directions"
|
|
529
|
+
value={filterDirection}
|
|
530
|
+
onChange={setFilterDirection}
|
|
531
|
+
onClear={() => setFilterDirection('')}
|
|
532
|
+
size="S"
|
|
533
|
+
label="Direction"
|
|
534
|
+
>
|
|
535
|
+
<SingleSelectOption value="push">Push Only</SingleSelectOption>
|
|
536
|
+
<SingleSelectOption value="pull">Pull Only</SingleSelectOption>
|
|
537
|
+
<SingleSelectOption value="both">Bidirectional</SingleSelectOption>
|
|
538
|
+
</SingleSelect>
|
|
539
|
+
</Box>
|
|
540
|
+
<Box style={{ minWidth: 140 }}>
|
|
541
|
+
<SingleSelect
|
|
542
|
+
placeholder="All statuses"
|
|
543
|
+
value={filterStatus}
|
|
544
|
+
onChange={setFilterStatus}
|
|
545
|
+
onClear={() => setFilterStatus('')}
|
|
546
|
+
size="S"
|
|
547
|
+
label="Status"
|
|
548
|
+
>
|
|
549
|
+
<SingleSelectOption value="active">Active</SingleSelectOption>
|
|
550
|
+
<SingleSelectOption value="inactive">Inactive</SingleSelectOption>
|
|
551
|
+
</SingleSelect>
|
|
552
|
+
</Box>
|
|
553
|
+
{(filterName || filterDirection || filterStatus) && (
|
|
554
|
+
<Button
|
|
555
|
+
variant="tertiary"
|
|
556
|
+
size="S"
|
|
557
|
+
onClick={() => { setFilterName(''); setFilterDirection(''); setFilterStatus(''); }}
|
|
558
|
+
>
|
|
559
|
+
Clear filters
|
|
560
|
+
</Button>
|
|
561
|
+
)}
|
|
562
|
+
</Flex>
|
|
563
|
+
|
|
475
564
|
{profiles.length === 0 ? (
|
|
476
565
|
<Box padding={6} background="neutral0" hasRadius>
|
|
477
566
|
<Typography textColor="neutral600">
|
|
@@ -479,6 +568,10 @@ const SyncProfilesTab = () => {
|
|
|
479
568
|
or create a custom profile.
|
|
480
569
|
</Typography>
|
|
481
570
|
</Box>
|
|
571
|
+
) : sortedProfiles.length === 0 ? (
|
|
572
|
+
<Box padding={6} background="neutral0" hasRadius>
|
|
573
|
+
<Typography textColor="neutral600">No profiles match the current filters.</Typography>
|
|
574
|
+
</Box>
|
|
482
575
|
) : (
|
|
483
576
|
<Table>
|
|
484
577
|
<Thead>
|
|
@@ -523,6 +616,12 @@ const SyncProfilesTab = () => {
|
|
|
523
616
|
<Badge active={!profile.isSimple}>
|
|
524
617
|
{profile.isSimple ? 'Simple' : 'Advanced'}
|
|
525
618
|
</Badge>
|
|
619
|
+
{' '}
|
|
620
|
+
<Badge active={profile.executionStrategy !== 'one_pass'} title={
|
|
621
|
+
profile.executionStrategy === 'one_pass' ? 'One-Pass (depth=1, owner-side only)' : 'Hybrid Two-Pass (entities then relations)'
|
|
622
|
+
}>
|
|
623
|
+
{profile.executionStrategy === 'one_pass' ? '1-Pass' : '2-Pass'}
|
|
624
|
+
</Badge>
|
|
526
625
|
</Td>
|
|
527
626
|
<Td>
|
|
528
627
|
{profile.isActive ? (
|
|
@@ -533,6 +632,23 @@ const SyncProfilesTab = () => {
|
|
|
533
632
|
</Td>
|
|
534
633
|
<Td>
|
|
535
634
|
<Flex gap={1}>
|
|
635
|
+
{profile.isActive ? (
|
|
636
|
+
<Button
|
|
637
|
+
variant="tertiary"
|
|
638
|
+
size="S"
|
|
639
|
+
onClick={() => handleDeactivate(profile)}
|
|
640
|
+
>
|
|
641
|
+
Deactivate
|
|
642
|
+
</Button>
|
|
643
|
+
) : (
|
|
644
|
+
<Button
|
|
645
|
+
variant="success"
|
|
646
|
+
size="S"
|
|
647
|
+
onClick={() => handleActivate(profile)}
|
|
648
|
+
>
|
|
649
|
+
Activate
|
|
650
|
+
</Button>
|
|
651
|
+
)}
|
|
536
652
|
<IconButton label="Edit" onClick={() => openEditModal(profile)}>
|
|
537
653
|
<Pencil />
|
|
538
654
|
</IconButton>
|
|
@@ -669,6 +785,26 @@ const SyncProfilesTab = () => {
|
|
|
669
785
|
</Field.Root>
|
|
670
786
|
</Box>
|
|
671
787
|
|
|
788
|
+
{/* Execution Strategy */}
|
|
789
|
+
<Box paddingBottom={4}>
|
|
790
|
+
<Field.Root>
|
|
791
|
+
<Field.Label>Execution Strategy</Field.Label>
|
|
792
|
+
<SingleSelect
|
|
793
|
+
value={formData.executionStrategy || 'hybrid_two_pass'}
|
|
794
|
+
onChange={(value) => setFormData((p) => ({ ...p, executionStrategy: value }))}
|
|
795
|
+
>
|
|
796
|
+
{EXECUTION_STRATEGY_OPTIONS.map((opt) => (
|
|
797
|
+
<SingleSelectOption key={opt.value} value={opt.value}>
|
|
798
|
+
{opt.label}
|
|
799
|
+
</SingleSelectOption>
|
|
800
|
+
))}
|
|
801
|
+
</SingleSelect>
|
|
802
|
+
<Field.Hint>
|
|
803
|
+
{EXECUTION_STRATEGY_OPTIONS.find(o => o.value === (formData.executionStrategy || 'hybrid_two_pass'))?.hint}
|
|
804
|
+
</Field.Hint>
|
|
805
|
+
</Field.Root>
|
|
806
|
+
</Box>
|
|
807
|
+
|
|
672
808
|
{/* Deletions Toggle */}
|
|
673
809
|
<Box paddingBottom={4}>
|
|
674
810
|
<Checkbox
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
Checkbox,
|
|
24
24
|
TextInput,
|
|
25
25
|
} from '@strapi/design-system';
|
|
26
|
-
import { Play, Clock, Cog, ArrowUp, ArrowDown } from '@strapi/icons';
|
|
26
|
+
import { Play, Clock, Cog, ArrowUp, ArrowDown, CaretUp, CaretDown } from '@strapi/icons';
|
|
27
27
|
import { useFetchClient } from '@strapi/strapi/admin';
|
|
28
28
|
|
|
29
29
|
|
|
@@ -59,9 +59,19 @@ const SyncTab = () => {
|
|
|
59
59
|
|
|
60
60
|
// Filter and ordering
|
|
61
61
|
const [profileFilter, setProfileFilter] = useState('all');
|
|
62
|
+
const [profileSearch, setProfileSearch] = useState('');
|
|
62
63
|
const [executionOrder, setExecutionOrder] = useState({}); // { profileId: order }
|
|
63
64
|
const [orderModified, setOrderModified] = useState(false);
|
|
64
65
|
|
|
66
|
+
// Sort state for Execute table
|
|
67
|
+
const [execSortField, setExecSortField] = useState('');
|
|
68
|
+
const [execSortDir, setExecSortDir] = useState('asc');
|
|
69
|
+
|
|
70
|
+
// Sort + search state for Status table
|
|
71
|
+
const [statusSearch, setStatusSearch] = useState('');
|
|
72
|
+
const [statusSortField, setStatusSortField] = useState('');
|
|
73
|
+
const [statusSortDir, setStatusSortDir] = useState('asc');
|
|
74
|
+
|
|
65
75
|
// Selection for batch execution
|
|
66
76
|
const [selectedProfiles, setSelectedProfiles] = useState([]);
|
|
67
77
|
|
|
@@ -85,12 +95,25 @@ const SyncTab = () => {
|
|
|
85
95
|
loadData();
|
|
86
96
|
}, []);
|
|
87
97
|
|
|
98
|
+
// Refresh data whenever this browser tab regains focus (e.g. user activated a
|
|
99
|
+
// profile in the Sync Profiles tab and switches back here).
|
|
100
|
+
useEffect(() => {
|
|
101
|
+
const onVisibility = () => {
|
|
102
|
+
if (document.visibilityState === 'visible') {
|
|
103
|
+
loadData();
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
document.addEventListener('visibilitychange', onVisibility);
|
|
107
|
+
return () => document.removeEventListener('visibilitychange', onVisibility);
|
|
108
|
+
}, []);
|
|
109
|
+
|
|
88
110
|
const loadData = async () => {
|
|
89
111
|
try {
|
|
90
112
|
const [profilesRes, statusRes, globalRes, depsRes, configRes] = await Promise.all([
|
|
91
113
|
get(`/${PLUGIN_ID}/sync-profiles`),
|
|
92
114
|
get(`/${PLUGIN_ID}/sync-execution/status`),
|
|
93
115
|
get(`/${PLUGIN_ID}/sync-execution/global-settings`),
|
|
116
|
+
// dependencies are recomputed from enabled types each load
|
|
94
117
|
get(`/${PLUGIN_ID}/dependencies/all`).catch(() => ({ data: { data: {} } })),
|
|
95
118
|
get(`/${PLUGIN_ID}/config`),
|
|
96
119
|
]);
|
|
@@ -407,7 +430,7 @@ const SyncTab = () => {
|
|
|
407
430
|
return { dependsOn, dependedBy };
|
|
408
431
|
};
|
|
409
432
|
|
|
410
|
-
// Filter and sort profiles
|
|
433
|
+
// Filter and sort profiles for Execute table
|
|
411
434
|
const filteredProfiles = useMemo(() => {
|
|
412
435
|
let result = [...profiles];
|
|
413
436
|
|
|
@@ -416,18 +439,87 @@ const SyncTab = () => {
|
|
|
416
439
|
result = result.filter(p => p.isActive);
|
|
417
440
|
}
|
|
418
441
|
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
442
|
+
// Apply name search
|
|
443
|
+
if (profileSearch.trim()) {
|
|
444
|
+
const q = profileSearch.trim().toLowerCase();
|
|
445
|
+
result = result.filter(
|
|
446
|
+
(p) => p.name.toLowerCase().includes(q) || p.contentType.toLowerCase().includes(q)
|
|
447
|
+
);
|
|
448
|
+
}
|
|
425
449
|
|
|
450
|
+
// Sort by explicit column if chosen, else by execution order
|
|
451
|
+
if (execSortField) {
|
|
452
|
+
result.sort((a, b) => {
|
|
453
|
+
let aVal = a[execSortField] ?? '';
|
|
454
|
+
let bVal = b[execSortField] ?? '';
|
|
455
|
+
if (typeof aVal === 'boolean') { aVal = aVal ? 1 : 0; bVal = bVal ? 1 : 0; }
|
|
456
|
+
if (typeof aVal === 'string') {
|
|
457
|
+
return execSortDir === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
|
|
458
|
+
}
|
|
459
|
+
return execSortDir === 'asc' ? aVal - bVal : bVal - aVal;
|
|
460
|
+
});
|
|
461
|
+
} else {
|
|
462
|
+
// Sort by execution order
|
|
463
|
+
result.sort((a, b) => {
|
|
464
|
+
const orderA = executionOrder[a.id] || 999;
|
|
465
|
+
const orderB = executionOrder[b.id] || 999;
|
|
466
|
+
return orderA - orderB;
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return result;
|
|
471
|
+
}, [profiles, profileFilter, profileSearch, executionOrder, execSortField, execSortDir]);
|
|
472
|
+
|
|
473
|
+
// Filter and sort for Status table
|
|
474
|
+
const filteredStatus = useMemo(() => {
|
|
475
|
+
let result = [...executionStatus];
|
|
476
|
+
if (statusSearch.trim()) {
|
|
477
|
+
const q = statusSearch.trim().toLowerCase();
|
|
478
|
+
result = result.filter((s) => (s.profileName || '').toLowerCase().includes(q));
|
|
479
|
+
}
|
|
480
|
+
if (statusSortField) {
|
|
481
|
+
result.sort((a, b) => {
|
|
482
|
+
let aVal = a[statusSortField] ?? '';
|
|
483
|
+
let bVal = b[statusSortField] ?? '';
|
|
484
|
+
if (typeof aVal === 'boolean') { aVal = aVal ? 1 : 0; bVal = bVal ? 1 : 0; }
|
|
485
|
+
if (typeof aVal === 'string') {
|
|
486
|
+
return statusSortDir === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal);
|
|
487
|
+
}
|
|
488
|
+
return statusSortDir === 'asc' ? aVal - bVal : bVal - aVal;
|
|
489
|
+
});
|
|
490
|
+
}
|
|
426
491
|
return result;
|
|
427
|
-
}, [
|
|
492
|
+
}, [executionStatus, statusSearch, statusSortField, statusSortDir]);
|
|
428
493
|
|
|
429
494
|
const activeProfilesInFilter = filteredProfiles.filter(p => p.isActive);
|
|
430
495
|
|
|
496
|
+
const handleExecSort = (field) => {
|
|
497
|
+
if (execSortField === field) {
|
|
498
|
+
setExecSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
|
|
499
|
+
} else {
|
|
500
|
+
setExecSortField(field);
|
|
501
|
+
setExecSortDir('asc');
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const handleStatusSort = (field) => {
|
|
506
|
+
if (statusSortField === field) {
|
|
507
|
+
setStatusSortDir((d) => (d === 'asc' ? 'desc' : 'asc'));
|
|
508
|
+
} else {
|
|
509
|
+
setStatusSortField(field);
|
|
510
|
+
setStatusSortDir('asc');
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
const SortableTh = ({ field, sortF, sortD, onSort, children }) => (
|
|
515
|
+
<Th onClick={() => onSort(field)} style={{ cursor: 'pointer', userSelect: 'none' }}>
|
|
516
|
+
<Flex alignItems="center" gap={1}>
|
|
517
|
+
<Typography variant="sigma">{children}</Typography>
|
|
518
|
+
{sortF === field && (sortD === 'asc' ? <CaretUp /> : <CaretDown />)}
|
|
519
|
+
</Flex>
|
|
520
|
+
</Th>
|
|
521
|
+
);
|
|
522
|
+
|
|
431
523
|
if (loading) return <Typography>Loading…</Typography>;
|
|
432
524
|
|
|
433
525
|
return (
|
|
@@ -463,6 +555,9 @@ const SyncTab = () => {
|
|
|
463
555
|
<Button onClick={handleSyncAll} loading={syncing} disabled={syncing}>
|
|
464
556
|
{syncing ? 'Syncing…' : 'Sync All Active'}
|
|
465
557
|
</Button>
|
|
558
|
+
<Button variant="tertiary" onClick={loadData} disabled={loading}>
|
|
559
|
+
Refresh
|
|
560
|
+
</Button>
|
|
466
561
|
</Flex>
|
|
467
562
|
</Flex>
|
|
468
563
|
|
|
@@ -508,21 +603,31 @@ const SyncTab = () => {
|
|
|
508
603
|
|
|
509
604
|
<Box paddingTop={4}>
|
|
510
605
|
{/* Filter and Order Controls */}
|
|
511
|
-
<Flex justifyContent="space-between" alignItems="
|
|
512
|
-
<Flex gap={
|
|
606
|
+
<Flex justifyContent="space-between" alignItems="flex-end" marginBottom={4} wrap="wrap" gap={3}>
|
|
607
|
+
<Flex gap={3} alignItems="flex-end" wrap="wrap">
|
|
513
608
|
<Typography variant="delta">Profiles</Typography>
|
|
514
|
-
<
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
609
|
+
<Box style={{ minWidth: 160 }}>
|
|
610
|
+
<SingleSelect
|
|
611
|
+
value={profileFilter}
|
|
612
|
+
onChange={setProfileFilter}
|
|
613
|
+
size="S"
|
|
614
|
+
>
|
|
615
|
+
{FILTER_OPTIONS.map(opt => (
|
|
616
|
+
<SingleSelectOption key={opt.value} value={opt.value}>
|
|
617
|
+
{opt.label}
|
|
618
|
+
</SingleSelectOption>
|
|
619
|
+
))}
|
|
620
|
+
</SingleSelect>
|
|
621
|
+
</Box>
|
|
622
|
+
<Box style={{ minWidth: 200 }}>
|
|
623
|
+
<TextInput
|
|
624
|
+
placeholder="Search by name or type…"
|
|
625
|
+
value={profileSearch}
|
|
626
|
+
onChange={(e) => setProfileSearch(e.target.value)}
|
|
627
|
+
size="S"
|
|
628
|
+
label=""
|
|
629
|
+
/>
|
|
630
|
+
</Box>
|
|
526
631
|
</Flex>
|
|
527
632
|
<Flex gap={2}>
|
|
528
633
|
{orderModified && (
|
|
@@ -544,8 +649,10 @@ const SyncTab = () => {
|
|
|
544
649
|
<Box padding={4} background="neutral0" hasRadius>
|
|
545
650
|
<Typography textColor="neutral600">
|
|
546
651
|
{profileFilter === 'active'
|
|
547
|
-
? 'No active profiles. Activate a profile in the Sync Profiles tab first.'
|
|
548
|
-
:
|
|
652
|
+
? 'No active profiles match the search. Activate a profile in the Sync Profiles tab first.'
|
|
653
|
+
: profileSearch
|
|
654
|
+
? 'No profiles match the search.'
|
|
655
|
+
: 'No profiles found. Create a profile in the Sync Profiles tab.'}
|
|
549
656
|
</Typography>
|
|
550
657
|
</Box>
|
|
551
658
|
) : (
|
|
@@ -561,10 +668,10 @@ const SyncTab = () => {
|
|
|
561
668
|
/>
|
|
562
669
|
</Th>
|
|
563
670
|
<Th style={{ width: 80 }}><Typography variant="sigma">Order</Typography></Th>
|
|
564
|
-
<
|
|
565
|
-
<
|
|
671
|
+
<SortableTh field="name" sortF={execSortField} sortD={execSortDir} onSort={handleExecSort}>Profile</SortableTh>
|
|
672
|
+
<SortableTh field="contentType" sortF={execSortField} sortD={execSortDir} onSort={handleExecSort}>Content Type</SortableTh>
|
|
566
673
|
<Th><Typography variant="sigma">Dependencies</Typography></Th>
|
|
567
|
-
<
|
|
674
|
+
<SortableTh field="isActive" sortF={execSortField} sortD={execSortDir} onSort={handleExecSort}>Status</SortableTh>
|
|
568
675
|
<Th><Typography variant="sigma">Execution Mode</Typography></Th>
|
|
569
676
|
<Th><Typography variant="sigma">Actions</Typography></Th>
|
|
570
677
|
</Tr>
|
|
@@ -614,7 +721,7 @@ const SyncTab = () => {
|
|
|
614
721
|
<TextInput
|
|
615
722
|
value={order}
|
|
616
723
|
onChange={(e) => handleOrderChange(profile.id, e.target.value)}
|
|
617
|
-
style={{ width:
|
|
724
|
+
style={{ width: 80, textAlign: 'center' }}
|
|
618
725
|
size="S"
|
|
619
726
|
type="number"
|
|
620
727
|
min={1}
|
|
@@ -719,20 +826,39 @@ const SyncTab = () => {
|
|
|
719
826
|
Monitor scheduled and live sync jobs.
|
|
720
827
|
</Typography>
|
|
721
828
|
|
|
722
|
-
<Box paddingTop={4}>
|
|
829
|
+
<Box paddingTop={4} paddingBottom={3}>
|
|
830
|
+
<TextInput
|
|
831
|
+
placeholder="Search by profile name…"
|
|
832
|
+
value={statusSearch}
|
|
833
|
+
onChange={(e) => setStatusSearch(e.target.value)}
|
|
834
|
+
label="Search"
|
|
835
|
+
size="S"
|
|
836
|
+
style={{ maxWidth: 280 }}
|
|
837
|
+
/>
|
|
838
|
+
</Box>
|
|
839
|
+
|
|
840
|
+
<Box paddingTop={2}>
|
|
723
841
|
<Table>
|
|
724
842
|
<Thead>
|
|
725
843
|
<Tr>
|
|
726
|
-
<
|
|
727
|
-
<
|
|
728
|
-
<
|
|
729
|
-
<
|
|
844
|
+
<SortableTh field="profileName" sortF={statusSortField} sortD={statusSortDir} onSort={handleStatusSort}>Profile</SortableTh>
|
|
845
|
+
<SortableTh field="executionMode" sortF={statusSortField} sortD={statusSortDir} onSort={handleStatusSort}>Mode</SortableTh>
|
|
846
|
+
<SortableTh field="enabled" sortF={statusSortField} sortD={statusSortDir} onSort={handleStatusSort}>Enabled</SortableTh>
|
|
847
|
+
<SortableTh field="lastExecutedAt" sortF={statusSortField} sortD={statusSortDir} onSort={handleStatusSort}>Last Run</SortableTh>
|
|
730
848
|
<Th><Typography variant="sigma">Next Run</Typography></Th>
|
|
731
849
|
<Th><Typography variant="sigma">Status</Typography></Th>
|
|
732
850
|
</Tr>
|
|
733
851
|
</Thead>
|
|
734
852
|
<Tbody>
|
|
735
|
-
{
|
|
853
|
+
{filteredStatus.length === 0 ? (
|
|
854
|
+
<Tr>
|
|
855
|
+
<Td colSpan={6}>
|
|
856
|
+
<Typography textColor="neutral500">
|
|
857
|
+
{statusSearch ? 'No profiles match the search.' : 'No execution status found.'}
|
|
858
|
+
</Typography>
|
|
859
|
+
</Td>
|
|
860
|
+
</Tr>
|
|
861
|
+
) : filteredStatus.map((status) => (
|
|
736
862
|
<Tr key={status.profileId}>
|
|
737
863
|
<Td>
|
|
738
864
|
<Typography fontWeight="bold">{status.profileName}</Typography>
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|