From 2147206b498a950d56048cb021d74337ab8ae2d4 Mon Sep 17 00:00:00 2001
From: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com>
Date: Tue, 21 Mar 2023 13:37:25 +0100
Subject: [PATCH] Bulk tags (#3337)
1-753/frontend-features-in-project-overview
---
.../FeatureOverviewSidePanelTags.tsx | 4 +-
.../ManageTagsDialog/ManageBulkTagsDialog.tsx | 278 ++++++++++++++++++
.../ManageTagsDialog.tsx} | 82 +++---
.../TagTypeSelect.tsx | 19 +-
.../TagsInput.tsx | 30 +-
.../feature/FeatureView/FeatureView.tsx | 4 +-
.../ProjectFeatureToggles.tsx | 2 +-
.../ArchiveButton.tsx | 1 -
.../ManageTags.tsx | 111 +++++++
.../MoreActions.tsx | 0
.../ProjectFeaturesBatchActions.tsx | 14 +-
.../hooks/api/actions/useTagApi/useTagApi.ts | 17 +-
frontend/src/hooks/useToast.tsx | 2 +-
13 files changed, 488 insertions(+), 76 deletions(-)
create mode 100644 frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageBulkTagsDialog.tsx
rename frontend/src/component/feature/FeatureView/FeatureOverview/{AddTagDialog/AddTagDialog.tsx => ManageTagsDialog/ManageTagsDialog.tsx} (82%)
rename frontend/src/component/feature/FeatureView/FeatureOverview/{AddTagDialog => ManageTagsDialog}/TagTypeSelect.tsx (83%)
rename frontend/src/component/feature/FeatureView/FeatureOverview/{AddTagDialog => ManageTagsDialog}/TagsInput.tsx (81%)
rename frontend/src/component/project/Project/ProjectFeatureToggles/{SelectionActionsBar/ArchiveButton => ProjectFeaturesBatchActions}/ArchiveButton.tsx (98%)
create mode 100644 frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ManageTags.tsx
rename frontend/src/component/project/Project/ProjectFeatureToggles/{SelectionActionsBar/MoreActions => ProjectFeaturesBatchActions}/MoreActions.tsx (100%)
rename frontend/src/component/project/Project/ProjectFeatureToggles/{SelectionActionsBar => ProjectFeaturesBatchActions}/ProjectFeaturesBatchActions.tsx (86%)
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags.tsx
index e735ed9fb4..b3c7b8562a 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags.tsx
@@ -3,7 +3,7 @@ import { useContext, useState } from 'react';
import { Button, Chip, Divider, styled } from '@mui/material';
import useFeatureTags from 'hooks/api/getters/useFeatureTags/useFeatureTags';
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 AccessContext from 'contexts/AccessContext';
import { Dialogue } from 'component/common/Dialogue/Dialogue';
@@ -120,7 +120,7 @@ export const FeatureOverviewSidePanelTags = ({
>
}
/>
-
+
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 = ({
+ open,
+ initialValues,
+ initialIndeterminateValues,
+ onCancel,
+ onSubmit,
+}) => {
+ const { tagTypes, loading: tagTypesLoading } = useTagTypes();
+ const [tagType, setTagType] = useState(emptyTagType);
+ const [selectedTags, setSelectedTags] = useState([]);
+ const [indeterminateTags, setIndeterminateTags] = useState([]);
+ 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 (
+ onSubmit(payload)}
+ disabledPrimaryButton={
+ payload.addedTags.length === 0 &&
+ payload.removedTags.length === 0
+ }
+ onClose={onClose}
+ formId={formId}
+ >
+ theme.spacing(2.5) }}
+ >
+ Tags allow you to group features together
+
+
+
+ );
+};
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/AddTagDialog.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageTagsDialog.tsx
similarity index 82%
rename from frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/AddTagDialog.tsx
rename to frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageTagsDialog.tsx
index e9d83ce443..feb5cbc012 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/AddTagDialog.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageTagsDialog.tsx
@@ -8,14 +8,15 @@ import { formatUnknownError } from 'utils/formatUnknownError';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { ITag, ITagType } from 'interfaces/tags';
import { TagOption, TagsInput } from './TagsInput';
-import TagTypeSelect from './TagTypeSelect';
+import { TagTypeSelect } from './TagTypeSelect';
import useTagApi from 'hooks/api/actions/useTagApi/useTagApi';
import { AutocompleteChangeReason } from '@mui/base/AutocompleteUnstyled/useAutocomplete';
import useTags from 'hooks/api/getters/useTags/useTags';
import cloneDeep from 'lodash.clonedeep';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
+import useTagTypes from 'hooks/api/getters/useTagTypes/useTagTypes';
-interface IAddTagDialogProps {
+interface IManageTagsProps {
open: boolean;
setOpen: React.Dispatch>;
}
@@ -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 { createTag } = useTagApi();
const { updateFeatureTags, loading: featureLoading } = useFeatureApi();
@@ -238,44 +240,40 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
const formId = 'add-tag-form';
return (
- <>
-
- <>
- theme.spacing(2.5) }}
- >
- Tags allow you to group features together
-
-
- >
-
- >
+
+ <>
+ theme.spacing(2.5) }}
+ >
+ Tags allow you to group features together
+
+
+ >
+
);
};
-
-export default AddTagDialog;
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/TagTypeSelect.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/TagTypeSelect.tsx
similarity index 83%
rename from frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/TagTypeSelect.tsx
rename to frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/TagTypeSelect.tsx
index 8f286a98a0..93c19011d4 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/TagTypeSelect.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/TagTypeSelect.tsx
@@ -1,5 +1,3 @@
-import React from 'react';
-import useTagTypes from 'hooks/api/getters/useTagTypes/useTagTypes';
import {
Autocomplete,
AutocompleteProps,
@@ -11,24 +9,31 @@ import {
import { ITagType } from 'interfaces/tags';
interface ITagSelect {
+ options: ITagType[];
value: ITagType;
+ disabled?: boolean;
onChange: AutocompleteProps['onChange'];
- autoFocus?: boolean;
}
const ListItem = styled('li')({
flexDirection: 'column',
});
-const TagTypeSelect = ({ value, onChange }: ITagSelect) => {
- const { tagTypes } = useTagTypes();
+
+export const TagTypeSelect = ({
+ options,
+ value,
+ disabled = false,
+ onChange,
+}: ITagSelect) => {
const theme = useTheme();
return (
theme.spacing(2), width: 500 }}
- options={tagTypes}
+ options={options}
disableClearable
value={value}
getOptionLabel={option => option.name}
@@ -54,5 +59,3 @@ const TagTypeSelect = ({ value, onChange }: ITagSelect) => {
/>
);
};
-
-export default TagTypeSelect;
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/TagsInput.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/TagsInput.tsx
similarity index 81%
rename from frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/TagsInput.tsx
rename to frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/TagsInput.tsx
index 4371255b8a..9cc6cd1879 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/TagsInput.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/TagsInput.tsx
@@ -10,6 +10,7 @@ import {
import React from 'react';
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
import CheckBoxIcon from '@mui/icons-material/CheckBox';
+import IndeterminateCheckBoxIcon from '@mui/icons-material/IndeterminateCheckBox';
import { ITag, ITagType } from 'interfaces/tags';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import { Add } from '@mui/icons-material';
@@ -19,12 +20,15 @@ export type TagOption = {
title: string;
inputValue?: string;
};
+
interface ITagsInputProps {
options: TagOption[];
existingTags: ITag[];
tagType: ITagType;
selectedOptions: TagOption[];
- onChange: AutocompleteProps['onChange'];
+ indeterminateOptions?: TagOption[];
+ disabled?: boolean;
+ onChange: AutocompleteProps['onChange'];
}
const filter = createFilterOptions();
@@ -32,12 +36,13 @@ const filter = createFilterOptions();
export const TagsInput = ({
options,
selectedOptions,
+ indeterminateOptions,
tagType,
existingTags,
+ disabled = false,
onChange,
}: ITagsInputProps) => {
const icon = ;
- const checkedIcon = ;
const getOptionLabel = (option: TagOption) => {
// Add "xxx" option created dynamically
@@ -55,6 +60,11 @@ export const TagsInput = ({
option: TagOption,
{ selected }: { selected: boolean }
) => {
+ const isIndeterminate =
+ indeterminateOptions?.some(
+ indeterminateOption =>
+ indeterminateOption.title === option.title
+ ) ?? false;
return (
}
+ indeterminateIcon={
+
+ }
sx={{ mr: theme => theme.spacing(0.5) }}
- checked={selected}
+ checked={selected && !isIndeterminate}
+ indeterminate={isIndeterminate}
/>
}
/>
@@ -77,19 +91,18 @@ export const TagsInput = ({
const renderTags = (
tagValue: TagOption[],
getTagProps: AutocompleteRenderGetTagProps
- ) => {
- return tagValue.map((option, index) => {
+ ) =>
+ tagValue.map((option, index) => {
const exists = existingTags.some(
existingTag =>
existingTag.value === option.title &&
existingTag.type === tagType.name
);
- if (exists) {
+ if (exists && indeterminateOptions === undefined) {
return null;
}
return ;
});
- };
const filterOptions = (
options: TagOption[],
@@ -139,6 +152,7 @@ export const TagsInput = ({
placeholder="Select values"
/>
)}
+ disabled={disabled}
/>
);
};
diff --git a/frontend/src/component/feature/FeatureView/FeatureView.tsx b/frontend/src/component/feature/FeatureView/FeatureView.tsx
index 121db836ab..fbd4665ed0 100644
--- a/frontend/src/component/feature/FeatureView/FeatureView.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureView.tsx
@@ -24,7 +24,7 @@ import { FeatureSettings } from './FeatureSettings/FeatureSettings';
import useLoading from 'hooks/useLoading';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
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 { FeatureNotFound } from 'component/feature/FeatureView/FeatureNotFound/FeatureNotFound';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
@@ -260,7 +260,7 @@ export const FeatureView = () => {
featureId={featureId}
projectId={projectId}
/>
-
+
);
};
diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx
index 73247b6d6a..a5f9ffa192 100644
--- a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx
+++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeatureToggles.tsx
@@ -65,7 +65,7 @@ import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
import { RowSelectCell } from './RowSelectCell/RowSelectCell';
import { BatchSelectionActionsBar } from '../../../common/BatchSelectionActionsBar/BatchSelectionActionsBar';
-import { ProjectFeaturesBatchActions } from './SelectionActionsBar/ProjectFeaturesBatchActions';
+import { ProjectFeaturesBatchActions } from './ProjectFeaturesBatchActions/ProjectFeaturesBatchActions';
const StyledResponsiveButton = styled(ResponsiveButton)(() => ({
whiteSpace: 'nowrap',
diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/SelectionActionsBar/ArchiveButton/ArchiveButton.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ArchiveButton.tsx
similarity index 98%
rename from frontend/src/component/project/Project/ProjectFeatureToggles/SelectionActionsBar/ArchiveButton/ArchiveButton.tsx
rename to frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ArchiveButton.tsx
index 742c8c366f..e313e084be 100644
--- a/frontend/src/component/project/Project/ProjectFeatureToggles/SelectionActionsBar/ArchiveButton/ArchiveButton.tsx
+++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ArchiveButton.tsx
@@ -21,7 +21,6 @@ export const ArchiveButton: VFC = ({
const onConfirm = async () => {
setIsDialogOpen(false);
await refetch();
- // TODO: toast
};
return (
diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ManageTags.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ManageTags.tsx
new file mode 100644
index 0000000000..15c5312e8d
--- /dev/null
+++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ManageTags.tsx
@@ -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 = ({ 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(
+ (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 (
+ <>
+
+ {({ hasAccess }) => (
+ }
+ variant="outlined"
+ size="small"
+ onClick={() => setIsOpen(true)}
+ >
+ Tags
+
+ )}
+
+ setIsOpen(false)}
+ onSubmit={onSubmit}
+ initialValues={initialValues}
+ initialIndeterminateValues={indeterminateValues}
+ />
+ >
+ );
+};
diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/SelectionActionsBar/MoreActions/MoreActions.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/MoreActions.tsx
similarity index 100%
rename from frontend/src/component/project/Project/ProjectFeatureToggles/SelectionActionsBar/MoreActions/MoreActions.tsx
rename to frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/MoreActions.tsx
diff --git a/frontend/src/component/project/Project/ProjectFeatureToggles/SelectionActionsBar/ProjectFeaturesBatchActions.tsx b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ProjectFeaturesBatchActions.tsx
similarity index 86%
rename from frontend/src/component/project/Project/ProjectFeatureToggles/SelectionActionsBar/ProjectFeaturesBatchActions.tsx
rename to frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ProjectFeaturesBatchActions.tsx
index cc88ab3b86..2390aabe3a 100644
--- a/frontend/src/component/project/Project/ProjectFeatureToggles/SelectionActionsBar/ProjectFeaturesBatchActions.tsx
+++ b/frontend/src/component/project/Project/ProjectFeatureToggles/ProjectFeaturesBatchActions/ProjectFeaturesBatchActions.tsx
@@ -5,8 +5,9 @@ import type { FeatureSchema } from 'openapi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import { ExportDialog } from 'component/feature/FeatureToggleList/ExportDialog';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
-import { ArchiveButton } from './ArchiveButton/ArchiveButton';
-import { MoreActions } from './MoreActions/MoreActions';
+import { ArchiveButton } from './ArchiveButton';
+import { MoreActions } from './MoreActions';
+import { ManageTags } from './ManageTags';
interface IProjectFeaturesBatchActionsProps {
selectedIds: string[];
@@ -43,14 +44,7 @@ export const ProjectFeaturesBatchActions: FC<
>
Export
- }
- variant="outlined"
- size="small"
- >
- Tags
-
+
{
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 {
createTag,
+ bulkUpdateTags,
errors,
loading,
};
diff --git a/frontend/src/hooks/useToast.tsx b/frontend/src/hooks/useToast.tsx
index f9bf8fcc5f..8510688d7a 100644
--- a/frontend/src/hooks/useToast.tsx
+++ b/frontend/src/hooks/useToast.tsx
@@ -30,7 +30,7 @@ const useToast = () => {
if (toast.persist) {
setToast({ ...toast, show: true });
} else {
- setToast({ ...toast, show: true, autoHideDuration: 6000 });
+ setToast({ autoHideDuration: 6000, ...toast, show: true });
}
},
[setToast]