diff --git a/.github/workflows/build_frontend_prs.yml b/.github/workflows/build_frontend_prs.yml
index acbd498707..e64e57e135 100644
--- a/.github/workflows/build_frontend_prs.yml
+++ b/.github/workflows/build_frontend_prs.yml
@@ -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
diff --git a/frontend/src/component/common/TagSelect/TagSelect.tsx b/frontend/src/component/common/TagSelect/TagSelect.tsx
deleted file mode 100644
index 13260d3c22..0000000000
--- a/frontend/src/component/common/TagSelect/TagSelect.tsx
+++ /dev/null
@@ -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 (
- <>
-
- >
- );
-};
-
-export default TagSelect;
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/AddTagDialog.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/AddTagDialog.tsx
index 42e11289fc..393c3a4fb2 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/AddTagDialog.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/AddTagDialog.tsx
@@ -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>;
}
-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({
+ name: 'simple',
+ description: 'Simple tag to get you started',
+ icon: '',
+ });
+
+ const [selectedTagOptions, setSelectedTagOptions] = useState(
+ []
+ );
+
+ 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
+ ) => {
+ 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) => {
<>
-
+ theme.spacing(2.5) }}
+ >
Tags allow you to group features together
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/TagTypeSelect.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/TagTypeSelect.tsx
new file mode 100644
index 0000000000..8f286a98a0
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/TagTypeSelect.tsx
@@ -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['onChange'];
+ autoFocus?: boolean;
+}
+
+const ListItem = styled('li')({
+ flexDirection: 'column',
+});
+const TagTypeSelect = ({ value, onChange }: ITagSelect) => {
+ const { tagTypes } = useTagTypes();
+ const theme = useTheme();
+
+ return (
+ theme.spacing(2), width: 500 }}
+ options={tagTypes}
+ disableClearable
+ value={value}
+ getOptionLabel={option => option.name}
+ renderOption={(props, option) => (
+
+ {option.name}
+
+ {option.description}
+
+
+ )}
+ renderInput={params => (
+
+ )}
+ onChange={onChange}
+ ListboxProps={{ style: { maxHeight: 200, overflow: 'auto' } }}
+ />
+ );
+};
+
+export default TagTypeSelect;
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/TagsInput.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/TagsInput.tsx
new file mode 100644
index 0000000000..30c2fb54d2
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/AddTagDialog/TagsInput.tsx
@@ -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['onChange'];
+}
+
+const filter = createFilterOptions();
+
+export const TagsInput = ({
+ options,
+ featureTags,
+ tagType,
+ onChange,
+}: ITagsInputProps) => {
+ const icon = ;
+ const checkedIcon = ;
+
+ 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 &
+ React.LiHTMLAttributes,
+ option: TagOption,
+ { selected }: { selected: boolean }
+ ) => {
+ const exists = featureTags.some(
+ tag => tag.type === tagType && tag.value === option.title
+ );
+ return (
+
+ theme.spacing(0.5) }} />}
+ elseShow={
+ theme.spacing(0.5) }}
+ checked={selected || exists}
+ />
+ }
+ />
+ {option.title}
+
+ );
+ };
+
+ const filterOptions = (
+ options: TagOption[],
+ params: FilterOptionsState
+ ) => {
+ 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 (
+ 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 => (
+
+ )}
+ />
+ );
+};
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx
index eead57694a..713da1e539 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx
@@ -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;
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewTags/FeatureOverviewTags.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewTags/FeatureOverviewTags.tsx
index 246cc10945..cee5999d8d 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewTags/FeatureOverviewTags.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewTags/FeatureOverviewTags.tsx
@@ -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 = ({
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();
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 8c0123ce16..2e940f773f 100644
--- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags.tsx
@@ -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);
diff --git a/frontend/src/component/project/Project/ProjectInfo/ProjectInfo.styles.ts b/frontend/src/component/project/Project/ProjectInfo/ProjectInfo.styles.ts
index 7723163dcb..e9d743ba5b 100644
--- a/frontend/src/component/project/Project/ProjectInfo/ProjectInfo.styles.ts
+++ b/frontend/src/component/project/Project/ProjectInfo/ProjectInfo.styles.ts
@@ -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,
diff --git a/frontend/src/hooks/api/actions/useTagApi/useTagApi.ts b/frontend/src/hooks/api/actions/useTagApi/useTagApi.ts
new file mode 100644
index 0000000000..c8659ee566
--- /dev/null
+++ b/frontend/src/hooks/api/actions/useTagApi/useTagApi.ts
@@ -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;
diff --git a/frontend/src/hooks/api/getters/useFeatureTags/useFeatureTags.ts b/frontend/src/hooks/api/getters/useFeatureTags/useFeatureTags.ts
new file mode 100644
index 0000000000..382ee6dc1b
--- /dev/null
+++ b/frontend/src/hooks/api/getters/useFeatureTags/useFeatureTags.ts
@@ -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;
diff --git a/frontend/src/hooks/api/getters/useTags/useTags.ts b/frontend/src/hooks/api/getters/useTags/useTags.ts
index bb91a5e12d..e265eaf1e2 100644
--- a/frontend/src/hooks/api/getters/useTags/useTags.ts
+++ b/frontend/src/hooks/api/getters/useTags/useTags.ts
@@ -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,
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
index 950b439e6a..2dcb13545f 100644
--- a/frontend/tsconfig.json
+++ b/frontend/tsconfig.json
@@ -20,6 +20,6 @@
"@server/*": ["./../../src/lib/*"]
}
},
- "include": ["src"],
+ "include": ["./src"],
"references": [{ "path": "./tsconfig.node.json" }]
}