Polish settings page titles and admin tables (#22305)
## Summary - Reuse the shared settings title presentation for read-only and editable settings page titles. - Polish settings AI/app icons, breadcrumb cropping, and admin detail title icons. - Align admin panel table/card typography and spacing with existing settings tables. ## Before/After <img width="1524" height="2214" alt="Settings pages before and after" src="https://github.com/user-attachments/assets/91037b25-b442-4eb2-b244-1d8280ce2cd9" /> <img width="2200" height="3268" alt="Additional settings UI before and after" src="https://github.com/user-attachments/assets/f10d7283-7031-4958-8539-649c3067cd31" /> <!-- This is an auto-generated description by cubic. --> <a href="https://cubic.dev/pr/twentyhq/twenty/pull/22305?utm_source=github" target="_blank" rel="noopener noreferrer" data-no-image-dialog="true"><picture><source media="(prefers-color-scheme: dark)" srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
committed by
GitHub
parent
b199dde840
commit
2bb7fb2e9b
+7
-12
@@ -1,5 +1,6 @@
|
||||
import { AppChip } from '@/applications/components/AppChip';
|
||||
import { ApplicationDisplay } from '@/applications/components/ApplicationDisplay';
|
||||
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
|
||||
import { StyledNameTableCell } from '@/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
@@ -209,20 +210,14 @@ const SettingsAdminAppsTableRow = ({
|
||||
mobileGridAutoColumns={TABLE_GRID_MOBILE}
|
||||
isClickable
|
||||
>
|
||||
<TableCell
|
||||
color={themeCssVariables.font.color.primary}
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
minWidth="0"
|
||||
overflow="hidden"
|
||||
>
|
||||
<AppChip
|
||||
size="md"
|
||||
fallbackApplicationData={{
|
||||
logo: registration.logoUrl,
|
||||
<StyledNameTableCell minWidth="0" overflow="hidden">
|
||||
<ApplicationDisplay
|
||||
application={{
|
||||
name: registration.name,
|
||||
logo: registration.logoUrl,
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
</StyledNameTableCell>
|
||||
<TableCell overflow="hidden" align="right">
|
||||
{getFormattedSource(registration)}
|
||||
</TableCell>
|
||||
|
||||
+37
-29
@@ -36,6 +36,10 @@ const StyledEmptyState = styled.div`
|
||||
padding: ${themeCssVariables.spacing[4]} 0;
|
||||
`;
|
||||
|
||||
const StyledSearchInputContainer = styled.div`
|
||||
padding-bottom: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const RECENT_USERS_GRID_TEMPLATE_COLUMNS = '1fr 2fr 1fr 36px';
|
||||
const TOP_WORKSPACES_GRID_TEMPLATE_COLUMNS = '2fr 1fr 36px';
|
||||
|
||||
@@ -99,13 +103,15 @@ export const SettingsAdminGeneral = () => {
|
||||
: t`Last 10 users created. Click to impersonate.`
|
||||
}
|
||||
/>
|
||||
<SettingsTextInput
|
||||
instanceId="admin-panel-user-search"
|
||||
value={userSearchTerm}
|
||||
onChange={setUserSearchTerm}
|
||||
placeholder={t`Search by name, email, or user ID...`}
|
||||
fullWidth
|
||||
/>
|
||||
<StyledSearchInputContainer>
|
||||
<SettingsTextInput
|
||||
instanceId="admin-panel-user-search"
|
||||
value={userSearchTerm}
|
||||
onChange={setUserSearchTerm}
|
||||
placeholder={t`Search by name, email, or user ID...`}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledSearchInputContainer>
|
||||
{isLoadingUsers ? (
|
||||
<SettingsSectionSkeletonLoader />
|
||||
) : recentUsers.length === 0 ? (
|
||||
@@ -114,15 +120,15 @@ export const SettingsAdminGeneral = () => {
|
||||
</StyledEmptyState>
|
||||
) : (
|
||||
<Table>
|
||||
<TableRow
|
||||
gridTemplateColumns={RECENT_USERS_GRID_TEMPLATE_COLUMNS}
|
||||
>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader>{t`Email`}</TableHeader>
|
||||
<TableHeader>{t`Workspace`}</TableHeader>
|
||||
<TableHeader />
|
||||
</TableRow>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
gridTemplateColumns={RECENT_USERS_GRID_TEMPLATE_COLUMNS}
|
||||
>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader>{t`Email`}</TableHeader>
|
||||
<TableHeader>{t`Workspace`}</TableHeader>
|
||||
<TableHeader />
|
||||
</TableRow>
|
||||
{recentUsers.map((user) => (
|
||||
<TableRow
|
||||
key={user.id}
|
||||
@@ -195,13 +201,15 @@ export const SettingsAdminGeneral = () => {
|
||||
title={t`Top Workspaces`}
|
||||
description={t`Top 10 workspaces by number of users`}
|
||||
/>
|
||||
<SettingsTextInput
|
||||
instanceId="admin-panel-workspace-search"
|
||||
value={workspaceSearchTerm}
|
||||
onChange={setWorkspaceSearchTerm}
|
||||
placeholder={t`Search by workspace name, subdomain, or ID...`}
|
||||
fullWidth
|
||||
/>
|
||||
<StyledSearchInputContainer>
|
||||
<SettingsTextInput
|
||||
instanceId="admin-panel-workspace-search"
|
||||
value={workspaceSearchTerm}
|
||||
onChange={setWorkspaceSearchTerm}
|
||||
placeholder={t`Search by workspace name, subdomain, or ID...`}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledSearchInputContainer>
|
||||
{isLoadingWorkspaces ? (
|
||||
<SettingsSectionSkeletonLoader />
|
||||
) : topWorkspaces.length === 0 ? (
|
||||
@@ -210,14 +218,14 @@ export const SettingsAdminGeneral = () => {
|
||||
</StyledEmptyState>
|
||||
) : (
|
||||
<Table>
|
||||
<TableRow
|
||||
gridTemplateColumns={TOP_WORKSPACES_GRID_TEMPLATE_COLUMNS}
|
||||
>
|
||||
<TableHeader>{t`Workspace`}</TableHeader>
|
||||
<TableHeader align="right">{t`Users`}</TableHeader>
|
||||
<TableHeader />
|
||||
</TableRow>
|
||||
<TableBody>
|
||||
<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}
|
||||
|
||||
+7
-7
@@ -18,7 +18,7 @@ import { Section } from 'twenty-ui/layout';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { GetServerAdminsDocument } from '~/generated-admin/graphql';
|
||||
|
||||
const SERVER_ADMINS_GRID_TEMPLATE_COLUMNS = '2fr 1fr 1fr 36px';
|
||||
const SERVER_ADMINS_GRID_TEMPLATE_COLUMNS = '1fr 2fr 1fr 36px';
|
||||
|
||||
const StyledEmptyState = styled.div`
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
@@ -49,13 +49,13 @@ export const SettingsAdminServerAdmins = () => {
|
||||
<StyledEmptyState>{t`No server administrators found.`}</StyledEmptyState>
|
||||
) : (
|
||||
<Table>
|
||||
<TableRow gridTemplateColumns={SERVER_ADMINS_GRID_TEMPLATE_COLUMNS}>
|
||||
<TableHeader>{t`Administrator`}</TableHeader>
|
||||
<TableHeader>{t`Admin panel`}</TableHeader>
|
||||
<TableHeader>{t`Impersonation`}</TableHeader>
|
||||
<TableHeader />
|
||||
</TableRow>
|
||||
<TableBody>
|
||||
<TableRow gridTemplateColumns={SERVER_ADMINS_GRID_TEMPLATE_COLUMNS}>
|
||||
<TableHeader>{t`Administrator`}</TableHeader>
|
||||
<TableHeader>{t`Admin panel`}</TableHeader>
|
||||
<TableHeader>{t`Impersonation`}</TableHeader>
|
||||
<TableHeader />
|
||||
</TableRow>
|
||||
{serverAdmins.map((admin) => {
|
||||
const adminLabel =
|
||||
`${admin.firstName || ''} ${admin.lastName || ''}`.trim() ||
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
TitleInput,
|
||||
type TitleInputProps,
|
||||
} from '@/ui/input/components/TitleInput';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const StyledTitleInputContainer = styled.div`
|
||||
max-width: 420px;
|
||||
min-width: 0;
|
||||
width: fit-content;
|
||||
`;
|
||||
|
||||
export type SettingsEditableTitleProps = TitleInputProps;
|
||||
|
||||
export const SettingsEditableTitle = (props: SettingsEditableTitleProps) => (
|
||||
<StyledTitleInputContainer>
|
||||
<TitleInput
|
||||
// oxlint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
sizeVariant={props.sizeVariant ?? 'sm'}
|
||||
/>
|
||||
</StyledTitleInputContainer>
|
||||
);
|
||||
+3
-2
@@ -9,11 +9,12 @@ import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
const StyledRowContainer = styled.div`
|
||||
> * {
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
min-height: ${themeCssVariables.spacing[6]};
|
||||
height: ${themeCssVariables.spacing[10]};
|
||||
padding: ${themeCssVariables.spacing[2]};
|
||||
padding-left: ${themeCssVariables.spacing[3]};
|
||||
}
|
||||
|
||||
+39
-22
@@ -1,8 +1,9 @@
|
||||
import { SettingsEditableTitle } from '@/settings/components/SettingsEditableTitle';
|
||||
import { SettingsSecondaryBar } from '@/settings/components/layout/SettingsSecondaryBar';
|
||||
import { PageCardHeader } from '@/ui/layout/page/components/PageCardHeader';
|
||||
import { PageCardLayout } from '@/ui/layout/page/components/PageCardLayout';
|
||||
import { type BreadcrumbProps } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
|
||||
import { type JSX, type ReactNode } from 'react';
|
||||
import { useId, type JSX, type ReactNode } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type SettingsPageLayoutProps = {
|
||||
@@ -25,25 +26,41 @@ export const SettingsPageLayout = ({
|
||||
children,
|
||||
tag,
|
||||
titleColor,
|
||||
}: SettingsPageLayoutProps) => (
|
||||
<PageCardLayout
|
||||
header={
|
||||
<PageCardHeader
|
||||
links={links}
|
||||
title={title}
|
||||
icon={icon}
|
||||
tag={tag}
|
||||
actionButton={actionButton}
|
||||
centerTitle
|
||||
titleColor={titleColor}
|
||||
}: SettingsPageLayoutProps) => {
|
||||
const titleInstanceId = useId();
|
||||
|
||||
const formattedTitle =
|
||||
typeof title === 'string' ? (
|
||||
<SettingsEditableTitle
|
||||
disabled
|
||||
instanceId={`settings-page-layout-title-${titleInstanceId}`}
|
||||
textColor={titleColor}
|
||||
value={title}
|
||||
/>
|
||||
}
|
||||
secondaryBar={
|
||||
isDefined(secondaryBar) ? (
|
||||
<SettingsSecondaryBar>{secondaryBar}</SettingsSecondaryBar>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</PageCardLayout>
|
||||
);
|
||||
) : (
|
||||
title
|
||||
);
|
||||
|
||||
return (
|
||||
<PageCardLayout
|
||||
header={
|
||||
<PageCardHeader
|
||||
links={links}
|
||||
title={formattedTitle}
|
||||
icon={icon}
|
||||
tag={tag}
|
||||
actionButton={actionButton}
|
||||
centerTitle
|
||||
titleColor={titleColor}
|
||||
/>
|
||||
}
|
||||
secondaryBar={
|
||||
isDefined(secondaryBar) ? (
|
||||
<SettingsSecondaryBar>{secondaryBar}</SettingsSecondaryBar>
|
||||
) : undefined
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</PageCardLayout>
|
||||
);
|
||||
};
|
||||
|
||||
+8
-24
@@ -1,18 +1,5 @@
|
||||
import { TitleInput } from '@/ui/input/components/TitleInput';
|
||||
import { styled } from '@linaria/react';
|
||||
import { SettingsEditableTitle } from '@/settings/components/SettingsEditableTitle';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledHeaderTitle = styled.div`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-size: ${themeCssVariables.font.size.lg};
|
||||
font-weight: ${themeCssVariables.font.weight.semiBold};
|
||||
max-width: 420px;
|
||||
width: fit-content;
|
||||
& > input:disabled {
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
type SettingsLogicFunctionLabelContainerProps = {
|
||||
value: string;
|
||||
@@ -26,15 +13,12 @@ export const SettingsLogicFunctionLabelContainer = ({
|
||||
readonly = false,
|
||||
}: SettingsLogicFunctionLabelContainerProps) => {
|
||||
return (
|
||||
<StyledHeaderTitle>
|
||||
<TitleInput
|
||||
instanceId="logic-function-name-input"
|
||||
sizeVariant="md"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={t`Function name`}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</StyledHeaderTitle>
|
||||
<SettingsEditableTitle
|
||||
instanceId="logic-function-name-input"
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={t`Function name`}
|
||||
disabled={readonly}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+8
-37
@@ -1,34 +1,8 @@
|
||||
import { settingsDraftRoleFamilyState } from '@/settings/roles/states/settingsDraftRoleFamilyState';
|
||||
import { SettingsEditableTitle } from '@/settings/components/SettingsEditableTitle';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
|
||||
import { TitleInput } from '@/ui/input/components/TitleInput';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledHeaderTitle = styled.div`
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
max-width: 420px;
|
||||
padding-left: ${themeCssVariables.spacing[1]};
|
||||
padding-right: ${themeCssVariables.spacing[1]};
|
||||
width: fit-content;
|
||||
|
||||
&:hover {
|
||||
background: ${themeCssVariables.background.transparent.light};
|
||||
}
|
||||
|
||||
& > div:hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
& > div :hover {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
& > input:disabled {
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
type SettingsRoleLabelContainerProps = {
|
||||
roleId: string;
|
||||
@@ -54,15 +28,12 @@ export const SettingsRoleLabelContainer = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledHeaderTitle>
|
||||
<TitleInput
|
||||
instanceId="role-label-input"
|
||||
disabled={!settingsDraftRole.isEditable}
|
||||
sizeVariant="sm"
|
||||
value={settingsDraftRole.label}
|
||||
onChange={handleChange}
|
||||
placeholder={t`Role name`}
|
||||
/>
|
||||
</StyledHeaderTitle>
|
||||
<SettingsEditableTitle
|
||||
instanceId="role-label-input"
|
||||
disabled={!settingsDraftRole.isEditable}
|
||||
value={settingsDraftRole.label}
|
||||
onChange={handleChange}
|
||||
placeholder={t`Role name`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,10 +11,9 @@ import { styled } from '@linaria/react';
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type InputProps = {
|
||||
type CommonInputProps = {
|
||||
instanceId: string;
|
||||
value?: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
onEnter?: () => void;
|
||||
onEscape?: () => void;
|
||||
@@ -24,22 +23,49 @@ type InputProps = {
|
||||
sizeVariant?: TextInputSize;
|
||||
};
|
||||
|
||||
export type TitleInputProps = {
|
||||
disabled?: boolean;
|
||||
shouldFocus?: boolean;
|
||||
onFocus?: () => void;
|
||||
} & InputProps;
|
||||
type InputProps = CommonInputProps & {
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
type TitleInputReadonlyProps = {
|
||||
disabled: true;
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
type TitleInputEditableProps = {
|
||||
disabled?: false;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
type TitleInputConditionallyReadonlyProps = {
|
||||
disabled: boolean;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
type TitleInputEditionProps =
|
||||
| TitleInputReadonlyProps
|
||||
| TitleInputEditableProps
|
||||
| TitleInputConditionallyReadonlyProps;
|
||||
|
||||
export type TitleInputProps = CommonInputProps &
|
||||
TitleInputEditionProps & {
|
||||
shouldFocus?: boolean;
|
||||
onFocus?: () => void;
|
||||
textColor?: string;
|
||||
};
|
||||
|
||||
const StyledDiv = styled.div<{
|
||||
sizeVariant: TextInputSize;
|
||||
disabled?: boolean;
|
||||
textColor?: string;
|
||||
}>`
|
||||
align-items: center;
|
||||
background: inherit;
|
||||
border: none;
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
box-sizing: border-box;
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
color: ${({ textColor }) =>
|
||||
textColor ?? themeCssVariables.font.color.primary};
|
||||
cursor: ${({ disabled }) => (disabled ? 'default' : 'pointer')};
|
||||
display: flex;
|
||||
height: ${({ sizeVariant }) =>
|
||||
@@ -52,7 +78,7 @@ const StyledDiv = styled.div<{
|
||||
: '32px'};
|
||||
overflow: hidden;
|
||||
padding: ${themeCssVariables.spacing[0]} 5px;
|
||||
:hover {
|
||||
&:hover {
|
||||
background: ${({ disabled }) =>
|
||||
disabled ? 'inherit' : themeCssVariables.background.transparent.light};
|
||||
}
|
||||
@@ -148,6 +174,7 @@ export const TitleInput = ({
|
||||
onShiftTab,
|
||||
shouldFocus,
|
||||
onFocus,
|
||||
textColor,
|
||||
}: TitleInputProps) => {
|
||||
const [isOpened, setIsOpened] = useState(false);
|
||||
|
||||
@@ -181,6 +208,7 @@ export const TitleInput = ({
|
||||
<StyledDiv
|
||||
sizeVariant={sizeVariant}
|
||||
disabled={disabled}
|
||||
textColor={textColor}
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
setIsOpened(true);
|
||||
|
||||
@@ -23,16 +23,17 @@ type PageCardHeaderProps = {
|
||||
titleColor?: string;
|
||||
};
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
const StyledHeader = styled.div<{ centerTitle?: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${themeCssVariables.background.secondary};
|
||||
border-bottom: 1px solid ${themeCssVariables.border.color.medium};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
column-gap: ${themeCssVariables.spacing[2]};
|
||||
display: grid;
|
||||
grid-template-columns: ${({ centerTitle }) =>
|
||||
centerTitle ? 'minmax(0, 1fr) auto minmax(0, 1fr)' : 'minmax(0, 1fr) auto'};
|
||||
min-height: ${SIDE_PANEL_TOP_BAR_HEIGHT}px;
|
||||
padding: 0 ${themeCssVariables.spacing[3]};
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
@@ -40,8 +41,10 @@ const StyledLeft = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
grid-column: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.div<{ titleColor?: string }>`
|
||||
@@ -51,26 +54,27 @@ const StyledTitle = styled.div<{ titleColor?: string }>`
|
||||
display: flex;
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: ${themeCssVariables.font.weight.semiBold};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const StyledCenteredTitle = styled(StyledTitle)`
|
||||
bottom: 0;
|
||||
grid-column: 2;
|
||||
justify-content: center;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transform: translateX(-50%);
|
||||
justify-self: center;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledRight = styled.div`
|
||||
const StyledRight = styled.div<{ centerTitle?: boolean }>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
grid-column: ${({ centerTitle }) => (centerTitle ? 3 : 2)};
|
||||
justify-content: flex-end;
|
||||
justify-self: end;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const PageCardHeader = ({
|
||||
@@ -88,6 +92,7 @@ export const PageCardHeader = ({
|
||||
|
||||
const hasTitleContent =
|
||||
!isMobile && (isDefined(icon) || isDefined(title) || isDefined(tag));
|
||||
const shouldCenterTitle = centerTitle && hasTitleContent;
|
||||
|
||||
const titleContent = (
|
||||
<>
|
||||
@@ -98,7 +103,7 @@ export const PageCardHeader = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledHeader>
|
||||
<StyledHeader centerTitle={shouldCenterTitle}>
|
||||
<StyledLeft>
|
||||
{!isNavigationDrawerExpanded && (
|
||||
<NavigationDrawerCollapseButton direction="right" />
|
||||
@@ -106,16 +111,17 @@ export const PageCardHeader = ({
|
||||
{isDefined(breadcrumb)
|
||||
? breadcrumb
|
||||
: isDefined(links) && <Breadcrumb links={links} />}
|
||||
{!centerTitle && hasTitleContent && (
|
||||
{!shouldCenterTitle && hasTitleContent && (
|
||||
<StyledTitle titleColor={titleColor}>{titleContent}</StyledTitle>
|
||||
)}
|
||||
</StyledLeft>
|
||||
{centerTitle && hasTitleContent && (
|
||||
{shouldCenterTitle && (
|
||||
<StyledCenteredTitle titleColor={titleColor}>
|
||||
{titleContent}
|
||||
</StyledCenteredTitle>
|
||||
)}
|
||||
<StyledRight
|
||||
centerTitle={shouldCenterTitle}
|
||||
data-click-outside-id={PAGE_ACTION_CONTAINER_CLICK_OUTSIDE_ID}
|
||||
>
|
||||
{actionButton}
|
||||
|
||||
+27
-20
@@ -13,34 +13,40 @@ export type BreadcrumbProps = {
|
||||
const StyledWrapper = styled.nav`
|
||||
align-items: center;
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
display: grid;
|
||||
display: flex;
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
grid-auto-flow: column;
|
||||
grid-column-gap: ${themeCssVariables.spacing[1]};
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
height: ${themeCssVariables.spacing[8]};
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledLinkContainer = styled.div`
|
||||
> a {
|
||||
color: inherit;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
const StyledCrumbContainer = styled.span<{ $isLast: boolean }>`
|
||||
flex: ${({ $isLast }) => ($isLast ? '0 1 auto' : '0 0 auto')};
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
color: inherit;
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledText = styled.span`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
display: block;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
const StyledDivider = styled.span`
|
||||
width: ${themeCssVariables.spacing[2]};
|
||||
flex: 0 0 ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
export const Breadcrumb = ({ className, links }: BreadcrumbProps) => {
|
||||
@@ -54,18 +60,19 @@ export const Breadcrumb = ({ className, links }: BreadcrumbProps) => {
|
||||
<StyledWrapper className={className}>
|
||||
{links.map((link, index) => {
|
||||
const text = typeof link.children === 'string' ? link.children : '';
|
||||
const isLast = index === links.length - 1;
|
||||
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{link.href ? (
|
||||
<StyledLinkContainer>
|
||||
<Link title={text} to={link.href}>
|
||||
<StyledCrumbContainer $isLast={isLast}>
|
||||
{link.href ? (
|
||||
<StyledLink title={text} to={link.href}>
|
||||
{link.children}
|
||||
</Link>
|
||||
</StyledLinkContainer>
|
||||
) : (
|
||||
<StyledText title={text}>{link.children}</StyledText>
|
||||
)}
|
||||
</StyledLink>
|
||||
) : (
|
||||
<StyledText title={text}>{link.children}</StyledText>
|
||||
)}
|
||||
</StyledCrumbContainer>
|
||||
{index < links.length - 1 && <StyledDivider>/</StyledDivider>}
|
||||
</Fragment>
|
||||
);
|
||||
|
||||
+9
-19
@@ -23,18 +23,10 @@ import { SettingsApplicationRegistrationGeneralTab } from '~/pages/settings/appl
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const REGISTRATION_DETAIL_TAB_LIST_ID =
|
||||
'admin-application-registration-detail-tab-list';
|
||||
|
||||
const StyledTitleContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
export const SettingsAdminApplicationRegistrationDetail = () => {
|
||||
const { t } = useLingui();
|
||||
const apolloAdminClient = useApolloAdminClient();
|
||||
@@ -104,17 +96,15 @@ export const SettingsAdminApplicationRegistrationDetail = () => {
|
||||
|
||||
return (
|
||||
<SettingsPageLayout
|
||||
title={
|
||||
<StyledTitleContainer>
|
||||
<Avatar
|
||||
type="app"
|
||||
size="md"
|
||||
avatarUrl={getAbsoluteImageUrl(registration.logoUrl ?? undefined)}
|
||||
placeholder={registration.name}
|
||||
placeholderColorSeed={registration.name}
|
||||
/>
|
||||
{registration.name}
|
||||
</StyledTitleContainer>
|
||||
title={registration.name}
|
||||
icon={
|
||||
<Avatar
|
||||
type="app"
|
||||
size="md"
|
||||
avatarUrl={getAbsoluteImageUrl(registration.logoUrl ?? undefined)}
|
||||
placeholder={registration.name}
|
||||
placeholderColorSeed={registration.name}
|
||||
/>
|
||||
}
|
||||
links={[
|
||||
{
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
IconMail,
|
||||
IconUser,
|
||||
} from 'twenty-ui/icon';
|
||||
import { Avatar } from 'twenty-ui/data-display';
|
||||
import { H2Title } from 'twenty-ui/typography';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
@@ -140,6 +141,15 @@ export const SettingsAdminUserDetail = () => {
|
||||
|
||||
return (
|
||||
<SettingsPageLayout
|
||||
title={displayName}
|
||||
icon={
|
||||
<Avatar
|
||||
placeholder={displayName}
|
||||
placeholderColorSeed={user?.id}
|
||||
size="md"
|
||||
type="rounded"
|
||||
/>
|
||||
}
|
||||
links={[
|
||||
{
|
||||
children: t`Other`,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useParams } from 'react-router-dom';
|
||||
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
@@ -29,6 +30,7 @@ import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { Avatar } from 'twenty-ui/data-display';
|
||||
@@ -196,6 +198,9 @@ export const SettingsAdminWorkspaceDetail = () => {
|
||||
];
|
||||
|
||||
const workspaceName = workspace?.name || workspaceId || '';
|
||||
const workspaceLogo = isNonEmptyString(workspace?.logo)
|
||||
? workspace.logo
|
||||
: DEFAULT_WORKSPACE_LOGO;
|
||||
|
||||
if (isLoadingWorkspace) {
|
||||
return <SettingsSkeletonLoader />;
|
||||
@@ -203,6 +208,15 @@ export const SettingsAdminWorkspaceDetail = () => {
|
||||
|
||||
return (
|
||||
<SettingsPageLayout
|
||||
title={workspaceName}
|
||||
icon={
|
||||
<Avatar
|
||||
avatarUrl={getAbsoluteImageUrl(workspaceLogo)}
|
||||
placeholder={workspaceName}
|
||||
placeholderColorSeed={workspace?.id}
|
||||
size="md"
|
||||
/>
|
||||
}
|
||||
links={[
|
||||
{
|
||||
children: t`Other`,
|
||||
|
||||
@@ -22,9 +22,10 @@ import {
|
||||
IconListCheck,
|
||||
IconLock,
|
||||
IconSettings,
|
||||
useIcons,
|
||||
} from 'twenty-ui/icon';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import {
|
||||
type CreateAgentInput,
|
||||
@@ -37,7 +38,7 @@ import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
import { useSetAtomFamilyState } from '@/ui/utilities/state/jotai/hooks/useSetAtomFamilyState';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useContext, useEffect, useState } from 'react';
|
||||
import { isDeeplyEqual } from '~/utils/isDeeplyEqual';
|
||||
import { SettingsAgentDetailSkeletonLoader } from './components/SettingsAgentDetailSkeletonLoader';
|
||||
import { SettingsAgentEvalsTab } from './components/SettingsAgentEvalsTab';
|
||||
@@ -60,6 +61,8 @@ const StyledTabListContainer = styled.div`
|
||||
`;
|
||||
|
||||
export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { getIcon } = useIcons();
|
||||
const { agentId = '' } = useParams<{ agentId: string }>();
|
||||
const navigate = useNavigateSettings();
|
||||
const navigateApp = useNavigateApp();
|
||||
@@ -381,6 +384,7 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
|
||||
? t`Agent`
|
||||
: agent?.label
|
||||
: t`New Agent`;
|
||||
const AgentIcon = getIcon(formValues.icon || 'IconRobot');
|
||||
const breadcrumbText = !isCreateMode
|
||||
? loading
|
||||
? t`Agent`
|
||||
@@ -403,6 +407,9 @@ export const SettingsAgentForm = ({ mode }: { mode: 'create' | 'edit' }) => {
|
||||
<SettingsRolesQueryEffect />
|
||||
<SettingsPageLayout
|
||||
title={title}
|
||||
icon={
|
||||
<AgentIcon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
||||
}
|
||||
actionButton={
|
||||
isCreateMode ? (
|
||||
<SaveAndCancelButtons
|
||||
|
||||
@@ -6,13 +6,13 @@ import { useDebouncedCallback } from 'use-debounce';
|
||||
import { FormAdvancedTextFieldInput } from '@/object-record/record-field/ui/form-types/components/FormAdvancedTextFieldInput';
|
||||
import { AdvancedSettingsWrapper } from '@/settings/components/AdvancedSettingsWrapper';
|
||||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { SettingsEditableTitle } from '@/settings/components/SettingsEditableTitle';
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { IconPicker } from '@/ui/input/components/IconPicker';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
import { TitleInput } from '@/ui/input/components/TitleInput';
|
||||
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { AppPath, SettingsPath } from 'twenty-shared/types';
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
IconInfoCircle,
|
||||
IconRefresh,
|
||||
IconTrash,
|
||||
useIcons,
|
||||
} from 'twenty-ui/icon';
|
||||
import { AppTooltip, Card, TooltipDelay } from 'twenty-ui/surfaces';
|
||||
import { H2Title } from 'twenty-ui/typography';
|
||||
@@ -75,17 +76,6 @@ const StyledAdvancedSettingsContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledHeaderTitle = styled.div`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-size: ${themeCssVariables.font.size.lg};
|
||||
font-weight: ${themeCssVariables.font.weight.semiBold};
|
||||
max-width: 420px;
|
||||
width: fit-content;
|
||||
& > input:disabled {
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledDangerButtonsContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
@@ -104,6 +94,7 @@ const DELETE_SKILL_MODAL_ID = 'delete-skill-modal';
|
||||
|
||||
export const SettingsSkillForm = ({ mode }: { mode: 'create' | 'edit' }) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { getIcon } = useIcons();
|
||||
const { skillId = '' } = useParams<{ skillId: string }>();
|
||||
const navigate = useNavigateSettings();
|
||||
const navigateApp = useNavigateApp();
|
||||
@@ -112,6 +103,9 @@ export const SettingsSkillForm = ({ mode }: { mode: 'create' | 'edit' }) => {
|
||||
const [isReadonlyMode, setIsReadonlyMode] = useState(false);
|
||||
const [originalFormValues, setOriginalFormValues] =
|
||||
useState<SkillFormValues | null>(null);
|
||||
const [initializedSkillId, setInitializedSkillId] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const { openModal, closeModal } = useModal();
|
||||
|
||||
const isEditMode = mode === 'edit';
|
||||
@@ -139,9 +133,13 @@ export const SettingsSkillForm = ({ mode }: { mode: 'create' | 'edit' }) => {
|
||||
if (data) {
|
||||
const skill = data?.skill;
|
||||
if (isDefined(skill)) {
|
||||
if (!skill.isCustom) {
|
||||
setIsReadonlyMode(true);
|
||||
if (initializedSkillId === skill.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInitializedSkillId(skill.id);
|
||||
setIsReadonlyMode(!skill.isCustom);
|
||||
|
||||
const computedNameFromLabel = computeMetadataNameFromLabel(skill.label);
|
||||
const isLabelSyncedWithName = skill.name === computedNameFromLabel;
|
||||
|
||||
@@ -162,7 +160,7 @@ export const SettingsSkillForm = ({ mode }: { mode: 'create' | 'edit' }) => {
|
||||
navigateApp(AppPath.NotFound);
|
||||
}
|
||||
}
|
||||
}, [data, enqueueErrorSnackBar, navigateApp]);
|
||||
}, [data, enqueueErrorSnackBar, initializedSkillId, navigateApp]);
|
||||
|
||||
useEffect(() => {
|
||||
if (skillQueryError) {
|
||||
@@ -408,21 +406,21 @@ export const SettingsSkillForm = ({ mode }: { mode: 'create' | 'edit' }) => {
|
||||
) : loading ? (
|
||||
t`Skill`
|
||||
) : (
|
||||
<StyledHeaderTitle>
|
||||
<TitleInput
|
||||
instanceId="skill-label-input"
|
||||
disabled={isReadonlyMode}
|
||||
sizeVariant="md"
|
||||
value={formValues.label}
|
||||
onChange={(value) => handleFieldChange('label', value)}
|
||||
placeholder={t`Skill name`}
|
||||
/>
|
||||
</StyledHeaderTitle>
|
||||
<SettingsEditableTitle
|
||||
instanceId="skill-label-input"
|
||||
disabled={isReadonlyMode}
|
||||
value={formValues.label}
|
||||
onChange={(value) => handleFieldChange('label', value)}
|
||||
placeholder={t`Skill name`}
|
||||
/>
|
||||
);
|
||||
|
||||
const SkillIcon = getIcon(formValues.icon || 'IconSparkles');
|
||||
|
||||
return (
|
||||
<SettingsPageLayout
|
||||
title={title}
|
||||
icon={<SkillIcon size={theme.icon.size.md} color={theme.color.blue9} />}
|
||||
actionButton={
|
||||
isCreateMode ? (
|
||||
<SaveAndCancelButtons
|
||||
@@ -466,7 +464,7 @@ export const SettingsSkillForm = ({ mode }: { mode: 'create' | 'edit' }) => {
|
||||
/>
|
||||
<StyledNameContainer>
|
||||
<SettingsTextInput
|
||||
instanceId="skill-label-input"
|
||||
instanceId="skill-label-field-input"
|
||||
placeholder={t`Skill name`}
|
||||
value={formValues.label}
|
||||
onChange={(value) => handleFieldChange('label', value)}
|
||||
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
GetToolIndexDocument,
|
||||
GetToolInputSchemaDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { SettingsToolIcon } from '~/pages/settings/ai/components/SettingsToolIcon';
|
||||
import { SettingsToolParameterTable } from '~/pages/settings/ai/components/SettingsToolParameterTable';
|
||||
|
||||
const DELETE_TOOL_MODAL_ID = 'delete-tool-modal';
|
||||
@@ -183,6 +184,13 @@ export const SettingsToolDetail = () => {
|
||||
(displayName ?? '')
|
||||
)
|
||||
}
|
||||
icon={
|
||||
<SettingsToolIcon
|
||||
icon={systemTool?.icon}
|
||||
toolName={isCustomTool ? logicFunction?.name : systemTool?.name}
|
||||
objectName={systemTool?.objectName ?? undefined}
|
||||
/>
|
||||
}
|
||||
links={[
|
||||
{
|
||||
children: t`Workspace`,
|
||||
|
||||
@@ -17,14 +17,6 @@ import {
|
||||
} from 'twenty-ui/icon';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type ApplicationInfo = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
type MarketplaceAppInfo = {
|
||||
logo?: string | null;
|
||||
};
|
||||
|
||||
type SettingsToolIconProps = {
|
||||
icon?: string | null;
|
||||
toolName?: string;
|
||||
@@ -33,6 +25,14 @@ type SettingsToolIconProps = {
|
||||
marketplaceApp?: MarketplaceAppInfo;
|
||||
};
|
||||
|
||||
type ApplicationInfo = {
|
||||
name: string;
|
||||
};
|
||||
|
||||
type MarketplaceAppInfo = {
|
||||
logo?: string | null;
|
||||
};
|
||||
|
||||
const getOperationIcon = (toolName: string): IconComponent | null => {
|
||||
if (toolName.startsWith('create_')) return IconPlus;
|
||||
if (toolName.startsWith('update_')) return IconEdit;
|
||||
@@ -95,11 +95,10 @@ export const SettingsToolIcon = ({
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
|
||||
// Custom tools: application/marketplace icons
|
||||
if (isDefined(application) && isDefined(marketplaceApp?.logo)) {
|
||||
return (
|
||||
<Avatar
|
||||
avatarUrl={getAbsoluteImageUrl(marketplaceApp?.logo ?? null)}
|
||||
avatarUrl={getAbsoluteImageUrl(marketplaceApp.logo)}
|
||||
placeholder={application.name}
|
||||
placeholderColorSeed={application.name}
|
||||
type="squared"
|
||||
@@ -119,7 +118,6 @@ export const SettingsToolIcon = ({
|
||||
);
|
||||
}
|
||||
|
||||
// System tools: icon from server, color derived from object metadata
|
||||
const MainIcon = isDefined(icon) ? getIcon(icon) : IconCode;
|
||||
const OperationIcon = isDefined(toolName) ? getOperationIcon(toolName) : null;
|
||||
|
||||
|
||||
+12
-2
@@ -12,6 +12,8 @@ import {
|
||||
IconArrowUpRight,
|
||||
IconInfoCircle,
|
||||
IllustrationIconArray,
|
||||
IllustrationIconCalendarEvent,
|
||||
IllustrationIconCalendarTime,
|
||||
IllustrationIconJson,
|
||||
IllustrationIconNumbers,
|
||||
IllustrationIconText,
|
||||
@@ -43,6 +45,8 @@ const PARAMETER_TABLE_GRID = '140px 1fr 80px 24px';
|
||||
|
||||
const TYPE_ICON_MAP: Record<string, ComponentType<{ size?: number }>> = {
|
||||
string: IllustrationIconText,
|
||||
date: IllustrationIconCalendarEvent,
|
||||
'date-time': IllustrationIconCalendarTime,
|
||||
number: IllustrationIconNumbers,
|
||||
integer: IllustrationIconNumbers,
|
||||
boolean: IllustrationIconToggle,
|
||||
@@ -83,6 +87,11 @@ const getDisplayType = (property: SchemaProperty): string => {
|
||||
return property.type ?? '';
|
||||
};
|
||||
|
||||
const getTypeIconKey = (property: SchemaProperty): string =>
|
||||
isDefined(property.format) && isDefined(TYPE_ICON_MAP[property.format])
|
||||
? property.format
|
||||
: (property.type ?? '');
|
||||
|
||||
export const SettingsToolParameterTable = ({
|
||||
schemaProperties,
|
||||
requiredFields,
|
||||
@@ -112,7 +121,8 @@ export const SettingsToolParameterTable = ({
|
||||
<StyledFieldsContainer>
|
||||
{entries.map(([paramName, property], index) => {
|
||||
const infoIconId = `param-info-${index}`;
|
||||
const TypeIcon = TYPE_ICON_MAP[property.type ?? ''];
|
||||
const displayType = getDisplayType(property);
|
||||
const TypeIcon = TYPE_ICON_MAP[getTypeIconKey(property)];
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
@@ -130,7 +140,7 @@ export const SettingsToolParameterTable = ({
|
||||
{isDefined(TypeIcon) && (
|
||||
<TypeIcon size={theme.icon.size.md} />
|
||||
)}
|
||||
{getDisplayType(property)}
|
||||
{displayType}
|
||||
</TableCell>
|
||||
<TableCell color={themeCssVariables.font.color.tertiary}>
|
||||
{requiredFields?.includes(paramName) ? t`Yes` : ''}
|
||||
|
||||
@@ -62,8 +62,6 @@ export const useSettingsAgentToolsTable = () => {
|
||||
]),
|
||||
);
|
||||
|
||||
// MarketplaceApp.id IS the universal identifier — see
|
||||
// marketplace-query.service.ts where `id: registration.universalIdentifier`.
|
||||
const marketplaceAppByUniversalIdentifier = new Map(
|
||||
(marketplaceAppsData?.findManyMarketplaceApps ?? []).map(
|
||||
(marketplaceApp) => [marketplaceApp.id, marketplaceApp],
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CurrentApplicationContext } from '@/applications/contexts/CurrentApplicationContext';
|
||||
import { AppChip } from '@/applications/components/AppChip';
|
||||
import { useResolvedApplicationDescription } from '@/applications/hooks/useResolvedApplicationDescription';
|
||||
import { isTwentyStandardApplication } from '@/applications/utils/isTwentyStandardApplication';
|
||||
import { isWorkspaceCustomApplication } from '@/applications/utils/isWorkspaceCustomApplication';
|
||||
@@ -313,6 +314,19 @@ export const SettingsApplicationDetails = () => {
|
||||
<CurrentApplicationContext.Provider value={application?.id ?? null}>
|
||||
<SettingsPageLayout
|
||||
title={displayName}
|
||||
icon={
|
||||
isDefined(application) ? (
|
||||
<AppChip
|
||||
applicationId={application.id}
|
||||
fallbackApplicationData={{
|
||||
logo: application.logo,
|
||||
name: displayName,
|
||||
}}
|
||||
size="md"
|
||||
chipOnly
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
links={[
|
||||
{
|
||||
children: t`Workspace`,
|
||||
|
||||
+9
-19
@@ -21,18 +21,10 @@ import { SettingsApplicationRegistrationGeneralTab } from '~/pages/settings/appl
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { getAbsoluteImageUrl } from '~/utils/image/getAbsoluteImageUrl';
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const REGISTRATION_DETAIL_TAB_LIST_ID =
|
||||
'application-registration-detail-tab-list';
|
||||
|
||||
const StyledTitleContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
export const SettingsApplicationRegistrationDetails = () => {
|
||||
const { t } = useLingui();
|
||||
|
||||
@@ -95,17 +87,15 @@ export const SettingsApplicationRegistrationDetails = () => {
|
||||
|
||||
return (
|
||||
<SettingsPageLayout
|
||||
title={
|
||||
<StyledTitleContainer>
|
||||
<Avatar
|
||||
type="app"
|
||||
size="md"
|
||||
avatarUrl={getAbsoluteImageUrl(registration.logoUrl ?? undefined)}
|
||||
placeholder={registration.name}
|
||||
placeholderColorSeed={registration.name}
|
||||
/>
|
||||
{registration.name}
|
||||
</StyledTitleContainer>
|
||||
title={registration.name}
|
||||
icon={
|
||||
<Avatar
|
||||
type="app"
|
||||
size="md"
|
||||
avatarUrl={getAbsoluteImageUrl(registration.logoUrl ?? undefined)}
|
||||
placeholder={registration.name}
|
||||
placeholderColorSeed={registration.name}
|
||||
/>
|
||||
}
|
||||
tag={<Tag text={t`Owner`} color={'gray'} />}
|
||||
links={[
|
||||
|
||||
+12
@@ -1,4 +1,5 @@
|
||||
import { CurrentApplicationContext } from '@/applications/contexts/CurrentApplicationContext';
|
||||
import { AppChip } from '@/applications/components/AppChip';
|
||||
import { SettingsApplicationInstallPermissionValidationModal } from '@/marketplace/components/SettingsApplicationInstallPermissionValidationModal';
|
||||
import { useInstallMarketplaceAppWithPermissionValidation } from '@/marketplace/hooks/useInstallMarketplaceAppWithPermissionValidation';
|
||||
import { useUpgradeApplication } from '@/marketplace/hooks/useUpgradeApplication';
|
||||
@@ -285,6 +286,17 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
{ children: displayName },
|
||||
]}
|
||||
title={displayName}
|
||||
icon={
|
||||
<AppChip
|
||||
applicationId={application?.id}
|
||||
fallbackApplicationData={{
|
||||
logo: app?.logoUrl,
|
||||
name: displayName,
|
||||
}}
|
||||
size="md"
|
||||
chipOnly
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
{isUnlisted && (
|
||||
|
||||
Reference in New Issue
Block a user