Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 114 additions & 38 deletions admin/src/components/ConfigList/index.jsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -12,22 +13,46 @@ 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';
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]
Expand Down Expand Up @@ -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 (
Expand All @@ -103,44 +147,76 @@ const ConfigList = ({ diff, isLoading }) => {
const allChecked = checkedItems && checkedItems.every(Boolean);
const isIndeterminate = checkedItems.some(Boolean) && !allChecked;

return (
<div>
<Table colCount={4} rowCount={rows.length + 1}>
<Thead>
<Tr>
<Th>
<Checkbox
aria-label={formatMessage({ id: 'config-sync.ConfigList.SelectAll' })}
checked={isIndeterminate ? "indeterminate" : allChecked}
onCheckedChange={(value) => setCheckedItems(checkedItems.map(() => value))}
const renderSortIcon = (field) => {
if (sortBy !== field) return null;
return sortOrder === 'asc' ? <CaretUp /> : <CaretDown />;
};

return (
<div>
<SearchBox paddingBottom={4}>
<TextInput
startAction={<Search />}
endAction={searchQuery ? (
<IconButton
label={formatMessage({ id: 'config-sync.ConfigList.ClearSearch' })}
onClick={() => setSearchQuery('')}
variant="ghost"
>
<Cross />
</IconButton>
) : null}
aria-label={formatMessage({ id: 'config-sync.ConfigList.Search' })}
placeholder={formatMessage({ id: 'config-sync.ConfigList.Search' })}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</SearchBox>
<Table colCount={4} rowCount={sortedRows.length + 1}>
<Thead>
<Tr>
<Th>
<Checkbox
aria-label={formatMessage({ id: 'config-sync.ConfigList.SelectAll' })}
checked={isIndeterminate ? "indeterminate" : allChecked}
onCheckedChange={(value) => setCheckedItems(checkedItems.map(() => value))}
/>
</Th>
<Th onClick={() => handleSort('configName')} style={{ cursor: 'pointer' }}>
<Flex gap={1}>
<Typography variant="sigma">{formatMessage({ id: 'config-sync.ConfigList.ConfigName' })}</Typography>
{renderSortIcon('configName')}
</Flex>
</Th>
<Th onClick={() => handleSort('configType')} style={{ cursor: 'pointer' }}>
<Flex gap={1}>
<Typography variant="sigma">{formatMessage({ id: 'config-sync.ConfigList.ConfigType' })}</Typography>
{renderSortIcon('configType')}
</Flex>
</Th>
<Th onClick={() => handleSort('state')} style={{ cursor: 'pointer' }}>
<Flex gap={1}>
<Typography variant="sigma">{formatMessage({ id: 'config-sync.ConfigList.State' })}</Typography>
{renderSortIcon('state')}
</Flex>
</Th>
</Tr>
</Thead>
<Tbody>
{sortedRows.map((row) => (
<ConfigListRow
key={`${row.configType}.${row.configName}`}
row={row}
checked={checkedItems[row.originalIndex]}
updateValue={() => {
checkedItems[row.originalIndex] = !checkedItems[row.originalIndex];
setCheckedItems([...checkedItems]);
}}
/>
</Th>
<Th>
<Typography variant="sigma">{formatMessage({ id: 'config-sync.ConfigList.ConfigName' })}</Typography>
</Th>
<Th>
<Typography variant="sigma">{formatMessage({ id: 'config-sync.ConfigList.ConfigType' })}</Typography>
</Th>
<Th>
<Typography variant="sigma">{formatMessage({ id: 'config-sync.ConfigList.State' })}</Typography>
</Th>
</Tr>
</Thead>
<Tbody>
{rows.map((row, index) => (
<ConfigListRow
key={`${row.configType}.${row.configName}`}
row={row}
checked={checkedItems[index]}
updateValue={() => {
checkedItems[index] = !checkedItems[index];
setCheckedItems([...checkedItems]);
}}
/>
))}
</Tbody>
</Table>
</div>
))}
</Tbody>
</Table>
</div>
);
};

Expand Down
2 changes: 2 additions & 0 deletions admin/src/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
11 changes: 9 additions & 2 deletions admin/src/translations/es.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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",
Expand All @@ -31,11 +37,12 @@

"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",

"Settings.Tool.Title": "Interfaz",

"plugin.name": "Config Sync"
}
}
6 changes: 4 additions & 2 deletions admin/src/translations/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@
"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",
"Header.Description": "Управляйте конфигурацией своей базы данных в разных средах.",

"ConfigList.Loading": "Загрузка содержимого...",
"ConfigList.SelectAll": "Выбрать все записи",
"ConfigList.Search": "Поиск",
"ConfigList.ClearSearch": "Очистить поиск",
"ConfigList.ConfigName": "Имя конфигурации",
"ConfigList.ConfigType": "Тип конфигурации",
"ConfigList.State": "Состояние",
Expand Down
17 changes: 2 additions & 15 deletions playground/types/generated/contentTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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'> &
Expand Down Expand Up @@ -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'> &
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<false>;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading