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 { 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
 | 
			
		||||
 | 
			
		||||
@ -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 }) => {
 | 
			
		||||
 | 
			
		||||
@ -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> = ({
 | 
			
		||||
 | 
			
		||||
@ -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]);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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}
 | 
			
		||||
 | 
			
		||||
@ -190,8 +190,8 @@ const CreateFeatureDialogContent = ({
 | 
			
		||||
                count: totalFlags ?? 0,
 | 
			
		||||
            },
 | 
			
		||||
            project: {
 | 
			
		||||
                limit: projectInfo.featureLimit,
 | 
			
		||||
                count: featuresCount(projectInfo),
 | 
			
		||||
                limit: projectInfo.featureLimit || undefined,
 | 
			
		||||
                count: featuresCount(projectInfo) ?? 0,
 | 
			
		||||
            },
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -282,7 +282,7 @@ export const Project = () => {
 | 
			
		||||
                        <StyledDiv>
 | 
			
		||||
                            <StyledFavoriteIconButton
 | 
			
		||||
                                onClick={onFavorite}
 | 
			
		||||
                                isFavorite={project?.favorite}
 | 
			
		||||
                                isFavorite={project?.favorite || false}
 | 
			
		||||
                            />
 | 
			
		||||
                            <StyledProjectTitle>
 | 
			
		||||
                                <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 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>
 | 
			
		||||
 | 
			
		||||
@ -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>
 | 
			
		||||
                    <ProjectFeatureToggles
 | 
			
		||||
                        environments={project.environments.map(
 | 
			
		||||
                        environments={
 | 
			
		||||
                            project.environments?.map(
 | 
			
		||||
                                (environment) => environment.environment,
 | 
			
		||||
                        )}
 | 
			
		||||
                            ) || []
 | 
			
		||||
                        }
 | 
			
		||||
                    />
 | 
			
		||||
                </StyledProjectToggles>
 | 
			
		||||
            </StyledContentContainer>
 | 
			
		||||
 | 
			
		||||
@ -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}
 | 
			
		||||
 | 
			
		||||
@ -37,7 +37,7 @@ export const useDefaultStrategy = (
 | 
			
		||||
        },
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const strategy = project.environments.find(
 | 
			
		||||
    const strategy = project.environments?.find(
 | 
			
		||||
        (env) => env.environment === environmentId,
 | 
			
		||||
    )?.defaultStrategy;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -49,7 +49,9 @@ const EditProject = () => {
 | 
			
		||||
                    condition={isEnterprise()}
 | 
			
		||||
                    show={<UpdateEnterpriseSettings project={project} />}
 | 
			
		||||
                />
 | 
			
		||||
                <ArchiveProjectForm featureCount={featuresCount(project)} />
 | 
			
		||||
                <ArchiveProjectForm
 | 
			
		||||
                    featureCount={featuresCount(project) ?? 0}
 | 
			
		||||
                />
 | 
			
		||||
            </StyledFormContainer>
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@ -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}
 | 
			
		||||
 | 
			
		||||
@ -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),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
@ -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,
 | 
			
		||||
 | 
			
		||||
@ -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],
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
 | 
			
		||||
@ -94,6 +94,7 @@ export type UiFlags = {
 | 
			
		||||
    cleanupReminder?: boolean;
 | 
			
		||||
    registerFrontendClient?: boolean;
 | 
			
		||||
    featureLinks?: boolean;
 | 
			
		||||
    projectLinkTemplates?: boolean;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export interface IVersionInfo {
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user