mirror of
https://github.com/Unleash/unleash.git
synced 2024-12-22 19:07:54 +01:00
Add multiple tags (#3032)
Add Tag dialog redesign to allow batch add Some Refactoring Signed-off-by: andreas-unleash <andreas@getunleash.ai> <!-- Thanks for creating a PR! To make it easier for reviewers and everyone else to understand what your changes relate to, please add some relevant content to the headings below. Feel free to ignore or delete sections that you don't think are relevant. Thank you! ❤️ --> ## About the changes <!-- Describe the changes introduced. What are they and why are they being introduced? Feel free to also add screenshots or steps to view the changes if they're visual. --> <!-- Does it close an issue? Multiple? --> Closes [1-611](https://linear.app/unleash/issue/1-611/create-a-auto-complete-component-for-tags) <!-- (For internal contributors): Does it relate to an issue on public roadmap? --> <!-- Relates to [roadmap](https://github.com/orgs/Unleash/projects/10) item: # --> ### Important files <!-- PRs can contain a lot of changes, but not all changes are equally important. Where should a reviewer start looking to get an overview of the changes? Are any files particularly important? --> ## Discussion points <!-- Anything about the PR you'd like to discuss before it gets merged? Got any questions or doubts? --> https://user-images.githubusercontent.com/104830839/216286897-4e392822-57c2-4e50-a5d8-e89d006b3fa5.mov --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
parent
a7cb20c42b
commit
e589e56373
20
.github/workflows/build_frontend_prs.yml
vendored
20
.github/workflows/build_frontend_prs.yml
vendored
@ -15,13 +15,13 @@ jobs:
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: yarn --frozen-lockfile
|
||||
- run: yarn run test
|
||||
- run: yarn run fmt:check
|
||||
- run: yarn run lint:check
|
||||
- run: yarn run ts:check # TODO: optimize
|
||||
- uses: actions/checkout@v3
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- run: yarn --frozen-lockfile
|
||||
- run: yarn run test
|
||||
- run: yarn run fmt:check
|
||||
- run: yarn run lint:check
|
||||
- run: yarn run ts:check # TODO: optimize
|
||||
|
@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
import GeneralSelect, {
|
||||
IGeneralSelectProps,
|
||||
} from '../GeneralSelect/GeneralSelect';
|
||||
import useTagTypes from 'hooks/api/getters/useTagTypes/useTagTypes';
|
||||
|
||||
interface ITagSelect {
|
||||
name: string;
|
||||
value: string;
|
||||
onChange: IGeneralSelectProps['onChange'];
|
||||
autoFocus?: boolean;
|
||||
}
|
||||
|
||||
const TagSelect = ({ value, onChange, ...rest }: ITagSelect) => {
|
||||
const { tagTypes } = useTagTypes();
|
||||
|
||||
const options = tagTypes.map(tagType => ({
|
||||
key: tagType.name,
|
||||
label: tagType.name,
|
||||
title: tagType.name,
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<GeneralSelect
|
||||
label="Tag type"
|
||||
id="tag-select"
|
||||
options={options}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
{...rest}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagSelect;
|
@ -1,96 +1,152 @@
|
||||
import { styled, Typography } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import { AutocompleteValue, styled, Typography } from '@mui/material';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { Dialogue } from 'component/common/Dialogue/Dialogue';
|
||||
import Input from 'component/common/Input/Input';
|
||||
import { trim } from 'component/common/util';
|
||||
import TagSelect from 'component/common/TagSelect/TagSelect';
|
||||
import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi';
|
||||
import useTags from 'hooks/api/getters/useTags/useTags';
|
||||
import useFeatureTags from 'hooks/api/getters/useFeatureTags/useFeatureTags';
|
||||
import useToast from 'hooks/useToast';
|
||||
import { formatUnknownError } from 'utils/formatUnknownError';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import { ITag } from 'interfaces/tags';
|
||||
|
||||
const StyledInput = styled(Input)(() => ({
|
||||
width: '100%',
|
||||
}));
|
||||
import { ITagType } from 'interfaces/tags';
|
||||
import { TagOption, TagsInput } from './TagsInput';
|
||||
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';
|
||||
|
||||
interface IAddTagDialogProps {
|
||||
open: boolean;
|
||||
setOpen: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
interface IDefaultTag {
|
||||
type: string;
|
||||
value: string;
|
||||
|
||||
[index: string]: string;
|
||||
}
|
||||
|
||||
const StyledDialogFormContent = styled('section')(({ theme }) => ({
|
||||
['& > *']: {
|
||||
margin: '0.5rem 0',
|
||||
margin: theme.spacing(1, 0),
|
||||
},
|
||||
}));
|
||||
|
||||
const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
|
||||
const DEFAULT_TAG: IDefaultTag = { type: 'simple', value: '' };
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const { createTag } = useTagApi();
|
||||
const { addTagToFeature, loading } = useFeatureApi();
|
||||
const { tags, refetch } = useTags(featureId);
|
||||
const [errors, setErrors] = useState({ tagError: '' });
|
||||
const { tags, refetch } = useFeatureTags(featureId);
|
||||
const { setToastData } = useToast();
|
||||
const [tag, setTag] = useState(DEFAULT_TAG);
|
||||
const [tagType, setTagType] = useState<ITagType>({
|
||||
name: 'simple',
|
||||
description: 'Simple tag to get you started',
|
||||
icon: '',
|
||||
});
|
||||
|
||||
const [selectedTagOptions, setSelectedTagOptions] = useState<TagOption[]>(
|
||||
[]
|
||||
);
|
||||
|
||||
const { tags: allTags, refetch: refetchAllTags } = useTags(tagType.name);
|
||||
|
||||
const tagTypeOptions: TagOption[] = useMemo(() => {
|
||||
return allTags.map(tag => {
|
||||
return {
|
||||
title: tag.value,
|
||||
};
|
||||
});
|
||||
}, [allTags]);
|
||||
|
||||
const onCancel = () => {
|
||||
setOpen(false);
|
||||
setErrors({ tagError: '' });
|
||||
setTag(DEFAULT_TAG);
|
||||
setSelectedTagOptions([]);
|
||||
};
|
||||
|
||||
const onSubmit = async (evt: React.SyntheticEvent) => {
|
||||
evt.preventDefault();
|
||||
if (!tag.type) {
|
||||
tag.type = 'simple';
|
||||
}
|
||||
try {
|
||||
await addTagToFeature(featureId, tag);
|
||||
|
||||
let added = 0;
|
||||
if (selectedTagOptions.length !== 0) {
|
||||
for (const tagOption of selectedTagOptions) {
|
||||
if (
|
||||
!tags.includes({
|
||||
type: tagType.name,
|
||||
value: tagOption.title,
|
||||
})
|
||||
) {
|
||||
try {
|
||||
if (!tagOption.title.startsWith('Create')) {
|
||||
await addTagToFeature(featureId, {
|
||||
type: tagType.name,
|
||||
value: tagOption.title,
|
||||
});
|
||||
added++;
|
||||
await refetch();
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const message = formatUnknownError(error);
|
||||
setToastData({
|
||||
type: 'error',
|
||||
title: `Failed to add tag`,
|
||||
text: message,
|
||||
confetti: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
added > 0 &&
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: `Added tag${added > 1 ? 's' : ''} to toggle`,
|
||||
text: `We successfully added ${added} new tag${
|
||||
added > 1 ? 's' : ''
|
||||
} to your toggle`,
|
||||
confetti: true,
|
||||
});
|
||||
setOpen(false);
|
||||
setTag(DEFAULT_TAG);
|
||||
refetch();
|
||||
setToastData({
|
||||
type: 'success',
|
||||
title: 'Added tag to toggle',
|
||||
text: 'We successfully added a tag to your toggle',
|
||||
confetti: true,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
const message = formatUnknownError(error);
|
||||
setErrors({ tagError: message });
|
||||
setSelectedTagOptions([]);
|
||||
}
|
||||
};
|
||||
|
||||
const isValueNotEmpty = (name: string) => name.length;
|
||||
const isTagUnique = (tag: ITag) =>
|
||||
!tags.some(
|
||||
({ type, value }) => type === tag.type && value === tag.value
|
||||
);
|
||||
const isValid = isValueNotEmpty(tag.value) && isTagUnique(tag);
|
||||
|
||||
const onUpdateTag = (key: string, value: string) => {
|
||||
setErrors({ tagError: '' });
|
||||
const updatedTag = { ...tag, [key]: trim(value) };
|
||||
|
||||
if (!isTagUnique(updatedTag)) {
|
||||
setErrors({
|
||||
tagError: 'Tag already exists for this feature toggle.',
|
||||
});
|
||||
const handleTagTypeChange = (
|
||||
event: React.SyntheticEvent,
|
||||
value: AutocompleteValue<ITagType, false, any, any>
|
||||
) => {
|
||||
if (value != null && typeof value !== 'string') {
|
||||
event.preventDefault();
|
||||
setTagType(value);
|
||||
}
|
||||
|
||||
setTag(updatedTag);
|
||||
};
|
||||
|
||||
const handleInputChange = (
|
||||
event: React.SyntheticEvent,
|
||||
newValue: AutocompleteValue<
|
||||
TagOption | string,
|
||||
true,
|
||||
undefined,
|
||||
undefined
|
||||
>,
|
||||
reason: AutocompleteChangeReason
|
||||
) => {
|
||||
if (reason === 'selectOption') {
|
||||
const clone = cloneDeep(newValue) as TagOption[];
|
||||
newValue.forEach((value, index) => {
|
||||
if (
|
||||
typeof value !== 'string' &&
|
||||
value.inputValue &&
|
||||
value.inputValue !== ''
|
||||
) {
|
||||
const payload = {
|
||||
value: value.inputValue,
|
||||
type: tagType.name,
|
||||
};
|
||||
createTag(payload).then(() => {
|
||||
refetchAllTags();
|
||||
});
|
||||
value.title = value.inputValue;
|
||||
value.inputValue = '';
|
||||
clone[index] = value;
|
||||
}
|
||||
});
|
||||
setSelectedTagOptions(clone);
|
||||
}
|
||||
};
|
||||
|
||||
const hasSelectedValues = selectedTagOptions.length !== 0;
|
||||
|
||||
const formId = 'add-tag-form';
|
||||
|
||||
return (
|
||||
@ -98,37 +154,32 @@ const AddTagDialog = ({ open, setOpen }: IAddTagDialogProps) => {
|
||||
<Dialogue
|
||||
open={open}
|
||||
secondaryButtonText="Cancel"
|
||||
primaryButtonText="Add tag"
|
||||
primaryButtonText={`Add tag (${selectedTagOptions.length})`}
|
||||
title="Add tags to feature toggle"
|
||||
onClick={onSubmit}
|
||||
disabledPrimaryButton={loading || !isValid}
|
||||
disabledPrimaryButton={loading || !hasSelectedValues}
|
||||
onClose={onCancel}
|
||||
formId={formId}
|
||||
>
|
||||
<>
|
||||
<Typography paragraph>
|
||||
<Typography
|
||||
paragraph
|
||||
sx={{ marginBottom: theme => theme.spacing(2.5) }}
|
||||
>
|
||||
Tags allow you to group features together
|
||||
</Typography>
|
||||
<form id={formId} onSubmit={onSubmit}>
|
||||
<StyledDialogFormContent>
|
||||
<TagSelect
|
||||
<TagTypeSelect
|
||||
autoFocus
|
||||
name="type"
|
||||
value={tag.type}
|
||||
onChange={type => onUpdateTag('type', type)}
|
||||
value={tagType}
|
||||
onChange={handleTagTypeChange}
|
||||
/>
|
||||
<br />
|
||||
<StyledInput
|
||||
label="Value"
|
||||
name="value"
|
||||
placeholder="Your tag"
|
||||
value={tag.value}
|
||||
error={Boolean(errors.tagError)}
|
||||
errorText={errors.tagError}
|
||||
onChange={e =>
|
||||
onUpdateTag('value', e.target.value)
|
||||
}
|
||||
required
|
||||
<TagsInput
|
||||
options={tagTypeOptions}
|
||||
tagType={tagType.name}
|
||||
featureTags={tags}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</StyledDialogFormContent>
|
||||
</form>
|
||||
|
@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
import useTagTypes from 'hooks/api/getters/useTagTypes/useTagTypes';
|
||||
import {
|
||||
Autocomplete,
|
||||
AutocompleteProps,
|
||||
styled,
|
||||
TextField,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from '@mui/material';
|
||||
import { ITagType } from 'interfaces/tags';
|
||||
|
||||
interface ITagSelect {
|
||||
value: ITagType;
|
||||
onChange: AutocompleteProps<ITagType, false, any, any>['onChange'];
|
||||
autoFocus?: boolean;
|
||||
}
|
||||
|
||||
const ListItem = styled('li')({
|
||||
flexDirection: 'column',
|
||||
});
|
||||
const TagTypeSelect = ({ value, onChange }: ITagSelect) => {
|
||||
const { tagTypes } = useTagTypes();
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
disablePortal
|
||||
id="tag-type-select"
|
||||
sx={{ marginTop: theme => theme.spacing(2), width: 500 }}
|
||||
options={tagTypes}
|
||||
disableClearable
|
||||
value={value}
|
||||
getOptionLabel={option => option.name}
|
||||
renderOption={(props, option) => (
|
||||
<ListItem
|
||||
{...props}
|
||||
style={{
|
||||
alignItems: 'flex-start',
|
||||
gap: theme.spacing(0.5),
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1">{option.name}</Typography>
|
||||
<Typography variant="caption">
|
||||
{option.description}
|
||||
</Typography>
|
||||
</ListItem>
|
||||
)}
|
||||
renderInput={params => (
|
||||
<TextField {...params} label="Tag type" value={value} />
|
||||
)}
|
||||
onChange={onChange}
|
||||
ListboxProps={{ style: { maxHeight: 200, overflow: 'auto' } }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TagTypeSelect;
|
@ -0,0 +1,131 @@
|
||||
import {
|
||||
Autocomplete,
|
||||
AutocompleteProps,
|
||||
Checkbox,
|
||||
createFilterOptions,
|
||||
FilterOptionsState,
|
||||
TextField,
|
||||
} from '@mui/material';
|
||||
import React from 'react';
|
||||
import CheckBoxOutlineBlankIcon from '@mui/icons-material/CheckBoxOutlineBlank';
|
||||
import CheckBoxIcon from '@mui/icons-material/CheckBox';
|
||||
import { ITag } from 'interfaces/tags';
|
||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||
import { Add } from '@mui/icons-material';
|
||||
|
||||
export type TagOption = {
|
||||
title: string;
|
||||
inputValue?: string;
|
||||
};
|
||||
interface ITagsInputProps {
|
||||
options: TagOption[];
|
||||
featureTags: ITag[];
|
||||
tagType: string;
|
||||
onChange: AutocompleteProps<TagOption | string, true, any, any>['onChange'];
|
||||
}
|
||||
|
||||
const filter = createFilterOptions<TagOption>();
|
||||
|
||||
export const TagsInput = ({
|
||||
options,
|
||||
featureTags,
|
||||
tagType,
|
||||
onChange,
|
||||
}: ITagsInputProps) => {
|
||||
const icon = <CheckBoxOutlineBlankIcon fontSize="small" />;
|
||||
const checkedIcon = <CheckBoxIcon fontSize="small" />;
|
||||
|
||||
const getOptionDisabled = (option: TagOption) => {
|
||||
return featureTags.some(
|
||||
tag => tag.type === tagType && tag.value === option.title
|
||||
);
|
||||
};
|
||||
|
||||
const getOptionLabel = (option: TagOption) => {
|
||||
// Add "xxx" option created dynamically
|
||||
if (option.inputValue) {
|
||||
return option.inputValue;
|
||||
}
|
||||
// Regular option
|
||||
return option.title;
|
||||
};
|
||||
|
||||
const renderOption = (
|
||||
props: JSX.IntrinsicAttributes &
|
||||
React.ClassAttributes<HTMLLIElement> &
|
||||
React.LiHTMLAttributes<HTMLLIElement>,
|
||||
option: TagOption,
|
||||
{ selected }: { selected: boolean }
|
||||
) => {
|
||||
const exists = featureTags.some(
|
||||
tag => tag.type === tagType && tag.value === option.title
|
||||
);
|
||||
return (
|
||||
<li {...props}>
|
||||
<ConditionallyRender
|
||||
condition={Boolean(option.inputValue)}
|
||||
show={<Add sx={{ mr: theme => theme.spacing(0.5) }} />}
|
||||
elseShow={
|
||||
<Checkbox
|
||||
icon={icon}
|
||||
checkedIcon={checkedIcon}
|
||||
sx={{ mr: theme => theme.spacing(0.5) }}
|
||||
checked={selected || exists}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{option.title}
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const filterOptions = (
|
||||
options: TagOption[],
|
||||
params: FilterOptionsState<TagOption>
|
||||
) => {
|
||||
const filtered = filter(options, params);
|
||||
|
||||
const { inputValue } = params;
|
||||
// Suggest the creation of a new value
|
||||
const isExisting = options.some(option => inputValue === option.title);
|
||||
if (inputValue !== '' && !isExisting) {
|
||||
filtered.push({
|
||||
inputValue,
|
||||
title: `Create new value "${inputValue}"`,
|
||||
});
|
||||
}
|
||||
|
||||
return filtered;
|
||||
};
|
||||
|
||||
return (
|
||||
<Autocomplete
|
||||
multiple
|
||||
id="checkboxes-tags-demo"
|
||||
sx={{ marginTop: theme => theme.spacing(2), width: 500 }}
|
||||
disableCloseOnSelect
|
||||
placeholder="Select Values"
|
||||
options={options}
|
||||
isOptionEqualToValue={(option, value) => {
|
||||
if (value.inputValue && value.inputValue !== '') {
|
||||
return option.title === value.inputValue;
|
||||
} else {
|
||||
return option.title === value.title;
|
||||
}
|
||||
}}
|
||||
getOptionDisabled={getOptionDisabled}
|
||||
getOptionLabel={getOptionLabel}
|
||||
renderOption={renderOption}
|
||||
filterOptions={filterOptions}
|
||||
ListboxProps={{ style: { maxHeight: 200, overflow: 'auto' } }}
|
||||
onChange={onChange}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Select values"
|
||||
placeholder="Select values"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
@ -6,7 +6,7 @@ import { ConditionallyRender } from 'component/common/ConditionallyRender/Condit
|
||||
import { Edit } from '@mui/icons-material';
|
||||
import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton';
|
||||
import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||
import useTags from 'hooks/api/getters/useTags/useTags';
|
||||
import useFeatureTags from 'hooks/api/getters/useFeatureTags/useFeatureTags';
|
||||
import FeatureOverviewTags from './FeatureOverviewTags/FeatureOverviewTags';
|
||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||
@ -69,7 +69,7 @@ const FeatureOverviewMetaData = () => {
|
||||
const { uiConfig } = useUiConfig();
|
||||
const projectId = useRequiredPathParam('projectId');
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const { tags } = useTags(featureId);
|
||||
const { tags } = useFeatureTags(featureId);
|
||||
const { feature } = useFeature(projectId, featureId);
|
||||
const { project, description, type } = feature;
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { Chip, styled } from '@mui/material';
|
||||
import { Close, Label } from '@mui/icons-material';
|
||||
import useTags from 'hooks/api/getters/useTags/useTags';
|
||||
import useFeatureTags from 'hooks/api/getters/useFeatureTags/useFeatureTags';
|
||||
import slackIcon from 'assets/icons/slack.svg';
|
||||
import jiraIcon from 'assets/icons/jira.svg';
|
||||
import webhookIcon from 'assets/icons/webhooks.svg';
|
||||
@ -58,7 +58,7 @@ const FeatureOverviewTags: React.FC<IFeatureOverviewTagsProps> = ({
|
||||
type: '',
|
||||
});
|
||||
const featureId = useRequiredPathParam('featureId');
|
||||
const { tags, refetch } = useTags(featureId);
|
||||
const { tags, refetch } = useFeatureTags(featureId);
|
||||
const { tagTypes } = useTagTypes();
|
||||
const { deleteTagFromFeature } = useFeatureApi();
|
||||
const { setToastData, setToastApiError } = useToast();
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { IFeatureToggle } from 'interfaces/featureToggle';
|
||||
import { useContext, useState } from 'react';
|
||||
import { Button, Chip, Divider, styled } from '@mui/material';
|
||||
import useTags from 'hooks/api/getters/useTags/useTags';
|
||||
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 { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions';
|
||||
@ -51,7 +51,7 @@ export const FeatureOverviewSidePanelTags = ({
|
||||
feature,
|
||||
header,
|
||||
}: IFeatureOverviewSidePanelTagsProps) => {
|
||||
const { tags, refetch } = useTags(feature.name);
|
||||
const { tags, refetch } = useFeatureTags(feature.name);
|
||||
const { deleteTagFromFeature } = useFeatureApi();
|
||||
|
||||
const [openTagDialog, setOpenTagDialog] = useState(false);
|
||||
|
@ -54,7 +54,7 @@ export const StyledWidgetTitle = styled('p')(({ theme }) => ({
|
||||
export const StyledParagraphGridRow = styled('div')(({ theme }) => ({
|
||||
display: 'grid',
|
||||
gridGap: theme.spacing(1.5),
|
||||
gridTemplateColumns: `${theme.spacing(1.25)} auto auto`, //20px auto auto
|
||||
gridTemplateColumns: `${theme.spacing(2.25)} auto auto`, //20px auto auto
|
||||
margin: theme.spacing(1, 0, 1, 0),
|
||||
fontSize: theme.fontSizes.smallBody,
|
||||
color: theme.palette.text.secondary,
|
||||
|
30
frontend/src/hooks/api/actions/useTagApi/useTagApi.ts
Normal file
30
frontend/src/hooks/api/actions/useTagApi/useTagApi.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import useAPI from '../useApi/useApi';
|
||||
import { TagSchema } from 'openapi/models/tagSchema';
|
||||
|
||||
const useTagApi = () => {
|
||||
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||
propagateErrors: true,
|
||||
});
|
||||
|
||||
const createTag = async (payload: TagSchema) => {
|
||||
const path = `api/admin/tags`;
|
||||
const req = createRequest(path, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
try {
|
||||
return await makeRequest(req.caller, req.id);
|
||||
} catch (e) {
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
createTag,
|
||||
errors,
|
||||
loading,
|
||||
};
|
||||
};
|
||||
|
||||
export default useTagApi;
|
@ -0,0 +1,44 @@
|
||||
import { mutate, SWRConfiguration } from 'swr';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { formatApiPath } from 'utils/formatPath';
|
||||
import { ITag } from 'interfaces/tags';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
|
||||
|
||||
const useFeatureTags = (featureId: string, options: SWRConfiguration = {}) => {
|
||||
const fetcher = async () => {
|
||||
const path = formatApiPath(`api/admin/features/${featureId}/tags`);
|
||||
const res = await fetch(path, {
|
||||
method: 'GET',
|
||||
}).then(handleErrorResponses('Tags'));
|
||||
return res.json();
|
||||
};
|
||||
|
||||
const KEY = `api/admin/features/${featureId}/tags`;
|
||||
|
||||
const { data, error } = useConditionalSWR<{ tags: ITag[] }>(
|
||||
Boolean(featureId),
|
||||
{ tags: [] },
|
||||
KEY,
|
||||
fetcher,
|
||||
options
|
||||
);
|
||||
const [loading, setLoading] = useState(!error && !data);
|
||||
|
||||
const refetch = () => {
|
||||
mutate(KEY);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLoading(!error && !data);
|
||||
}, [data, error]);
|
||||
|
||||
return {
|
||||
tags: (data?.tags as ITag[]) || [],
|
||||
error,
|
||||
loading,
|
||||
refetch,
|
||||
};
|
||||
};
|
||||
|
||||
export default useFeatureTags;
|
@ -5,19 +5,19 @@ import { ITag } from 'interfaces/tags';
|
||||
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||
import { useConditionalSWR } from '../useConditionalSWR/useConditionalSWR';
|
||||
|
||||
const useTags = (featureId: string, options: SWRConfiguration = {}) => {
|
||||
const useTags = (type: string, options: SWRConfiguration = {}) => {
|
||||
const fetcher = async () => {
|
||||
const path = formatApiPath(`api/admin/features/${featureId}/tags`);
|
||||
const path = formatApiPath(`api/admin/tags/${type}`);
|
||||
const res = await fetch(path, {
|
||||
method: 'GET',
|
||||
}).then(handleErrorResponses('Tags'));
|
||||
return res.json();
|
||||
};
|
||||
|
||||
const KEY = `api/admin/features/${featureId}/tags`;
|
||||
const KEY = `api/admin/tags/${type}`;
|
||||
|
||||
const { data, error } = useConditionalSWR<{ tags: ITag[] }>(
|
||||
Boolean(featureId),
|
||||
Boolean(type),
|
||||
{ tags: [] },
|
||||
KEY,
|
||||
fetcher,
|
||||
|
@ -20,6 +20,6 @@
|
||||
"@server/*": ["./../../src/lib/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"include": ["./src"],
|
||||
"references": [{ "path": "./tsconfig.node.json" }]
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user