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:
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(
|
||||
(environment) => environment.environment,
|
||||
)}
|
||||
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