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:
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -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(),
|
||||
},
|
||||
};
|
||||
@ -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,
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user