1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

feat: ui for external link templates (#9945)

Support for project link templates to the frontend UI
This commit is contained in:
Tymoteusz Czech 2025-05-12 11:05:04 +02:00 committed by GitHub
parent ea26e008d0
commit 5614cb56d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 415 additions and 71 deletions

View File

@ -15,11 +15,8 @@ import {
import { Button } from '@mui/material';
import { CreateButton } from 'component/common/CreateButton/CreateButton';
import type { IPermissionButtonProps } from 'component/common/PermissionButton/PermissionButton';
import type { FeatureNamingType } from 'interfaces/project';
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
import { NamingPatternInfo } from './NamingPatternInfo';
type NamingPattern = FeatureNamingType;
import type { CreateFeatureNamingPatternSchema } from 'openapi';
type FormProps = {
createButtonProps: IPermissionButtonProps;
@ -35,7 +32,7 @@ type FormProps = {
setDescription: (newDescription: string) => void;
setName: (newName: string) => void;
validateName?: () => void;
namingPattern?: NamingPattern;
namingPattern?: CreateFeatureNamingPatternSchema;
};
export const DialogFormTemplate: React.FC<FormProps> = ({
@ -54,8 +51,6 @@ export const DialogFormTemplate: React.FC<FormProps> = ({
createButtonProps,
validateName = () => {},
}) => {
const displayNamingPattern = Boolean(namingPattern?.pattern);
return (
<StyledForm onSubmit={handleSubmit}>
<TopGrid>
@ -66,7 +61,7 @@ export const DialogFormTemplate: React.FC<FormProps> = ({
label={`${resource} name`}
aria-required
aria-details={
displayNamingPattern
namingPattern?.pattern
? 'naming-pattern-info'
: undefined
}
@ -89,10 +84,9 @@ export const DialogFormTemplate: React.FC<FormProps> = ({
size='medium'
/>
<ConditionallyRender
condition={displayNamingPattern}
show={<NamingPatternInfo naming={namingPattern!} />}
/>
{namingPattern?.pattern ? (
<NamingPatternInfo naming={namingPattern!} />
) : null}
</NameContainer>
<DescriptionContainer>
<StyledInput

View File

@ -5,8 +5,8 @@ import {
styled,
} from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import type { FeatureNamingType } from 'interfaces/project';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import type { CreateFeatureNamingPatternSchema } from 'openapi';
const StyledFlagNamingInfo = styled('article')(({ theme }) => ({
fontSize: theme.typography.body2.fontSize,
@ -35,7 +35,7 @@ const StyledAccordion = styled(Accordion)(({ theme }) => ({
}));
type Props = {
naming: FeatureNamingType;
naming: CreateFeatureNamingPatternSchema;
};
export const NamingPatternInfo: React.FC<Props> = ({ naming }) => {

View File

@ -1,6 +1,6 @@
import { styled } from '@mui/material';
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
import type { FeatureNamingType } from 'interfaces/project';
import type { CreateFeatureNamingPatternSchema } from 'openapi';
const StyledFlagNamingInfo = styled('article')(({ theme }) => ({
fontSize: theme.fontSizes.smallBody,
@ -25,7 +25,7 @@ const StyledFlagNamingInfo = styled('article')(({ theme }) => ({
}));
type Props = {
featureNaming: FeatureNamingType;
featureNaming: CreateFeatureNamingPatternSchema;
};
export const FeatureNamingPatternInfo: React.FC<Props> = ({

View File

@ -50,7 +50,8 @@ const FeatureSettingsProjectConfirm = ({
const hasSameEnvironments: boolean = useMemo(() => {
return arraysHaveSameItems(
feature.environments.map((env) => env.name),
project.environments.map((projectEnv) => projectEnv.environment),
project.environments?.map((projectEnv) => projectEnv.environment) ||
[],
);
}, [feature, project]);

View File

@ -32,7 +32,7 @@ export const ImportOptions: FC<IImportOptionsProps> = ({
onChange,
}) => {
const { project: projectInfo } = useProjectOverview(project);
const environmentOptions = projectInfo.environments.map(
const environmentOptions = projectInfo.environments?.map(
({ environment }) => ({
key: environment,
label: environment,
@ -41,7 +41,7 @@ export const ImportOptions: FC<IImportOptionsProps> = ({
);
useEffect(() => {
if (environment === '' && environmentOptions[0]) {
if (environment === '' && environmentOptions?.[0]) {
onChange(environmentOptions[0].key);
}
}, [JSON.stringify(environmentOptions)]);
@ -54,7 +54,7 @@ export const ImportOptions: FC<IImportOptionsProps> = ({
</ImportOptionsDescription>
<GeneralSelect
sx={{ width: '180px' }}
options={environmentOptions}
options={environmentOptions || []}
onChange={onChange}
label={'Environment'}
value={environment}

View File

@ -190,8 +190,8 @@ const CreateFeatureDialogContent = ({
count: totalFlags ?? 0,
},
project: {
limit: projectInfo.featureLimit,
count: featuresCount(projectInfo),
limit: projectInfo.featureLimit || undefined,
count: featuresCount(projectInfo) ?? 0,
},
});

View File

@ -282,7 +282,7 @@ export const Project = () => {
<StyledDiv>
<StyledFavoriteIconButton
onClick={onFavorite}
isFavorite={project?.favorite}
isFavorite={project?.favorite || false}
/>
<StyledProjectTitle>
<ConditionallyRender

View File

@ -1,4 +1,9 @@
import React, { useEffect } from 'react';
import React, {
type Dispatch,
type ReactNode,
type SetStateAction,
useEffect,
} from 'react';
import Select from 'component/common/select';
import type { ProjectMode } from '../hooks/useProjectEnterpriseSettingsForm';
import { Box, InputAdornment, styled, TextField } from '@mui/material';
@ -6,6 +11,9 @@ import { CollaborationModeTooltip } from './CollaborationModeTooltip';
import Input from 'component/common/Input/Input';
import { FeatureFlagNamingTooltip } from './FeatureFlagNamingTooltip';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import type { ProjectLinkTemplateSchema } from 'openapi';
import { useUiFlag } from 'hooks/useUiFlag';
import ProjectLinkTemplates from './ProjectLinkTemplates/ProjectLinkTemplates';
interface IProjectEnterpriseSettingsForm {
projectId: string;
@ -13,14 +21,16 @@ interface IProjectEnterpriseSettingsForm {
featureNamingPattern?: string;
featureNamingExample?: string;
featureNamingDescription?: string;
setFeatureNamingPattern?: React.Dispatch<React.SetStateAction<string>>;
setFeatureNamingExample?: React.Dispatch<React.SetStateAction<string>>;
setFeatureNamingDescription?: React.Dispatch<React.SetStateAction<string>>;
setProjectMode?: React.Dispatch<React.SetStateAction<ProjectMode>>;
linkTemplates?: ProjectLinkTemplateSchema[];
setFeatureNamingPattern?: Dispatch<SetStateAction<string>>;
setFeatureNamingExample?: Dispatch<SetStateAction<string>>;
setFeatureNamingDescription?: Dispatch<SetStateAction<string>>;
setProjectMode?: Dispatch<SetStateAction<ProjectMode>>;
setLinkTemplates?: Dispatch<SetStateAction<ProjectLinkTemplateSchema[]>>;
handleSubmit: (e: any) => void;
errors: { [key: string]: string };
clearErrors: () => void;
children?: React.ReactNode;
children?: ReactNode;
}
const StyledForm = styled('form')(({ theme }) => ({
@ -128,10 +138,12 @@ const ProjectEnterpriseSettingsForm: React.FC<
featureNamingExample,
featureNamingPattern,
featureNamingDescription,
linkTemplates = [],
setFeatureNamingExample,
setFeatureNamingPattern,
setFeatureNamingDescription,
setProjectMode,
setLinkTemplates,
errors,
}) => {
const { setPreviousPattern, trackPattern } =
@ -143,6 +155,8 @@ const ProjectEnterpriseSettingsForm: React.FC<
{ key: 'private', label: 'private' },
];
const projectLinkTemplatesEnabled = useUiFlag('projectLinkTemplates');
useEffect(() => {
setPreviousPattern(featureNamingPattern || '');
}, [projectId]);
@ -253,7 +267,7 @@ const ProjectEnterpriseSettingsForm: React.FC<
gap: 1,
}}
>
<legend>Feature flag naming pattern?</legend>
<legend>Feature flag naming pattern</legend>
<FeatureFlagNamingTooltip />
</Box>
<StyledSubtitle>
@ -339,6 +353,13 @@ The flag name should contain the project name, the feature name, and the ticket
}
/>
</StyledFlagNamingContainer>
{projectLinkTemplatesEnabled && (
<ProjectLinkTemplates
linkTemplates={linkTemplates || []}
setLinkTemplates={setLinkTemplates}
/>
)}
</StyledFieldset>
<StyledButtonContainer>{children}</StyledButtonContainer>
</StyledForm>

View File

@ -0,0 +1,105 @@
import { useState } from 'react';
import { Button, styled, TextField, Typography } from '@mui/material';
import type { ProjectLinkTemplateSchema } from 'openapi';
interface IProjectLinkTemplateEditorProps {
template?: ProjectLinkTemplateSchema;
onSave: (template: ProjectLinkTemplateSchema) => void;
onCancel: () => void;
isAdding: boolean;
}
const StyledContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
gap: theme.spacing(2),
padding: theme.spacing(3),
}));
const StyledDialogActions = styled('div')(({ theme }) => ({
display: 'flex',
justifyContent: 'flex-end',
gap: theme.spacing(1.5),
}));
const ProjectLinkTemplateEditor = ({
template,
onSave,
onCancel,
isAdding,
}: IProjectLinkTemplateEditorProps) => {
const [templateTitle, setTemplateTitle] = useState(template?.title || '');
const [templateUrl, setTemplateUrl] = useState(template?.urlTemplate || '');
const [templateErrors, setTemplateErrors] = useState<{
title?: string;
url?: string;
}>({});
const validateTemplateForm = () => {
const errors: { title?: string; url?: string } = {};
if (!templateUrl) {
errors.url = 'URL template is required';
}
setTemplateErrors(errors);
return Object.keys(errors).length === 0;
};
const handleSave = () => {
if (validateTemplateForm()) {
onSave({
title: templateTitle || null,
urlTemplate: templateUrl,
});
}
};
return (
<StyledContainer>
<Typography
variant='h5'
sx={(theme) => ({ fontSize: theme.typography.body1.fontSize })}
>
{isAdding ? 'Add new link template' : 'Edit link template'}
</Typography>
<TextField
label='Title (optional)'
fullWidth
value={templateTitle}
onChange={(e) => setTemplateTitle(e.target.value)}
placeholder='e.g., GitHub Issue, Ticket number'
helperText='A descriptive name for the link.'
size='small'
/>
<TextField
label='URL Template'
fullWidth
required
value={templateUrl}
onChange={(e) => setTemplateUrl(e.target.value)}
placeholder='https://github.com/{{project}}/{{feature}}'
helperText={
templateErrors.url ||
'You can optionally use placeholders {{project}} and {{feature}} that will be replaced with actual values.'
}
size='small'
error={Boolean(templateErrors.url)}
/>
<StyledDialogActions>
<Button variant='outlined' onClick={onCancel}>
Cancel
</Button>
<Button
variant='contained'
color='primary'
onClick={handleSave}
>
{isAdding ? 'Add' : 'Update'}
</Button>
</StyledDialogActions>
</StyledContainer>
);
};
export default ProjectLinkTemplateEditor;

View File

@ -0,0 +1,218 @@
import { useState, type Dispatch, type SetStateAction } from 'react';
import {
Box,
Button,
IconButton,
List,
ListItem,
ListItemText,
styled,
Tooltip,
Typography,
} from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import DeleteIcon from '@mui/icons-material/Delete';
import EditIcon from '@mui/icons-material/Edit';
import HelpOutlineIcon from '@mui/icons-material/HelpOutline';
import type { ProjectLinkTemplateSchema } from 'openapi';
import ProjectLinkTemplateEditor from './ProjectLinkTemplateEditor';
import { Truncator } from 'component/common/Truncator/Truncator';
interface IProjectLinkTemplatesProps {
linkTemplates: ProjectLinkTemplateSchema[];
setLinkTemplates?: Dispatch<SetStateAction<ProjectLinkTemplateSchema[]>>;
}
const StyledSubtitle = styled('div')(({ theme }) => ({
color: theme.palette.text.secondary,
fontSize: theme.fontSizes.smallerBody,
lineHeight: 1.25,
paddingBottom: theme.spacing(1),
}));
const StyledLinkTemplatesContainer = styled('div')(({ theme }) => ({
display: 'flex',
flexDirection: 'column',
marginTop: theme.spacing(4),
marginBottom: theme.spacing(2),
gap: theme.spacing(2),
}));
const StyledLinkTemplatesList = styled(List)(({ theme }) => ({
width: '100%',
backgroundColor: theme.palette.background.paper,
borderRadius: theme.shape.borderRadius,
border: `1px solid ${theme.palette.divider}`,
margin: 0,
padding: 0,
}));
const StyledLinkTemplateItem = styled(ListItem)(({ theme }) => ({
borderBottom: `1px solid ${theme.palette.divider}`,
'&:last-child': {
borderBottom: 'none',
},
borderRight: 0,
}));
const ProjectLinkTemplates = ({
linkTemplates = [],
setLinkTemplates,
}: IProjectLinkTemplatesProps) => {
const [isAddingTemplate, setIsAddingTemplate] = useState(false);
const [editingTemplateIndex, setEditingTemplateIndex] = useState<
number | null
>(null);
const handleEditTemplate = (index: number) => {
setEditingTemplateIndex(index);
};
const handleSaveTemplate = (template: ProjectLinkTemplateSchema) => {
if (editingTemplateIndex !== null) {
const updatedTemplates = [...linkTemplates];
updatedTemplates[editingTemplateIndex] = template;
setLinkTemplates?.(updatedTemplates);
setEditingTemplateIndex(null);
} else {
setLinkTemplates?.([...linkTemplates, template]);
setIsAddingTemplate(false);
}
};
const handleCancelEdit = () => {
setEditingTemplateIndex(null);
setIsAddingTemplate(false);
};
const handleDeleteTemplate = (index: number) => {
const updatedTemplates = [...linkTemplates];
updatedTemplates.splice(index, 1);
setLinkTemplates?.(updatedTemplates);
};
return (
<StyledLinkTemplatesContainer>
<Box display='flex' alignItems='center' gap={1}>
<Typography variant='h4'>Project Link Templates</Typography>
<Tooltip
title={
<Box
sx={(theme) => ({
fontWeight: theme.typography.body1.fontWeight,
})}
>
<p>
Link templates can be automatically added to new
feature flags. They can include placeholders
like <code>{`{{project}}`}</code> and
<code>{`{{feature}}`}</code> that will be
replaced with actual values.
</p>
</Box>
}
>
<IconButton size='small' sx={{ ml: 1 }}>
<HelpOutlineIcon fontSize='small' />
</IconButton>
</Tooltip>
</Box>
<StyledSubtitle>
<p>
Define link templates that can be automatically added to new
feature flags in this project.
</p>
</StyledSubtitle>
{linkTemplates.length > 0 ? (
<StyledLinkTemplatesList>
{linkTemplates.map((template, index) => {
if (editingTemplateIndex === index) {
return (
<StyledLinkTemplateItem
key={index}
style={{ listStyleType: 'none' }}
>
<ProjectLinkTemplateEditor
template={template}
onSave={handleSaveTemplate}
onCancel={handleCancelEdit}
isAdding={false}
/>
</StyledLinkTemplateItem>
);
}
return (
<StyledLinkTemplateItem key={index}>
<ListItemText
primary={
template.title ? (
<Truncator>
{template.title}
</Truncator>
) : null
}
secondary={
<Truncator>
{template.urlTemplate}
</Truncator>
}
/>
<Box
sx={(theme) => ({
display: 'flex',
marginRight: theme.spacing(-1),
})}
>
<IconButton
edge='end'
aria-label='edit'
onClick={() =>
handleEditTemplate(index)
}
sx={{ margin: 0 }}
>
<EditIcon />
</IconButton>
<IconButton
edge='end'
aria-label='delete'
onClick={() =>
handleDeleteTemplate(index)
}
sx={{ margin: 0 }}
>
<DeleteIcon />
</IconButton>
</Box>
</StyledLinkTemplateItem>
);
})}
</StyledLinkTemplatesList>
) : null}
{isAddingTemplate && (
<ProjectLinkTemplateEditor
onSave={handleSaveTemplate}
onCancel={handleCancelEdit}
isAdding={true}
/>
)}
{!isAddingTemplate && editingTemplateIndex === null && (
<Box display='flex' justifyContent='flex-start'>
<Button
startIcon={<AddIcon />}
variant='outlined'
onClick={() => setIsAddingTemplate(true)}
>
Add link template
</Button>
</Box>
)}
</StyledLinkTemplatesContainer>
);
};
export default ProjectLinkTemplates;

View File

@ -59,9 +59,11 @@ const ProjectOverview: FC = () => {
/>
<StyledProjectToggles>
<ProjectFeatureToggles
environments={project.environments.map(
(environment) => environment.environment,
)}
environments={
project.environments?.map(
(environment) => environment.environment,
) || []
}
/>
</StyledProjectToggles>
</StyledContentContainer>

View File

@ -61,7 +61,7 @@ export const ProjectDefaultStrategySettings = () => {
specific environment. These will be used when you enable a
toggle environment that has no strategies defined
</StyledAlert>
{project?.environments.map((environment) => (
{project?.environments?.map((environment) => (
<ProjectEnvironment
environment={environment}
key={environment.environment}

View File

@ -37,7 +37,7 @@ export const useDefaultStrategy = (
},
};
const strategy = project.environments.find(
const strategy = project.environments?.find(
(env) => env.environment === environmentId,
)?.defaultStrategy;

View File

@ -49,7 +49,9 @@ const EditProject = () => {
condition={isEnterprise()}
show={<UpdateEnterpriseSettings project={project} />}
/>
<ArchiveProjectForm featureCount={featuresCount(project)} />
<ArchiveProjectForm
featureCount={featuresCount(project) ?? 0}
/>
</StyledFormContainer>
</>
);

View File

@ -9,10 +9,10 @@ import FormTemplate from 'component/common/FormTemplate/FormTemplate';
import ProjectEnterpriseSettingsForm from 'component/project/Project/ProjectEnterpriseSettingsForm/ProjectEnterpriseSettingsForm';
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
import type { IProjectOverview } from 'component/../interfaces/project';
import { styled } from '@mui/material';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
import type { ProjectOverviewSchema } from 'openapi';
const StyledContainer = styled('div')(({ theme }) => ({
minHeight: 0,
@ -33,7 +33,7 @@ const StyledFormContainer = styled('div')(({ theme }) => ({
}));
interface IUpdateEnterpriseSettings {
project: IProjectOverview;
project: ProjectOverviewSchema;
}
const EDIT_PROJECT_SETTINGS_BTN = 'EDIT_PROJECT_SETTINGS_BTN';
@ -65,18 +65,21 @@ export const UpdateEnterpriseSettings = ({
featureNamingExample,
featureNamingDescription,
featureNamingPattern,
linkTemplates,
setFeatureNamingPattern,
setFeatureNamingExample,
setFeatureNamingDescription,
setProjectMode,
setLinkTemplates,
getEnterpriseSettingsPayload,
errors: settingsErrors = {},
clearErrors: clearSettingsErrors,
} = useProjectEnterpriseSettingsForm(
project.mode,
project?.featureNaming?.pattern,
project?.featureNaming?.example,
project?.featureNaming?.description,
project?.featureNaming?.pattern || undefined,
project?.featureNaming?.example || undefined,
project?.featureNaming?.description || undefined,
project?.linkTemplates || [],
);
const formatProjectSettingsApiCode = () => {
@ -161,12 +164,14 @@ export const UpdateEnterpriseSettings = ({
featureNamingPattern={featureNamingPattern}
featureNamingExample={featureNamingExample}
featureNamingDescription={featureNamingDescription}
linkTemplates={linkTemplates}
setFeatureNamingPattern={setFeatureNamingPattern}
setFeatureNamingExample={setFeatureNamingExample}
setFeatureNamingDescription={
setFeatureNamingDescription
}
setProjectMode={setProjectMode}
setLinkTemplates={setLinkTemplates}
handleSubmit={handleEditProjectSettings}
errors={settingsErrors}
clearErrors={clearSettingsErrors}

View File

@ -14,10 +14,10 @@ import useToast from 'hooks/useToast';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
import type { IProjectOverview } from 'interfaces/project';
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
import { styled } from '@mui/material';
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
import type { ProjectOverviewSchema } from 'openapi';
const StyledContainer = styled('div')(({ theme }) => ({
minHeight: 0,
@ -38,7 +38,7 @@ const StyledFormContainer = styled('div')(({ theme }) => ({
}));
interface IUpdateProject {
project: IProjectOverview;
project: ProjectOverviewSchema;
}
const EDIT_PROJECT_BTN = 'EDIT_PROJECT_BTN';
export const UpdateProject = ({ project }: IUpdateProject) => {
@ -66,7 +66,7 @@ export const UpdateProject = ({ project }: IUpdateProject) => {
} = useProjectForm(
id,
project.name,
project.description,
project.description || undefined,
defaultStickiness,
String(project.featureLimit),
);

View File

@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import type { ProjectLinkTemplateSchema } from 'openapi';
export type ProjectMode = 'open' | 'protected' | 'private';
const useProjectEnterpriseSettingsForm = (
@ -6,6 +7,7 @@ const useProjectEnterpriseSettingsForm = (
initialFeatureNamingPattern = '',
initialFeatureNamingExample = '',
initialFeatureNamingDescription = '',
initialLinkTemplates: ProjectLinkTemplateSchema[] = [],
) => {
const [projectMode, setProjectMode] =
useState<ProjectMode>(initialProjectMode);
@ -20,6 +22,9 @@ const useProjectEnterpriseSettingsForm = (
initialFeatureNamingDescription,
);
const [linkTemplates, setLinkTemplates] =
useState<ProjectLinkTemplateSchema[]>(initialLinkTemplates);
const [errors, setErrors] = useState({});
useEffect(() => {
@ -38,6 +43,10 @@ const useProjectEnterpriseSettingsForm = (
setFeatureNamingDescription(initialFeatureNamingDescription);
}, [initialFeatureNamingDescription]);
useEffect(() => {
setLinkTemplates(initialLinkTemplates);
}, [initialLinkTemplates]);
const getEnterpriseSettingsPayload = () => {
return {
mode: projectMode,
@ -46,6 +55,7 @@ const useProjectEnterpriseSettingsForm = (
example: featureNamingExample,
description: featureNamingDescription,
},
linkTemplates,
};
};
@ -58,10 +68,12 @@ const useProjectEnterpriseSettingsForm = (
featureNamingPattern,
featureNamingExample,
featureNamingDescription,
linkTemplates,
setFeatureNamingPattern,
setFeatureNamingExample,
setFeatureNamingDescription,
setProjectMode,
setLinkTemplates,
getEnterpriseSettingsPayload,
clearErrors,
errors,

View File

@ -75,7 +75,9 @@ const ProjectEnvironmentList = () => {
environments.map((environment) => ({
...environment,
projectVisible: project?.environments
.map((projectEnvironment) => projectEnvironment.environment)
?.map(
(projectEnvironment) => projectEnvironment.environment,
)
.includes(environment.name),
})),
[environments, project?.environments],

View File

@ -1,15 +1,15 @@
import useSWR, { type SWRConfiguration } from 'swr';
import { useCallback } from 'react';
import { getProjectOverviewFetcher } from './getProjectOverviewFetcher';
import type { IProjectOverview } from 'interfaces/project';
import type { ProjectOverviewSchema } from 'openapi';
const fallbackProject: IProjectOverview = {
const fallbackProject: ProjectOverviewSchema = {
featureTypeCounts: [],
environments: [],
name: '',
health: 0,
members: 0,
version: '1',
version: 1,
description: 'Default',
favorite: false,
mode: 'open',
@ -31,7 +31,7 @@ const fallbackProject: IProjectOverview = {
const useProjectOverview = (id: string, options: SWRConfiguration = {}) => {
const { KEY, fetcher } = getProjectOverviewFetcher(id);
const { data, error, mutate } = useSWR<IProjectOverview>(
const { data, error, mutate } = useSWR<ProjectOverviewSchema>(
KEY,
fetcher,
options,
@ -54,10 +54,10 @@ export const useProjectOverviewNameOrId = (id: string): string => {
};
export const featuresCount = (
project: Pick<IProjectOverview, 'featureTypeCounts'>,
project: Pick<ProjectOverviewSchema, 'featureTypeCounts'>,
) => {
return project.featureTypeCounts
.map((count) => count.count)
?.map((count) => count.count)
.reduce((a, b) => a + b, 0);
};

View File

@ -1,4 +1,4 @@
import type { ProjectOverviewSchema, ProjectStatsSchema } from 'openapi';
import type { ProjectStatsSchema } from 'openapi';
import type { IFeatureFlagListItem } from './featureToggle';
import type { ProjectEnvironmentType } from 'component/project/Project/ProjectFeatureToggles/hooks/useEnvironmentsRef';
import type { ProjectMode } from 'component/project/Project/hooks/useProjectEnterpriseSettingsForm';
@ -31,25 +31,6 @@ export interface IProject {
featureNaming?: FeatureNamingType;
}
export interface IProjectOverview {
id?: string;
members: number;
version: string;
name: string;
description?: string;
environments: Array<ProjectEnvironmentType>;
health: number;
stats: ProjectStatsSchema;
featureTypeCounts: FeatureTypeCount[];
favorite: boolean;
mode: ProjectMode;
defaultStickiness: string;
featureLimit?: number;
featureNaming?: FeatureNamingType;
archivedAt?: Date;
onboardingStatus: ProjectOverviewSchema['onboardingStatus'];
}
export interface IProjectHealthReport extends IProject {
staleCount: number;
potentiallyStaleCount: number;

View File

@ -94,6 +94,7 @@ export type UiFlags = {
cleanupReminder?: boolean;
registerFrontendClient?: boolean;
featureLinks?: boolean;
projectLinkTemplates?: boolean;
};
export interface IVersionInfo {