diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverview.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverview.tsx index 3d6b7a0f14..d1320c8ef1 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverview.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverview.tsx @@ -1,4 +1,4 @@ -import FeatureOverviewMetaData from './FeatureOverviewMetaData/FeatureOverviewMetaData'; +import NewFeatureOverviewMetaData from './FeatureOverviewMetaData/FeatureOverviewMetaData'; import FeatureOverviewEnvironments from './FeatureOverviewEnvironments/FeatureOverviewEnvironments'; import { Route, Routes, useNavigate } from 'react-router-dom'; import { SidebarModal } from 'component/common/SidebarModal/SidebarModal'; @@ -8,12 +8,15 @@ import { } from 'component/feature/FeatureStrategy/FeatureStrategyEdit/FeatureStrategyEdit'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { usePageTitle } from 'hooks/usePageTitle'; -import { FeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel'; +import { FeatureOverviewSidePanel as NewFeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel'; import { useHiddenEnvironments } from 'hooks/useHiddenEnvironments'; import { styled } from '@mui/material'; import { FeatureStrategyCreate } from 'component/feature/FeatureStrategy/FeatureStrategyCreate/FeatureStrategyCreate'; import { useEffect } from 'react'; import { useLastViewedFlags } from 'hooks/useLastViewedFlags'; +import { useUiFlag } from 'hooks/useUiFlag'; +import OldFeatureOverviewMetaData from './FeatureOverviewMetaData/OldFeatureOverviewMetaData'; +import { OldFeatureOverviewSidePanel } from 'component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/OldFeatureOverviewSidePanel'; const StyledContainer = styled('div')(({ theme }) => ({ display: 'flex', @@ -46,6 +49,14 @@ const FeatureOverview = () => { setLastViewed({ featureId, projectId }); }, [featureId]); + const flagOverviewRedesign = useUiFlag('flagOverviewRedesign'); + const FeatureOverviewMetaData = flagOverviewRedesign + ? NewFeatureOverviewMetaData + : OldFeatureOverviewMetaData; + const FeatureOverviewSidePanel = flagOverviewRedesign + ? NewFeatureOverviewSidePanel + : OldFeatureOverviewSidePanel; + return (
diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/DependencyActions.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/DependencyActions.tsx index 375fa6c7a1..b3742cac02 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/DependencyActions.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/DependencyActions.tsx @@ -1,5 +1,5 @@ import type React from 'react'; -import { type FC, useState } from 'react'; +import { useState } from 'react'; import { IconButton, ListItemIcon, @@ -16,16 +16,27 @@ import Delete from '@mui/icons-material/Delete'; import Edit from '@mui/icons-material/Edit'; import MoreVert from '@mui/icons-material/MoreVert'; +const StyledIconButton = styled(IconButton)(({ theme }) => ({ + height: theme.spacing(3.5), + width: theme.spacing(3.5), +})); + const StyledPopover = styled(Popover)(({ theme }) => ({ borderRadius: theme.shape.borderRadiusLarge, padding: theme.spacing(1, 1.5), })); -export const DependencyActions: FC<{ +interface IDependencyActionsProps { feature: string; onEdit: () => void; onDelete: () => void; -}> = ({ feature, onEdit, onDelete }) => { +} + +export const DependencyActions = ({ + feature, + onEdit, + onDelete, +}: IDependencyActionsProps) => { const id = `dependency-${feature}-actions`; const menuId = `${id}-menu`; @@ -42,8 +53,7 @@ export const DependencyActions: FC<{ return ( - - + ({ + '&&&': { + fontSize: theme.fontSizes.smallBody, + lineHeight: 1, + margin: 0, + }, +})); const useDeleteDependency = (project: string, featureId: string) => { const { trackEvent } = usePlausibleTracker(); @@ -83,7 +92,11 @@ const useDeleteDependency = (project: string, featureId: string) => { return deleteDependency; }; -export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => { +interface IDependencyRowProps { + feature: IFeatureToggle; +} + +export const DependencyRow = ({ feature }: IDependencyRowProps) => { const [showDependencyDialogue, setShowDependencyDialogue] = useState(false); const canAddParentDependency = Boolean(feature.project) && @@ -103,55 +116,54 @@ export const DependencyRow: FC<{ feature: IFeatureToggle }> = ({ feature }) => { - - Dependency: - { - setShowDependencyDialogue(true); - }} - sx={(theme) => ({ - marginBottom: theme.spacing(0.4), - })} - > - Add parent feature - - - + + + Dependency: + + { + setShowDependencyDialogue(true); + }} + > + Add parent feature + + } /> - - Dependency: + + + Dependency: + + {feature.dependencies[0]?.feature} - - - setShowDependencyDialogue(true) - } - onDelete={deleteDependency} - /> - } - /> - + + setShowDependencyDialogue(true) + } + onDelete={deleteDependency} + /> + } + /> + + } /> = ({ feature }) => { hasParentDependency && !feature.dependencies[0]?.enabled } show={ - - - Dependency value: - disabled - - + + + Dependency value: + + disabled + } /> = ({ feature }) => { Boolean(feature.dependencies[0]?.variants?.length) } show={ - - - Dependency value: - - - + + + Dependency value: + + + } /> - - Children: - - - + + + Children: + + + } /> diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx index fc3421697d..ba8c165639 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/FeatureOverviewMetaData.tsx @@ -1,262 +1,201 @@ -import { Box, capitalize, styled } from '@mui/material'; -import { Link, useNavigate } from 'react-router-dom'; +import { capitalize, styled } from '@mui/material'; +import { useNavigate } from 'react-router-dom'; import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons'; import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; -import Edit from '@mui/icons-material/Edit'; -import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; -import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog'; import { useState } from 'react'; import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog'; -import { StyledDetail } from '../FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow'; import { formatDateYMD } from 'utils/formatDate'; import { parseISO } from 'date-fns'; -import { FeatureEnvironmentSeen } from '../../FeatureEnvironmentSeen/FeatureEnvironmentSeen'; import { DependencyRow } from './DependencyRow'; import { useLocationSettings } from 'hooks/useLocationSettings'; import { useShowDependentFeatures } from './useShowDependentFeatures'; -import type { ILastSeenEnvironments } from 'interfaces/featureToggle'; import { FeatureLifecycle } from '../FeatureLifecycle/FeatureLifecycle'; import { MarkCompletedDialogue } from '../FeatureLifecycle/MarkCompletedDialogue'; import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; +import { TagRow } from './TagRow'; -const StyledContainer = styled('div')(({ theme }) => ({ +const StyledMetaDataContainer = styled('div')(({ theme }) => ({ + padding: theme.spacing(3), borderRadius: theme.shape.borderRadiusLarge, backgroundColor: theme.palette.background.paper, display: 'flex', flexDirection: 'column', - maxWidth: '350px', - minWidth: '350px', - marginRight: theme.spacing(2), + gap: theme.spacing(2), + width: '350px', [theme.breakpoints.down(1000)]: { width: '100%', - maxWidth: 'none', - minWidth: 'auto', }, })); -const StyledPaddingContainerTop = styled('div')({ - padding: '1.5rem 1.5rem 0 1.5rem', -}); - -const StyledMetaDataHeader = styled('div')({ +const StyledMetaDataHeader = styled('div')(({ theme }) => ({ display: 'flex', alignItems: 'center', -}); - -const StyledHeader = styled('h2')(({ theme }) => ({ - fontSize: theme.fontSizes.mainHeader, - fontWeight: 'normal', - margin: 0, + gap: theme.spacing(2), + '& > svg': { + height: theme.spacing(5), + width: theme.spacing(5), + padding: theme.spacing(0.5), + backgroundColor: theme.palette.background.alternative, + fill: theme.palette.primary.contrastText, + borderRadius: theme.shape.borderRadiusMedium, + }, + '& > h2': { + fontSize: theme.fontSizes.mainHeader, + fontWeight: 'normal', + }, })); -const StyledBody = styled('div')(({ theme }) => ({ - margin: theme.spacing(2, 0), +const StyledBody = styled('div')({ display: 'flex', flexDirection: 'column', +}); + +export const StyledMetaDataItem = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + minHeight: theme.spacing(4.25), fontSize: theme.fontSizes.smallBody, })); -const BodyItemWithIcon = styled('div')(({ theme }) => ({})); - -const SpacedBodyItem = styled('div')(({ theme }) => ({ - display: 'flex', - justifyContent: 'space-between', - padding: theme.spacing(1, 0), -})); - -const StyledDescriptionContainer = styled('div')(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', -})); - -const StyledDetailsContainer = styled('div')(({ theme }) => ({ - display: 'flex', - alignItems: 'center', - justifyContent: 'space-between', -})); - -const StyledDescription = styled('p')({ - wordBreak: 'break-word', -}); - -const StyledUserAvatar = styled(UserAvatar)(({ theme }) => ({ - margin: theme.spacing(1), -})); - -export const StyledLabel = styled('span')(({ theme }) => ({ +export const StyledMetaDataItemLabel = styled('span')(({ theme }) => ({ color: theme.palette.text.secondary, marginRight: theme.spacing(1), })); +const StyledMetaDataItemText = styled('span')({ + overflowWrap: 'anywhere', +}); + +export const StyledMetaDataItemValue = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + gap: theme.spacing(1), +})); + +const StyledUserAvatar = styled(UserAvatar)(({ theme }) => ({ + height: theme.spacing(3.5), + width: theme.spacing(3.5), +})); + const FeatureOverviewMetaData = () => { const projectId = useRequiredPathParam('projectId'); const featureId = useRequiredPathParam('featureId'); const { feature, refetchFeature } = useFeature(projectId, featureId); - const { project, description, type } = feature; - const navigate = useNavigate(); - const [showDelDialog, setShowDelDialog] = useState(false); - const [showMarkCompletedDialogue, setShowMarkCompletedDialogue] = - useState(false); const { locationSettings } = useLocationSettings(); - const showDependentFeatures = useShowDependentFeatures(feature.project); + const navigate = useNavigate(); - const lastSeenEnvironments: ILastSeenEnvironments[] = - feature.environments?.map((env) => ({ - name: env.name, - lastSeenAt: env.lastSeenAt, - enabled: env.enabled, - yes: env.yes, - no: env.no, - })); + const [archiveDialogOpen, setArchiveDialogOpen] = useState(false); + const [markCompletedDialogueOpen, setMarkCompletedDialogueOpen] = + useState(false); - const IconComponent = getFeatureTypeIcons(type); + const { project, description, type } = feature; + + const showDependentFeatures = useShowDependentFeatures(project); + + const FlagTypeIcon = getFeatureTypeIcons(type); return ( - - + <> + - ({ - marginRight: theme.spacing(2), - height: '40px', - width: '40px', - padding: theme.spacing(0.5), - backgroundColor: - theme.palette.background.alternative, - fill: theme.palette.primary.contrastText, - borderRadius: `${theme.shape.borderRadiusMedium}px`, - })} - />{' '} - {capitalize(type || '')} toggle + +

{capitalize(type || '')} flag

+ + + {description} + + + } + /> - - Project: - {project} - + + + Project: + + + {project} + + - Lifecycle: + + + Lifecycle: + setShowDelDialog(true)} + onArchive={() => setArchiveDialogOpen(true)} onComplete={() => - setShowMarkCompletedDialogue(true) + setMarkCompletedDialogueOpen(true) } onUncomplete={refetchFeature} /> - + } /> - - - Description: - - - {description} - - - - - - - } - elseShow={ -
- - No description.{' '} - - - - -
- } - /> - - - - Created at: - - {formatDateYMD( - parseISO(feature.createdAt), - locationSettings.locale, - )} - - - - - - + + + Created at: + + + {formatDateYMD( + parseISO(feature.createdAt), + locationSettings.locale, + )} + + ( - - - - Created by: - {feature.createdBy?.name} - + + + Created by: + + + + {feature.createdBy?.name} + - - + + )} /> } /> +
-
+ 0} show={ setShowDelDialog(false)} + isOpen={archiveDialogOpen} + onClose={() => setArchiveDialogOpen(false)} /> } elseShow={ { navigate(`/projects/${projectId}`); }} - onClose={() => setShowDelDialog(false)} + onClose={() => setArchiveDialogOpen(false)} projectId={projectId} featureIds={[featureId]} /> @@ -266,15 +205,15 @@ const FeatureOverviewMetaData = () => { condition={Boolean(feature.project)} show={ } /> -
+ ); }; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/OldDependencyActions.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/OldDependencyActions.tsx new file mode 100644 index 0000000000..0e294a04c5 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/OldDependencyActions.tsx @@ -0,0 +1,104 @@ +import type React from 'react'; +import { type FC, useState } from 'react'; +import { + IconButton, + ListItemIcon, + ListItemText, + MenuItem, + MenuList, + Popover, + styled, + Tooltip, + Typography, + Box, +} from '@mui/material'; +import Delete from '@mui/icons-material/Delete'; +import Edit from '@mui/icons-material/Edit'; +import MoreVert from '@mui/icons-material/MoreVert'; + +const StyledPopover = styled(Popover)(({ theme }) => ({ + borderRadius: theme.shape.borderRadiusLarge, + padding: theme.spacing(1, 1.5), +})); + +export const OldDependencyActions: FC<{ + feature: string; + onEdit: () => void; + onDelete: () => void; +}> = ({ feature, onEdit, onDelete }) => { + const id = `dependency-${feature}-actions`; + const menuId = `${id}-menu`; + + const [anchorEl, setAnchorEl] = useState(null); + + const open = Boolean(anchorEl); + const openActions = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + const closeActions = () => { + setAnchorEl(null); + }; + + return ( + + + + + + + + + { + onEdit(); + closeActions(); + }} + > + + + + + Edit + + + + { + onDelete(); + closeActions(); + }} + > + + + + + Delete + + + + + + ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/OldDependencyRow.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/OldDependencyRow.tsx new file mode 100644 index 0000000000..b27acdf879 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/OldDependencyRow.tsx @@ -0,0 +1,219 @@ +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { AddDependencyDialogue } from 'component/feature/Dependencies/AddDependencyDialogue'; +import type { IFeatureToggle } from 'interfaces/featureToggle'; +import { type FC, useState } from 'react'; +import { + FlexRow, + StyledDetail, + StyledLabel, + StyledLink, +} from '../FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow'; +import { OldDependencyActions } from './OldDependencyActions'; +import { useDependentFeaturesApi } from 'hooks/api/actions/useDependentFeaturesApi/useDependentFeaturesApi'; +import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; +import { ChildrenTooltip } from './ChildrenTooltip'; +import PermissionButton from 'component/common/PermissionButton/PermissionButton'; +import { UPDATE_FEATURE_DEPENDENCY } from 'component/providers/AccessProvider/permissions'; +import { useCheckProjectAccess } from 'hooks/useHasAccess'; +import { useChangeRequestApi } from 'hooks/api/actions/useChangeRequestApi/useChangeRequestApi'; +import { usePendingChangeRequests } from 'hooks/api/getters/usePendingChangeRequests/usePendingChangeRequests'; +import useToast from 'hooks/useToast'; +import { useHighestPermissionChangeRequestEnvironment } from 'hooks/useHighestPermissionChangeRequestEnvironment'; +import { useChangeRequestsEnabled } from 'hooks/useChangeRequestsEnabled'; +import { formatUnknownError } from 'utils/formatUnknownError'; +import { usePlausibleTracker } from 'hooks/usePlausibleTracker'; +import { VariantsTooltip } from './VariantsTooltip'; + +const useDeleteDependency = (project: string, featureId: string) => { + const { trackEvent } = usePlausibleTracker(); + const { addChange } = useChangeRequestApi(); + const { refetch: refetchChangeRequests } = + usePendingChangeRequests(project); + const { setToastData, setToastApiError } = useToast(); + const { refetchFeature } = useFeature(project, featureId); + const environment = useHighestPermissionChangeRequestEnvironment(project)(); + const { isChangeRequestConfiguredInAnyEnv } = + useChangeRequestsEnabled(project); + const { removeDependencies } = useDependentFeaturesApi(project); + + const handleAddChange = async () => { + if (!environment) { + console.error('No change request environment'); + return; + } + await addChange(project, environment, [ + { + action: 'deleteDependency', + feature: featureId, + payload: undefined, + }, + ]); + }; + + const deleteDependency = async () => { + try { + if (isChangeRequestConfiguredInAnyEnv()) { + await handleAddChange(); + trackEvent('dependent_features', { + props: { + eventType: 'delete dependency added to change request', + }, + }); + setToastData({ + text: `${featureId} dependency will be removed`, + type: 'success', + title: 'Change added to a draft', + }); + await refetchChangeRequests(); + } else { + await removeDependencies(featureId); + trackEvent('dependent_features', { + props: { + eventType: 'dependency removed', + }, + }); + setToastData({ title: 'Dependency removed', type: 'success' }); + await refetchFeature(); + } + } catch (error) { + setToastApiError(formatUnknownError(error)); + } + }; + + return deleteDependency; +}; + +export const OldDependencyRow: FC<{ feature: IFeatureToggle }> = ({ + feature, +}) => { + const [showDependencyDialogue, setShowDependencyDialogue] = useState(false); + const canAddParentDependency = + Boolean(feature.project) && + feature.dependencies.length === 0 && + feature.children.length === 0; + const hasParentDependency = + Boolean(feature.project) && Boolean(feature.dependencies.length > 0); + const hasChildren = Boolean(feature.project) && feature.children.length > 0; + const environment = useHighestPermissionChangeRequestEnvironment( + feature.project, + )(); + const checkAccess = useCheckProjectAccess(feature.project); + const deleteDependency = useDeleteDependency(feature.project, feature.name); + + return ( + <> + + + Dependency: + { + setShowDependencyDialogue(true); + }} + sx={(theme) => ({ + marginBottom: theme.spacing(0.4), + })} + > + Add parent feature + + + + } + /> + + + Dependency: + + {feature.dependencies[0]?.feature} + + + + setShowDependencyDialogue(true) + } + onDelete={deleteDependency} + /> + } + /> + + } + /> + + + Dependency value: + disabled + + + } + /> + + + Dependency value: + + + + } + /> + + + Children: + + + + } + /> + + setShowDependencyDialogue(false)} + showDependencyDialogue={showDependencyDialogue} + /> + } + /> + + ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/OldFeatureOverviewMetaData.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/OldFeatureOverviewMetaData.tsx new file mode 100644 index 0000000000..a6e86dedbc --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/OldFeatureOverviewMetaData.tsx @@ -0,0 +1,281 @@ +import { Box, capitalize, styled } from '@mui/material'; +import { Link, useNavigate } from 'react-router-dom'; +import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; +import { getFeatureTypeIcons } from 'utils/getFeatureTypeIcons'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import Edit from '@mui/icons-material/Edit'; +import PermissionIconButton from 'component/common/PermissionIconButton/PermissionIconButton'; +import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions'; +import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; +import { FeatureArchiveDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveDialog'; +import { useState } from 'react'; +import { FeatureArchiveNotAllowedDialog } from 'component/common/FeatureArchiveDialog/FeatureArchiveNotAllowedDialog'; +import { StyledDetail } from '../FeatureOverviewSidePanel/FeatureOverviewSidePanelDetails/StyledRow'; +import { formatDateYMD } from 'utils/formatDate'; +import { parseISO } from 'date-fns'; +import { FeatureEnvironmentSeen } from '../../FeatureEnvironmentSeen/FeatureEnvironmentSeen'; +import { OldDependencyRow } from './OldDependencyRow'; +import { useLocationSettings } from 'hooks/useLocationSettings'; +import { useShowDependentFeatures } from './useShowDependentFeatures'; +import type { ILastSeenEnvironments } from 'interfaces/featureToggle'; +import { FeatureLifecycle } from '../FeatureLifecycle/FeatureLifecycle'; +import { MarkCompletedDialogue } from '../FeatureLifecycle/MarkCompletedDialogue'; +import { UserAvatar } from 'component/common/UserAvatar/UserAvatar'; + +const StyledContainer = styled('div')(({ theme }) => ({ + borderRadius: theme.shape.borderRadiusLarge, + backgroundColor: theme.palette.background.paper, + display: 'flex', + flexDirection: 'column', + maxWidth: '350px', + minWidth: '350px', + marginRight: theme.spacing(2), + [theme.breakpoints.down(1000)]: { + width: '100%', + maxWidth: 'none', + minWidth: 'auto', + }, +})); + +const StyledPaddingContainerTop = styled('div')({ + padding: '1.5rem 1.5rem 0 1.5rem', +}); + +const StyledMetaDataHeader = styled('div')({ + display: 'flex', + alignItems: 'center', +}); + +const StyledHeader = styled('h2')(({ theme }) => ({ + fontSize: theme.fontSizes.mainHeader, + fontWeight: 'normal', + margin: 0, +})); + +const StyledBody = styled('div')(({ theme }) => ({ + margin: theme.spacing(2, 0), + display: 'flex', + flexDirection: 'column', + fontSize: theme.fontSizes.smallBody, +})); + +const BodyItemWithIcon = styled('div')(({ theme }) => ({})); + +const SpacedBodyItem = styled('div')(({ theme }) => ({ + display: 'flex', + justifyContent: 'space-between', + padding: theme.spacing(1, 0), +})); + +const StyledDescriptionContainer = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', +})); + +const StyledDetailsContainer = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', +})); + +const StyledDescription = styled('p')({ + wordBreak: 'break-word', +}); + +const StyledUserAvatar = styled(UserAvatar)(({ theme }) => ({ + margin: theme.spacing(1), +})); + +export const StyledLabel = styled('span')(({ theme }) => ({ + color: theme.palette.text.secondary, + marginRight: theme.spacing(1), +})); + +const OldFeatureOverviewMetaData = () => { + const projectId = useRequiredPathParam('projectId'); + const featureId = useRequiredPathParam('featureId'); + const { feature, refetchFeature } = useFeature(projectId, featureId); + const { project, description, type } = feature; + const navigate = useNavigate(); + const [showDelDialog, setShowDelDialog] = useState(false); + const [showMarkCompletedDialogue, setShowMarkCompletedDialogue] = + useState(false); + + const { locationSettings } = useLocationSettings(); + const showDependentFeatures = useShowDependentFeatures(feature.project); + + const lastSeenEnvironments: ILastSeenEnvironments[] = + feature.environments?.map((env) => ({ + name: env.name, + lastSeenAt: env.lastSeenAt, + enabled: env.enabled, + yes: env.yes, + no: env.no, + })); + + const IconComponent = getFeatureTypeIcons(type); + + return ( + + + + ({ + marginRight: theme.spacing(2), + height: '40px', + width: '40px', + padding: theme.spacing(0.5), + backgroundColor: + theme.palette.background.alternative, + fill: theme.palette.primary.contrastText, + borderRadius: `${theme.shape.borderRadiusMedium}px`, + })} + />{' '} + {capitalize(type || '')} toggle + + + + Project: + {project} + + + Lifecycle: + setShowDelDialog(true)} + onComplete={() => + setShowMarkCompletedDialogue(true) + } + onUncomplete={refetchFeature} + /> + + } + /> + + + Description: + + + {description} + + + + + + + } + elseShow={ +
+ + No description.{' '} + + + + +
+ } + /> + + + + Created at: + + {formatDateYMD( + parseISO(feature.createdAt), + locationSettings.locale, + )} + + + + + + + ( + + + + Created by: + {feature.createdBy?.name} + + + + + )} + /> + } + /> +
+
+ 0} + show={ + setShowDelDialog(false)} + /> + } + elseShow={ + { + navigate(`/projects/${projectId}`); + }} + onClose={() => setShowDelDialog(false)} + projectId={projectId} + featureIds={[featureId]} + /> + } + /> + + } + /> +
+ ); +}; + +export default OldFeatureOverviewMetaData; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx new file mode 100644 index 0000000000..1ec11675a0 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewMetaData/TagRow.tsx @@ -0,0 +1,203 @@ +import type { IFeatureToggle } from 'interfaces/featureToggle'; +import { useContext, useState } from 'react'; +import { Chip, styled, Tooltip } from '@mui/material'; +import useFeatureTags from 'hooks/api/getters/useFeatureTags/useFeatureTags'; +import Add from '@mui/icons-material/Add'; +import ClearIcon from '@mui/icons-material/Clear'; +import { ManageTagsDialog } from 'component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageTagsDialog'; +import { UPDATE_FEATURE } from 'component/providers/AccessProvider/permissions'; +import AccessContext from 'contexts/AccessContext'; +import { Dialogue } from 'component/common/Dialogue/Dialogue'; +import type { ITag } from 'interfaces/tags'; +import useFeatureApi from 'hooks/api/actions/useFeatureApi/useFeatureApi'; +import useToast from 'hooks/useToast'; +import { formatUnknownError } from 'utils/formatUnknownError'; +import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender'; +import { + StyledMetaDataItem, + StyledMetaDataItemLabel, +} from './FeatureOverviewMetaData'; +import PermissionButton from 'component/common/PermissionButton/PermissionButton'; + +const StyledPermissionButton = styled(PermissionButton)(({ theme }) => ({ + '&&&': { + fontSize: theme.fontSizes.smallBody, + lineHeight: 1, + margin: 0, + }, +})); + +const StyledTagRow = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'start', + minHeight: theme.spacing(4.25), + lineHeight: theme.spacing(4.25), + fontSize: theme.fontSizes.smallBody, + justifyContent: 'start', +})); + +const StyledTagContainer = styled('div')(({ theme }) => ({ + display: 'flex', + flex: 1, + overflow: 'hidden', + gap: theme.spacing(1), + flexWrap: 'wrap', + marginTop: theme.spacing(0.75), +})); + +const StyledChip = styled(Chip)(({ theme }) => ({ + fontSize: theme.fontSizes.smallerBody, + overflowWrap: 'anywhere', + backgroundColor: theme.palette.neutral.light, + color: theme.palette.neutral.dark, + '&&& > svg': { + color: theme.palette.neutral.dark, + fontSize: theme.fontSizes.smallBody, + }, +})); + +const StyledAddedTag = styled(StyledChip)(({ theme }) => ({ + backgroundColor: theme.palette.secondary.light, + color: theme.palette.secondary.dark, + '&&& > svg': { + color: theme.palette.secondary.dark, + fontSize: theme.fontSizes.smallBody, + }, +})); + +interface IFeatureOverviewSidePanelTagsProps { + feature: IFeatureToggle; +} + +export const TagRow = ({ feature }: IFeatureOverviewSidePanelTagsProps) => { + const { tags, refetch } = useFeatureTags(feature.name); + const { deleteTagFromFeature } = useFeatureApi(); + + const [manageTagsOpen, setManageTagsOpen] = useState(false); + const [removeTagOpen, setRemoveTagOpen] = useState(false); + const [selectedTag, setSelectedTag] = useState(); + + const { setToastData, setToastApiError } = useToast(); + const { hasAccess } = useContext(AccessContext); + const canUpdateTags = hasAccess(UPDATE_FEATURE, feature.project); + + const handleRemove = async () => { + if (!selectedTag) return; + try { + await deleteTagFromFeature( + feature.name, + selectedTag.type, + selectedTag.value, + ); + refetch(); + setToastData({ + type: 'success', + title: 'Tag removed', + text: 'Successfully removed tag', + }); + } catch (error: unknown) { + setToastApiError(formatUnknownError(error)); + } + }; + + return ( + <> + + Tags: + { + setManageTagsOpen(true); + }} + > + Add tag + + + } + elseShow={ + + Tags: + + {tags.map((tag) => { + const tagLabel = `${tag.type}:${tag.value}`; + return ( + 35 ? tagLabel : '' + } + arrow + > + + + + } + onDelete={ + canUpdateTags + ? () => { + setRemoveTagOpen( + true, + ); + setSelectedTag(tag); + } + : undefined + } + /> + + ); + })} + } + label='Add tag' + size='small' + onClick={() => setManageTagsOpen(true)} + /> + } + /> + + + } + /> + + { + setRemoveTagOpen(false); + setSelectedTag(undefined); + }} + onClick={() => { + setRemoveTagOpen(false); + handleRemove(); + setSelectedTag(undefined); + }} + title='Remove tag' + > + You are about to remove tag:{' '} + + {selectedTag?.type}:{selectedTag?.value} + + + + ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel.tsx index adfdab5678..623aa34d3d 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/FeatureOverviewSidePanel.tsx @@ -1,9 +1,8 @@ -import { Box, Divider, styled } from '@mui/material'; +import { Box, styled } from '@mui/material'; import { HelpIcon } from 'component/common/HelpIcon/HelpIcon'; import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; import { FeatureOverviewSidePanelEnvironmentSwitches } from './FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches'; -import { FeatureOverviewSidePanelTags } from './FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags'; import { Sticky } from 'component/common/Sticky/Sticky'; const StyledContainer = styled(Box)(({ theme }) => ({ @@ -75,15 +74,6 @@ export const FeatureOverviewSidePanel = ({ hiddenEnvironments={hiddenEnvironments} setHiddenEnvironments={setHiddenEnvironments} /> - - - Tags for this feature flag - - } - feature={feature} - /> ); }; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/OldFeatureOverviewSidePanel.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/OldFeatureOverviewSidePanel.tsx new file mode 100644 index 0000000000..f26d7811a6 --- /dev/null +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/FeatureOverviewSidePanel/OldFeatureOverviewSidePanel.tsx @@ -0,0 +1,89 @@ +import { Box, Divider, styled } from '@mui/material'; +import { HelpIcon } from 'component/common/HelpIcon/HelpIcon'; +import { useFeature } from 'hooks/api/getters/useFeature/useFeature'; +import { useRequiredPathParam } from 'hooks/useRequiredPathParam'; +import { FeatureOverviewSidePanelEnvironmentSwitches } from './FeatureOverviewSidePanelEnvironmentSwitches/FeatureOverviewSidePanelEnvironmentSwitches'; +import { FeatureOverviewSidePanelTags } from './FeatureOverviewSidePanelTags/FeatureOverviewSidePanelTags'; +import { Sticky } from 'component/common/Sticky/Sticky'; + +const StyledContainer = styled(Box)(({ theme }) => ({ + top: theme.spacing(2), + borderRadius: theme.shape.borderRadiusLarge, + backgroundColor: theme.palette.background.paper, + display: 'flex', + flexDirection: 'column', + maxWidth: '350px', + minWidth: '350px', + marginRight: '1rem', + marginTop: '1rem', + [theme.breakpoints.down(1000)]: { + marginBottom: '1rem', + width: '100%', + maxWidth: 'none', + minWidth: 'auto', + }, +})); + +const StyledHeader = styled('h3')(({ theme }) => ({ + display: 'flex', + gap: theme.spacing(1), + alignItems: 'center', + fontSize: theme.fontSizes.bodySize, + margin: 0, + marginBottom: theme.spacing(3), + + // Make the help icon align with the text. + '& > :last-child': { + position: 'relative', + top: 1, + }, +})); + +interface IFeatureOverviewSidePanelProps { + hiddenEnvironments: Set; + setHiddenEnvironments: (environment: string) => void; +} + +export const OldFeatureOverviewSidePanel = ({ + hiddenEnvironments, + setHiddenEnvironments, +}: IFeatureOverviewSidePanelProps) => { + const projectId = useRequiredPathParam('projectId'); + const featureId = useRequiredPathParam('featureId'); + const { feature } = useFeature(projectId, featureId); + const isSticky = feature.environments?.length <= 3; + + return ( + + + Enabled in environments ( + { + feature.environments.filter( + ({ enabled }) => enabled, + ).length + } + ) + + + } + feature={feature} + hiddenEnvironments={hiddenEnvironments} + setHiddenEnvironments={setHiddenEnvironments} + /> + + + Tags for this feature flag + + } + feature={feature} + /> + + ); +}; diff --git a/frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageTagsDialog.tsx b/frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageTagsDialog.tsx index ed516a5415..df0a742e51 100644 --- a/frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageTagsDialog.tsx +++ b/frontend/src/component/feature/FeatureView/FeatureOverview/ManageTagsDialog/ManageTagsDialog.tsx @@ -88,7 +88,7 @@ export const ManageTagsDialog = ({ open, setOpen }: IManageTagsProps) => { tagsToOptions(tags.filter((tag) => tag.type === tagType.name)), ); } - }, [JSON.stringify(tags), tagType]); + }, [JSON.stringify(tags), tagType, open]); const onCancel = () => { setOpen(false);