Add admin avatars and app logos (#20001)
## Summary - Add user avatars and workspace logos to the admin general table and workspace member detail view - Show app icons in the admin app registrations table and reuse the shared application display component - Expose the needed avatar and logo fields through admin GraphQL queries and backend lookup/statistics services - Keep workspace fallback behavior consistent when no logo is set and clean up a few local table styling duplicates ## Testing - `./node_modules/.bin/tsc -p packages/twenty-front/tsconfig.json --noEmit --pretty false` - Manual UI verification of the admin general, workspace detail, and apps tables --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
committed by
GitHub
parent
499067ae14
commit
fb8b9cb86c
+53
-37
@@ -1,3 +1,4 @@
|
||||
import { ApplicationDisplay } from '@/applications/components/ApplicationDisplay';
|
||||
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
@@ -13,12 +14,7 @@ import { t } from '@lingui/core/macro';
|
||||
import { type ReactNode, useContext, useState } from 'react';
|
||||
import { assertUnreachable, getSettingsPath } from 'twenty-shared/utils';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import {
|
||||
H2Title,
|
||||
IconChevronRight,
|
||||
IconPinned,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui/display';
|
||||
import { H2Title, IconChevronRight, IconPinned } from 'twenty-ui/display';
|
||||
import { SearchInput } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { MenuItemToggle } from 'twenty-ui/navigation';
|
||||
@@ -41,7 +37,6 @@ export const SettingsAdminApps = () => {
|
||||
const apolloAdminClient = useApolloAdminClient();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [showPreInstalledOnly, setShowPreInstalledOnly] = useState(false);
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const { data } = useQuery(FindAllApplicationRegistrationsDocument, {
|
||||
client: apolloAdminClient,
|
||||
@@ -136,37 +131,11 @@ export const SettingsAdminApps = () => {
|
||||
</TableRow>
|
||||
<TableBody>
|
||||
{filtered.map((registration) => (
|
||||
<TableRow
|
||||
<SettingsAdminAppsTableRow
|
||||
key={registration.id}
|
||||
to={getSettingsPath(
|
||||
SettingsPath.AdminPanelApplicationRegistrationDetail,
|
||||
{ applicationRegistrationId: registration.id },
|
||||
)}
|
||||
gridAutoColumns={TABLE_GRID}
|
||||
mobileGridAutoColumns={TABLE_GRID_MOBILE}
|
||||
isClickable
|
||||
>
|
||||
<TableCell
|
||||
color={themeCssVariables.font.color.primary}
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
<OverflowingTextWithTooltip text={registration.name} />
|
||||
</TableCell>
|
||||
<TableCell overflow="hidden" align="right">
|
||||
{getFormattedSource(registration)}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{registration.isListed ? t`Yes` : t`No`}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
registration={registration}
|
||||
getFormattedSource={getFormattedSource}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
@@ -174,3 +143,50 @@ export const SettingsAdminApps = () => {
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
type SettingsAdminAppsTableRowProps = {
|
||||
registration: ApplicationRegistrationFragmentFragment;
|
||||
getFormattedSource: (
|
||||
registration: ApplicationRegistrationFragmentFragment,
|
||||
) => string;
|
||||
};
|
||||
|
||||
const SettingsAdminAppsTableRow = ({
|
||||
registration,
|
||||
getFormattedSource,
|
||||
}: SettingsAdminAppsTableRowProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
to={getSettingsPath(
|
||||
SettingsPath.AdminPanelApplicationRegistrationDetail,
|
||||
{ applicationRegistrationId: registration.id },
|
||||
)}
|
||||
gridAutoColumns={TABLE_GRID}
|
||||
mobileGridAutoColumns={TABLE_GRID_MOBILE}
|
||||
isClickable
|
||||
>
|
||||
<TableCell
|
||||
color={themeCssVariables.font.color.primary}
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
minWidth="0"
|
||||
overflow="hidden"
|
||||
>
|
||||
<ApplicationDisplay application={registration} />
|
||||
</TableCell>
|
||||
<TableCell overflow="hidden" align="right">
|
||||
{getFormattedSource(registration)}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{registration.isListed ? t`Yes` : t`No`}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
};
|
||||
|
||||
+111
-78
@@ -2,9 +2,6 @@ import { canManageFeatureFlagsState } from '@/client-config/states/canManageFeat
|
||||
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
|
||||
import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader';
|
||||
import { SettingsAdminVersionContainer } from '@/settings/admin-panel/components/SettingsAdminVersionContainer';
|
||||
import { ADMIN_PANEL_RECENT_USERS } from '@/settings/admin-panel/graphql/queries/adminPanelRecentUsers';
|
||||
import { ADMIN_PANEL_TOP_WORKSPACES } from '@/settings/admin-panel/graphql/queries/adminPanelTopWorkspaces';
|
||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
@@ -15,38 +12,35 @@ import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomState
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { useContext, useState } from 'react';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getImageAbsoluteURI, getSettingsPath } from 'twenty-shared/utils';
|
||||
import { AvatarOrIcon } from 'twenty-ui/components';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import {
|
||||
Avatar,
|
||||
H2Title,
|
||||
IconChevronRight,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
AdminPanelRecentUsersDocument,
|
||||
AdminPanelTopWorkspacesDocument,
|
||||
} from '~/generated-admin/graphql';
|
||||
|
||||
const StyledEmptyState = styled.div`
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
padding: ${themeCssVariables.spacing[4]} 0;
|
||||
`;
|
||||
|
||||
const StyledWorkspaceCell = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const StyledWorkspaceName = styled.span`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
const RECENT_USERS_GRID_TEMPLATE_COLUMNS = '1fr 2fr 1fr 36px';
|
||||
const TOP_WORKSPACES_GRID_TEMPLATE_COLUMNS = '2fr 1fr 36px';
|
||||
|
||||
export const SettingsAdminGeneral = () => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const apolloAdminClient = useApolloAdminClient();
|
||||
const [userSearchTerm, setUserSearchTerm] = useState('');
|
||||
const [debouncedUserSearchTerm] = useDebounce(userSearchTerm, 300);
|
||||
@@ -59,35 +53,23 @@ export const SettingsAdminGeneral = () => {
|
||||
const canImpersonate = currentUser?.canImpersonate;
|
||||
const canManageFeatureFlags = useAtomStateValue(canManageFeatureFlagsState);
|
||||
|
||||
const { data: recentUsersData, loading: isLoadingUsers } = useQuery<{
|
||||
adminPanelRecentUsers: {
|
||||
id: string;
|
||||
email: string;
|
||||
firstName?: string | null;
|
||||
lastName?: string | null;
|
||||
createdAt: string;
|
||||
workspaceName?: string | null;
|
||||
workspaceId?: string | null;
|
||||
}[];
|
||||
}>(ADMIN_PANEL_RECENT_USERS, {
|
||||
client: apolloAdminClient,
|
||||
variables: { searchTerm: debouncedUserSearchTerm },
|
||||
skip: !canImpersonate,
|
||||
});
|
||||
const { data: recentUsersData, loading: isLoadingUsers } = useQuery(
|
||||
AdminPanelRecentUsersDocument,
|
||||
{
|
||||
client: apolloAdminClient,
|
||||
variables: { searchTerm: debouncedUserSearchTerm },
|
||||
skip: !canImpersonate,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: topWorkspacesData, loading: isLoadingWorkspaces } = useQuery<{
|
||||
adminPanelTopWorkspaces: {
|
||||
id: string;
|
||||
name: string;
|
||||
totalUsers: number;
|
||||
subdomain: string;
|
||||
logo: string | null;
|
||||
}[];
|
||||
}>(ADMIN_PANEL_TOP_WORKSPACES, {
|
||||
client: apolloAdminClient,
|
||||
variables: { searchTerm: debouncedWorkspaceSearchTerm },
|
||||
skip: !canImpersonate,
|
||||
});
|
||||
const { data: topWorkspacesData, loading: isLoadingWorkspaces } = useQuery(
|
||||
AdminPanelTopWorkspacesDocument,
|
||||
{
|
||||
client: apolloAdminClient,
|
||||
variables: { searchTerm: debouncedWorkspaceSearchTerm },
|
||||
skip: !canImpersonate,
|
||||
},
|
||||
);
|
||||
|
||||
const recentUsers = recentUsersData?.adminPanelRecentUsers ?? [];
|
||||
const topWorkspaces = topWorkspacesData?.adminPanelTopWorkspaces ?? [];
|
||||
@@ -131,26 +113,71 @@ export const SettingsAdminGeneral = () => {
|
||||
) : (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow gridTemplateColumns="1fr 2fr 1fr">
|
||||
<TableRow
|
||||
gridTemplateColumns={RECENT_USERS_GRID_TEMPLATE_COLUMNS}
|
||||
>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader>{t`Email`}</TableHeader>
|
||||
<TableHeader align="right">{t`Workspace`}</TableHeader>
|
||||
<TableHeader>{t`Workspace`}</TableHeader>
|
||||
<TableHeader />
|
||||
</TableRow>
|
||||
{recentUsers.map((user) => (
|
||||
<TableRow
|
||||
key={user.id}
|
||||
gridTemplateColumns="1fr 2fr 1fr"
|
||||
gridTemplateColumns={RECENT_USERS_GRID_TEMPLATE_COLUMNS}
|
||||
to={getSettingsPath(SettingsPath.AdminPanelUserDetail, {
|
||||
userId: user.id,
|
||||
})}
|
||||
>
|
||||
<TableCell color={themeCssVariables.font.color.primary}>
|
||||
{`${user.firstName || ''} ${user.lastName || ''}`.trim() ||
|
||||
'\u2014'}
|
||||
<TableCell
|
||||
color={themeCssVariables.font.color.primary}
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
overflow="hidden"
|
||||
>
|
||||
<Avatar
|
||||
avatarUrl={user.avatarUrl}
|
||||
placeholder={
|
||||
`${user.firstName || ''} ${user.lastName || ''}`.trim() ||
|
||||
user.email
|
||||
}
|
||||
placeholderColorSeed={user.id}
|
||||
size="md"
|
||||
type="rounded"
|
||||
/>
|
||||
<OverflowingTextWithTooltip
|
||||
text={
|
||||
`${user.firstName || ''} ${user.lastName || ''}`.trim() ||
|
||||
'\u2014'
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell align="right">
|
||||
{user.workspaceName || '\u2014'}
|
||||
<TableCell
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
overflow="hidden"
|
||||
>
|
||||
{user.workspaceId ? (
|
||||
<>
|
||||
<Avatar
|
||||
avatarUrl={user.workspaceLogo}
|
||||
placeholder={user.workspaceName || ''}
|
||||
placeholderColorSeed={user.workspaceId}
|
||||
size="sm"
|
||||
/>
|
||||
<OverflowingTextWithTooltip
|
||||
text={user.workspaceName || '\u2014'}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
'\u2014'
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
@@ -180,41 +207,47 @@ export const SettingsAdminGeneral = () => {
|
||||
) : (
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow gridTemplateColumns="2fr 1fr">
|
||||
<TableRow
|
||||
gridTemplateColumns={TOP_WORKSPACES_GRID_TEMPLATE_COLUMNS}
|
||||
>
|
||||
<TableHeader>{t`Workspace`}</TableHeader>
|
||||
<TableHeader align="right">{t`Users`}</TableHeader>
|
||||
<TableHeader />
|
||||
</TableRow>
|
||||
{topWorkspaces.map((workspace) => (
|
||||
<TableRow
|
||||
key={workspace.id}
|
||||
gridTemplateColumns="2fr 1fr"
|
||||
gridTemplateColumns={TOP_WORKSPACES_GRID_TEMPLATE_COLUMNS}
|
||||
to={getSettingsPath(
|
||||
SettingsPath.AdminPanelWorkspaceDetail,
|
||||
{ workspaceId: workspace.id },
|
||||
)}
|
||||
>
|
||||
<TableCell color={themeCssVariables.font.color.primary}>
|
||||
<StyledWorkspaceCell>
|
||||
<AvatarOrIcon
|
||||
avatarUrl={
|
||||
getImageAbsoluteURI({
|
||||
imageUrl: isNonEmptyString(workspace.logo)
|
||||
? workspace.logo
|
||||
: DEFAULT_WORKSPACE_LOGO,
|
||||
baseUrl: REACT_APP_SERVER_BASE_URL,
|
||||
}) ?? ''
|
||||
}
|
||||
placeholder={workspace.name}
|
||||
avatarType="squared"
|
||||
/>
|
||||
<StyledWorkspaceName>
|
||||
{workspace.name || '\u2014'}
|
||||
</StyledWorkspaceName>
|
||||
</StyledWorkspaceCell>
|
||||
<TableCell
|
||||
color={themeCssVariables.font.color.primary}
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
overflow="hidden"
|
||||
>
|
||||
<Avatar
|
||||
avatarUrl={workspace.logoUrl}
|
||||
placeholder={workspace.name || ''}
|
||||
placeholderColorSeed={workspace.id}
|
||||
size="md"
|
||||
/>
|
||||
<OverflowingTextWithTooltip
|
||||
text={workspace.name || '\u2014'}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{workspace.totalUsers}
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
+2
@@ -7,9 +7,11 @@ export const ADMIN_PANEL_RECENT_USERS = gql`
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
avatarUrl
|
||||
createdAt
|
||||
workspaceName
|
||||
workspaceId
|
||||
workspaceLogo
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
+1
-1
@@ -4,10 +4,10 @@ export const ADMIN_PANEL_TOP_WORKSPACES = gql`
|
||||
query AdminPanelTopWorkspaces($searchTerm: String) {
|
||||
adminPanelTopWorkspaces(searchTerm: $searchTerm) {
|
||||
id
|
||||
logoUrl
|
||||
name
|
||||
totalUsers
|
||||
subdomain
|
||||
logo
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
+1
@@ -26,6 +26,7 @@ export const WORKSPACE_LOOKUP_ADMIN_PANEL = gql`
|
||||
email
|
||||
firstName
|
||||
lastName
|
||||
avatarUrl
|
||||
}
|
||||
featureFlags {
|
||||
key
|
||||
|
||||
+1
@@ -5,6 +5,7 @@ export const APPLICATION_REGISTRATION_FRAGMENT = gql`
|
||||
id
|
||||
universalIdentifier
|
||||
name
|
||||
logoUrl
|
||||
oAuthClientId
|
||||
oAuthRedirectUris
|
||||
oAuthScopes
|
||||
|
||||
Reference in New Issue
Block a user