diff --git a/admin/src/components/ConfigList/index.jsx b/admin/src/components/ConfigList/index.jsx
index 7b0202a..4c19359 100644
--- a/admin/src/components/ConfigList/index.jsx
+++ b/admin/src/components/ConfigList/index.jsx
@@ -1,7 +1,8 @@
-import React, { useState, useEffect } from 'react';
+import React, { useState, useEffect, useMemo } from 'react';
import { useIntl } from 'react-intl';
import { isEmpty } from 'lodash';
import { useDispatch } from 'react-redux';
+import styled from 'styled-components';
import {
Table,
@@ -12,7 +13,12 @@ import {
Typography,
Checkbox,
Loader,
+ Flex,
+ Box,
+ TextInput,
+ IconButton,
} from '@strapi/design-system';
+import { CaretDown, CaretUp, Search, Cross } from '@strapi/icons';
import ConfigDiff from '../ConfigDiff';
import FirstExport from '../FirstExport';
@@ -20,6 +26,13 @@ import NoChanges from '../NoChanges';
import ConfigListRow from './ConfigListRow';
import { setConfigPartialDiffInState } from '../../state/actions/Config';
+const SearchBox = styled(Box)`
+ width: 100%;
+
+ @media (min-width: 768px) {
+ width: 40%;
+ }
+`;
const ConfigList = ({ diff, isLoading }) => {
const [originalConfig, setOriginalConfig] = useState({});
@@ -27,9 +40,21 @@ const ConfigList = ({ diff, isLoading }) => {
const [cName, setCname] = useState('');
const [rows, setRows] = useState([]);
const [checkedItems, setCheckedItems] = useState([]);
+ const [sortBy, setSortBy] = useState('configName');
+ const [sortOrder, setSortOrder] = useState('asc');
+ const [searchQuery, setSearchQuery] = useState('');
const dispatch = useDispatch();
const { formatMessage } = useIntl();
+ const handleSort = (field) => {
+ if (sortBy === field) {
+ setSortOrder(sortOrder === 'asc' ? 'desc' : 'asc');
+ } else {
+ setSortBy(field);
+ setSortOrder('asc');
+ }
+ };
+
const getConfigState = (configName) => {
if (
diff.fileConfig[configName]
@@ -87,6 +112,25 @@ const ConfigList = ({ diff, isLoading }) => {
dispatch(setConfigPartialDiffInState(newPartialDiff));
}, [checkedItems]);
+ const sortedRows = useMemo(() => {
+ const rowsWithIndex = rows.map((row, originalIndex) => ({ ...row, originalIndex }));
+
+ const filteredRows = searchQuery
+ ? rowsWithIndex.filter((row) => (
+ `${row.configType}.${row.configName}`.toLowerCase().includes(searchQuery.toLowerCase())
+ ))
+ : rowsWithIndex;
+
+ return filteredRows.sort((a, b) => {
+ const aValue = (a[sortBy] || '').toLowerCase();
+ const bValue = (b[sortBy] || '').toLowerCase();
+
+ if (aValue < bValue) return sortOrder === 'asc' ? -1 : 1;
+ if (aValue > bValue) return sortOrder === 'asc' ? 1 : -1;
+ return 0;
+ });
+ }, [rows, sortBy, sortOrder, searchQuery]);
+
if (isLoading) {
return (
@@ -106,9 +150,32 @@ const ConfigList = ({ diff, isLoading }) => {
const allChecked = checkedItems && checkedItems.every(Boolean);
const isIndeterminate = checkedItems.some(Boolean) && !allChecked;
+ const renderSortIcon = (field) => {
+ if (sortBy !== field) return null;
+ return sortOrder === 'asc' ?
:
;
+ };
+
return (
-
+
+ }
+ endAction={searchQuery ? (
+ setSearchQuery('')}
+ variant="ghost"
+ >
+
+
+ ) : null}
+ aria-label={formatMessage({ id: 'config-sync.ConfigList.Search' })}
+ placeholder={formatMessage({ id: 'config-sync.ConfigList.Search' })}
+ value={searchQuery}
+ onChange={(e) => setSearchQuery(e.target.value)}
+ />
+
+
|
@@ -118,19 +185,28 @@ const ConfigList = ({ diff, isLoading }) => {
onCheckedChange={(value) => setCheckedItems(checkedItems.map(() => value))}
/>
|
-
- {formatMessage({ id: 'config-sync.ConfigList.ConfigName' })}
+ | handleSort('configName')} style={{ cursor: 'pointer' }}>
+
+ {formatMessage({ id: 'config-sync.ConfigList.ConfigName' })}
+ {renderSortIcon('configName')}
+
|
-
- {formatMessage({ id: 'config-sync.ConfigList.ConfigType' })}
+ | handleSort('configType')} style={{ cursor: 'pointer' }}>
+
+ {formatMessage({ id: 'config-sync.ConfigList.ConfigType' })}
+ {renderSortIcon('configType')}
+
|
-
- {formatMessage({ id: 'config-sync.ConfigList.State' })}
+ | handleSort('state')} style={{ cursor: 'pointer' }}>
+
+ {formatMessage({ id: 'config-sync.ConfigList.State' })}
+ {renderSortIcon('state')}
+
|
- {rows.map((row, index) => (
+ {sortedRows.map((row) => (
{
trigger={(
{
- checkedItems[index] = !checkedItems[index];
+ checkedItems[row.originalIndex] = !checkedItems[row.originalIndex];
setCheckedItems([...checkedItems]);
}}
/>
diff --git a/admin/src/translations/en.json b/admin/src/translations/en.json
index e043d09..0b4d20f 100644
--- a/admin/src/translations/en.json
+++ b/admin/src/translations/en.json
@@ -18,6 +18,8 @@
"ConfigList.Loading": "Loading content...",
"ConfigList.SelectAll": "Select all entries",
+ "ConfigList.Search": "Search",
+ "ConfigList.ClearSearch": "Clear search",
"ConfigList.ConfigName": "Config name",
"ConfigList.ConfigType": "Config type",
"ConfigList.State": "State",
diff --git a/admin/src/translations/es.json b/admin/src/translations/es.json
index f624d91..c182011 100644
--- a/admin/src/translations/es.json
+++ b/admin/src/translations/es.json
@@ -1,4 +1,4 @@
-{
+{
"popUpWarning.warning.import_1": "Si continuas todos tus ficheros de configuración locales",
"popUpWarning.warning.import_2": "se importarán a la base de datos.",
"popUpWarning.warning.export_1": "Si continuas las configuraciones de tu base de datos",
@@ -9,11 +9,17 @@
"popUpWarning.force": "Forzar",
"popUpWarning.Confirmation": "Confirmación",
+ "envWarning.production.heading": "Estás en el entorno de producción",
+ "envWarning.production.message_1": "Ten cuidado al sincronizar la configuración en producción.",
+ "envWarning.production.message_2": "Asegúrate de no sobrescribir cambios de configuración importantes al importar.",
+
"Header.Title": "Config Sync",
"Header.Description": "Gestiona las configuraciones de tu base de datos entre diferentes entornos o instancias.",
"ConfigList.Loading": "Cargando contenido...",
"ConfigList.SelectAll": "Seleccionar todas las entradas",
+ "ConfigList.Search": "Buscar",
+ "ConfigList.ClearSearch": "Borrar búsqueda",
"ConfigList.ConfigName": "Nombre",
"ConfigList.ConfigType": "Tipo",
"ConfigList.State": "Estado",
@@ -29,6 +35,7 @@
"Buttons.Import": "Importar",
"Buttons.Export": "Exportar",
+ "Buttons.DownloadConfig": "Descargar configuración",
"FirstExport.Message": "Parece ser la primera vez que se usa config-sync en este proyecto.",
"FirstExport.Button": "Hacer la exportación inicial",
@@ -36,4 +43,4 @@
"Settings.Tool.Title": "Interfaz",
"plugin.name": "Config Sync"
-}
\ No newline at end of file
+}
diff --git a/admin/src/translations/ru.json b/admin/src/translations/ru.json
index f9fa0a3..552d8e6 100644
--- a/admin/src/translations/ru.json
+++ b/admin/src/translations/ru.json
@@ -9,8 +9,8 @@
"popUpWarning.force": "Принудительно",
"popUpWarning.Confirmation": "Подтверждение",
- "envWarning.production.heading": "Вы находитесь в prodoction-среде",
- "envWarning.production.message_1": "Пожалуйста, будьте осторожны при синхронизации конфигурации в prodoction-среде.",
+ "envWarning.production.heading": "Вы находитесь в производственной среде",
+ "envWarning.production.message_1": "Будьте осторожны при синхронизации конфигурации в производственной среде.",
"envWarning.production.message_2": "Убедитесь, что вы не переопределяете важные изменения конфигурации при импорте.",
"Header.Title": "Config Sync",
@@ -18,6 +18,8 @@
"ConfigList.Loading": "Загрузка содержимого...",
"ConfigList.SelectAll": "Выбрать все записи",
+ "ConfigList.Search": "Поиск",
+ "ConfigList.ClearSearch": "Очистить поиск",
"ConfigList.ConfigName": "Имя конфигурации",
"ConfigList.ConfigType": "Тип конфигурации",
"ConfigList.State": "Состояние",