add stories to roles components (#10503)

## Context
Adding stories for roles components. Also moving modules components to
the proper "modules" folder, "pages" folder being only for entry points.

## Test
Run storybook

<img width="1145" alt="Screenshot 2025-02-26 at 13 40 40"
src="https://github.com/user-attachments/assets/bc184ab0-c590-4362-8c5a-1bf5ef176e6c"
/>
<img width="1149" alt="Screenshot 2025-02-26 at 13 40 32"
src="https://github.com/user-attachments/assets/699cd205-31db-45e9-b9c1-caff1832bd47"
/>
<img width="1153" alt="Screenshot 2025-02-26 at 13 40 11"
src="https://github.com/user-attachments/assets/72e45a67-ea89-4999-8b16-6f7d027d07f6"
/>
<img width="471" alt="Screenshot 2025-02-26 at 13 38 16"
src="https://github.com/user-attachments/assets/62676943-9935-42b5-b769-5544f7eed85f"
/>
<img width="472" alt="Screenshot 2025-02-26 at 13 38 12"
src="https://github.com/user-attachments/assets/946baab9-1be4-439e-bf99-0ebeab0995f7"
/>
This commit is contained in:
Weiko
2025-02-26 18:16:05 +01:00
committed by GitHub
parent d40a5ed74f
commit 431da37cdf
34 changed files with 195 additions and 23 deletions

View File

@ -0,0 +1,46 @@
import { Table } from '@/ui/layout/table/components/Table';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { RolesTableHeader } from '@/settings/roles/components/RolesTableHeader';
import { RolesTableRow } from '@/settings/roles/components/RolesTableRow';
import { Button, H2Title, IconPlus, Section } from 'twenty-ui';
import { Role } from '~/generated-metadata/graphql';
const StyledTable = styled(Table)`
margin-top: ${({ theme }) => theme.spacing(0.5)};
`;
const StyledBottomSection = styled(Section)`
border-top: 1px solid ${({ theme }) => theme.border.color.light};
margin-top: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(4)};
display: flex;
justify-content: flex-end;
`;
export const Roles = ({ roles }: { roles: Role[] }) => {
return (
<Section>
<H2Title
title={t`All roles`}
description={t`Assign roles to specify each member's access permissions`}
/>
<StyledTable>
<RolesTableHeader />
{roles.map((role) => (
<RolesTableRow key={role.id} role={role} />
))}
</StyledTable>
<StyledBottomSection>
<Button
Icon={IconPlus}
title={t`Create Role`}
variant="secondary"
size="small"
soon
/>
</StyledBottomSection>
</Section>
);
};

View File

@ -0,0 +1,80 @@
import {
CurrentWorkspace,
currentWorkspaceState,
} from '@/auth/states/currentWorkspaceState';
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
import { Select } from '@/ui/input/components/Select';
import { t } from '@lingui/core/macro';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import { Card, H2Title, IconUserPin, Section } from 'twenty-ui';
import {
Role,
UpdateWorkspaceMutation,
useUpdateWorkspaceMutation,
} from '~/generated/graphql';
export const RolesDefaultRole = ({ roles }: { roles: Role[] }) => {
const [updateWorkspace] = useUpdateWorkspaceMutation();
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
);
const defaultRole = currentWorkspace?.defaultRole;
const updateDefaultRole = (
defaultRoleId: string | null,
currentWorkspace: CurrentWorkspace,
) => {
updateWorkspace({
variables: {
input: {
defaultRoleId: isDefined(defaultRoleId) ? defaultRoleId : null,
},
},
onCompleted: (data: UpdateWorkspaceMutation) => {
const defaultRole = data.updateWorkspace.defaultRole;
setCurrentWorkspace({
...currentWorkspace,
defaultRole: isDefined(defaultRole) ? defaultRole : null,
});
},
});
};
if (!currentWorkspace || !defaultRole) {
return null;
}
return (
<Section>
<H2Title
title={t`Options`}
description={t`Adjust the role-related settings`}
/>
<Card rounded>
<SettingsOptionCardContentSelect
Icon={IconUserPin}
title="Default Role"
description={t`Set a default role for this workspace`}
>
<Select
selectSizeVariant="small"
withSearchInput
dropdownId="default-role-select"
options={roles.map((role) => ({
label: role.label,
value: role.id,
}))}
value={defaultRole?.id ?? ''}
onChange={(value) =>
updateDefaultRole(value as string, currentWorkspace)
}
/>
</SettingsOptionCardContentSelect>
</Card>
</Section>
);
};

View File

@ -0,0 +1,25 @@
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import styled from '@emotion/styled';
import { Trans } from '@lingui/react/macro';
const StyledTableHeaderRow = styled(Table)`
margin-bottom: ${({ theme }) => theme.spacing(2)};
`;
export const RolesTableHeader = () => {
return (
<StyledTableHeaderRow>
<TableRow>
<TableHeader>
<Trans>Name</Trans>
</TableHeader>
<TableHeader align={'right'}>
<Trans>Assigned to</Trans>
</TableHeader>
<TableHeader align={'right'}></TableHeader>
</TableRow>
</StyledTableHeaderRow>
);
};

