1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-10-18 11:14:57 +02:00
1-753/frontend-features-in-project-overview
This commit is contained in:
Tymoteusz Czech 2023-03-21 13:37:25 +01:00 committed by GitHub
parent 535b74f454
commit 2147206b49
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 488 additions and 76 deletions

View File

@ -3,7 +3,7 @@ import { useContext, useState } from 'react';
import { Button, Chip, Divider, styled } from '@mui/material'; import { Button, Chip, Divider, styled } from '@mui/material';
import useFeatureTags from 'hooks/api/getters/useFeatureTags/useFeatureTags'; import useFeatureTags from 'hooks/api/getters/useFeatureTags/useFeatureTags';
import { Add, Cancel } from '@mui/icons-material'; import { Add, Cancel } from '@mui/icons-material';
import AddTagDialog from 'component/feature/FeatureView/FeatureOverview/AddTagDialog/AddTagDialog'; import { ManageTagsDialog } from 'component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageTagsDialog';
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions'; import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
import AccessContext from 'contexts/AccessContext'; import AccessContext from 'contexts/AccessContext';
import { Dialogue } from 'component/common/Dialogue/Dialogue'; import { Dialogue } from 'component/common/Dialogue/Dialogue';
@ -120,7 +120,7 @@ export const FeatureOverviewSidePanelTags = ({
</> </>
} }
/> />
<AddTagDialog open={openTagDialog} setOpen={setOpenTagDialog} /> <ManageTagsDialog open={openTagDialog} setOpen={setOpenTagDialog} />
<Dialogue <Dialogue
open={showDelDialog} open={showDelDialog}
primaryButtonText="Delete tag" primaryButtonText="Delete tag"

View File

