1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-04 13:48:56 +02: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 { FeatureSeenCell } from 'component/common/Table/cells/FeatureSeenCell/FeatureSeenCell';
import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell'; import { FeatureTypeCell } from 'component/common/Table/cells/FeatureTypeCell/FeatureTypeCell';
import { IProject } from 'interfaces/project'; import { IProject } from 'interfaces/project';
import { import { PaginatedTable, VirtualizedTable } from 'component/common/Table';
PaginatedTable,
TablePlaceholder,
VirtualizedTable,
} from 'component/common/Table';
import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext'; import { SearchHighlightProvider } from 'component/common/Table/SearchHighlightContext/SearchHighlightContext';
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog'; import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog'; 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 { FeatureToggleCell } from './FeatureToggleCell/FeatureToggleCell';
import { ProjectOverviewFilters } from './ProjectOverviewFilters'; import { ProjectOverviewFilters } from './ProjectOverviewFilters';
import { useDefaultColumnVisibility } from './hooks/useDefaultColumnVisibility'; import { useDefaultColumnVisibility } from './hooks/useDefaultColumnVisibility';
import { Placeholder } from './TablePlaceholder/TablePlaceholder';
import { useRowActions } from './hooks/useRowActions';
import { useUiFlag } from 'hooks/useUiFlag';
interface IExperimentalProjectFeatureTogglesProps { interface IExperimentalProjectFeatureTogglesProps {
environments: IProject['environments']; environments: IProject['environments'];
@ -111,6 +110,8 @@ export const ExperimentalProjectFeatureToggles = ({
}: IExperimentalProjectFeatureTogglesProps) => { }: IExperimentalProjectFeatureTogglesProps) => {
const projectId = useRequiredPathParam('projectId'); const projectId = useRequiredPathParam('projectId');
const featuresExportImport = useUiFlag('featuresExportImport');
const stateConfig = { const stateConfig = {
offset: withDefault(NumberParam, 0), offset: withDefault(NumberParam, 0),
limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT), limit: withDefault(NumberParam, DEFAULT_PAGE_LIMIT),
@ -160,7 +161,13 @@ export const ExperimentalProjectFeatureToggles = ({
const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId); const { isChangeRequestConfigured } = useChangeRequestsEnabled(projectId);
const { onToggle: onFeatureToggle, modals: featureToggleModals } = const { onToggle: onFeatureToggle, modals: featureToggleModals } =
useFeatureToggleSwitch(projectId); useFeatureToggleSwitch(projectId);
const bodyLoadingRef = useLoading(loading); const {
rowActionsDialogs,
setFeatureArchiveState,
setFeatureStaleDialogState,
} = useRowActions(refetch, projectId);
const [showExportDialog, setShowExportDialog] = useState(false);
const columns = useMemo( const columns = useMemo(
() => [ () => [
columnHelper.display({ columnHelper.display({
@ -181,6 +188,9 @@ export const ExperimentalProjectFeatureToggles = ({
onChange={row?.getToggleSelectedHandler()} onChange={row?.getToggleSelectedHandler()}
/> />
), ),
meta: {
width: '1%',
},
}), }),
columnHelper.accessor('favorite', { columnHelper.accessor('favorite', {
id: 'favorite', id: 'favorite',
@ -203,6 +213,7 @@ export const ExperimentalProjectFeatureToggles = ({
enableSorting: false, enableSorting: false,
meta: { meta: {
align: 'center', align: 'center',
width: '1%',
}, },
}), }),
columnHelper.accessor('lastSeenAt', { columnHelper.accessor('lastSeenAt', {
@ -217,6 +228,7 @@ export const ExperimentalProjectFeatureToggles = ({
size: 50, size: 50,
meta: { meta: {
align: 'center', align: 'center',
width: '1%',
}, },
}), }),
columnHelper.accessor('type', { columnHelper.accessor('type', {
@ -225,12 +237,14 @@ export const ExperimentalProjectFeatureToggles = ({
cell: FeatureTypeCell, cell: FeatureTypeCell,
meta: { meta: {
align: 'center', align: 'center',
width: '1%',
}, },
}), }),
columnHelper.accessor('name', { columnHelper.accessor('name', {
id: 'name', id: 'name',
header: 'Name', header: 'Name',
cell: FeatureNameCell, cell: FeatureNameCell,
enableHiding: false,
meta: { meta: {
width: '50%', width: '50%',
}, },
@ -266,6 +280,7 @@ export const ExperimentalProjectFeatureToggles = ({
header: name, header: name,
meta: { meta: {
align: 'center', align: 'center',
width: '1%',
}, },
cell: ({ getValue }) => { cell: ({ getValue }) => {
const { 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], [projectId, environments, tableState.favoritesFirst, refetch],
); );
@ -323,12 +356,15 @@ export const ExperimentalProjectFeatureToggles = ({
[tableState.limit], [tableState.limit],
); );
const isPlaceholder = Boolean(initialLoad || (loading && total));
const bodyLoadingRef = useLoading(isPlaceholder);
const data = useMemo(() => { const data = useMemo(() => {
if (initialLoad || (loading && total)) { if (isPlaceholder) {
return placeholderData; return placeholderData;
} }
return features; return features;
}, [loading, features]); }, [isPlaceholder, features]);
const allColumnIds = useMemo( const allColumnIds = useMemo(
() => columns.map((column) => column.id).filter(Boolean) as string[], () => columns.map((column) => column.id).filter(Boolean) as string[],
[columns], [columns],
@ -347,7 +383,7 @@ export const ExperimentalProjectFeatureToggles = ({
}), }),
); );
const { columnVisibility } = table.getState(); const { columnVisibility, rowSelection } = table.getState();
const onToggleColumnVisibility = useCallback( const onToggleColumnVisibility = useCallback(
(columnId) => { (columnId) => {
const isVisible = columnVisibility[columnId]; const isVisible = columnVisibility[columnId];
@ -429,7 +465,7 @@ export const ExperimentalProjectFeatureToggles = ({
> >
<div <div
ref={bodyLoadingRef} ref={bodyLoadingRef}
aria-busy={loading} aria-busy={isPlaceholder}
aria-live='polite' aria-live='polite'
> >
<ProjectOverviewFilters <ProjectOverviewFilters
@ -442,59 +478,13 @@ export const ExperimentalProjectFeatureToggles = ({
totalItems={total} totalItems={total}
/> />
</SearchHighlightProvider> </SearchHighlightProvider>
{/* <Placeholder total={total} query={tableState.query || ''} />
{rowActionsDialogs}
<ConditionallyRender <ConditionallyRender
condition={rows.length === 0} condition={featuresExportImport && !loading}
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
}
show={ show={
// FIXME: export only selected rows?
<ExportDialog <ExportDialog
showExportDialog={showExportDialog} showExportDialog={showExportDialog}
data={data} data={data}
@ -505,19 +495,17 @@ export const ExperimentalProjectFeatureToggles = ({
/> />
} }
/> />
{featureToggleModals} */} {featureToggleModals}
</div> </div>
</PageContent> </PageContent>
{/* <BatchSelectionActionsBar <BatchSelectionActionsBar count={Object.keys(rowSelection).length}>
count={Object.keys(selectedRowIds).length}
>
<ProjectFeaturesBatchActions <ProjectFeaturesBatchActions
selectedIds={Object.keys(selectedRowIds)} selectedIds={Object.keys(rowSelection)}
data={features} data={features}
projectId={projectId} 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'); 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, false,
]), ]),
); );
const showAlwaysVisibleColumns = Object.fromEntries(
options.columns
.filter(({ enableHiding }) => enableHiding === false)
.map((column) => [column.id, true]),
);
const columnVisibility = tableState.columns const columnVisibility = tableState.columns
? { ? {
...hideAllColumns, ...hideAllColumns,
...createColumnVisibilityState(tableState).columnVisibility, ...createColumnVisibilityState(tableState).columnVisibility,
...showAlwaysVisibleColumns,
} }
: options.state?.columnVisibility; : options.state?.columnVisibility;