mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
Fix Apps UI: replace 'Managed' label with actual app name and unify app icons (#19897)
## Summary
- The Data Model table was labeling core Twenty objects (e.g. Person,
Company) as **Managed** even though they are part of the standard
application. This PR teaches the frontend to resolve an `applicationId`
back to its real application name (`Standard`, `Custom`, or any
installed app), and removes the misleading **Managed** label entirely.
- Introduces a single, consistent way to render an "app badge" across
the settings UI:
- new `Avatar` variant `type="app"` (rounded 4px corners + 1px
deterministic border derived from `placeholderColorSeed`)
- new `AppChip` component (icon + name) backed by a new
`useApplicationChipData` hook
- new `useApplicationsByIdMap` hook + `CurrentApplicationContext` so the
chip can render **This app** when shown inside the matching app's detail
page
- Reuses these primitives on:
- the application detail page header (`SettingsApplicationDetailTitle`)
- the Installed / My apps tables (`SettingsApplicationTableRow`)
- the NPM packages list (`SettingsApplicationsDeveloperTab`)
- Backend: exposes a minimal `installedApplications { id name
universalIdentifier }` field on `Workspace` (resolved from the workspace
cache, soft-deleted entries filtered out) so the frontend can resolve
`applicationId` -> name without N+1 fetches.
- Cleanup: deletes `getItemTagInfo` and inlines its tiny
responsibilities into the components that need them, matching the
`RecordChip` pattern.
This commit is contained in:
parent
9a963ddeca
commit
96fc98e710
42 changed files with 564 additions and 290 deletions
|
|
@ -830,6 +830,7 @@ type Workspace {
|
|||
workspaceCustomApplication: Application
|
||||
featureFlags: [FeatureFlag!]
|
||||
billingSubscriptions: [BillingSubscription!]!
|
||||
installedApplications: [Application!]!
|
||||
currentBillingSubscription: BillingSubscription
|
||||
billingEntitlements: [BillingEntitlement!]!
|
||||
hasValidEnterpriseKey: Boolean!
|
||||
|
|
|
|||
|
|
@ -619,6 +619,7 @@ export interface Workspace {
|
|||
workspaceCustomApplication?: Application
|
||||
featureFlags?: FeatureFlag[]
|
||||
billingSubscriptions: BillingSubscription[]
|
||||
installedApplications: Application[]
|
||||
currentBillingSubscription?: BillingSubscription
|
||||
billingEntitlements: BillingEntitlement[]
|
||||
hasValidEnterpriseKey: Scalars['Boolean']
|
||||
|
|
@ -3504,6 +3505,7 @@ export interface WorkspaceGenqlSelection{
|
|||
workspaceCustomApplication?: ApplicationGenqlSelection
|
||||
featureFlags?: FeatureFlagGenqlSelection
|
||||
billingSubscriptions?: BillingSubscriptionGenqlSelection
|
||||
installedApplications?: ApplicationGenqlSelection
|
||||
currentBillingSubscription?: BillingSubscriptionGenqlSelection
|
||||
billingEntitlements?: BillingEntitlementGenqlSelection
|
||||
hasValidEnterpriseKey?: boolean | number
|
||||
|
|
|
|||
|
|
@ -1765,6 +1765,9 @@ export default {
|
|||
"billingSubscriptions": [
|
||||
138
|
||||
],
|
||||
"installedApplications": [
|
||||
52
|
||||
],
|
||||
"currentBillingSubscription": [
|
||||
138
|
||||
],
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -87,6 +87,7 @@ const mockWorkspace = {
|
|||
useRecommendedModels: true,
|
||||
workspaceCustomApplication: CUSTOM_WORKSPACE_APPLICATION_MOCK,
|
||||
workspaceCustomApplicationId: CUSTOM_WORKSPACE_APPLICATION_MOCK.id,
|
||||
installedApplications: [],
|
||||
};
|
||||
|
||||
const createMockOptions = (): Options => ({
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
import { useApplicationChipData } from '@/applications/hooks/useApplicationChipData';
|
||||
import { Avatar } from 'twenty-ui/display';
|
||||
import {
|
||||
Chip,
|
||||
ChipAccent,
|
||||
type ChipSize,
|
||||
ChipVariant,
|
||||
LinkChip,
|
||||
} from 'twenty-ui/components';
|
||||
|
||||
type AppChipProps = {
|
||||
applicationId: string;
|
||||
variant?: ChipVariant;
|
||||
size?: ChipSize;
|
||||
to?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const AppChip = ({
|
||||
applicationId,
|
||||
variant,
|
||||
size,
|
||||
to,
|
||||
className,
|
||||
}: AppChipProps) => {
|
||||
const { applicationChipData } = useApplicationChipData({ applicationId });
|
||||
|
||||
const leftComponent = (
|
||||
<Avatar
|
||||
type="app"
|
||||
size="md"
|
||||
placeholder={applicationChipData.name}
|
||||
placeholderColorSeed={applicationChipData.seed}
|
||||
color={applicationChipData.colors?.color}
|
||||
backgroundColor={applicationChipData.colors?.backgroundColor}
|
||||
borderColor={applicationChipData.colors?.borderColor}
|
||||
/>
|
||||
);
|
||||
|
||||
if (to) {
|
||||
return (
|
||||
<LinkChip
|
||||
className={className}
|
||||
label={applicationChipData.name}
|
||||
leftComponent={leftComponent}
|
||||
size={size}
|
||||
variant={variant ?? ChipVariant.Highlighted}
|
||||
accent={ChipAccent.TextPrimary}
|
||||
to={to}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Chip
|
||||
className={className}
|
||||
label={applicationChipData.name}
|
||||
leftComponent={leftComponent}
|
||||
size={size}
|
||||
variant={variant ?? ChipVariant.Transparent}
|
||||
accent={ChipAccent.TextPrimary}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
import { createContext } from 'react';
|
||||
|
||||
export const CurrentApplicationContext = createContext<string | null>(null);
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useContext } from 'react';
|
||||
import {
|
||||
TWENTY_STANDARD_APPLICATION_NAME,
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
} from 'twenty-shared/application';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
|
||||
export type ApplicationAvatarColors = {
|
||||
color: string;
|
||||
backgroundColor: string;
|
||||
borderColor: string;
|
||||
};
|
||||
|
||||
type UseApplicationAvatarColorsArgs = {
|
||||
id?: string | null;
|
||||
name?: string | null;
|
||||
universalIdentifier?: string | null;
|
||||
};
|
||||
|
||||
export const useApplicationAvatarColors = (
|
||||
application: UseApplicationAvatarColorsArgs | null | undefined,
|
||||
): ApplicationAvatarColors | undefined => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
|
||||
if (!isDefined(application) || !isDefined(application.id)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isStandard =
|
||||
application.universalIdentifier ===
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER ||
|
||||
application.name === TWENTY_STANDARD_APPLICATION_NAME;
|
||||
|
||||
const isCustom =
|
||||
isDefined(currentWorkspace?.workspaceCustomApplication?.id) &&
|
||||
currentWorkspace.workspaceCustomApplication.id === application.id;
|
||||
|
||||
if (isStandard) {
|
||||
return {
|
||||
backgroundColor: theme.color.blue5,
|
||||
borderColor: theme.color.blue6,
|
||||
color: theme.color.blue12,
|
||||
};
|
||||
}
|
||||
|
||||
if (isCustom) {
|
||||
return {
|
||||
backgroundColor: theme.color.orange5,
|
||||
borderColor: theme.color.orange6,
|
||||
color: theme.color.orange12,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import { CurrentApplicationContext } from '@/applications/contexts/CurrentApplicationContext';
|
||||
import {
|
||||
useApplicationAvatarColors,
|
||||
type ApplicationAvatarColors,
|
||||
} from '@/applications/hooks/useApplicationAvatarColors';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type UseApplicationChipDataArgs = {
|
||||
applicationId: string;
|
||||
};
|
||||
|
||||
type ApplicationChipData = {
|
||||
name: string;
|
||||
seed: string;
|
||||
colors?: ApplicationAvatarColors;
|
||||
};
|
||||
|
||||
type UseApplicationChipDataReturnType = {
|
||||
applicationChipData: ApplicationChipData;
|
||||
};
|
||||
|
||||
export const useApplicationChipData = ({
|
||||
applicationId,
|
||||
}: UseApplicationChipDataArgs): UseApplicationChipDataReturnType => {
|
||||
const currentApplicationId = useContext(CurrentApplicationContext);
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
|
||||
const application = currentWorkspace?.installedApplications.find(
|
||||
(installedApplication) => installedApplication.id === applicationId,
|
||||
);
|
||||
|
||||
const colors = useApplicationAvatarColors(application);
|
||||
|
||||
if (!isDefined(application)) {
|
||||
return {
|
||||
applicationChipData: {
|
||||
name: '',
|
||||
seed: applicationId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const isCurrent =
|
||||
isDefined(currentApplicationId) && currentApplicationId === applicationId;
|
||||
|
||||
return {
|
||||
applicationChipData: {
|
||||
name: isCurrent ? t`This app` : application.name,
|
||||
seed: application.universalIdentifier ?? application.name,
|
||||
colors,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
@ -45,6 +45,10 @@ export type CurrentWorkspace = Pick<
|
|||
> & {
|
||||
defaultRole?: Omit<Role, 'workspaceMembers' | 'agents' | 'apiKeys'> | null;
|
||||
workspaceCustomApplication: Pick<Application, 'id'> | null;
|
||||
installedApplications: Pick<
|
||||
Application,
|
||||
'id' | 'name' | 'universalIdentifier'
|
||||
>[];
|
||||
};
|
||||
|
||||
export const currentWorkspaceState = createAtomState<CurrentWorkspace | null>({
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export const CREATE_ONE_OBJECT_METADATA_ITEM = gql`
|
|||
labelIdentifierFieldMetadataId
|
||||
imageIdentifierFieldMetadataId
|
||||
isLabelSyncedWithName
|
||||
applicationId
|
||||
fieldsList {
|
||||
id
|
||||
universalIdentifier
|
||||
|
|
@ -207,6 +208,7 @@ export const UPDATE_ONE_OBJECT_METADATA_ITEM = gql`
|
|||
labelIdentifierFieldMetadataId
|
||||
imageIdentifierFieldMetadataId
|
||||
isLabelSyncedWithName
|
||||
applicationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -230,6 +232,7 @@ export const DELETE_ONE_OBJECT_METADATA_ITEM = gql`
|
|||
labelIdentifierFieldMetadataId
|
||||
imageIdentifierFieldMetadataId
|
||||
isLabelSyncedWithName
|
||||
applicationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export const query = gql`
|
|||
labelIdentifierFieldMetadataId
|
||||
imageIdentifierFieldMetadataId
|
||||
isLabelSyncedWithName
|
||||
applicationId
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -41,4 +42,5 @@ export const responseData = {
|
|||
updatedAt: '',
|
||||
labelIdentifierFieldMetadataId: '20202020-72ba-4e11-a36d-e17b544541e1',
|
||||
imageIdentifierFieldMetadataId: '',
|
||||
applicationId: null,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ describe('useColumnDefinitionsFromObjectMetadata', () => {
|
|||
workspaceCustomApplication: {
|
||||
id: CUSTOM_WORKSPACE_APPLICATION_MOCK.id,
|
||||
},
|
||||
installedApplications: [],
|
||||
id: '1',
|
||||
featureFlags: [],
|
||||
allowImpersonation: false,
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type ComponentType, type ReactNode } from 'react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { type ReactNode } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { Tag } from 'twenty-ui/components';
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconBrandNpm,
|
||||
type IconComponent,
|
||||
IconLink,
|
||||
IconMail,
|
||||
IconWorld,
|
||||
|
|
@ -13,7 +15,7 @@ import {
|
|||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
export type ContentEntry = {
|
||||
icon: ComponentType<{ size?: number }>;
|
||||
icon: IconComponent;
|
||||
count: number;
|
||||
one: string;
|
||||
many: string;
|
||||
|
|
@ -62,17 +64,11 @@ const StyledSidebarValue = styled.div`
|
|||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledContentItem = styled.div`
|
||||
align-items: center;
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
const StyledContentList = styled.div`
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
margin-bottom: ${themeCssVariables.spacing[2]};
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLink = styled.a`
|
||||
|
|
@ -149,12 +145,18 @@ export const SettingsApplicationAboutSidebar = ({
|
|||
{filteredContentEntries.length > 0 && (
|
||||
<StyledSidebarSection>
|
||||
<StyledSidebarLabel>{t`Content`}</StyledSidebarLabel>
|
||||
{filteredContentEntries.map((entry) => (
|
||||
<StyledContentItem key={entry.one}>
|
||||
<entry.icon size={16} />
|
||||
{entry.count} {entry.count === 1 ? entry.one : entry.many}
|
||||
</StyledContentItem>
|
||||
))}
|
||||
<StyledContentList>
|
||||
{filteredContentEntries.map((entry) => (
|
||||
<Tag
|
||||
key={entry.one}
|
||||
color="gray"
|
||||
Icon={entry.icon}
|
||||
text={`${entry.count} ${
|
||||
entry.count === 1 ? entry.one : entry.many
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</StyledContentList>
|
||||
</StyledSidebarSection>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,7 @@
|
|||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { getItemTagInfo } from '@/settings/data-model/utils/getItemTagInfo';
|
||||
import { styled } from '@linaria/react';
|
||||
import { AppChip } from '@/applications/components/AppChip';
|
||||
import { Avatar } from 'twenty-ui/display';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useContext } from 'react';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { Chip, ChipAccent, ChipVariant } from 'twenty-ui/components';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type SettingsItemTypeTagProps = {
|
||||
item: {
|
||||
|
|
@ -15,38 +12,30 @@ type SettingsItemTypeTagProps = {
|
|||
className?: string;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
align-items: center;
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
display: flex;
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
export const SettingsItemTypeTag = ({
|
||||
className,
|
||||
item: { isCustom, isRemote, applicationId },
|
||||
item: { isRemote, applicationId },
|
||||
}: SettingsItemTypeTagProps) => {
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const itemTagInfo = getItemTagInfo({
|
||||
item: { isCustom, isRemote, applicationId },
|
||||
workspaceCustomApplicationId:
|
||||
currentWorkspace?.workspaceCustomApplication?.id,
|
||||
});
|
||||
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
return (
|
||||
<StyledContainer className={className}>
|
||||
<Avatar
|
||||
placeholder={itemTagInfo.labelText}
|
||||
placeholderColorSeed={itemTagInfo.labelText}
|
||||
type="squared"
|
||||
size="xs"
|
||||
color={theme.tag.text[itemTagInfo.labelColor]}
|
||||
backgroundColor={theme.tag.background[itemTagInfo.labelColor]}
|
||||
if (isDefined(applicationId)) {
|
||||
return <AppChip applicationId={applicationId} className={className} />;
|
||||
} else if (isRemote === true) {
|
||||
return (
|
||||
<Chip
|
||||
className={className}
|
||||
label="Remote"
|
||||
variant={ChipVariant.Transparent}
|
||||
accent={ChipAccent.TextPrimary}
|
||||
leftComponent={
|
||||
<Avatar
|
||||
type="app"
|
||||
size="md"
|
||||
placeholder="Remote"
|
||||
placeholderColorSeed="Remote"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{itemTagInfo.labelText}
|
||||
</StyledContainer>
|
||||
);
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,56 +0,0 @@
|
|||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export type ItemTagInfo =
|
||||
| StandardItemTagInfo
|
||||
| CustomItemTagInfo
|
||||
| RemoteItemTagInfo
|
||||
| ManagedItemTagInfo;
|
||||
|
||||
type StandardItemTagInfo = {
|
||||
labelText: 'Standard';
|
||||
labelColor: 'blue';
|
||||
};
|
||||
|
||||
type CustomItemTagInfo = {
|
||||
labelText: 'Custom';
|
||||
labelColor: 'orange';
|
||||
};
|
||||
|
||||
type RemoteItemTagInfo = {
|
||||
labelText: 'Remote';
|
||||
labelColor: 'green';
|
||||
};
|
||||
|
||||
type ManagedItemTagInfo = {
|
||||
labelText: 'Managed';
|
||||
labelColor: 'sky';
|
||||
};
|
||||
|
||||
export const getItemTagInfo = ({
|
||||
item: { isCustom, isRemote, applicationId },
|
||||
workspaceCustomApplicationId,
|
||||
}: {
|
||||
item: {
|
||||
isCustom?: boolean;
|
||||
isRemote?: boolean;
|
||||
applicationId?: string | null;
|
||||
};
|
||||
workspaceCustomApplicationId?: string;
|
||||
}): ItemTagInfo => {
|
||||
if (
|
||||
isDefined(applicationId) &&
|
||||
applicationId !== workspaceCustomApplicationId
|
||||
) {
|
||||
return { labelText: 'Managed', labelColor: 'sky' };
|
||||
}
|
||||
|
||||
if (isCustom!!) {
|
||||
return { labelText: 'Custom', labelColor: 'orange' };
|
||||
}
|
||||
|
||||
if (isRemote!!) {
|
||||
return { labelText: 'Remote', labelColor: 'green' };
|
||||
}
|
||||
|
||||
return { labelText: 'Standard', labelColor: 'blue' };
|
||||
};
|
||||
|
|
@ -17,11 +17,13 @@ const StyledHeaderTitle = styled.div`
|
|||
type SettingsLogicFunctionLabelContainerProps = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
readonly?: boolean;
|
||||
};
|
||||
|
||||
export const SettingsLogicFunctionLabelContainer = ({
|
||||
value,
|
||||
onChange,
|
||||
readonly = false,
|
||||
}: SettingsLogicFunctionLabelContainerProps) => {
|
||||
return (
|
||||
<StyledHeaderTitle>
|
||||
|
|
@ -31,6 +33,7 @@ export const SettingsLogicFunctionLabelContainer = ({
|
|||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={t`Function name`}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</StyledHeaderTitle>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const StyledIconContainer = styled.span`
|
|||
`;
|
||||
|
||||
const StyledIconChevronRightContainer = styled(StyledIconContainer)`
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
color: ${themeCssVariables.font.color.light};
|
||||
`;
|
||||
|
||||
export const SettingsLogicFunctionsFieldItemTableRow = ({
|
||||
|
|
|
|||
|
|
@ -5,17 +5,20 @@ import { type LogicFunctionFormValues } from '@/logic-functions/hooks/useLogicFu
|
|||
export const SettingsLogicFunctionSettingsTab = ({
|
||||
formValues,
|
||||
onChange,
|
||||
readonly = false,
|
||||
}: {
|
||||
formValues: LogicFunctionFormValues;
|
||||
onChange: <TKey extends keyof LogicFunctionFormValues>(
|
||||
key: TKey,
|
||||
) => (value: LogicFunctionFormValues[TKey]) => void;
|
||||
readonly?: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<SettingsLogicFunctionNewForm
|
||||
formValues={formValues}
|
||||
onChange={onChange}
|
||||
readonly={readonly}
|
||||
/>
|
||||
<SettingsLogicFunctionTabEnvironmentVariablesSection />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -57,11 +57,13 @@ export const TableSection = ({
|
|||
<IconChevronUp
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.font.color.light}
|
||||
/>
|
||||
) : (
|
||||
<IconChevronDown
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.font.color.light}
|
||||
/>
|
||||
)}
|
||||
</StyledSectionHeader>
|
||||
|
|
|
|||
|
|
@ -66,6 +66,11 @@ export const USER_QUERY_FRAGMENT = gql`
|
|||
workspaceCustomApplication {
|
||||
id
|
||||
}
|
||||
installedApplications {
|
||||
id
|
||||
name
|
||||
universalIdentifier
|
||||
}
|
||||
isCustomDomainEnabled
|
||||
workspaceUrls {
|
||||
...WorkspaceUrlsFragment
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { CurrentApplicationContext } from '@/applications/contexts/CurrentApplicationContext';
|
||||
import { useUpgradeApplication } from '@/marketplace/hooks/useUpgradeApplication';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
|
|
@ -287,30 +288,35 @@ export const SettingsApplicationDetails = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={
|
||||
<SettingsApplicationDetailTitle
|
||||
displayName={displayName}
|
||||
description={description}
|
||||
logoUrl={logoUrl}
|
||||
/>
|
||||
}
|
||||
links={[
|
||||
{
|
||||
children: t`Workspace`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: t`Applications`,
|
||||
href: getSettingsPath(SettingsPath.Applications),
|
||||
},
|
||||
{ children: displayName },
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<TabList tabs={tabs} componentInstanceId={APPLICATION_DETAIL_ID} />
|
||||
{renderActiveTabContent()}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
<CurrentApplicationContext.Provider value={application?.id ?? null}>
|
||||
<SubMenuTopBarContainer
|
||||
title={
|
||||
<SettingsApplicationDetailTitle
|
||||
displayName={displayName}
|
||||
description={description}
|
||||
logoUrl={logoUrl}
|
||||
applicationId={application?.id}
|
||||
applicationName={application?.name}
|
||||
universalIdentifier={application?.universalIdentifier}
|
||||
/>
|
||||
}
|
||||
links={[
|
||||
{
|
||||
children: t`Workspace`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: t`Applications`,
|
||||
href: getSettingsPath(SettingsPath.Applications),
|
||||
},
|
||||
{ children: displayName },
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<TabList tabs={tabs} componentInstanceId={APPLICATION_DETAIL_ID} />
|
||||
{renderActiveTabContent()}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
</CurrentApplicationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { CurrentApplicationContext } from '@/applications/contexts/CurrentApplicationContext';
|
||||
import { useInstallMarketplaceApp } from '@/marketplace/hooks/useInstallMarketplaceApp';
|
||||
import { useUpgradeApplication } from '@/marketplace/hooks/useUpgradeApplication';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
|
|
@ -249,36 +250,41 @@ export const SettingsAvailableApplicationDetails = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
links={[
|
||||
{
|
||||
children: t`Workspace`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: t`Applications`,
|
||||
href: getSettingsPath(SettingsPath.Applications),
|
||||
},
|
||||
{ children: displayName },
|
||||
]}
|
||||
title={
|
||||
<SettingsApplicationDetailTitle
|
||||
displayName={displayName}
|
||||
description={description}
|
||||
logoUrl={app?.logoUrl}
|
||||
isUnlisted={isUnlisted}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
componentInstanceId={AVAILABLE_APPLICATION_DETAIL_ID}
|
||||
behaveAsLinks={false}
|
||||
/>
|
||||
<CurrentApplicationContext.Provider value={application?.id ?? null}>
|
||||
<SubMenuTopBarContainer
|
||||
links={[
|
||||
{
|
||||
children: t`Workspace`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: t`Applications`,
|
||||
href: getSettingsPath(SettingsPath.Applications),
|
||||
},
|
||||
{ children: displayName },
|
||||
]}
|
||||
title={
|
||||
<SettingsApplicationDetailTitle
|
||||
displayName={displayName}
|
||||
description={description}
|
||||
logoUrl={app?.logoUrl}
|
||||
applicationId={application?.id}
|
||||
applicationName={application?.name}
|
||||
universalIdentifier={detail.universalIdentifier}
|
||||
isUnlisted={isUnlisted}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
componentInstanceId={AVAILABLE_APPLICATION_DETAIL_ID}
|
||||
behaveAsLinks={false}
|
||||
/>
|
||||
|
||||
{renderActiveTabContent()}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
{renderActiveTabContent()}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
</CurrentApplicationContext.Provider>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -5,15 +5,30 @@ import {
|
|||
} from '@/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconChevronRight, useIcons } from 'twenty-ui/display';
|
||||
import { ThemeContext } from 'twenty-ui/theme-constants';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { type ApplicationDataTableRow } from '~/pages/settings/applications/components/SettingsApplicationDataTable';
|
||||
|
||||
const MAIN_ROW_GRID_COLUMNS = '180px 1fr 98.7px 36px';
|
||||
|
||||
const StyledNameContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const StyledNameLabel = styled.div`
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
export const SettingsApplicationDataTableRow = ({
|
||||
row,
|
||||
}: {
|
||||
|
|
@ -26,13 +41,17 @@ export const SettingsApplicationDataTableRow = ({
|
|||
|
||||
return (
|
||||
<TableRow gridAutoColumns={MAIN_ROW_GRID_COLUMNS} to={row.link}>
|
||||
<StyledNameTableCell>
|
||||
<StyledNameTableCell minWidth="0" overflow="hidden">
|
||||
{isDefined(Icon) && (
|
||||
<Icon size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
|
||||
)}
|
||||
{row.labelPlural}
|
||||
<StyledNameContainer>
|
||||
<StyledNameLabel title={row.labelPlural}>
|
||||
{row.labelPlural}
|
||||
</StyledNameLabel>
|
||||
</StyledNameContainer>
|
||||
</StyledNameTableCell>
|
||||
<TableCell>
|
||||
<TableCell minWidth="0" overflow="hidden">
|
||||
<SettingsItemTypeTag item={row.tagItem} />
|
||||
</TableCell>
|
||||
<TableCell align="right">{row.fieldsCount}</TableCell>
|
||||
|
|
@ -41,7 +60,7 @@ export const SettingsApplicationDataTableRow = ({
|
|||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.font.color.tertiary}
|
||||
color={theme.font.color.light}
|
||||
/>
|
||||
)}
|
||||
</StyledActionTableCell>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
import { useApplicationAvatarColors } from '@/applications/hooks/useApplicationAvatarColors';
|
||||
import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSettings';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconEyeOff } from 'twenty-ui/display';
|
||||
import { Avatar, IconEyeOff } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { OBJECT_SETTINGS_WIDTH } from '@/settings/data-model/constants/ObjectSettings';
|
||||
|
||||
type SettingsApplicationDetailTitleProps = {
|
||||
displayName: string;
|
||||
description?: string;
|
||||
logoUrl?: string;
|
||||
applicationId?: string;
|
||||
applicationName?: string;
|
||||
universalIdentifier?: string;
|
||||
isUnlisted?: boolean;
|
||||
};
|
||||
|
||||
|
|
@ -36,37 +40,6 @@ const StyledHeaderTop = styled.div`
|
|||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledLogo = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${themeCssVariables.background.tertiary};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: 24px;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
width: 24px;
|
||||
`;
|
||||
|
||||
const StyledLogoImage = styled.img`
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
width: 32px;
|
||||
`;
|
||||
|
||||
const StyledLogoPlaceholder = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${themeCssVariables.color.blue};
|
||||
border-radius: ${themeCssVariables.border.radius.xs};
|
||||
color: ${themeCssVariables.font.color.inverted};
|
||||
display: flex;
|
||||
font-size: ${themeCssVariables.font.size.lg};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
height: 32px;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
`;
|
||||
|
||||
const StyledAppName = styled.div`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-size: ${themeCssVariables.font.size.lg};
|
||||
|
|
@ -99,8 +72,17 @@ export const SettingsApplicationDetailTitle = ({
|
|||
displayName,
|
||||
description,
|
||||
logoUrl,
|
||||
applicationId,
|
||||
applicationName,
|
||||
universalIdentifier,
|
||||
isUnlisted = false,
|
||||
}: SettingsApplicationDetailTitleProps) => {
|
||||
const colors = useApplicationAvatarColors({
|
||||
id: applicationId,
|
||||
name: applicationName,
|
||||
universalIdentifier,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledTitleContainer>
|
||||
{isUnlisted && (
|
||||
|
|
@ -112,15 +94,16 @@ export const SettingsApplicationDetailTitle = ({
|
|||
<StyledHeader>
|
||||
<StyledHeaderLeft>
|
||||
<StyledHeaderTop>
|
||||
<StyledLogo>
|
||||
{logoUrl ? (
|
||||
<StyledLogoImage src={logoUrl} alt={displayName} />
|
||||
) : (
|
||||
<StyledLogoPlaceholder>
|
||||
{displayName.charAt(0).toUpperCase()}
|
||||
</StyledLogoPlaceholder>
|
||||
)}
|
||||
</StyledLogo>
|
||||
<Avatar
|
||||
type="app"
|
||||
size="lg"
|
||||
avatarUrl={logoUrl}
|
||||
placeholder={displayName}
|
||||
placeholderColorSeed={universalIdentifier ?? displayName}
|
||||
color={colors?.color}
|
||||
backgroundColor={colors?.backgroundColor}
|
||||
borderColor={colors?.borderColor}
|
||||
/>
|
||||
<StyledAppName>{displayName}</StyledAppName>
|
||||
</StyledHeaderTop>
|
||||
{description && (
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
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 { TableSection } from '@/ui/layout/table/components/TableSection';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { H2Title, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
|
|
@ -43,10 +43,14 @@ export const SettingsApplicationNameDescriptionTable = ({
|
|||
<TableCell
|
||||
color={themeCssVariables.font.color.primary}
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
minWidth="0"
|
||||
overflow="hidden"
|
||||
>
|
||||
{item.name}
|
||||
<OverflowingTextWithTooltip text={item.name} />
|
||||
</TableCell>
|
||||
<TableCell minWidth="0" overflow="hidden">
|
||||
<OverflowingTextWithTooltip text={item.description ?? ''} />
|
||||
</TableCell>
|
||||
<TableCell>{item.description ?? ''}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableSection>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { type ReactNode } from 'react';
|
||||
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useApplicationAvatarColors } from '@/applications/hooks/useApplicationAvatarColors';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Tag } from 'twenty-ui/components';
|
||||
import { Avatar, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { type ApplicationWithoutRelation } from '~/pages/settings/applications/types/applicationWithoutRelation';
|
||||
|
||||
|
|
@ -24,6 +25,8 @@ export const SettingsApplicationTableRow = ({
|
|||
hasUpdate,
|
||||
link,
|
||||
}: SettingsApplicationTableRowProps) => {
|
||||
const colors = useApplicationAvatarColors(application);
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
gridTemplateColumns={APPLICATION_TABLE_ROW_GRID_TEMPLATE_COLUMNS}
|
||||
|
|
@ -36,6 +39,17 @@ export const SettingsApplicationTableRow = ({
|
|||
minWidth="0"
|
||||
overflow="hidden"
|
||||
>
|
||||
<Avatar
|
||||
type="app"
|
||||
size="md"
|
||||
placeholder={application.name}
|
||||
placeholderColorSeed={
|
||||
application.universalIdentifier ?? application.name
|
||||
}
|
||||
color={colors?.color}
|
||||
backgroundColor={colors?.backgroundColor}
|
||||
borderColor={colors?.borderColor}
|
||||
/>
|
||||
<OverflowingTextWithTooltip text={application.name} />
|
||||
</TableCell>
|
||||
<TableCell gap={themeCssVariables.spacing[2]} minWidth="0">
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ export const SettingsApplicationsTable = ({
|
|||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.font.color.light}
|
||||
/>
|
||||
}
|
||||
link={getSettingsPath(SettingsPath.ApplicationDetail, {
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
import { useApplicationAvatarColors } from '@/applications/hooks/useApplicationAvatarColors';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { SettingsEmptyPlaceholder } from '@/settings/components/SettingsEmptyPlaceholder';
|
||||
import {
|
||||
|
|
@ -59,6 +60,31 @@ const StyledTableRowsContainer = styled.div`
|
|||
|
||||
const NPM_PACKAGES_GRID_COLUMNS = '200px 1fr 36px';
|
||||
|
||||
type MarketplaceAppAvatarProps = {
|
||||
application: { id: string; name: string; logo?: string | null };
|
||||
};
|
||||
|
||||
const MarketplaceAppAvatar = ({ application }: MarketplaceAppAvatarProps) => {
|
||||
const colors = useApplicationAvatarColors({
|
||||
id: application.id,
|
||||
name: application.name,
|
||||
universalIdentifier: application.id,
|
||||
});
|
||||
|
||||
return (
|
||||
<Avatar
|
||||
avatarUrl={application.logo || null}
|
||||
placeholder={application.name}
|
||||
placeholderColorSeed={application.id ?? application.name}
|
||||
size="md"
|
||||
type="app"
|
||||
color={colors?.color}
|
||||
backgroundColor={colors?.backgroundColor}
|
||||
borderColor={colors?.borderColor}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const SettingsApplicationsDeveloperTab = () => {
|
||||
const { t } = useLingui();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
|
@ -183,6 +209,7 @@ export const SettingsApplicationsDeveloperTab = () => {
|
|||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.font.color.light}
|
||||
/>
|
||||
}
|
||||
link={getRegistrationLink(registration)}
|
||||
|
|
@ -240,13 +267,7 @@ export const SettingsApplicationsDeveloperTab = () => {
|
|||
)}
|
||||
>
|
||||
<StyledNameTableCell>
|
||||
<Avatar
|
||||
avatarUrl={application.logo || null}
|
||||
placeholder={application.name}
|
||||
placeholderColorSeed={application.name}
|
||||
size="md"
|
||||
type="squared"
|
||||
/>
|
||||
<MarketplaceAppAvatar application={application} />
|
||||
<OverflowingTextWithTooltip text={application.name} />
|
||||
</StyledNameTableCell>
|
||||
<TableCell
|
||||
|
|
@ -262,7 +283,7 @@ export const SettingsApplicationsDeveloperTab = () => {
|
|||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.font.color.tertiary}
|
||||
color={theme.font.color.light}
|
||||
/>
|
||||
</StyledActionTableCell>
|
||||
</TableRow>
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export type ApplicationWithoutRelation = Pick<
|
|||
| 'name'
|
||||
| 'description'
|
||||
| 'version'
|
||||
| 'universalIdentifier'
|
||||
| 'applicationRegistrationId'
|
||||
| 'applicationRegistration'
|
||||
>;
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import {
|
|||
StyledStickyFirstCell,
|
||||
} from '@/settings/data-model/object-details/components/SettingsObjectItemTableRowStyledComponents';
|
||||
import { SettingsObjectInactiveMenuDropDown } from '@/settings/data-model/objects/components/SettingsObjectInactiveMenuDropDown';
|
||||
import { getItemTagInfo } from '@/settings/data-model/utils/getItemTagInfo';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
|
|
@ -86,6 +85,7 @@ export const SettingsObjectTable = ({
|
|||
useCombinedGetTotalCount();
|
||||
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const installedApplications = currentWorkspace?.installedApplications;
|
||||
|
||||
const allObjectSettingsArray = useMemo(
|
||||
() =>
|
||||
|
|
@ -94,11 +94,11 @@ export const SettingsObjectTable = ({
|
|||
({
|
||||
objectMetadataItem,
|
||||
labelPlural: objectMetadataItem.labelPlural,
|
||||
objectTypeLabel: getItemTagInfo({
|
||||
item: objectMetadataItem,
|
||||
workspaceCustomApplicationId:
|
||||
currentWorkspace?.workspaceCustomApplication?.id,
|
||||
}).labelText,
|
||||
objectTypeLabel:
|
||||
installedApplications?.find(
|
||||
(application) =>
|
||||
application.id === objectMetadataItem.applicationId,
|
||||
)?.name ?? (objectMetadataItem.isRemote ? 'Remote' : ''),
|
||||
fieldsCount: objectMetadataItem.fields.filter(
|
||||
(field) => !isHiddenSystemField(field),
|
||||
).length,
|
||||
|
|
@ -111,7 +111,7 @@ export const SettingsObjectTable = ({
|
|||
[
|
||||
objectMetadataItems,
|
||||
totalCountByObjectMetadataItemNamePlural,
|
||||
currentWorkspace,
|
||||
installedApplications,
|
||||
],
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ import { useExecuteLogicFunction } from '@/logic-functions/hooks/useExecuteLogic
|
|||
const LOGIC_FUNCTION_DETAIL_ID = 'logic-function-detail';
|
||||
|
||||
export const SettingsLogicFunctionDetail = () => {
|
||||
const { logicFunctionId = '', applicationId = '' } = useParams();
|
||||
const { logicFunctionId = '', applicationId } = useParams();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
|
|
@ -37,8 +37,8 @@ export const SettingsLogicFunctionDetail = () => {
|
|||
const { data, loading: applicationLoading } = useQuery(
|
||||
FindOneApplicationDocument,
|
||||
{
|
||||
variables: { id: applicationId },
|
||||
skip: !applicationId,
|
||||
variables: { id: applicationId ?? '' },
|
||||
skip: !isDefined(applicationId),
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -47,7 +47,8 @@ export const SettingsLogicFunctionDetail = () => {
|
|||
const workspaceCustomApplicationId =
|
||||
currentWorkspace?.workspaceCustomApplication?.id;
|
||||
|
||||
const isManaged = applicationId !== workspaceCustomApplicationId;
|
||||
const isReadonly =
|
||||
isDefined(applicationId) && applicationId !== workspaceCustomApplicationId;
|
||||
|
||||
const instanceId = `${LOGIC_FUNCTION_DETAIL_ID}-${logicFunctionId}`;
|
||||
|
||||
|
|
@ -74,8 +75,8 @@ export const SettingsLogicFunctionDetail = () => {
|
|||
id: 'editor',
|
||||
title: t`Editor`,
|
||||
Icon: IconCode,
|
||||
disabled: isManaged,
|
||||
hide: isManaged,
|
||||
disabled: isReadonly,
|
||||
hide: isReadonly,
|
||||
},
|
||||
{ id: 'settings', title: t`Settings`, Icon: IconSettings },
|
||||
{ id: 'test', title: t`Test`, Icon: IconPlayerPlay },
|
||||
|
|
@ -87,41 +88,40 @@ export const SettingsLogicFunctionDetail = () => {
|
|||
const isSettingsTab = activeTabId === 'settings';
|
||||
const isTestTab = activeTabId === 'test';
|
||||
|
||||
const breadcrumbLinks =
|
||||
isDefined(applicationId) && applicationId !== ''
|
||||
? [
|
||||
{
|
||||
children: t`Workspace`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: t`Applications`,
|
||||
href: getSettingsPath(SettingsPath.Applications),
|
||||
},
|
||||
{
|
||||
children: `${applicationName}`,
|
||||
href: getSettingsPath(
|
||||
SettingsPath.ApplicationDetail,
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
undefined,
|
||||
'content',
|
||||
),
|
||||
},
|
||||
{ children: `${logicFunction?.name}` },
|
||||
]
|
||||
: [
|
||||
{
|
||||
children: t`Workspace`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: t`AI`,
|
||||
href: getSettingsPath(SettingsPath.AI),
|
||||
},
|
||||
{ children: `${logicFunction?.name}` },
|
||||
];
|
||||
const breadcrumbLinks = isDefined(applicationId)
|
||||
? [
|
||||
{
|
||||
children: t`Workspace`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: t`Applications`,
|
||||
href: getSettingsPath(SettingsPath.Applications),
|
||||
},
|
||||
{
|
||||
children: `${applicationName}`,
|
||||
href: getSettingsPath(
|
||||
SettingsPath.ApplicationDetail,
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
undefined,
|
||||
'content',
|
||||
),
|
||||
},
|
||||
{ children: `${logicFunction?.name}` },
|
||||
]
|
||||
: [
|
||||
{
|
||||
children: t`Workspace`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: t`AI`,
|
||||
href: getSettingsPath(SettingsPath.AI),
|
||||
},
|
||||
{ children: `${logicFunction?.name}` },
|
||||
];
|
||||
|
||||
const files = [
|
||||
{
|
||||
|
|
@ -139,6 +139,7 @@ export const SettingsLogicFunctionDetail = () => {
|
|||
<SettingsLogicFunctionLabelContainer
|
||||
value={formValues.name}
|
||||
onChange={onChange('name')}
|
||||
readonly={isReadonly}
|
||||
/>
|
||||
}
|
||||
links={breadcrumbLinks}
|
||||
|
|
@ -160,6 +161,7 @@ export const SettingsLogicFunctionDetail = () => {
|
|||
<SettingsLogicFunctionSettingsTab
|
||||
formValues={formValues}
|
||||
onChange={onChange}
|
||||
readonly={isReadonly}
|
||||
/>
|
||||
)}
|
||||
{isTestTab && (
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ const PRO_METERED_MONTHLY_PRICE = PRO_METERED_PRODUCT?.prices?.find(
|
|||
export const mockCurrentWorkspace = {
|
||||
workspaceCustomApplication: CUSTOM_WORKSPACE_APPLICATION_MOCK,
|
||||
workspaceCustomApplicationId: CUSTOM_WORKSPACE_APPLICATION_MOCK.id,
|
||||
installedApplications: [CUSTOM_WORKSPACE_APPLICATION_MOCK],
|
||||
subdomain: 'acme.twenty.com',
|
||||
id: '7dfbc3f7-6e5e-4128-957e-8d86808cdf6w',
|
||||
displayName: 'Twenty',
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
import { getDefaultApplicationPackageFields } from 'src/engine/core-modules/application/application-package/utils/get-default-application-package-fields.util';
|
||||
import { parseAvailablePackagesFromPackageJsonAndYarnLock } from 'src/engine/core-modules/application/application-package/utils/parse-available-packages-from-package-json-and-yarn-lock.util';
|
||||
import { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity';
|
||||
import { type FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
|
|
@ -138,6 +139,20 @@ export class ApplicationService {
|
|||
});
|
||||
}
|
||||
|
||||
async findManyInstalledFlatApplications(
|
||||
workspaceId: string,
|
||||
): Promise<FlatApplication[]> {
|
||||
const { flatApplicationMaps } =
|
||||
await this.workspaceCacheService.getOrRecompute(workspaceId, [
|
||||
'flatApplicationMaps',
|
||||
]);
|
||||
|
||||
return Object.values(flatApplicationMaps.byId).filter(
|
||||
(flatApplication): flatApplication is FlatApplication =>
|
||||
isDefined(flatApplication) && !isDefined(flatApplication.deletedAt),
|
||||
);
|
||||
}
|
||||
|
||||
async findOneApplication({
|
||||
id,
|
||||
universalIdentifier,
|
||||
|
|
|
|||
|
|
@ -258,6 +258,18 @@ export class WorkspaceResolver {
|
|||
}
|
||||
}
|
||||
|
||||
@ResolveField(() => [ApplicationDTO])
|
||||
async installedApplications(
|
||||
@Parent() workspace: WorkspaceEntity,
|
||||
): Promise<ApplicationDTO[]> {
|
||||
const flatApplications =
|
||||
await this.applicationService.findManyInstalledFlatApplications(
|
||||
workspace.id,
|
||||
);
|
||||
|
||||
return flatApplications.map(fromFlatApplicationToApplicationDto);
|
||||
}
|
||||
|
||||
@ResolveField(() => BillingSubscriptionEntity, { nullable: true })
|
||||
async currentBillingSubscription(
|
||||
@Parent() workspace: WorkspaceEntity,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,14 @@
|
|||
import {
|
||||
TWENTY_STANDARD_APPLICATION_NAME,
|
||||
TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
} from 'twenty-shared/application';
|
||||
|
||||
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
|
||||
import { type ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
|
||||
export const TWENTY_STANDARD_APPLICATION = {
|
||||
universalIdentifier: '20202020-64aa-4b6f-b003-9c74b97cee20',
|
||||
name: 'Standard',
|
||||
universalIdentifier: TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER,
|
||||
name: TWENTY_STANDARD_APPLICATION_NAME,
|
||||
description:
|
||||
'Twenty is an open-source CRM that allows you to manage your sales and customer relationships',
|
||||
version: '1.0.1',
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export const TWENTY_STANDARD_APPLICATION_NAME = 'Standard';
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
export const TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER =
|
||||
'20202020-64aa-4b6f-b003-9c74b97cee20';
|
||||
|
|
@ -18,6 +18,8 @@ export { DEFAULT_APP_ACCESS_TOKEN_NAME } from './constants/DefaultAppAccessToken
|
|||
export { GENERATED_DIR } from './constants/GeneratedDirectory';
|
||||
export { NODE_ESM_CJS_BANNER } from './constants/NodeEsmCjsBanner';
|
||||
export { OUTPUT_DIR } from './constants/OutputDirectory';
|
||||
export { TWENTY_STANDARD_APPLICATION_NAME } from './constants/TwentyStandardApplicationName';
|
||||
export { TWENTY_STANDARD_APPLICATION_UNIVERSAL_IDENTIFIER } from './constants/TwentyStandardApplicationUniversalIdentifier';
|
||||
export { SyncableEntity } from './enums/syncable-entities.enum';
|
||||
export type {
|
||||
RegularFieldManifest,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ const StyledAvatar = styled.div<{
|
|||
clickable?: boolean;
|
||||
color: string;
|
||||
backgroundColor: string;
|
||||
borderColor?: string;
|
||||
backgroundTransparentLight: string;
|
||||
type?: Nullable<AvatarType>;
|
||||
}>`
|
||||
|
|
@ -29,8 +30,13 @@ const StyledAvatar = styled.div<{
|
|||
user-select: none;
|
||||
|
||||
border-radius: ${({ rounded, type }) => {
|
||||
return rounded ? '50%' : type === 'icon' ? '4px' : '2px';
|
||||
if (rounded) return '50%';
|
||||
if (type === 'icon') return '4px';
|
||||
return '2px';
|
||||
}};
|
||||
border: ${({ type, borderColor }) =>
|
||||
type === 'app' && borderColor ? `1px solid ${borderColor}` : 'none'};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
font-size: ${({ size }) => AVATAR_PROPERTIES_BY_SIZE[size].fontSize};
|
||||
height: ${({ size }) => AVATAR_PROPERTIES_BY_SIZE[size].width};
|
||||
|
|
@ -69,6 +75,7 @@ export type AvatarProps = {
|
|||
type?: Nullable<AvatarType>;
|
||||
color?: string;
|
||||
backgroundColor?: string;
|
||||
borderColor?: string;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
|
|
@ -83,6 +90,7 @@ export const Avatar = ({
|
|||
type = 'squared',
|
||||
color,
|
||||
backgroundColor,
|
||||
borderColor,
|
||||
}: AvatarProps) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
|
|
@ -124,10 +132,22 @@ export const Avatar = ({
|
|||
: (backgroundColor ??
|
||||
stringToThemeColorP3String({
|
||||
string: placeholderColorSeed ?? '',
|
||||
variant: 4,
|
||||
variant: type === 'app' ? 5 : 4,
|
||||
theme,
|
||||
}));
|
||||
|
||||
const fixedBorderColor =
|
||||
type === 'app'
|
||||
? (borderColor ??
|
||||
(isPlaceholderFirstCharEmpty
|
||||
? undefined
|
||||
: stringToThemeColorP3String({
|
||||
string: placeholderColorSeed ?? '',
|
||||
variant: 6,
|
||||
theme,
|
||||
})))
|
||||
: undefined;
|
||||
|
||||
const showBackgroundColor = showPlaceholder;
|
||||
|
||||
return (
|
||||
|
|
@ -136,6 +156,7 @@ export const Avatar = ({
|
|||
backgroundColor={
|
||||
Icon ? 'inherit' : showBackgroundColor ? fixedBackgroundColor : 'none'
|
||||
}
|
||||
borderColor={fixedBorderColor}
|
||||
color={fixedColor}
|
||||
clickable={!isUndefined(onClick)}
|
||||
rounded={type === 'rounded'}
|
||||
|
|
|
|||
|
|
@ -39,3 +39,12 @@ export const NoAvatarPictureSquared: Story = {
|
|||
...Squared.args,
|
||||
},
|
||||
};
|
||||
|
||||
export const App: Story = {
|
||||
args: {
|
||||
type: 'app',
|
||||
avatarUrl: '',
|
||||
placeholder: 'Acme',
|
||||
placeholderColorSeed: 'acme-app',
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export type AvatarType = 'squared' | 'rounded' | 'icon';
|
||||
export type AvatarType = 'squared' | 'rounded' | 'icon' | 'app';
|
||||
|
|
|
|||
Loading…
Reference in a new issue