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:
parent
d886c910da
commit
29bd636273
@ -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
|
|
||||||
“
|
|
||||||
{tableState.query}
|
|
||||||
”
|
|
||||||
</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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 “
|
||||||
|
{query}
|
||||||
|
”
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
};
|
@ -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,
|
||||||
|
};
|
||||||
|
};
|
@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user