add role update (#11217)

## Context
This PR introduces the new Create and Edit role components, behind the
PERMISSIONS_ENABLED_V2 feature flag.
This commit is contained in:
Weiko
2025-03-31 17:57:14 +02:00
committed by GitHub
parent 3c9bf2294f
commit 06ff16e086
58 changed files with 1527 additions and 624 deletions
@@ -1,49 +0,0 @@
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 StyledCreateRoleSection = styled(Section)`
border-top: 1px solid ${({ theme }) => theme.border.color.light};
display: flex;
justify-content: flex-end;
padding-top: ${({ theme }) => theme.spacing(2)};
padding-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledTableRows = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(2)};
`;
export const Roles = ({ roles }: { roles: Role[] }) => {
return (
<Section>
<H2Title
title={t`All roles`}
description={t`Assign roles to specify each member's access permissions`}
/>
<Table>
<RolesTableHeader />
<StyledTableRows>
{roles.map((role) => (
<RolesTableRow key={role.id} role={role} />
))}
</StyledTableRows>
</Table>
<StyledCreateRoleSection>
<Button
Icon={IconPlus}
title={t`Create Role`}
variant="secondary"
size="small"
soon
/>
</StyledCreateRoleSection>
</Section>
);
};
@@ -0,0 +1,44 @@
import { H3Title } from 'twenty-ui';
import { SettingsPath } from '@/types/SettingsPath';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsRoleDefaultRole } from '@/settings/roles/components/SettingsRolesDefaultRole';
import { SettingsRolesList } from '@/settings/roles/components/SettingsRolesList';
import { settingsAllRolesSelector } from '@/settings/roles/states/settingsAllRolesSelector';
import { settingsRolesIsLoadingState } from '@/settings/roles/states/settingsRolesIsLoadingState';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { Trans, useLingui } from '@lingui/react/macro';
import { useRecoilValue } from 'recoil';
export const SettingsRolesContainer = () => {
const { t } = useLingui();
const settingsAllRoles = useRecoilValue(settingsAllRolesSelector);
const settingsRolesIsLoading = useRecoilValue(settingsRolesIsLoadingState);
if (settingsRolesIsLoading) {
return null;
}
return (
<SubMenuTopBarContainer
title={<H3Title title={t`Roles`} />}
links={[
{
children: <Trans>Workspace</Trans>,
href: getSettingsPath(SettingsPath.Workspace),
},
{ children: <Trans>Roles</Trans> },
]}
>
<SettingsPageContainer>
<SettingsRolesList />
<SettingsRoleDefaultRole roles={settingsAllRoles} />
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};
@@ -6,15 +6,15 @@ import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsO
import { Select } from '@/ui/input/components/Select';
import { t } from '@lingui/core/macro';
import { useRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { Card, H2Title, IconUserPin, Section } from 'twenty-ui';
import {
Role,
UpdateWorkspaceMutation,
useUpdateWorkspaceMutation,
} from '~/generated/graphql';
import { isDefined } from 'twenty-shared/utils';
export const RolesDefaultRole = ({ roles }: { roles: Role[] }) => {
export const SettingsRoleDefaultRole = ({ roles }: { roles: Role[] }) => {
const [updateWorkspace] = useUpdateWorkspaceMutation();
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
@@ -0,0 +1,72 @@
import { Table } from '@/ui/layout/table/components/Table';
import styled from '@emotion/styled';
import { t } from '@lingui/core/macro';
import { SettingsRolesTableHeader } from '@/settings/roles/components/SettingsRolesTableHeader';
import { SettingsRolesTableRow } from '@/settings/roles/components/SettingsRolesTableRow';
import { settingsAllRolesSelector } from '@/settings/roles/states/settingsAllRolesSelector';
import { SettingsPath } from '@/types/SettingsPath';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { useRecoilValue } from 'recoil';
import { Button, H2Title, IconPlus, Section } from 'twenty-ui';
import { FeatureFlagKey } from '~/generated/graphql';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
const StyledCreateRoleSection = styled(Section)`
border-top: 1px solid ${({ theme }) => theme.border.color.light};
display: flex;
justify-content: flex-end;
padding-top: ${({ theme }) => theme.spacing(2)};
padding-bottom: ${({ theme }) => theme.spacing(2)};
`;
const StyledTableRows = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(2)};
`;
const StyledNoRoles = styled(TableCell)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
export const SettingsRolesList = () => {
const navigateSettings = useNavigateSettings();
const isPermissionsV2Enabled = useIsFeatureEnabled(
FeatureFlagKey.IsPermissionsV2Enabled,
);
const settingsAllRoles = useRecoilValue(settingsAllRolesSelector);
return (
<Section>
<H2Title
title={t`All roles`}
description={t`Assign roles to specify each member's access permissions`}
/>
<Table>
<SettingsRolesTableHeader />
<StyledTableRows>
{settingsAllRoles.length === 0 ? (
<StyledNoRoles>{t`No roles found`}</StyledNoRoles>
) : (
settingsAllRoles.map((role) => (
<SettingsRolesTableRow key={role.id} role={role} />
))
)}
</StyledTableRows>
</Table>
<StyledCreateRoleSection>
<Button
Icon={IconPlus}
title={t`Create Role`}
variant="secondary"
size="small"
soon={!isPermissionsV2Enabled}
disabled={!isPermissionsV2Enabled}
onClick={() => navigateSettings(SettingsPath.RoleCreate)}
/>
</StyledCreateRoleSection>
</Section>
);
};
@@ -0,0 +1,43 @@
import { settingsPersistedRoleFamilyState } from '@/settings/roles/states/settingsPersistedRoleFamilyState';
import { settingsRoleIdsState } from '@/settings/roles/states/settingsRoleIdsState';
import { settingsRolesIsLoadingState } from '@/settings/roles/states/settingsRolesIsLoadingState';
import { useEffect } from 'react';
import { useRecoilCallback, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { Role, useGetRolesQuery } from '~/generated/graphql';
export const SettingsRolesQueryEffect = () => {
const { data, loading } = useGetRolesQuery({
fetchPolicy: 'network-only',
});
const setSettingsRolesIsLoading = useSetRecoilState(
settingsRolesIsLoadingState,
);
const populateRoles = useRecoilCallback(
({ set }) =>
(roles: Role[]) => {
const roleIds = roles.map((role) => role.id);
set(settingsRoleIdsState, roleIds);
roles.forEach((role) => {
set(settingsPersistedRoleFamilyState(role.id), role);
});
},
[],
);
useEffect(() => {
setSettingsRolesIsLoading(loading);
if (!loading) {
const roles = data?.getRoles;
if (!isDefined(roles)) {
return;
}
populateRoles(roles);
}
}, [data, loading, populateRoles, setSettingsRolesIsLoading]);
return null;
};
@@ -3,7 +3,7 @@ import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { Trans } from '@lingui/react/macro';
export const RolesTableHeader = () => {
export const SettingsRolesTableHeader = () => {
return (
<Table>
<TableRow gridAutoColumns="332px 3fr 2fr 1fr">
@@ -52,7 +52,7 @@ const StyledTableRow = styled(TableRow)`
}
`;
export const RolesTableRow = ({ role }: { role: Role }) => {
export const SettingsRolesTableRow = ({ role }: { role: Role }) => {
const theme = useTheme();
const navigateSettings = useNavigateSettings();
@@ -1,24 +0,0 @@
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(),
},
};
@@ -1,43 +0,0 @@
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,
},
};