diff --git a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/NewGroupCardAvatars.tsx b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/NewGroupCardAvatars.tsx
index adb8abfb4b..584c8065da 100644
--- a/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/NewGroupCardAvatars.tsx
+++ b/frontend/src/component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/NewGroupCardAvatars.tsx
@@ -19,13 +19,14 @@ const StyledAvatars = styled('div')(({ theme }) => ({
marginLeft: theme.spacing(1),
}));
-const StyledAvatar = styled(UserAvatar)(({ theme }) => ({
- outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`,
- marginLeft: theme.spacing(-1),
- '&:hover': {
- outlineColor: theme.palette.primary.main,
- },
-}));
+const StyledAvatar = (component: typeof UserAvatar) =>
+ styled(component)(({ theme }) => ({
+ outline: `${theme.spacing(0.25)} solid ${theme.palette.background.paper}`,
+ marginLeft: theme.spacing(-1),
+ '&:hover': {
+ outlineColor: theme.palette.primary.main,
+ },
+ }));
const StyledHeader = styled('h3')(({ theme }) => ({
margin: theme.spacing(0, 0, 1),
@@ -42,13 +43,31 @@ interface IGroupCardAvatarsProps {
}[];
header?: ReactNode;
avatarLimit?: number;
+ AvatarComponent?: typeof UserAvatar;
}
export const GroupCardAvatars = ({
+ AvatarComponent,
+ ...props
+}: IGroupCardAvatarsProps) => {
+ const Avatar = StyledAvatar(AvatarComponent ?? UserAvatar);
+
+ return ;
+};
+
+type GroupCardAvatarsInnerProps = Omit<
+ IGroupCardAvatarsProps,
+ 'AvatarComponent'
+> & {
+ AvatarComponent: typeof UserAvatar;
+};
+
+const GroupCardAvatarsInner = ({
users = [],
header = null,
avatarLimit = 9,
-}: IGroupCardAvatarsProps) => {
+ AvatarComponent,
+}: GroupCardAvatarsInnerProps) => {
const shownUsers = useMemo(
() =>
users
@@ -94,7 +113,7 @@ export const GroupCardAvatars = ({
/>
{shownUsers.map((user) => (
- {
@@ -107,9 +126,9 @@ export const GroupCardAvatars = ({
avatarLimit}
show={
-
+
+{users.length - shownUsers.length}
-
+
}
/>
prop !== 'avatarWidth',
-})<{ avatarWidth?: (theme: Theme) => string }>(({ theme, avatarWidth }) => {
- const width = avatarWidth ? avatarWidth(theme) : theme.spacing(3.5);
-
- return {
- width,
- height: width,
- margin: 'auto',
- backgroundColor: theme.palette.secondary.light,
- color: theme.palette.text.primary,
- fontSize: theme.fontSizes.smallerBody,
- fontWeight: theme.fontWeight.bold,
- };
-});
+const StyledAvatar = styled(Avatar)(({ theme }) => ({
+ width: theme.spacing(3.5),
+ height: theme.spacing(3.5),
+ margin: 'auto',
+ backgroundColor: theme.palette.secondary.light,
+ color: theme.palette.text.primary,
+ fontSize: theme.fontSizes.smallerBody,
+ fontWeight: theme.fontWeight.bold,
+}));
export interface IUserAvatarProps extends AvatarProps {
user?: Partial<
@@ -35,7 +29,6 @@ export interface IUserAvatarProps extends AvatarProps {
onMouseLeave?: () => void;
className?: string;
sx?: SxProps;
- avatarWidth?: (theme: Theme) => string;
hideTitle?: boolean;
}
diff --git a/frontend/src/component/feature/FeatureView/Collaborators.test.tsx b/frontend/src/component/feature/FeatureView/Collaborators.test.tsx
new file mode 100644
index 0000000000..c47e07d690
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView/Collaborators.test.tsx
@@ -0,0 +1,14 @@
+import { render } from '@testing-library/react';
+import { Collaborators } from './Collaborators';
+
+test('renders nothing if collaborators is undefined', () => {
+ const { container } = render();
+
+ expect(container).toBeEmptyDOMElement();
+});
+
+test('renders nothing if users is empty', () => {
+ const { container } = render();
+
+ expect(container).toBeEmptyDOMElement();
+});
diff --git a/frontend/src/component/feature/FeatureView/Collaborators.tsx b/frontend/src/component/feature/FeatureView/Collaborators.tsx
new file mode 100644
index 0000000000..95cc6e92d3
--- /dev/null
+++ b/frontend/src/component/feature/FeatureView/Collaborators.tsx
@@ -0,0 +1,95 @@
+import { styled } from '@mui/material';
+import { GroupCardAvatars } from 'component/admin/groups/GroupsList/GroupCard/GroupCardAvatars/NewGroupCardAvatars';
+import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
+import { UserAvatar } from 'component/common/UserAvatar/UserAvatar';
+import type { Collaborator } from 'interfaces/featureToggle';
+import type { FC } from 'react';
+import { Link } from 'react-router-dom';
+
+const StyledAvatar = styled(UserAvatar)(({ theme }) => ({
+ width: theme.spacing(3),
+ height: theme.spacing(3),
+}));
+
+const LastModifiedByContainer = styled('div')(({ theme }) => ({
+ display: 'grid',
+ gridTemplateAreas: `
+ 'description description'
+ 'avatar link'
+ `,
+ rowGap: theme.spacing(0.5),
+ columnGap: theme.spacing(1),
+ alignItems: 'center',
+ height: 'min-content',
+}));
+
+const GridDescription = styled('span')({ gridArea: 'description' });
+const GridTooltip = styled(HtmlTooltip)({ gridArea: 'avatar' });
+const GridLink = styled(Link)({ gridArea: 'link' });
+
+const LastModifiedBy: FC = ({ id, name, imageUrl }) => {
+ return (
+
+ Last modified by
+
+
+
+
+
+ view change
+
+ );
+};
+
+const CollaboratorListContainer = styled('div')(({ theme }) => ({
+ display: 'flex',
+ flexFlow: 'column',
+ gap: theme.spacing(0.5),
+ alignItems: 'flex-start',
+ height: 'min-content',
+}));
+
+const CollaboratorList: FC<{ collaborators: Collaborator[] }> = ({
+ collaborators,
+}) => {
+ return (
+
+ Collaborators
+
+
+ );
+};
+
+const Container = styled('article')(({ theme }) => ({
+ display: 'flex',
+ flexDirection: 'row',
+ gap: theme.spacing(10),
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ [theme.breakpoints.down('xl')]: {
+ display: 'none',
+ },
+}));
+
+type Props = {
+ collaborators: Collaborator[] | undefined;
+};
+
+export const Collaborators: FC = ({ collaborators }) => {
+ if (!collaborators || collaborators.length === 0) {
+ return null;
+ }
+
+ const lastModifiedBy = collaborators[0];
+
+ return (
+
+
+
+
+ );
+};
diff --git a/frontend/src/component/feature/FeatureView/FeatureView.tsx b/frontend/src/component/feature/FeatureView/FeatureView.tsx
index 94430d25c6..485f27e44d 100644
--- a/frontend/src/component/feature/FeatureView/FeatureView.tsx
+++ b/frontend/src/component/feature/FeatureView/FeatureView.tsx
@@ -50,6 +50,7 @@ import copy from 'copy-to-clipboard';
import useToast from 'hooks/useToast';
import { useUiFlag } from 'hooks/useUiFlag';
import type { IFeatureToggle } from 'interfaces/featureToggle';
+import { Collaborators } from './Collaborators';
const StyledHeader = styled('div')(({ theme }) => ({
backgroundColor: theme.palette.background.paper,
@@ -113,8 +114,12 @@ const StyledSeparator = styled('div')(({ theme }) => ({
height: '1px',
}));
-const StyledTabContainer = styled('div')(({ theme }) => ({
- padding: theme.spacing(0, 4),
+const StyledTabRow = styled('div')(({ theme }) => ({
+ display: 'flex',
+ flexFlow: 'row nowrap',
+ gap: theme.spacing(4),
+ paddingInline: theme.spacing(4),
+ justifyContent: 'space-between',
}));
const StyledTabButton = styled(Tab)(({ theme }) => ({
@@ -155,6 +160,7 @@ export const FeatureView = () => {
const [openStaleDialog, setOpenStaleDialog] = useState(false);
const [isFeatureNameCopied, setIsFeatureNameCopied] = useState(false);
const smallScreen = useMediaQuery(`(max-width:${500}px)`);
+ const showCollaborators = useUiFlag('featureCollaborators');
const { feature, loading, error, status } = useFeature(
projectId,
@@ -355,7 +361,7 @@ export const FeatureView = () => {
-
+
{
/>
))}
-
+
+ }
+ />
+
} />
diff --git a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/AvatarCell.tsx b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/AvatarCell.tsx
index d299ed68dd..4f9fcc5ede 100644
--- a/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/AvatarCell.tsx
+++ b/frontend/src/component/project/Project/PaginatedProjectFeatureToggles/AvatarCell.tsx
@@ -1,4 +1,4 @@
-import { type Theme, styled } from '@mui/material';
+import { styled } from '@mui/material';
import type { FC } from 'react';
import { ScreenReaderOnly } from 'component/common/ScreenReaderOnly/ScreenReaderOnly';
import { HtmlTooltip } from 'component/common/HtmlTooltip/HtmlTooltip';
@@ -35,6 +35,11 @@ const StyledSecondaryText = styled('p')(({ theme }) => ({
color: theme.palette.text.secondary,
}));
+const StyledAvatar = styled(UserAvatar)(({ theme }) => ({
+ width: theme.spacing(3),
+ height: theme.spacing(3),
+}));
+
export const AvatarCell =
(onAvatarClick: (userId: number) => void): FC =>
({ row: { original } }) => {
@@ -67,14 +72,13 @@ export const AvatarCell =
- theme.spacing(3)}
/>
diff --git a/frontend/src/interfaces/featureToggle.ts b/frontend/src/interfaces/featureToggle.ts
index 3b97ce95d7..205cfcd366 100644
--- a/frontend/src/interfaces/featureToggle.ts
+++ b/frontend/src/interfaces/featureToggle.ts
@@ -38,6 +38,16 @@ export type Lifecycle = {
enteredStageAt: string;
};
+export type Collaborator = {
+ id: number;
+ name: string;
+ imageUrl: string;
+};
+
+export type CollaboratorData = {
+ users: Collaborator[];
+};
+
/**
* @deprecated use FeatureSchema from openapi
*/
@@ -65,6 +75,7 @@ export interface IFeatureToggle {
name: string;
imageUrl: string;
};
+ collaborators?: CollaboratorData;
}
export interface IDependency {
diff --git a/src/lib/types/experimental.ts b/src/lib/types/experimental.ts
index 09c5c630d0..c9e4705945 100644
--- a/src/lib/types/experimental.ts
+++ b/src/lib/types/experimental.ts
@@ -331,7 +331,7 @@ const flags: IFlags = {
false,
),
featureCollaborators: parseEnvVarBoolean(
- process.env.UNEASH_EXPERIMENTAL_FEATURE_COLLABORATORS,
+ process.env.UNLEASH_EXPERIMENTAL_FEATURE_COLLABORATORS,
false,
),
};