@ -0,0 +1,278 @@
import { useEffect, useReducer, useState, VFC } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { AutocompleteProps, Link, styled, Typography } from '@mui/material';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
import { TagTypeSelect } from './TagTypeSelect';
import { TagOption, TagsInput } from './TagsInput';
import useTags from 'hooks/api/getters/useTags/useTags';
import useTagTypes from 'hooks/api/getters/useTagTypes/useTagTypes';
import { ITag, ITagType } from 'interfaces/tags';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import useTagApi from 'hooks/api/actions/useTagApi/useTagApi';
type Payload = {
addedTags: ITag[];
removedTags: ITag[];
};
interface IManageBulkTagsDialogProps {
open: boolean;
initialValues: ITag[];
initialIndeterminateValues: ITag[];
onCancel: () => void;
onSubmit: (payload: Payload) => void;
}
const StyledDialogFormContent = styled('section')(({ theme }) => ({
['& > *']: {
margin: theme.spacing(1, 0),
},
}));
const formId = 'manage-tags-form';
const mergeTags = (tags: ITag[], newTag: ITag) => [
...tags,
...(tags.some(x => x.value === newTag.value && x.type === newTag.type)
? []
: [newTag]),
];
const filterTags = (tags: ITag[], tag: ITag) =>
tags.filter(x => !(x.value === tag.value && x.type === tag.type));
const payloadReducer = (
state: Payload,
action:
| {
type: 'add' | 'remove';
payload: ITag;
}
| {
type: 'clear';
payload: ITag[];
}
) => {
switch (action.type) {
case 'add':
return {
...state,
addedTags: mergeTags(state.addedTags, action.payload),
removedTags: filterTags(state.removedTags, action.payload),
};
case 'remove':
return {
...state,
addedTags: filterTags(state.addedTags, action.payload),
removedTags: mergeTags(state.removedTags, action.payload),
};
case 'clear':
return {
addedTags: [],
removedTags: action.payload,
};
default:
return state;
}
};
const emptyTagType = {
name: '',
description: '',
icon: '',
};
export const ManageBulkTagsDialog: VFC<IManageBulkTagsDialogProps> = ({
open,
initialValues,
initialIndeterminateValues,
onCancel,
onSubmit,
}) => {
const { tagTypes, loading: tagTypesLoading } = useTagTypes();
const [tagType, setTagType] = useState<typeof tagTypes[0]>(emptyTagType);
const [selectedTags, setSelectedTags] = useState<TagOption[]>([]);
const [indeterminateTags, setIndeterminateTags] = useState<TagOption[]>([]);
const { tags, refetch: refetchTags } = useTags(tagType.name);
const { createTag } = useTagApi();
const tagsOptions = tags.map(({ value }) => ({ title: value }));
const [payload, dispatch] = useReducer(payloadReducer, {
addedTags: [],
removedTags: [],
});
const resetTagType = (
tagType: ITagType = tagTypes.length > 0 ? tagTypes[0] : emptyTagType
) => {
setTagType(tagType);
const newIndeterminateValues = initialIndeterminateValues.filter(
({ type }) => type === tagType.name
);
setSelectedTags(
initialValues
.filter(({ type }) => type === tagType.name)
.filter(
({ type, value }) =>
!newIndeterminateValues.some(
tag => tag.value === value && tag.type === type
)
)
.map(({ value }) => ({
title: value,
}))
);
setIndeterminateTags(
newIndeterminateValues.map(({ value }) => ({
title: value,
}))
);
dispatch({
type: 'clear',
payload: [],
});
};
useEffect(() => {
if (tagTypes.length > 0) {
resetTagType();
}
}, [tagTypesLoading]);
const handleTagTypeChange: AutocompleteProps<
ITagType,
false,
any,
any
>['onChange'] = (event, value) => {
if (value != null && typeof value !== 'string') {
event.preventDefault();
resetTagType(value);
}
};
const createNewTagOnTheFly = (value: string, type: string) =>
createTag({
value,
type,
}).then(async () => {
await refetchTags();
setSelectedTags(prev => [...prev, { title: value }]);
dispatch({
type: 'add',
payload: { value, type },
});
});
const handleInputChange: AutocompleteProps<
TagOption,
true,
false,
false
>['onChange'] = (_event, newValue, reason, selected) => {
if (reason === 'selectOption') {
newValue.forEach(value => {
if (
typeof value !== 'string' &&
typeof value.inputValue === 'string' &&
value.inputValue &&
value.title.startsWith('Create new value')
) {
return createNewTagOnTheFly(value.inputValue, tagType.name);
}
setSelectedTags(newValue as TagOption[]);
setIndeterminateTags((prev: TagOption[]) =>
prev.filter(({ title }) => title !== value.title)
);
if (selected?.option) {
dispatch({
type: 'add',
payload: {
value: selected.option.title,
type: tagType.name,
},
});
}
});
} else if (reason === 'clear') {
setSelectedTags([]);
dispatch({
type: 'clear',
payload: initialValues,
});
} else if (reason === 'removeOption') {
setSelectedTags(newValue as TagOption[]);
if (selected?.option) {
dispatch({
type: 'remove',
payload: {
value: selected.option.title,
type: tagType.name,
},
});
}
}
};
const onClose = () => {
resetTagType();
onCancel();
};
return (
<Dialogue
open={open}
secondaryButtonText="Cancel"
primaryButtonText="Save tags"
title="Update tags to feature toggle"
onClick={() => onSubmit(payload)}
disabledPrimaryButton={
payload.addedTags.length === 0 &&
payload.removedTags.length === 0
}
onClose={onClose}
formId={formId}
>
<Typography
paragraph
sx={{ marginBottom: theme => theme.spacing(2.5) }}
>
Tags allow you to group features together
</Typography>
<form id={formId} onSubmit={() => onSubmit(payload)}>
<StyledDialogFormContent>
<TagTypeSelect
key={tagTypesLoading ? 'loading' : tagTypes.length}
options={tagTypes}
disabled={tagTypesLoading || tagTypes.length === 0}
value={tagType}
onChange={handleTagTypeChange}
/>
<ConditionallyRender
condition={!tagTypesLoading && tagTypes.length === 0}
show={
<Typography variant="body1">
No{' '}
<Link component={RouterLink} to="/tag-types">
tag types
</Link>{' '}
available.
</Typography>
}
elseShow={
<TagsInput
disabled={tagTypesLoading}
options={tagsOptions}
existingTags={initialValues}
indeterminateOptions={indeterminateTags}
tagType={tagType}
selectedOptions={selectedTags}
onChange={handleInputChange}
/>
}
/>
</StyledDialogFormContent>
</form>
</Dialogue>
);
};

View File

