mirror of
https://github.com/Unleash/unleash.git
synced 2025-10-27 11:02:16 +01:00
feat: ui for external link templates (#9945)
Support for project link templates to the frontend UI
This commit is contained in:
parent
ea26e008d0
commit
5614cb56d3
@ -15,11 +15,8 @@ import {
|
|||||||
import { Button } from '@mui/material';
|
import { Button } from '@mui/material';
|
||||||
import { CreateButton } from 'component/common/CreateButton/CreateButton';
|
import { CreateButton } from 'component/common/CreateButton/CreateButton';
|
||||||
import type { IPermissionButtonProps } from 'component/common/PermissionButton/PermissionButton';
|
import type { IPermissionButtonProps } from 'component/common/PermissionButton/PermissionButton';
|
||||||
import type { FeatureNamingType } from 'interfaces/project';
|
|
||||||
import { ConditionallyRender } from '../ConditionallyRender/ConditionallyRender';
|
|
||||||
import { NamingPatternInfo } from './NamingPatternInfo';
|
import { NamingPatternInfo } from './NamingPatternInfo';
|
||||||
|
import type { CreateFeatureNamingPatternSchema } from 'openapi';
|
||||||
type NamingPattern = FeatureNamingType;
|
|
||||||
|
|
||||||
type FormProps = {
|
type FormProps = {
|
||||||
createButtonProps: IPermissionButtonProps;
|
createButtonProps: IPermissionButtonProps;
|
||||||
@ -35,7 +32,7 @@ type FormProps = {
|
|||||||
setDescription: (newDescription: string) => void;
|
setDescription: (newDescription: string) => void;
|
||||||
setName: (newName: string) => void;
|
setName: (newName: string) => void;
|
||||||
validateName?: () => void;
|
validateName?: () => void;
|
||||||
namingPattern?: NamingPattern;
|
namingPattern?: CreateFeatureNamingPatternSchema;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DialogFormTemplate: React.FC<FormProps> = ({
|
export const DialogFormTemplate: React.FC<FormProps> = ({
|
||||||
@ -54,8 +51,6 @@ export const DialogFormTemplate: React.FC<FormProps> = ({
|
|||||||
createButtonProps,
|
createButtonProps,
|
||||||
validateName = () => {},
|
validateName = () => {},
|
||||||
}) => {
|
}) => {
|
||||||
const displayNamingPattern = Boolean(namingPattern?.pattern);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledForm onSubmit={handleSubmit}>
|
<StyledForm onSubmit={handleSubmit}>
|
||||||
<TopGrid>
|
<TopGrid>
|
||||||
@ -66,7 +61,7 @@ export const DialogFormTemplate: React.FC<FormProps> = ({
|
|||||||
label={`${resource} name`}
|
label={`${resource} name`}
|
||||||
aria-required
|
aria-required
|
||||||
aria-details={
|
aria-details={
|
||||||
displayNamingPattern
|
namingPattern?.pattern
|
||||||
? 'naming-pattern-info'
|
? 'naming-pattern-info'
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
@ -89,10 +84,9 @@ export const DialogFormTemplate: React.FC<FormProps> = ({
|
|||||||
size='medium'
|
size='medium'
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ConditionallyRender
|
{namingPattern?.pattern ? (
|
||||||
condition={displayNamingPattern}
|
<NamingPatternInfo naming={namingPattern!} />
|
||||||
show={<NamingPatternInfo naming={namingPattern!} />}
|
) : null}
|
||||||
/>
|
|
||||||
</NameContainer>
|
</NameContainer>
|
||||||
<DescriptionContainer>
|
<DescriptionContainer>
|
||||||
<StyledInput
|
<StyledInput
|
||||||
|
|||||||
@ -5,8 +5,8 @@ import {
|
|||||||
styled,
|
styled,
|
||||||
} from '@mui/material';
|
} from '@mui/material';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import type { FeatureNamingType } from 'interfaces/project';
|
|
||||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||||
|
import type { CreateFeatureNamingPatternSchema } from 'openapi';
|
||||||
|
|
||||||
const StyledFlagNamingInfo = styled('article')(({ theme }) => ({
|
const StyledFlagNamingInfo = styled('article')(({ theme }) => ({
|
||||||
fontSize: theme.typography.body2.fontSize,
|
fontSize: theme.typography.body2.fontSize,
|
||||||
@ -35,7 +35,7 @@ const StyledAccordion = styled(Accordion)(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
naming: FeatureNamingType;
|
naming: CreateFeatureNamingPatternSchema;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const NamingPatternInfo: React.FC<Props> = ({ naming }) => {
|
export const NamingPatternInfo: React.FC<Props> = ({ naming }) => {
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
import type { FeatureNamingType } from 'interfaces/project';
|
import type { CreateFeatureNamingPatternSchema } from 'openapi';
|
||||||
|
|
||||||
const StyledFlagNamingInfo = styled('article')(({ theme }) => ({
|
const StyledFlagNamingInfo = styled('article')(({ theme }) => ({
|
||||||
fontSize: theme.fontSizes.smallBody,
|
fontSize: theme.fontSizes.smallBody,
|
||||||
@ -25,7 +25,7 @@ const StyledFlagNamingInfo = styled('article')(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
featureNaming: FeatureNamingType;
|
featureNaming: CreateFeatureNamingPatternSchema;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FeatureNamingPatternInfo: React.FC<Props> = ({
|
export const FeatureNamingPatternInfo: React.FC<Props> = ({
|
||||||
|
|||||||
@ -50,7 +50,8 @@ const FeatureSettingsProjectConfirm = ({
|
|||||||
const hasSameEnvironments: boolean = useMemo(() => {
|
const hasSameEnvironments: boolean = useMemo(() => {
|
||||||
return arraysHaveSameItems(
|
return arraysHaveSameItems(
|
||||||
feature.environments.map((env) => env.name),
|
feature.environments.map((env) => env.name),
|
||||||
project.environments.map((projectEnv) => projectEnv.environment),
|
project.environments?.map((projectEnv) => projectEnv.environment) ||
|
||||||
|
[],
|
||||||
);
|
);
|
||||||
}, [feature, project]);
|
}, [feature, project]);
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@ export const ImportOptions: FC<IImportOptionsProps> = ({
|
|||||||
onChange,
|
onChange,
|
||||||
}) => {
|
}) => {
|
||||||
const { project: projectInfo } = useProjectOverview(project);
|
const { project: projectInfo } = useProjectOverview(project);
|
||||||
const environmentOptions = projectInfo.environments.map(
|
const environmentOptions = projectInfo.environments?.map(
|
||||||
({ environment }) => ({
|
({ environment }) => ({
|
||||||
key: environment,
|
key: environment,
|
||||||
label: environment,
|
label: environment,
|
||||||
@ -41,7 +41,7 @@ export const ImportOptions: FC<IImportOptionsProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (environment === '' && environmentOptions[0]) {
|
if (environment === '' && environmentOptions?.[0]) {
|
||||||
onChange(environmentOptions[0].key);
|
onChange(environmentOptions[0].key);
|
||||||
}
|
}
|
||||||
}, [JSON.stringify(environmentOptions)]);
|
}, [JSON.stringify(environmentOptions)]);
|
||||||
@ -54,7 +54,7 @@ export const ImportOptions: FC<IImportOptionsProps> = ({
|
|||||||
</ImportOptionsDescription>
|
</ImportOptionsDescription>
|
||||||
<GeneralSelect
|
<GeneralSelect
|
||||||
sx={{ width: '180px' }}
|
sx={{ width: '180px' }}
|
||||||
options={environmentOptions}
|
options={environmentOptions || []}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
label={'Environment'}
|
label={'Environment'}
|
||||||
value={environment}
|
value={environment}
|
||||||
|
|||||||
@ -190,8 +190,8 @@ const CreateFeatureDialogContent = ({
|
|||||||
count: totalFlags ?? 0,
|
count: totalFlags ?? 0,
|
||||||
},
|
},
|
||||||
project: {
|
project: {
|
||||||
limit: projectInfo.featureLimit,
|
limit: projectInfo.featureLimit || undefined,
|
||||||
count: featuresCount(projectInfo),
|
count: featuresCount(projectInfo) ?? 0,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -282,7 +282,7 @@ export const Project = () => {
|
|||||||
<StyledDiv>
|
<StyledDiv>
|
||||||
<StyledFavoriteIconButton
|
<StyledFavoriteIconButton
|
||||||
onClick={onFavorite}
|
onClick={onFavorite}
|
||||||
isFavorite={project?.favorite}
|
isFavorite={project?.favorite || false}
|
||||||
/>
|
/>
|
||||||
<StyledProjectTitle>
|
<StyledProjectTitle>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
|
|||||||
@ -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 Select from 'component/common/select';
|
||||||
import type { ProjectMode } from '../hooks/useProjectEnterpriseSettingsForm';
|
import type { ProjectMode } from '../hooks/useProjectEnterpriseSettingsForm';
|
||||||
import { Box, InputAdornment, styled, TextField } from '@mui/material';
|
import { Box, InputAdornment, styled, TextField } from '@mui/material';
|
||||||
@ -6,6 +11,9 @@ import { CollaborationModeTooltip } from './CollaborationModeTooltip';
|
|||||||
import Input from 'component/common/Input/Input';
|
import Input from 'component/common/Input/Input';
|
||||||
import { FeatureFlagNamingTooltip } from './FeatureFlagNamingTooltip';
|
import { FeatureFlagNamingTooltip } from './FeatureFlagNamingTooltip';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
import type { ProjectLinkTemplateSchema } from 'openapi';
|
||||||
|
import { useUiFlag } from 'hooks/useUiFlag';
|
||||||
|
import ProjectLinkTemplates from './ProjectLinkTemplates/ProjectLinkTemplates';
|
||||||
|
|
||||||
interface IProjectEnterpriseSettingsForm {
|
interface IProjectEnterpriseSettingsForm {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@ -13,14 +21,16 @@ interface IProjectEnterpriseSettingsForm {
|
|||||||
featureNamingPattern?: string;
|
featureNamingPattern?: string;
|
||||||
featureNamingExample?: string;
|
featureNamingExample?: string;
|
||||||
featureNamingDescription?: string;
|
featureNamingDescription?: string;
|
||||||
setFeatureNamingPattern?: React.Dispatch<React.SetStateAction<string>>;
|
linkTemplates?: ProjectLinkTemplateSchema[];
|
||||||
setFeatureNamingExample?: React.Dispatch<React.SetStateAction<string>>;
|
setFeatureNamingPattern?: Dispatch<SetStateAction<string>>;
|
||||||
setFeatureNamingDescription?: React.Dispatch<React.SetStateAction<string>>;
|
setFeatureNamingExample?: Dispatch<SetStateAction<string>>;
|
||||||
setProjectMode?: React.Dispatch<React.SetStateAction<ProjectMode>>;
|
setFeatureNamingDescription?: Dispatch<SetStateAction<string>>;
|
||||||
|
setProjectMode?: Dispatch<SetStateAction<ProjectMode>>;
|
||||||
|
setLinkTemplates?: Dispatch<SetStateAction<ProjectLinkTemplateSchema[]>>;
|
||||||
handleSubmit: (e: any) => void;
|
handleSubmit: (e: any) => void;
|
||||||
errors: { [key: string]: string };
|
errors: { [key: string]: string };
|
||||||
clearErrors: () => void;
|
clearErrors: () => void;
|
||||||
children?: React.ReactNode;
|
children?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StyledForm = styled('form')(({ theme }) => ({
|
const StyledForm = styled('form')(({ theme }) => ({
|
||||||
@ -128,10 +138,12 @@ const ProjectEnterpriseSettingsForm: React.FC<
|
|||||||
featureNamingExample,
|
featureNamingExample,
|
||||||
featureNamingPattern,
|
featureNamingPattern,
|
||||||
featureNamingDescription,
|
featureNamingDescription,
|
||||||
|
linkTemplates = [],
|
||||||
setFeatureNamingExample,
|
setFeatureNamingExample,
|
||||||
setFeatureNamingPattern,
|
setFeatureNamingPattern,
|
||||||
setFeatureNamingDescription,
|
setFeatureNamingDescription,
|
||||||
setProjectMode,
|
setProjectMode,
|
||||||
|
setLinkTemplates,
|
||||||
errors,
|
errors,
|
||||||
}) => {
|
}) => {
|
||||||
const { setPreviousPattern, trackPattern } =
|
const { setPreviousPattern, trackPattern } =
|
||||||
@ -143,6 +155,8 @@ const ProjectEnterpriseSettingsForm: React.FC<
|
|||||||
{ key: 'private', label: 'private' },
|
{ key: 'private', label: 'private' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const projectLinkTemplatesEnabled = useUiFlag('projectLinkTemplates');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPreviousPattern(featureNamingPattern || '');
|
setPreviousPattern(featureNamingPattern || '');
|
||||||
}, [projectId]);
|
}, [projectId]);
|
||||||
@ -253,7 +267,7 @@ const ProjectEnterpriseSettingsForm: React.FC<
|
|||||||
gap: 1,
|
gap: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<legend>Feature flag naming pattern?</legend>
|
<legend>Feature flag naming pattern</legend>
|
||||||
<FeatureFlagNamingTooltip />
|
<FeatureFlagNamingTooltip />
|
||||||
</Box>
|
</Box>
|
||||||
<StyledSubtitle>
|
<StyledSubtitle>
|
||||||
@ -339,6 +353,13 @@ The flag name should contain the project name, the feature name, and the ticket
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</StyledFlagNamingContainer>
|
</StyledFlagNamingContainer>
|
||||||
|
|
||||||
|
{projectLinkTemplatesEnabled && (
|
||||||
|
<ProjectLinkTemplates
|
||||||
|
linkTemplates={linkTemplates || []}
|
||||||
|
setLinkTemplates={setLinkTemplates}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</StyledFieldset>
|
</StyledFieldset>
|
||||||
<StyledButtonContainer>{children}</StyledButtonContainer>
|
<StyledButtonContainer>{children}</StyledButtonContainer>
|
||||||
</StyledForm>
|
</StyledForm>
|
||||||
|
|||||||
@ -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;
|
||||||
@ -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;
|
||||||
@ -59,9 +59,11 @@ const ProjectOverview: FC = () => {
|
|||||||
/>
|
/>
|
||||||
<StyledProjectToggles>
|
<StyledProjectToggles>
|
||||||
<ProjectFeatureToggles
|
<ProjectFeatureToggles
|
||||||
environments={project.environments.map(
|
environments={
|
||||||
(environment) => environment.environment,
|
project.environments?.map(
|
||||||
)}
|
(environment) => environment.environment,
|
||||||
|
) || []
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</StyledProjectToggles>
|
</StyledProjectToggles>
|
||||||
</StyledContentContainer>
|
</StyledContentContainer>
|
||||||
|
|||||||
@ -61,7 +61,7 @@ export const ProjectDefaultStrategySettings = () => {
|
|||||||
specific environment. These will be used when you enable a
|
specific environment. These will be used when you enable a
|
||||||
toggle environment that has no strategies defined
|
toggle environment that has no strategies defined
|
||||||
</StyledAlert>
|
</StyledAlert>
|
||||||
{project?.environments.map((environment) => (
|
{project?.environments?.map((environment) => (
|
||||||
<ProjectEnvironment
|
<ProjectEnvironment
|
||||||
environment={environment}
|
environment={environment}
|
||||||
key={environment.environment}
|
key={environment.environment}
|
||||||
|
|||||||
@ -37,7 +37,7 @@ export const useDefaultStrategy = (
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const strategy = project.environments.find(
|
const strategy = project.environments?.find(
|
||||||
(env) => env.environment === environmentId,
|
(env) => env.environment === environmentId,
|
||||||
)?.defaultStrategy;
|
)?.defaultStrategy;
|
||||||
|
|
||||||
|
|||||||
@ -49,7 +49,9 @@ const EditProject = () => {
|
|||||||
condition={isEnterprise()}
|
condition={isEnterprise()}
|
||||||
show={<UpdateEnterpriseSettings project={project} />}
|
show={<UpdateEnterpriseSettings project={project} />}
|
||||||
/>
|
/>
|
||||||
<ArchiveProjectForm featureCount={featuresCount(project)} />
|
<ArchiveProjectForm
|
||||||
|
featureCount={featuresCount(project) ?? 0}
|
||||||
|
/>
|
||||||
</StyledFormContainer>
|
</StyledFormContainer>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -9,10 +9,10 @@ import FormTemplate from 'component/common/FormTemplate/FormTemplate';
|
|||||||
import ProjectEnterpriseSettingsForm from 'component/project/Project/ProjectEnterpriseSettingsForm/ProjectEnterpriseSettingsForm';
|
import ProjectEnterpriseSettingsForm from 'component/project/Project/ProjectEnterpriseSettingsForm/ProjectEnterpriseSettingsForm';
|
||||||
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
import PermissionButton from 'component/common/PermissionButton/PermissionButton';
|
||||||
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
|
import { UPDATE_PROJECT } from 'component/providers/AccessProvider/permissions';
|
||||||
import type { IProjectOverview } from 'component/../interfaces/project';
|
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||||
|
import type { ProjectOverviewSchema } from 'openapi';
|
||||||
|
|
||||||
const StyledContainer = styled('div')(({ theme }) => ({
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
@ -33,7 +33,7 @@ const StyledFormContainer = styled('div')(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
interface IUpdateEnterpriseSettings {
|
interface IUpdateEnterpriseSettings {
|
||||||
project: IProjectOverview;
|
project: ProjectOverviewSchema;
|
||||||
}
|
}
|
||||||
const EDIT_PROJECT_SETTINGS_BTN = 'EDIT_PROJECT_SETTINGS_BTN';
|
const EDIT_PROJECT_SETTINGS_BTN = 'EDIT_PROJECT_SETTINGS_BTN';
|
||||||
|
|
||||||
@ -65,18 +65,21 @@ export const UpdateEnterpriseSettings = ({
|
|||||||
featureNamingExample,
|
featureNamingExample,
|
||||||
featureNamingDescription,
|
featureNamingDescription,
|
||||||
featureNamingPattern,
|
featureNamingPattern,
|
||||||
|
linkTemplates,
|
||||||
setFeatureNamingPattern,
|
setFeatureNamingPattern,
|
||||||
setFeatureNamingExample,
|
setFeatureNamingExample,
|
||||||
setFeatureNamingDescription,
|
setFeatureNamingDescription,
|
||||||
setProjectMode,
|
setProjectMode,
|
||||||
|
setLinkTemplates,
|
||||||
getEnterpriseSettingsPayload,
|
getEnterpriseSettingsPayload,
|
||||||
errors: settingsErrors = {},
|
errors: settingsErrors = {},
|
||||||
clearErrors: clearSettingsErrors,
|
clearErrors: clearSettingsErrors,
|
||||||
} = useProjectEnterpriseSettingsForm(
|
} = useProjectEnterpriseSettingsForm(
|
||||||
project.mode,
|
project.mode,
|
||||||
project?.featureNaming?.pattern,
|
project?.featureNaming?.pattern || undefined,
|
||||||
project?.featureNaming?.example,
|
project?.featureNaming?.example || undefined,
|
||||||
project?.featureNaming?.description,
|
project?.featureNaming?.description || undefined,
|
||||||
|
project?.linkTemplates || [],
|
||||||
);
|
);
|
||||||
|
|
||||||
const formatProjectSettingsApiCode = () => {
|
const formatProjectSettingsApiCode = () => {
|
||||||
@ -161,12 +164,14 @@ export const UpdateEnterpriseSettings = ({
|
|||||||
featureNamingPattern={featureNamingPattern}
|
featureNamingPattern={featureNamingPattern}
|
||||||
featureNamingExample={featureNamingExample}
|
featureNamingExample={featureNamingExample}
|
||||||
featureNamingDescription={featureNamingDescription}
|
featureNamingDescription={featureNamingDescription}
|
||||||
|
linkTemplates={linkTemplates}
|
||||||
setFeatureNamingPattern={setFeatureNamingPattern}
|
setFeatureNamingPattern={setFeatureNamingPattern}
|
||||||
setFeatureNamingExample={setFeatureNamingExample}
|
setFeatureNamingExample={setFeatureNamingExample}
|
||||||
setFeatureNamingDescription={
|
setFeatureNamingDescription={
|
||||||
setFeatureNamingDescription
|
setFeatureNamingDescription
|
||||||
}
|
}
|
||||||
setProjectMode={setProjectMode}
|
setProjectMode={setProjectMode}
|
||||||
|
setLinkTemplates={setLinkTemplates}
|
||||||
handleSubmit={handleEditProjectSettings}
|
handleSubmit={handleEditProjectSettings}
|
||||||
errors={settingsErrors}
|
errors={settingsErrors}
|
||||||
clearErrors={clearSettingsErrors}
|
clearErrors={clearSettingsErrors}
|
||||||
|
|||||||
@ -14,10 +14,10 @@ import useToast from 'hooks/useToast';
|
|||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
|
import useProjectApi from 'hooks/api/actions/useProjectApi/useProjectApi';
|
||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import type { IProjectOverview } from 'interfaces/project';
|
|
||||||
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
import { useRequiredPathParam } from 'hooks/useRequiredPathParam';
|
||||||
import { styled } from '@mui/material';
|
import { styled } from '@mui/material';
|
||||||
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
import useProjectOverview from 'hooks/api/getters/useProjectOverview/useProjectOverview';
|
||||||
|
import type { ProjectOverviewSchema } from 'openapi';
|
||||||
|
|
||||||
const StyledContainer = styled('div')(({ theme }) => ({
|
const StyledContainer = styled('div')(({ theme }) => ({
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
@ -38,7 +38,7 @@ const StyledFormContainer = styled('div')(({ theme }) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
interface IUpdateProject {
|
interface IUpdateProject {
|
||||||
project: IProjectOverview;
|
project: ProjectOverviewSchema;
|
||||||
}
|
}
|
||||||
const EDIT_PROJECT_BTN = 'EDIT_PROJECT_BTN';
|
const EDIT_PROJECT_BTN = 'EDIT_PROJECT_BTN';
|
||||||
export const UpdateProject = ({ project }: IUpdateProject) => {
|
export const UpdateProject = ({ project }: IUpdateProject) => {
|
||||||
@ -66,7 +66,7 @@ export const UpdateProject = ({ project }: IUpdateProject) => {
|
|||||||
} = useProjectForm(
|
} = useProjectForm(
|
||||||
id,
|
id,
|
||||||
project.name,
|
project.name,
|
||||||
project.description,
|
project.description || undefined,
|
||||||
defaultStickiness,
|
defaultStickiness,
|
||||||
String(project.featureLimit),
|
String(project.featureLimit),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
|
import type { ProjectLinkTemplateSchema } from 'openapi';
|
||||||
|
|
||||||
export type ProjectMode = 'open' | 'protected' | 'private';
|
export type ProjectMode = 'open' | 'protected' | 'private';
|
||||||
const useProjectEnterpriseSettingsForm = (
|
const useProjectEnterpriseSettingsForm = (
|
||||||
@ -6,6 +7,7 @@ const useProjectEnterpriseSettingsForm = (
|
|||||||
initialFeatureNamingPattern = '',
|
initialFeatureNamingPattern = '',
|
||||||
initialFeatureNamingExample = '',
|
initialFeatureNamingExample = '',
|
||||||
initialFeatureNamingDescription = '',
|
initialFeatureNamingDescription = '',
|
||||||
|
initialLinkTemplates: ProjectLinkTemplateSchema[] = [],
|
||||||
) => {
|
) => {
|
||||||
const [projectMode, setProjectMode] =
|
const [projectMode, setProjectMode] =
|
||||||
useState<ProjectMode>(initialProjectMode);
|
useState<ProjectMode>(initialProjectMode);
|
||||||
@ -20,6 +22,9 @@ const useProjectEnterpriseSettingsForm = (
|
|||||||
initialFeatureNamingDescription,
|
initialFeatureNamingDescription,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const [linkTemplates, setLinkTemplates] =
|
||||||
|
useState<ProjectLinkTemplateSchema[]>(initialLinkTemplates);
|
||||||
|
|
||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -38,6 +43,10 @@ const useProjectEnterpriseSettingsForm = (
|
|||||||
setFeatureNamingDescription(initialFeatureNamingDescription);
|
setFeatureNamingDescription(initialFeatureNamingDescription);
|
||||||
}, [initialFeatureNamingDescription]);
|
}, [initialFeatureNamingDescription]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setLinkTemplates(initialLinkTemplates);
|
||||||
|
}, [initialLinkTemplates]);
|
||||||
|
|
||||||
const getEnterpriseSettingsPayload = () => {
|
const getEnterpriseSettingsPayload = () => {
|
||||||
return {
|
return {
|
||||||
mode: projectMode,
|
mode: projectMode,
|
||||||
@ -46,6 +55,7 @@ const useProjectEnterpriseSettingsForm = (
|
|||||||
example: featureNamingExample,
|
example: featureNamingExample,
|
||||||
description: featureNamingDescription,
|
description: featureNamingDescription,
|
||||||
},
|
},
|
||||||
|
linkTemplates,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -58,10 +68,12 @@ const useProjectEnterpriseSettingsForm = (
|
|||||||
featureNamingPattern,
|
featureNamingPattern,
|
||||||
featureNamingExample,
|
featureNamingExample,
|
||||||
featureNamingDescription,
|
featureNamingDescription,
|
||||||
|
linkTemplates,
|
||||||
setFeatureNamingPattern,
|
setFeatureNamingPattern,
|
||||||
setFeatureNamingExample,
|
setFeatureNamingExample,
|
||||||
setFeatureNamingDescription,
|
setFeatureNamingDescription,
|
||||||
setProjectMode,
|
setProjectMode,
|
||||||
|
setLinkTemplates,
|
||||||
getEnterpriseSettingsPayload,
|
getEnterpriseSettingsPayload,
|
||||||
clearErrors,
|
clearErrors,
|
||||||
errors,
|
errors,
|
||||||
|
|||||||
@ -75,7 +75,9 @@ const ProjectEnvironmentList = () => {
|
|||||||
environments.map((environment) => ({
|
environments.map((environment) => ({
|
||||||
...environment,
|
...environment,
|
||||||
projectVisible: project?.environments
|
projectVisible: project?.environments
|
||||||
.map((projectEnvironment) => projectEnvironment.environment)
|
?.map(
|
||||||
|
(projectEnvironment) => projectEnvironment.environment,
|
||||||
|
)
|
||||||
.includes(environment.name),
|
.includes(environment.name),
|
||||||
})),
|
})),
|
||||||
[environments, project?.environments],
|
[environments, project?.environments],
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import useSWR, { type SWRConfiguration } from 'swr';
|
import useSWR, { type SWRConfiguration } from 'swr';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { getProjectOverviewFetcher } from './getProjectOverviewFetcher';
|
import { getProjectOverviewFetcher } from './getProjectOverviewFetcher';
|
||||||
import type { IProjectOverview } from 'interfaces/project';
|
import type { ProjectOverviewSchema } from 'openapi';
|
||||||
|
|
||||||
const fallbackProject: IProjectOverview = {
|
const fallbackProject: ProjectOverviewSchema = {
|
||||||
featureTypeCounts: [],
|
featureTypeCounts: [],
|
||||||
environments: [],
|
environments: [],
|
||||||
name: '',
|
name: '',
|
||||||
health: 0,
|
health: 0,
|
||||||
members: 0,
|
members: 0,
|
||||||
version: '1',
|
version: 1,
|
||||||
description: 'Default',
|
description: 'Default',
|
||||||
favorite: false,
|
favorite: false,
|
||||||
mode: 'open',
|
mode: 'open',
|
||||||
@ -31,7 +31,7 @@ const fallbackProject: IProjectOverview = {
|
|||||||
|
|
||||||
const useProjectOverview = (id: string, options: SWRConfiguration = {}) => {
|
const useProjectOverview = (id: string, options: SWRConfiguration = {}) => {
|
||||||
const { KEY, fetcher } = getProjectOverviewFetcher(id);
|
const { KEY, fetcher } = getProjectOverviewFetcher(id);
|
||||||
const { data, error, mutate } = useSWR<IProjectOverview>(
|
const { data, error, mutate } = useSWR<ProjectOverviewSchema>(
|
||||||
KEY,
|
KEY,
|
||||||
fetcher,
|
fetcher,
|
||||||
options,
|
options,
|
||||||
@ -54,10 +54,10 @@ export const useProjectOverviewNameOrId = (id: string): string => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const featuresCount = (
|
export const featuresCount = (
|
||||||
project: Pick<IProjectOverview, 'featureTypeCounts'>,
|
project: Pick<ProjectOverviewSchema, 'featureTypeCounts'>,
|
||||||
) => {
|
) => {
|
||||||
return project.featureTypeCounts
|
return project.featureTypeCounts
|
||||||
.map((count) => count.count)
|
?.map((count) => count.count)
|
||||||
.reduce((a, b) => a + b, 0);
|
.reduce((a, b) => a + b, 0);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { ProjectOverviewSchema, ProjectStatsSchema } from 'openapi';
|
import type { ProjectStatsSchema } from 'openapi';
|
||||||
import type { IFeatureFlagListItem } from './featureToggle';
|
import type { IFeatureFlagListItem } from './featureToggle';
|
||||||
import type { ProjectEnvironmentType } from 'component/project/Project/ProjectFeatureToggles/hooks/useEnvironmentsRef';
|
import type { ProjectEnvironmentType } from 'component/project/Project/ProjectFeatureToggles/hooks/useEnvironmentsRef';
|
||||||
import type { ProjectMode } from 'component/project/Project/hooks/useProjectEnterpriseSettingsForm';
|
import type { ProjectMode } from 'component/project/Project/hooks/useProjectEnterpriseSettingsForm';
|
||||||
@ -31,25 +31,6 @@ export interface IProject {
|
|||||||
featureNaming?: FeatureNamingType;
|
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 {
|
export interface IProjectHealthReport extends IProject {
|
||||||
staleCount: number;
|
staleCount: number;
|
||||||
potentiallyStaleCount: number;
|
potentiallyStaleCount: number;
|
||||||
|
|||||||
@ -94,6 +94,7 @@ export type UiFlags = {
|
|||||||
cleanupReminder?: boolean;
|
cleanupReminder?: boolean;
|
||||||
registerFrontendClient?: boolean;
|
registerFrontendClient?: boolean;
|
||||||
featureLinks?: boolean;
|
featureLinks?: boolean;
|
||||||
|
projectLinkTemplates?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IVersionInfo {
|
export interface IVersionInfo {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user