View File

@ -0,0 +1,117 @@
import { SettingsPath } from '@/types/SettingsPath';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import React from 'react';
import {
AppTooltip,
Avatar,
IconChevronRight,
IconLock,
IconUser,
TooltipDelay,
} from 'twenty-ui';
import { Role } from '~/generated-metadata/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
const StyledIconChevronRight = styled(IconChevronRight)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
const StyledAvatarContainer = styled.div`
border: 0px;
`;
const StyledAssignedText = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
`;
const StyledNameCell = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledAssignedCell = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(1)};
`;
const StyledAvatarGroup = styled.div`
align-items: center;
display: flex;
margin-right: ${({ theme }) => theme.spacing(1)};
> * {
margin-left: -5px;
&:first-of-type {
margin-left: 0;
}
}
`;
const StyledTableRow = styled(TableRow)`
&:hover {
background: ${({ theme }) => theme.background.transparent.light};
cursor: pointer;
}
`;
export const RolesTableRow = ({ role }: { role: Role }) => {
const theme = useTheme();
const navigateSettings = useNavigateSettings();
const handleRoleClick = (roleId: string) => {
navigateSettings(SettingsPath.RoleDetail, { roleId });
};
return (
<StyledTableRow key={role.id} onClick={() => handleRoleClick(role.id)}>
<TableCell>
<StyledNameCell>
<IconUser size={theme.icon.size.md} />
{role.label}
{!role.isEditable && <IconLock size={theme.icon.size.sm} />}
</StyledNameCell>
</TableCell>
<TableCell align={'right'}>
<StyledAssignedCell>
<StyledAvatarGroup>
{role.workspaceMembers.slice(0, 5).map((workspaceMember) => (
<React.Fragment key={workspaceMember.id}>
<StyledAvatarContainer id={`avatar-${workspaceMember.id}`}>
<Avatar
avatarUrl={workspaceMember.avatarUrl}
placeholderColorSeed={workspaceMember.id}
placeholder={workspaceMember.name.firstName ?? ''}
type="rounded"
size="md"
/>
</StyledAvatarContainer>
<AppTooltip
anchorSelect={`#avatar-${workspaceMember.id}`}
content={`${workspaceMember.name.firstName} ${workspaceMember.name.lastName}`}
noArrow
place="top"
positionStrategy="fixed"
delay={TooltipDelay.shortDelay}
/>
</React.Fragment>
))}
</StyledAvatarGroup>
<StyledAssignedText>
{role.workspaceMembers.length}
</StyledAssignedText>
</StyledAssignedCell>
</TableCell>
<TableCell align={'right'}>
<StyledIconChevronRight size={theme.icon.size.md} />
</TableCell>
</StyledTableRow>
);
};

View File

@ -0,0 +1,24 @@
import { Meta, StoryObj } from '@storybook/react';
import { ComponentDecorator, RouterDecorator } from 'twenty-ui';
import { Roles } from '@/settings/roles/components/Roles';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { getRolesMock } from '~/testing/mock-data/roles';
const meta: Meta<typeof Roles> = {
title: 'Modules/Settings/Roles/Roles',
component: Roles,
decorators: [ComponentDecorator, I18nFrontDecorator, RouterDecorator],
parameters: {
maxWidth: 800,
},
};
export default meta;
type Story = StoryObj<typeof Roles>;
export const Default: Story = {
args: {
roles: getRolesMock(),
},
};

View File

@ -0,0 +1,43 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { RolesDefaultRole } from '@/settings/roles/components/RolesDefaultRole';
import { Meta, StoryObj } from '@storybook/react';
import { RecoilRoot } from 'recoil';
import { ComponentDecorator } from 'twenty-ui';
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
import { getRolesMock } from '~/testing/mock-data/roles';
import { mockCurrentWorkspace } from '~/testing/mock-data/users';
const rolesMock = getRolesMock();
const RolesDefaultRoleWrapper = () => {
return (
<RecoilRoot
initializeState={(snapshot) => {
snapshot.set(currentWorkspaceState, {
...mockCurrentWorkspace,
defaultRole: rolesMock[1],
});
}}
>
<RolesDefaultRole roles={rolesMock} />
</RecoilRoot>
);
};
const meta: Meta<typeof RolesDefaultRoleWrapper> = {
title: 'Modules/Settings/Roles/RolesDefaultRole',
component: RolesDefaultRoleWrapper,
decorators: [ComponentDecorator, I18nFrontDecorator],
parameters: {
maxWidth: 800,
},
};
export default meta;
type Story = StoryObj<typeof RolesDefaultRoleWrapper>;
export const Default: Story = {
args: {
roles: rolesMock,
},
};