1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-12-22 19:07:54 +01:00

Feat: row actions (#5635)

- add table placeholder back
- add row actions column
- refactor actions into hook
- batch actions
This commit is contained in:
Tymoteusz Czech 2023-12-14 09:04:56 +01:00 committed by GitHub
parent d886c910da
commit 29bd636273
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 188 additions and 68 deletions

View File

@ -35,11 +35,7 @@ import { LinkCell } from 'component/common/Table/cells/LinkCell/LinkCell';
import { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell';
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
import { IProject } from 'interfaces/project';
import {
PaginatedTable,
TablePlaceholder,
VirtualizedTable,
} from 'component/common/Table';
import { PaginatedTable, VirtualizedTable } from 'component/common/Table';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
@ -92,6 +88,9 @@ import { FeatureNameCell } from 'component/common/Table/cells/FeatureNameCell/Fe
import { FeatureToggleCell } from './FeatureToggleCell/FeatureToggleCell';
import { ProjectOverviewFilters } from './ProjectOverviewFilters';
import { useDefaultColumnVisibility } from './hooks/useDefaultColumnVisibility';
import { Placeholder } from './TablePlaceholder/TablePlaceholder';
import { useRowActions } from './hooks/useRowActions';
import { useUiFlag } from 'hooks/useUiFlag';
interface IExperimentalProjectFeatureTogglesProps {
environments: IProject['environments'];
@ -111,6 +110,8 @@ export const ExperimentalProjectFeatureToggles = ({
}: IExperimentalProjectFeatureTogglesProps) => {
const projectId = useRequiredPathParam('projectId');
const featuresExportImport = useUiFlag('featuresExportImport');
const stateConfig = {
offset: withDefault(NumberParam, 0),
limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT),
@ -160,7 +161,13 @@ export const ExperimentalProjectFeatureToggles = ({
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const { onToggle: onFeatureToggle, modals: featureToggleModals } =
useFeatureToggleSwitch(projectId);
const bodyLoadingRef = useLoading(loading);
const {
rowActionsDialogs,
setFeatureArchiveState,
setFeatureStaleDialogState,
} = useRowActions(refetch, projectId);
const [showExportDialog, setShowExportDialog] = useState(false);
const columns = useMemo(
() => [
columnHelper.display({
@ -181,6 +188,9 @@ export const ExperimentalProjectFeatureToggles = ({
onChange={row?.getToggleSelectedHandler()}
/>
),
meta: {
width: '1%',
},
}),
columnHelper.accessor('favorite', {
id: 'favorite',
@ -203,6 +213,7 @@ export const ExperimentalProjectFeatureToggles = ({
enableSorting: false,
meta: {
align: 'center',
width: '1%',
},
}),
columnHelper.accessor('lastSeenAt', {
@ -217,6 +228,7 @@ export const ExperimentalProjectFeatureToggles = ({
size: 50,
meta: {
align: 'center',
width: '1%',
},
}),
columnHelper.accessor('type', {
@ -225,12 +237,14 @@ export const ExperimentalProjectFeatureToggles = ({
cell: FeatureTypeCell,
meta: {
align: 'center',
width: '1%',
},
}),
columnHelper.accessor('name', {
id: 'name',
header: 'Name',
cell: FeatureNameCell,
enableHiding: false,
meta: {
width: '50%',
},
@ -266,6 +280,7 @@ export const ExperimentalProjectFeatureToggles = ({
header: name,
meta: {
align: 'center',
width: '1%',
},
cell: ({ getValue }) => {
const {
@ -296,6 +311,24 @@ export const ExperimentalProjectFeatureToggles = ({
);
},
),
columnHelper.display({
id: 'actions',
header: '',
cell: ({ row }) => (
<ActionsCell
row={row}
projectId={projectId}
onOpenArchiveDialog={setFeatureArchiveState}
onOpenStaleDialog={setFeatureStaleDialogState}
/>
),
enableSorting: false,
enableHiding: false,
meta: {
align: 'right',
width: '1%',
},
}),
],
[projectId, environments, tableState.favoritesFirst, refetch],
);
@ -323,12 +356,15 @@ export const ExperimentalProjectFeatureToggles = ({
[tableState.limit],
);
const isPlaceholder = Boolean(initialLoad || (loading && total));
const bodyLoadingRef = useLoading(isPlaceholder);
const data = useMemo(() => {
if (initialLoad || (loading && total)) {
if (isPlaceholder) {
return placeholderData;
}
return features;
}, [loading, features]);
}, [isPlaceholder, features]);
const allColumnIds = useMemo(
() => columns.map((column) => column.id).filter(Boolean) as string[],
[columns],
@ -347,7 +383,7 @@ export const ExperimentalProjectFeatureToggles = ({
}),
);
const { columnVisibility } = table.getState();
const { columnVisibility, rowSelection } = table.getState();
const onToggleColumnVisibility = useCallback(
(columnId) => {
const isVisible = columnVisibility[columnId];
@ -429,7 +465,7 @@ export const ExperimentalProjectFeatureToggles = ({
>
<div
ref={bodyLoadingRef}
aria-busy={loading}
aria-busy={isPlaceholder}
aria-live='polite'
>
<ProjectOverviewFilters
@ -442,59 +478,13 @@ export const ExperimentalProjectFeatureToggles = ({
totalItems={total}
/>
</SearchHighlightProvider>
{/*
<Placeholder total={total} query={tableState.query || ''} />
{rowActionsDialogs}
<ConditionallyRender
condition={rows.length === 0}
show={
<ConditionallyRender
condition={(tableState.query || '')?.length > 0}
show={
<Box sx={{ padding: theme.spacing(3) }}>
<TablePlaceholder>
No feature toggles found matching
&ldquo;
{tableState.query}
&rdquo;
</TablePlaceholder>
</Box>
}
elseShow={
<Box sx={{ padding: theme.spacing(3) }}>
<TablePlaceholder>
No feature toggles available. Get
started by adding a new feature
toggle.
</TablePlaceholder>
</Box>
}
/>
}
/>
<FeatureStaleDialog
isStale={featureStaleDialogState.stale === true}
isOpen={Boolean(featureStaleDialogState.featureId)}
onClose={() => {
setFeatureStaleDialogState({});
onChange();
}}
featureId={featureStaleDialogState.featureId || ''}
projectId={projectId}
/>
<FeatureArchiveDialog
isOpen={Boolean(featureArchiveState)}
onConfirm={onChange}
onClose={() => {
setFeatureArchiveState(undefined);
}}
featureIds={[featureArchiveState || '']}
projectId={projectId}
/>
<ConditionallyRender
condition={
Boolean(uiConfig?.flags?.featuresExportImport) &&
!loading
}
condition={featuresExportImport && !loading}
show={
// FIXME: export only selected rows?
<ExportDialog
showExportDialog={showExportDialog}
data={data}
@ -505,19 +495,17 @@ export const ExperimentalProjectFeatureToggles = ({
/>
}
/>
{featureToggleModals} */}
{featureToggleModals}
</div>
</PageContent>
{/* <BatchSelectionActionsBar
count={Object.keys(selectedRowIds).length}
>
<BatchSelectionActionsBar count={Object.keys(rowSelection).length}>
<ProjectFeaturesBatchActions
selectedIds={Object.keys(selectedRowIds)}
selectedIds={Object.keys(rowSelection)}
data={features}
projectId={projectId}
onResetSelection={() => toggleAllRowsSelected(false)}
onResetSelection={table.resetRowSelection}
/>
</BatchSelectionActionsBar> */}
</BatchSelectionActionsBar>
</>
);
};

View File

@ -0,0 +1,43 @@
import { FC } from 'react';
import { Box } from '@mui/material';
import { TablePlaceholder } from 'component/common/Table';
interface ITablePlaceholderProps {
total?: number;
query?: string;
}
export const Placeholder: FC<ITablePlaceholderProps> = ({ total, query }) => {
if (total !== 0) {
return null;
}
if ((query || '')?.length > 0) {
return (
<Box
sx={(theme) => ({
padding: theme.spacing(3),
})}
>
<TablePlaceholder>
No feature toggles found matching &ldquo;
{query}
&rdquo;
</TablePlaceholder>
</Box>
);
}
return (
<Box
sx={(theme) => ({
padding: theme.spacing(3),
})}
>
<TablePlaceholder>
No feature toggles available. Get started by adding a new
feature toggle.
</TablePlaceholder>
</Box>
);
};

View File

@ -0,0 +1,45 @@
import { useState } from 'react';
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog';
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
export const useRowActions = (onChange: () => void, projectId: string) => {
const [featureArchiveState, setFeatureArchiveState] = useState<
string | undefined
>();
const [featureStaleDialogState, setFeatureStaleDialogState] = useState<{
featureId?: string;
stale?: boolean;
}>({});
const rowActionsDialogs = (
<>
<FeatureStaleDialog
isStale={Boolean(featureStaleDialogState.stale)}
isOpen={Boolean(featureStaleDialogState.featureId)}
onClose={() => {
setFeatureStaleDialogState({});
onChange();
}}
featureId={featureStaleDialogState.featureId || ''}
projectId={projectId}
/>
<FeatureArchiveDialog
isOpen={Boolean(featureArchiveState)}
onConfirm={onChange}
onClose={() => {
setFeatureArchiveState(undefined);
}}
featureIds={[featureArchiveState || '']}
projectId={projectId}
/>
</>
);
return {
rowActionsDialogs,
setFeatureArchiveState,
setFeatureStaleDialogState,
};
};

View File

@ -304,4 +304,42 @@ describe('withTableState', () => {
expect(getByTestId('sort')).toHaveValue('createdAt');
});
it('always shows columns that have `enableHiding: false`', () => {
const mockTableState = {
limit: 10,
offset: 10,
sortBy: 'name',
sortOrder: 'asc',
columns: ['createdAt'],
};
const mockSetTableState = vi.fn();
const mockOptions = {
data: [],
columns: [
{
id: 'name',
show: false,
enableHiding: false,
},
{
id: 'createdAt',
show: true,
},
],
};
const result = withTableState(
mockTableState,
mockSetTableState,
mockOptions,
);
expect(result.state).toMatchObject({
columnVisibility: {
name: true,
createdAt: true,
},
});
});
});

View File

@ -169,10 +169,16 @@ export const withTableState = <T extends Object>(
false,
]),
);
const showAlwaysVisibleColumns = Object.fromEntries(
options.columns
.filter(({ enableHiding }) => enableHiding === false)
.map((column) => [column.id, true]),
);
const columnVisibility = tableState.columns
? {
...hideAllColumns,
...createColumnVisibilityState(tableState).columnVisibility,
...showAlwaysVisibleColumns,
}
: options.state?.columnVisibility;