diff --git a/admin/src/components/ConfigList/index.jsx b/admin/src/components/ConfigList/index.jsx index 4045a26..c4df491 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,14 +26,33 @@ 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 [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] @@ -83,6 +108,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]); + const renderList = () => { if (isLoading) { return ( @@ -103,44 +147,76 @@ const ConfigList = ({ diff, isLoading }) => { const allChecked = checkedItems && checkedItems.every(Boolean); const isIndeterminate = checkedItems.some(Boolean) && !allChecked; - return ( -
- - - -
- setCheckedItems(checkedItems.map(() => value))} + 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)} + /> + + + + + + + + + + + + {sortedRows.map((row) => ( + { + checkedItems[row.originalIndex] = !checkedItems[row.originalIndex]; + setCheckedItems([...checkedItems]); + }} /> - - - - - - - - {rows.map((row, index) => ( - { - checkedItems[index] = !checkedItems[index]; - setCheckedItems([...checkedItems]); - }} - /> - ))} - -
+ setCheckedItems(checkedItems.map(() => value))} + /> + handleSort('configName')} style={{ cursor: 'pointer' }}> + + {formatMessage({ id: 'config-sync.ConfigList.ConfigName' })} + {renderSortIcon('configName')} + + handleSort('configType')} style={{ cursor: 'pointer' }}> + + {formatMessage({ id: 'config-sync.ConfigList.ConfigType' })} + {renderSortIcon('configType')} + + handleSort('state')} style={{ cursor: 'pointer' }}> + + {formatMessage({ id: 'config-sync.ConfigList.State' })} + {renderSortIcon('state')} + +
- {formatMessage({ id: 'config-sync.ConfigList.ConfigName' })} - - {formatMessage({ id: 'config-sync.ConfigList.ConfigType' })} - - {formatMessage({ id: 'config-sync.ConfigList.State' })} -
-
+ ))} + +
+
); }; diff --git a/admin/src/translations/en.json b/admin/src/translations/en.json index 0e807e4..b872d96 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 a6a6f7e..d9d7cf0 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", @@ -31,6 +37,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", @@ -38,4 +45,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 a9e19e8..9fa243d 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": "Состояние", diff --git a/playground/types/generated/contentTypes.d.ts b/playground/types/generated/contentTypes.d.ts index e4d7a16..1531c93 100644 --- a/playground/types/generated/contentTypes.d.ts +++ b/playground/types/generated/contentTypes.d.ts @@ -26,11 +26,6 @@ export interface AdminApiToken extends Struct.CollectionTypeSchema { Schema.Attribute.SetMinMaxLength<{ minLength: 1; }>; - adminPermissions: Schema.Attribute.Relation< - 'oneToMany', - 'admin::permission' - >; - adminUserOwner: Schema.Attribute.Relation<'manyToOne', 'admin::user'>; createdAt: Schema.Attribute.DateTime; createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & Schema.Attribute.Private; @@ -44,9 +39,6 @@ export interface AdminApiToken extends Struct.CollectionTypeSchema { minLength: 1; }>; expiresAt: Schema.Attribute.DateTime; - kind: Schema.Attribute.Enumeration<['content-api', 'admin']> & - Schema.Attribute.Required & - Schema.Attribute.DefaultTo<'content-api'>; lastUsedAt: Schema.Attribute.DateTime; lifespan: Schema.Attribute.BigInteger; locale: Schema.Attribute.String & Schema.Attribute.Private; @@ -64,6 +56,7 @@ export interface AdminApiToken extends Struct.CollectionTypeSchema { >; publishedAt: Schema.Attribute.DateTime; type: Schema.Attribute.Enumeration<['read-only', 'full-access', 'custom']> & + Schema.Attribute.Required & Schema.Attribute.DefaultTo<'read-only'>; updatedAt: Schema.Attribute.DateTime; updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & @@ -141,7 +134,6 @@ export interface AdminPermission extends Struct.CollectionTypeSchema { minLength: 1; }>; actionParameters: Schema.Attribute.JSON & Schema.Attribute.DefaultTo<{}>; - apiToken: Schema.Attribute.Relation<'manyToOne', 'admin::api-token'>; conditions: Schema.Attribute.JSON & Schema.Attribute.DefaultTo<[]>; createdAt: Schema.Attribute.DateTime; createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> & @@ -249,7 +241,6 @@ export interface AdminSession extends Struct.CollectionTypeSchema { locale: Schema.Attribute.String & Schema.Attribute.Private; localizations: Schema.Attribute.Relation<'oneToMany', 'admin::session'> & Schema.Attribute.Private; - metadata: Schema.Attribute.JSON & Schema.Attribute.Private; origin: Schema.Attribute.String & Schema.Attribute.Required & Schema.Attribute.Private; @@ -394,8 +385,6 @@ export interface AdminUser extends Struct.CollectionTypeSchema { }; }; attributes: { - apiTokens: Schema.Attribute.Relation<'oneToMany', 'admin::api-token'> & - Schema.Attribute.Private; blocked: Schema.Attribute.Boolean & Schema.Attribute.Private & Schema.Attribute.DefaultTo; @@ -432,8 +421,6 @@ export interface AdminUser extends Struct.CollectionTypeSchema { publishedAt: Schema.Attribute.DateTime; registrationToken: Schema.Attribute.String & Schema.Attribute.Private; resetPasswordToken: Schema.Attribute.String & Schema.Attribute.Private; - resetPasswordTokenExpiresAt: Schema.Attribute.DateTime & - Schema.Attribute.Private; roles: Schema.Attribute.Relation<'manyToMany', 'admin::role'> & Schema.Attribute.Private; updatedAt: Schema.Attribute.DateTime; @@ -996,7 +983,7 @@ export interface PluginUsersPermissionsUser } declare module '@strapi/strapi' { - export namespace Public { + export module Public { export interface ContentTypeSchemas { 'admin::api-token': AdminApiToken; 'admin::api-token-permission': AdminApiTokenPermission;