@ -8,14 +8,15 @@ import { formatUnknownError } from 'utils/formatUnknownError';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { ITag, ITagType } from 'interfaces/tags'; import { ITag, ITagType } from 'interfaces/tags';
import { TagOption, TagsInput } from './TagsInput'; import { TagOption, TagsInput } from './TagsInput';
import TagTypeSelect from './TagTypeSelect'; import { TagTypeSelect } from './TagTypeSelect';
import useTagApi from 'hooks/api/actions/useTagApi/useTagApi'; import useTagApi from 'hooks/api/actions/useTagApi/useTagApi';
import { AutocompleteChangeReason } from '@mui/base/AutocompleteUnstyled/useAutocomplete'; import { AutocompleteChangeReason } from '@mui/base/AutocompleteUnstyled/useAutocomplete';
import useTags from 'hooks/api/getters/useTags/useTags'; import useTags from 'hooks/api/getters/useTags/useTags';
import cloneDeep from 'lodash.clonedeep'; import cloneDeep from 'lodash.clonedeep';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import useTagTypes from 'hooks/api/getters/useTagTypes/useTagTypes';
interface IAddTagDialogProps { interface IManageTagsProps {
open: boolean; open: boolean;
setOpen: React.Dispatch<React.SetStateAction<boolean>>; setOpen: React.Dispatch<React.SetStateAction<boolean>>;
} }
@ -43,7 +44,8 @@ const optionsToTags = (options: TagOption[], type: string): ITag[] => {
}); });
}; };
const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => { export const ManageTagsDialog = ({ open, setOpen }: IManageTagsProps) => {
const { tagTypes } = useTagTypes();
const featureId = useRequiredPathParam('featureId'); const featureId = useRequiredPathParam('featureId');
const { createTag } = useTagApi(); const { createTag } = useTagApi();
const { updateFeatureTags, loading: featureLoading } = useFeatureApi(); const { updateFeatureTags, loading: featureLoading } = useFeatureApi();
@ -238,44 +240,40 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
const formId = 'add-tag-form'; const formId = 'add-tag-form';
return ( return (
<> <Dialogue
<Dialogue open={open}
open={open} secondaryButtonText="Cancel"
secondaryButtonText="Cancel" primaryButtonText="Save tags"
primaryButtonText={`Save tags`} title="Update tags to feature toggle"
title="Update tags to feature toggle" onClick={onSubmit}
onClick={onSubmit} disabledPrimaryButton={loading || differenceCount === 0}
disabledPrimaryButton={loading || differenceCount === 0} onClose={onCancel}
onClose={onCancel} formId={formId}
formId={formId} >
> <>
<> <Typography
<Typography paragraph
paragraph sx={{ marginBottom: theme => theme.spacing(2.5) }}
sx={{ marginBottom: theme => theme.spacing(2.5) }} >
> Tags allow you to group features together
Tags allow you to group features together </Typography>
</Typography> <form id={formId} onSubmit={onSubmit}>
<form id={formId} onSubmit={onSubmit}> <StyledDialogFormContent>
<StyledDialogFormContent> <TagTypeSelect
<TagTypeSelect options={tagTypes}
autoFocus value={tagType}
value={tagType} onChange={handleTagTypeChange}
onChange={handleTagTypeChange} />
/> <TagsInput
<TagsInput options={tagTypeOptions}
options={tagTypeOptions} existingTags={tags}
existingTags={tags} tagType={tagType}
tagType={tagType} selectedOptions={selectedTagOptions}
selectedOptions={selectedTagOptions} onChange={handleInputChange}
onChange={handleInputChange} />
/> </StyledDialogFormContent>
</StyledDialogFormContent> </form>
</form> </>
</> </Dialogue>
</Dialogue>
</>
); );
}; };
export default AddTagDialog;

View File

