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.
Files changed (59) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +32 -14
  3. package/admin/src/components/BulkTransferTab.jsx +185 -20
  4. package/admin/src/components/ConfigTab.jsx +81 -3
  5. package/admin/src/components/ContentTypesTab.jsx +28 -1
  6. package/admin/src/components/HelpTab.jsx +34 -0
  7. package/admin/src/components/LogsTab.jsx +66 -8
  8. package/admin/src/components/MediaTab.jsx +253 -36
  9. package/admin/src/components/SyncProfilesTab.jsx +140 -4
  10. package/admin/src/components/SyncTab.jsx +161 -35
  11. package/docs/Screenshot 2026-04-22 183540.png +0 -0
  12. package/docs/Screenshot 2026-04-22 183552.png +0 -0
  13. package/docs/Screenshot 2026-04-23 114332.png +0 -0
  14. package/docs/Screenshot 2026-04-23 114644.png +0 -0
  15. package/docs/Screenshot 2026-04-23 114651.png +0 -0
  16. package/docs/Screenshot 2026-04-23 114737.png +0 -0
  17. package/docs/Screenshot 2026-04-23 114904.png +0 -0
  18. package/docs/Screenshot 2026-04-23 114940.png +0 -0
  19. package/docs/Screenshot 2026-04-23 115003.png +0 -0
  20. package/docs/Screenshot 2026-04-23 115024.png +0 -0
  21. package/docs/Screenshot 2026-04-23 115116.png +0 -0
  22. package/docs/Screenshot 2026-04-23 115141.png +0 -0
  23. package/docs/Screenshot 2026-04-23 115252.png +0 -0
  24. package/docs/Screenshot 2026-04-23 115448.png +0 -0
  25. package/docs/Screenshot 2026-04-23 120534.png +0 -0
  26. package/docs/Screenshot 2026-04-23 122544.png +0 -0
  27. package/docs/Screenshot 2026-04-23 122712.png +0 -0
  28. package/docs/Screenshot 2026-04-23 122730.png +0 -0
  29. package/docs/Screenshot 2026-04-23 122858.png +0 -0
  30. package/docs/Screenshot 2026-04-23 122924.png +0 -0
  31. package/docs/Screenshot 2026-04-23 122937.png +0 -0
  32. package/docs/sync-strategy-approach-review.md +127 -0
  33. package/package.json +1 -1
  34. package/server/src/controllers/config.js +76 -3
  35. package/server/src/controllers/sync-media.js +24 -0
  36. package/server/src/routes/index.js +3 -0
  37. package/server/src/services/bulk-transfer.js +45 -1
  38. package/server/src/services/dependency-resolver.js +37 -0
  39. package/server/src/services/sync-execution.js +21 -9
  40. package/server/src/services/sync-media.js +168 -32
  41. package/server/src/services/sync-profiles.js +36 -15
  42. package/server/src/services/sync.js +234 -134
  43. package/server/src/utils/fetcher.js +7 -0
  44. package/docs/Screenshot 2026-04-20 160506.png +0 -0
  45. package/docs/Screenshot 2026-04-20 160558.png +0 -0
  46. package/docs/Screenshot 2026-04-20 175903.png +0 -0
  47. package/docs/Screenshot 2026-04-20 175931.png +0 -0
  48. package/docs/Screenshot 2026-04-20 180001.png +0 -0
  49. package/docs/Screenshot 2026-04-20 180041.png +0 -0
  50. package/docs/Screenshot 2026-04-20 180116.png +0 -0
  51. package/docs/Screenshot 2026-04-20 180135.png +0 -0
  52. package/docs/Screenshot 2026-04-20 180202.png +0 -0
  53. package/docs/Screenshot 2026-04-20 180228.png +0 -0
  54. package/docs/Screenshot 2026-04-20 180251.png +0 -0
  55. package/docs/Screenshot 2026-04-20 180301.png +0 -0
  56. package/docs/clipchamp-screen-recording-script.md +0 -0
  57. package/docs/production-readiness-status.md +0 -34
  58. package/docs/production-readiness-test-matrix.md +0 -151
  59. 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
- const sorted = [...profiles].sort((a, b) => {
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 sorted;
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
- // Sort by execution order
420
- result.sort((a, b) => {
421
- const orderA = executionOrder[a.id] || 999;
422
- const orderB = executionOrder[b.id] || 999;
423
- return orderA - orderB;
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
- }, [profiles, profileFilter, executionOrder]);
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="center" marginBottom={4}>
512
- <Flex gap={4} alignItems="center">
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
- <SingleSelect
515
- value={profileFilter}
516
- onChange={setProfileFilter}
517
- size="S"
518
- style={{ width: 180 }}
519
- >
520
- {FILTER_OPTIONS.map(opt => (
521
- <SingleSelectOption key={opt.value} value={opt.value}>
522
- {opt.label}
523
- </SingleSelectOption>
524
- ))}
525
- </SingleSelect>
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
- : 'No profiles found. Create a profile in the Sync Profiles tab.'}
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
- <Th><Typography variant="sigma">Profile</Typography></Th>
565
- <Th><Typography variant="sigma">Content Type</Typography></Th>
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
- <Th><Typography variant="sigma">Status</Typography></Th>
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: 50, textAlign: 'center' }}
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
- <Th><Typography variant="sigma">Profile</Typography></Th>
727
- <Th><Typography variant="sigma">Mode</Typography></Th>
728
- <Th><Typography variant="sigma">Enabled</Typography></Th>
729
- <Th><Typography variant="sigma">Last Run</Typography></Th>
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
- {executionStatus.map((status) => (
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>