@ -1,5 +1,3 @@
import React from 'react';
import useTagTypes from 'hooks/api/getters/useTagTypes/useTagTypes';
import { import {
Autocomplete, Autocomplete,
AutocompleteProps, AutocompleteProps,
@ -11,24 +9,31 @@ import {
import { ITagType } from 'interfaces/tags'; import { ITagType } from 'interfaces/tags';
interface ITagSelect { interface ITagSelect {
options: ITagType[];
value: ITagType; value: ITagType;
disabled?: boolean;
onChange: AutocompleteProps<ITagType, false, any, any>['onChange']; onChange: AutocompleteProps<ITagType, false, any, any>['onChange'];
autoFocus?: boolean;
} }
const ListItem = styled('li')({ const ListItem = styled('li')({
flexDirection: 'column', flexDirection: 'column',
}); });
const TagTypeSelect = ({ value, onChange }: ITagSelect) => {
const { tagTypes } = useTagTypes(); export const TagTypeSelect = ({
options,
value,
disabled = false,
onChange,
}: ITagSelect) => {
const theme = useTheme(); const theme = useTheme();
return ( return (
<Autocomplete <Autocomplete
disablePortal disablePortal
disabled={disabled}
id="tag-type-select" id="tag-type-select"
sx={{ marginTop: theme => theme.spacing(2), width: 500 }} sx={{ marginTop: theme => theme.spacing(2), width: 500 }}
options={tagTypes} options={options}
disableClearable disableClearable
value={value} value={value}
getOptionLabel={option => option.name} getOptionLabel={option => option.name}
@ -54,5 +59,3 @@ const TagTypeSelect = ({ value, onChange }: ITagSelect) => {
/> />
); );
}; };
export default TagTypeSelect;

View File

@ -10,6 +10,7 @@ import {
import React from 'react'; import React from 'react';
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank'; import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
import CheckBoxIcon from '@mui/icons-material/CheckBox'; import CheckBoxIcon from '@mui/icons-material/CheckBox';
import IndeterminateCheckBoxIcon from '@mui/icons-material/IndeterminateCheckBox';
import { ITag, ITagType } from 'interfaces/tags'; import { ITag, ITagType } from 'interfaces/tags';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Add } from '@mui/icons-material'; import { Add } from '@mui/icons-material';
@ -19,12 +20,15 @@ export type TagOption = {
title: string; title: string;
inputValue?: string; inputValue?: string;
}; };
interface ITagsInputProps { interface ITagsInputProps {
options: TagOption[]; options: TagOption[];
existingTags: ITag[]; existingTags: ITag[];
tagType: ITagType; tagType: ITagType;
selectedOptions: TagOption[]; selectedOptions: TagOption[];
onChange: AutocompleteProps<TagOption | string, true, any, any>['onChange']; indeterminateOptions?: TagOption[];
disabled?: boolean;
onChange: AutocompleteProps<TagOption, true, false, false>['onChange'];
} }
const filter = createFilterOptions<TagOption>(); const filter = createFilterOptions<TagOption>();
@ -32,12 +36,13 @@ const filter = createFilterOptions<TagOption>();
export const TagsInput = ({ export const TagsInput = ({
options, options,
selectedOptions, selectedOptions,
indeterminateOptions,
tagType, tagType,
existingTags, existingTags,
disabled = false,
onChange, onChange,
}: ITagsInputProps) => { }: ITagsInputProps) => {
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />; const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
const checkedIcon = <CheckBoxIcon fontSize="small" />;
const getOptionLabel = (option: TagOption) => { const getOptionLabel = (option: TagOption) => {
// Add "xxx" option created dynamically // Add "xxx" option created dynamically
@ -55,6 +60,11 @@ export const TagsInput = ({
option: TagOption, option: TagOption,
{ selected }: { selected: boolean } { selected }: { selected: boolean }
) => { ) => {
const isIndeterminate =
indeterminateOptions?.some(
indeterminateOption =>
indeterminateOption.title === option.title
) ?? false;
return ( return (
<li {...props}> <li {...props}>
<ConditionallyRender <ConditionallyRender
@ -63,9 +73,13 @@ export const TagsInput = ({
elseShow={ elseShow={
<Checkbox <Checkbox
icon={icon} icon={icon}
checkedIcon={checkedIcon} checkedIcon={<CheckBoxIcon fontSize="small" />}
indeterminateIcon={
<IndeterminateCheckBoxIcon fontSize="small" />
}
sx={{ mr: theme => theme.spacing(0.5) }} sx={{ mr: theme => theme.spacing(0.5) }}
checked={selected} checked={selected && !isIndeterminate}
indeterminate={isIndeterminate}
/> />
} }
/> />
@ -77,19 +91,18 @@ export const TagsInput = ({
const renderTags = ( const renderTags = (
tagValue: TagOption[], tagValue: TagOption[],
getTagProps: AutocompleteRenderGetTagProps getTagProps: AutocompleteRenderGetTagProps
) => { ) =>
return tagValue.map((option, index) => { tagValue.map((option, index) => {
const exists = existingTags.some( const exists = existingTags.some(
existingTag => existingTag =>
existingTag.value === option.title && existingTag.value === option.title &&
existingTag.type === tagType.name existingTag.type === tagType.name
); );
if (exists) { if (exists && indeterminateOptions === undefined) {
return null; return null;
} }
return <Chip {...getTagProps({ index })} label={option.title} />; return <Chip {...getTagProps({ index })} label={option.title} />;
}); });
};
const filterOptions = ( const filterOptions = (
options: TagOption[], options: TagOption[],
@ -139,6 +152,7 @@ export const TagsInput = ({
placeholder="Select values" placeholder="Select values"
/> />
)} )}
disabled={disabled}
/> />
); );
}; };

View File

@ -24,7 +24,7 @@ import { FeatureSettings } from './FeatureSettings/FeatureSettings';
import useLoading from 'hooks/useLoading'; import useLoading from 'hooks/useLoading';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog'; import { FeatureStaleDialog } from 'component/common/FeatureStaleDialog/FeatureStaleDialog';
import AddTagDialog from './FeatureOverview/AddTagDialog/AddTagDialog'; import { ManageTagsDialog } from './FeatureOverview/ManageTagsDialog/ManageTagsDialog';
import { FeatureStatusChip } from 'component/common/FeatureStatusChip/FeatureStatusChip'; import { FeatureStatusChip } from 'component/common/FeatureStatusChip/FeatureStatusChip';
import { FeatureNotFound } from 'component/feature/FeatureView/FeatureNotFound/FeatureNotFound'; import { FeatureNotFound } from 'component/feature/FeatureView/FeatureNotFound/FeatureNotFound';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
@ -260,7 +260,7 @@ export const FeatureView = () => {
featureId={featureId} featureId={featureId}
projectId={projectId} projectId={projectId}
/> />
<AddTagDialog open={openTagDialog} setOpen={setOpenTagDialog} /> <ManageTagsDialog open={openTagDialog} setOpen={setOpenTagDialog} />
</div> </div>
); );
}; };

View File

@ -65,7 +65,7 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog'; import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
import { RowSelectCell } from './RowSelectCell/RowSelectCell'; import { RowSelectCell } from './RowSelectCell/RowSelectCell';
import { BatchSelectionActionsBar } from '../../../common/BatchSelectionActionsBar/BatchSelectionActionsBar'; import { BatchSelectionActionsBar } from '../../../common/BatchSelectionActionsBar/BatchSelectionActionsBar';
import { ProjectFeaturesBatchActions } from './SelectionActionsBar/ProjectFeaturesBatchActions'; import { ProjectFeaturesBatchActions } from './ProjectFeaturesBatchActions/ProjectFeaturesBatchActions';
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({ const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
whiteSpace: 'nowrap', whiteSpace: 'nowrap',

View File

@ -21,7 +21,6 @@ export const ArchiveButton: VFC<IArchiveButtonProps> = ({
const onConfirm = async () => { const onConfirm = async () => {
setIsDialogOpen(false); setIsDialogOpen(false);
await refetch(); await refetch();
// TODO: toast
}; };
return ( return (

View File

@ -0,0 +1,111 @@
import { useMemo, useState, VFC } from 'react';
import { Label } from '@mui/icons-material';
import { Button } from '@mui/material';
import { ManageBulkTagsDialog } from 'component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageBulkTagsDialog';
import type { FeatureSchema } from 'openapi';
import { ITag } from 'interfaces/tags';
import useTagApi from 'hooks/api/actions/useTagApi/useTagApi';
import useToast from 'hooks/useToast';
import { formatUnknownError } from 'utils/formatUnknownError';
import useProject from 'hooks/api/getters/useProject/useProject';
import { PermissionHOC } from 'component/common/PermissionHOC/PermissionHOC';
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
interface IManageTagsProps {
data: FeatureSchema[];
projectId: string;
}
export const ManageTags: VFC<IManageTagsProps> = ({ projectId, data }) => {
const { bulkUpdateTags } = useTagApi();
const { refetch } = useProject(projectId);
const { setToastData, setToastApiError } = useToast();
const [isOpen, setIsOpen] = useState(false);
const [initialValues, indeterminateValues] = useMemo(() => {
const uniqueTags = data
.flatMap(({ tags }) => tags || [])
.reduce<ITag[]>(
(acc, tag) => [
...acc,
...(acc.some(
x => x.type === tag.type && x.value === tag.value
)
? []
: [tag]),
],
[]
);
const tagsNotPresentInEveryFeature = uniqueTags.filter(
tag =>
!data.every(({ tags }) =>
tags?.some(
x => x.type === tag.type && x.value === tag.value
)
)
);
return [uniqueTags, tagsNotPresentInEveryFeature];
}, [data]);
const onSubmit = async ({
addedTags,
removedTags,
}: {
addedTags: ITag[];
removedTags: ITag[];
}) => {
const features = data.map(({ name }) => name);
const payload = { features, tags: { addedTags, removedTags } };
try {
await bulkUpdateTags(payload);
refetch();
const added = addedTags.length
? `Added tags: ${addedTags
.map(({ type, value }) => `${type}:${value}`)
.join(', ')}.`
: '';
const removed = removedTags.length
? `Removed tags: ${removedTags
.map(({ type, value }) => `${type}:${value}`)
.join(', ')}.`
: '';
setToastData({
title: 'Tags updated',
text: `${features.length} feature toggles updated. ${added} ${removed}`,
type: 'success',
autoHideDuration: 12000,
});
} catch (error: unknown) {
setToastApiError(formatUnknownError(error));
}
setIsOpen(false);
};
return (
<>
<PermissionHOC projectId={projectId} permission={UPDATE_FEATURE}>
{({ hasAccess }) => (
<Button
disabled={!hasAccess || isOpen}
startIcon={<Label />}
variant="outlined"
size="small"
onClick={() => setIsOpen(true)}
>
Tags
</Button>
)}
</PermissionHOC>
<ManageBulkTagsDialog
key={data.length}
open={isOpen}
onCancel={() => setIsOpen(false)}
onSubmit={onSubmit}
initialValues={initialValues}
initialIndeterminateValues={indeterminateValues}
/>
</>
);
};

View File

@ -5,8 +5,9 @@ import type { FeatureSchema } from 'openapi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig'; import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog'; import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { ArchiveButton } from './ArchiveButton/ArchiveButton'; import { ArchiveButton } from './ArchiveButton';
import { MoreActions } from './MoreActions/MoreActions'; import { MoreActions } from './MoreActions';
import { ManageTags } from './ManageTags';
interface IProjectFeaturesBatchActionsProps { interface IProjectFeaturesBatchActionsProps {
selectedIds: string[]; selectedIds: string[];
@ -43,14 +44,7 @@ export const ProjectFeaturesBatchActions: FC<
> >
Export Export
</Button> </Button>
<Button <ManageTags projectId={projectId} data={selectedData} />
disabled
startIcon={<Label />}
variant="outlined"
size="small"
>
Tags
</Button>
<MoreActions projectId={projectId} data={selectedData} /> <MoreActions projectId={projectId} data={selectedData} />
<ConditionallyRender <ConditionallyRender
condition={Boolean(uiConfig?.flags?.featuresExportImport)} condition={Boolean(uiConfig?.flags?.featuresExportImport)}

View File

@ -1,5 +1,5 @@
import useAPI from '../useApi/useApi'; import useAPI from '../useApi/useApi';
import { TagSchema } from 'openapi/models/tagSchema'; import type { TagSchema, TagsBulkAddSchema } from 'openapi';
const useTagApi = () => { const useTagApi = () => {
const { makeRequest, createRequest, errors, loading } = useAPI({ const { makeRequest, createRequest, errors, loading } = useAPI({
@ -20,8 +20,23 @@ const useTagApi = () => {
} }
}; };
const bulkUpdateTags = async (payload: TagsBulkAddSchema) => {
const path = `api/admin/tags/features`;
const req = createRequest(path, {
method: 'PUT',
body: JSON.stringify(payload),
});
try {
return await makeRequest(req.caller, req.id);
} catch (e) {
throw e;
}
};
return { return {
createTag, createTag,
bulkUpdateTags,
errors, errors,
loading, loading,
}; };

View File

@ -30,7 +30,7 @@ const useToast = () => {
if (toast.persist) { if (toast.persist) {
setToast({ ...toast, show: true }); setToast({ ...toast, show: true });
} else { } else {
setToast({ ...toast, show: true, autoHideDuration: 6000 }); setToast({ autoHideDuration: 6000, ...toast, show: true });
} }
}, },
[setToast] [setToast]