Fix app design 6 (#19827)
Unify application display page and isntalled page --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "1.23.0-canary.1",
|
||||
"version": "1.23.0-canary.2",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
|
||||
@@ -149,6 +149,25 @@ describe('copyBaseApplicationProject', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should create an empty public directory in the scaffolded project', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
});
|
||||
|
||||
const publicDirectoryPath = join(testAppDirectory, 'public');
|
||||
|
||||
expect(await fs.pathExists(publicDirectoryPath)).toBe(true);
|
||||
|
||||
const publicDirectoryStats = await fs.stat(publicDirectoryPath);
|
||||
expect(publicDirectoryStats.isDirectory()).toBe(true);
|
||||
|
||||
const publicDirectoryContents = await fs.readdir(publicDirectoryPath);
|
||||
expect(publicDirectoryContents).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle empty description', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
|
||||
@@ -23,6 +23,8 @@ export const copyBaseApplicationProject = async ({
|
||||
|
||||
await renameDotfiles({ appDirectory });
|
||||
|
||||
await addEmptyPublicDirectory({ appDirectory });
|
||||
|
||||
await generateUniversalIdentifiers({
|
||||
appDisplayName,
|
||||
appDescription,
|
||||
@@ -49,6 +51,14 @@ const renameDotfiles = async ({ appDirectory }: { appDirectory: string }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const addEmptyPublicDirectory = async ({
|
||||
appDirectory,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
}) => {
|
||||
await fs.ensureDir(join(appDirectory, 'public'));
|
||||
};
|
||||
|
||||
const generateUniversalIdentifiers = async ({
|
||||
appDisplayName,
|
||||
appDescription,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-client-sdk",
|
||||
"version": "1.23.0-canary.1",
|
||||
"version": "1.23.0-canary.2",
|
||||
"sideEffects": false,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@@ -296,6 +296,33 @@ type ApplicationVariable {
|
||||
isSecret: Boolean!
|
||||
}
|
||||
|
||||
type AuthToken {
|
||||
token: String!
|
||||
expiresAt: DateTime!
|
||||
}
|
||||
|
||||
type ApplicationTokenPair {
|
||||
applicationAccessToken: AuthToken!
|
||||
applicationRefreshToken: AuthToken!
|
||||
}
|
||||
|
||||
type FrontComponent {
|
||||
id: UUID!
|
||||
name: String!
|
||||
description: String
|
||||
sourceComponentPath: String!
|
||||
builtComponentPath: String!
|
||||
componentName: String!
|
||||
builtComponentChecksum: String!
|
||||
universalIdentifier: UUID
|
||||
applicationId: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
isHeadless: Boolean!
|
||||
usesSdkClient: Boolean!
|
||||
applicationTokenPair: ApplicationTokenPair
|
||||
}
|
||||
|
||||
type LogicFunction {
|
||||
id: UUID!
|
||||
name: String!
|
||||
@@ -565,6 +592,7 @@ type Application {
|
||||
settingsCustomTabFrontComponentId: UUID
|
||||
defaultLogicFunctionRole: Role
|
||||
agents: [Agent!]!
|
||||
frontComponents: [FrontComponent!]!
|
||||
logicFunctions: [LogicFunction!]!
|
||||
objects: [Object!]!
|
||||
applicationVariables: [ApplicationVariable!]!
|
||||
@@ -2032,33 +2060,6 @@ type UpsertRowLevelPermissionPredicatesResult {
|
||||
predicateGroups: [RowLevelPermissionPredicateGroup!]!
|
||||
}
|
||||
|
||||
type AuthToken {
|
||||
token: String!
|
||||
expiresAt: DateTime!
|
||||
}
|
||||
|
||||
type ApplicationTokenPair {
|
||||
applicationAccessToken: AuthToken!
|
||||
applicationRefreshToken: AuthToken!
|
||||
}
|
||||
|
||||
type FrontComponent {
|
||||
id: UUID!
|
||||
name: String!
|
||||
description: String
|
||||
sourceComponentPath: String!
|
||||
builtComponentPath: String!
|
||||
componentName: String!
|
||||
builtComponentChecksum: String!
|
||||
universalIdentifier: UUID
|
||||
applicationId: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
isHeadless: Boolean!
|
||||
usesSdkClient: Boolean!
|
||||
applicationTokenPair: ApplicationTokenPair
|
||||
}
|
||||
|
||||
type LogicFunctionLogs {
|
||||
"""Execution Logs"""
|
||||
logs: String!
|
||||
|
||||
@@ -247,6 +247,36 @@ export interface ApplicationVariable {
|
||||
__typename: 'ApplicationVariable'
|
||||
}
|
||||
|
||||
export interface AuthToken {
|
||||
token: Scalars['String']
|
||||
expiresAt: Scalars['DateTime']
|
||||
__typename: 'AuthToken'
|
||||
}
|
||||
|
||||
export interface ApplicationTokenPair {
|
||||
applicationAccessToken: AuthToken
|
||||
applicationRefreshToken: AuthToken
|
||||
__typename: 'ApplicationTokenPair'
|
||||
}
|
||||
|
||||
export interface FrontComponent {
|
||||
id: Scalars['UUID']
|
||||
name: Scalars['String']
|
||||
description?: Scalars['String']
|
||||
sourceComponentPath: Scalars['String']
|
||||
builtComponentPath: Scalars['String']
|
||||
componentName: Scalars['String']
|
||||
builtComponentChecksum: Scalars['String']
|
||||
universalIdentifier?: Scalars['UUID']
|
||||
applicationId: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
isHeadless: Scalars['Boolean']
|
||||
usesSdkClient: Scalars['Boolean']
|
||||
applicationTokenPair?: ApplicationTokenPair
|
||||
__typename: 'FrontComponent'
|
||||
}
|
||||
|
||||
export interface LogicFunction {
|
||||
id: Scalars['UUID']
|
||||
name: Scalars['String']
|
||||
@@ -396,6 +426,7 @@ export interface Application {
|
||||
settingsCustomTabFrontComponentId?: Scalars['UUID']
|
||||
defaultLogicFunctionRole?: Role
|
||||
agents: Agent[]
|
||||
frontComponents: FrontComponent[]
|
||||
logicFunctions: LogicFunction[]
|
||||
objects: Object[]
|
||||
applicationVariables: ApplicationVariable[]
|
||||
@@ -1738,36 +1769,6 @@ export interface UpsertRowLevelPermissionPredicatesResult {
|
||||
__typename: 'UpsertRowLevelPermissionPredicatesResult'
|
||||
}
|
||||
|
||||
export interface AuthToken {
|
||||
token: Scalars['String']
|
||||
expiresAt: Scalars['DateTime']
|
||||
__typename: 'AuthToken'
|
||||
}
|
||||
|
||||
export interface ApplicationTokenPair {
|
||||
applicationAccessToken: AuthToken
|
||||
applicationRefreshToken: AuthToken
|
||||
__typename: 'ApplicationTokenPair'
|
||||
}
|
||||
|
||||
export interface FrontComponent {
|
||||
id: Scalars['UUID']
|
||||
name: Scalars['String']
|
||||
description?: Scalars['String']
|
||||
sourceComponentPath: Scalars['String']
|
||||
builtComponentPath: Scalars['String']
|
||||
componentName: Scalars['String']
|
||||
builtComponentChecksum: Scalars['String']
|
||||
universalIdentifier?: Scalars['UUID']
|
||||
applicationId: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
isHeadless: Scalars['Boolean']
|
||||
usesSdkClient: Scalars['Boolean']
|
||||
applicationTokenPair?: ApplicationTokenPair
|
||||
__typename: 'FrontComponent'
|
||||
}
|
||||
|
||||
export interface LogicFunctionLogs {
|
||||
/** Execution Logs */
|
||||
logs: Scalars['String']
|
||||
@@ -3101,6 +3102,39 @@ export interface ApplicationVariableGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface AuthTokenGenqlSelection{
|
||||
token?: boolean | number
|
||||
expiresAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ApplicationTokenPairGenqlSelection{
|
||||
applicationAccessToken?: AuthTokenGenqlSelection
|
||||
applicationRefreshToken?: AuthTokenGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface FrontComponentGenqlSelection{
|
||||
id?: boolean | number
|
||||
name?: boolean | number
|
||||
description?: boolean | number
|
||||
sourceComponentPath?: boolean | number
|
||||
builtComponentPath?: boolean | number
|
||||
componentName?: boolean | number
|
||||
builtComponentChecksum?: boolean | number
|
||||
universalIdentifier?: boolean | number
|
||||
applicationId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
isHeadless?: boolean | number
|
||||
usesSdkClient?: boolean | number
|
||||
applicationTokenPair?: ApplicationTokenPairGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface LogicFunctionGenqlSelection{
|
||||
id?: boolean | number
|
||||
name?: boolean | number
|
||||
@@ -3287,6 +3321,7 @@ export interface ApplicationGenqlSelection{
|
||||
settingsCustomTabFrontComponentId?: boolean | number
|
||||
defaultLogicFunctionRole?: RoleGenqlSelection
|
||||
agents?: AgentGenqlSelection
|
||||
frontComponents?: FrontComponentGenqlSelection
|
||||
logicFunctions?: LogicFunctionGenqlSelection
|
||||
objects?: ObjectGenqlSelection
|
||||
applicationVariables?: ApplicationVariableGenqlSelection
|
||||
@@ -4685,39 +4720,6 @@ export interface UpsertRowLevelPermissionPredicatesResultGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface AuthTokenGenqlSelection{
|
||||
token?: boolean | number
|
||||
expiresAt?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface ApplicationTokenPairGenqlSelection{
|
||||
applicationAccessToken?: AuthTokenGenqlSelection
|
||||
applicationRefreshToken?: AuthTokenGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface FrontComponentGenqlSelection{
|
||||
id?: boolean | number
|
||||
name?: boolean | number
|
||||
description?: boolean | number
|
||||
sourceComponentPath?: boolean | number
|
||||
builtComponentPath?: boolean | number
|
||||
componentName?: boolean | number
|
||||
builtComponentChecksum?: boolean | number
|
||||
universalIdentifier?: boolean | number
|
||||
applicationId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
isHeadless?: boolean | number
|
||||
usesSdkClient?: boolean | number
|
||||
applicationTokenPair?: ApplicationTokenPairGenqlSelection
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface LogicFunctionLogsGenqlSelection{
|
||||
/** Execution Logs */
|
||||
logs?: boolean | number
|
||||
@@ -6377,6 +6379,30 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const AuthToken_possibleTypes: string[] = ['AuthToken']
|
||||
export const isAuthToken = (obj?: { __typename?: any } | null): obj is AuthToken => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isAuthToken"')
|
||||
return AuthToken_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ApplicationTokenPair_possibleTypes: string[] = ['ApplicationTokenPair']
|
||||
export const isApplicationTokenPair = (obj?: { __typename?: any } | null): obj is ApplicationTokenPair => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isApplicationTokenPair"')
|
||||
return ApplicationTokenPair_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const FrontComponent_possibleTypes: string[] = ['FrontComponent']
|
||||
export const isFrontComponent = (obj?: { __typename?: any } | null): obj is FrontComponent => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isFrontComponent"')
|
||||
return FrontComponent_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const LogicFunction_possibleTypes: string[] = ['LogicFunction']
|
||||
export const isLogicFunction = (obj?: { __typename?: any } | null): obj is LogicFunction => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isLogicFunction"')
|
||||
@@ -7473,30 +7499,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const AuthToken_possibleTypes: string[] = ['AuthToken']
|
||||
export const isAuthToken = (obj?: { __typename?: any } | null): obj is AuthToken => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isAuthToken"')
|
||||
return AuthToken_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const ApplicationTokenPair_possibleTypes: string[] = ['ApplicationTokenPair']
|
||||
export const isApplicationTokenPair = (obj?: { __typename?: any } | null): obj is ApplicationTokenPair => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isApplicationTokenPair"')
|
||||
return ApplicationTokenPair_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const FrontComponent_possibleTypes: string[] = ['FrontComponent']
|
||||
export const isFrontComponent = (obj?: { __typename?: any } | null): obj is FrontComponent => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isFrontComponent"')
|
||||
return FrontComponent_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const LogicFunctionLogs_possibleTypes: string[] = ['LogicFunctionLogs']
|
||||
export const isLogicFunctionLogs = (obj?: { __typename?: any } | null): obj is LogicFunctionLogs => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isLogicFunctionLogs"')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
+6
@@ -34,6 +34,12 @@ export const APPLICATION_FRAGMENT = gql`
|
||||
agents {
|
||||
...AgentFields
|
||||
}
|
||||
frontComponents {
|
||||
id
|
||||
name
|
||||
description
|
||||
applicationId
|
||||
}
|
||||
objects {
|
||||
...ObjectMetadataFields
|
||||
}
|
||||
|
||||
+1
@@ -4,6 +4,7 @@ export const CUSTOM_WORKSPACE_APPLICATION_MOCK = {
|
||||
id: 'dc75f982-35a2-4c1b-a63d-bd1131215377',
|
||||
agents: [],
|
||||
applicationVariables: [],
|
||||
frontComponents: [],
|
||||
availablePackages: {},
|
||||
canBeUninstalled: false,
|
||||
description: 'workpace custom application',
|
||||
|
||||
+6
-1
@@ -112,7 +112,12 @@ export const SettingsAdminApps = () => {
|
||||
mobileGridAutoColumns={TABLE_GRID_MOBILE}
|
||||
isClickable
|
||||
>
|
||||
<TableCell color={themeCssVariables.font.color.primary}>
|
||||
<TableCell
|
||||
color={themeCssVariables.font.color.primary}
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
<OverflowingTextWithTooltip text={registration.name} />
|
||||
</TableCell>
|
||||
<TableCell overflow="hidden" align="right">
|
||||
|
||||
+232
@@ -0,0 +1,232 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { type ComponentType, type ReactNode } from 'react';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconBrandNpm,
|
||||
IconLink,
|
||||
IconMail,
|
||||
IconWorld,
|
||||
} from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
export type ContentEntry = {
|
||||
icon: ComponentType<{ size?: number }>;
|
||||
count: number;
|
||||
one: string;
|
||||
many: string;
|
||||
};
|
||||
|
||||
export type DeveloperLinks = {
|
||||
websiteUrl?: string;
|
||||
termsUrl?: string;
|
||||
emailSupport?: string;
|
||||
issueReportUrl?: string;
|
||||
sourcePackageUrl?: string;
|
||||
};
|
||||
|
||||
type SettingsApplicationAboutSidebarProps = {
|
||||
actionButton?: ReactNode;
|
||||
author?: string;
|
||||
category?: string;
|
||||
contentEntries?: ContentEntry[];
|
||||
currentVersion?: string;
|
||||
latestAvailableVersion?: string;
|
||||
developerLinks?: DeveloperLinks;
|
||||
};
|
||||
|
||||
const StyledSidebar = styled.div`
|
||||
flex-shrink: 0;
|
||||
width: 140px;
|
||||
`;
|
||||
|
||||
const StyledSidebarSection = styled.div`
|
||||
padding: ${themeCssVariables.spacing[3]} 0;
|
||||
|
||||
&:first-of-type {
|
||||
padding-top: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSidebarLabel = styled.div`
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
margin-bottom: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledSidebarValue = styled.div`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledContentItem = styled.div`
|
||||
align-items: center;
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
display: flex;
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
margin-bottom: ${themeCssVariables.spacing[2]};
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLink = styled.a`
|
||||
align-items: center;
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
display: flex;
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
margin-bottom: ${themeCssVariables.spacing[2]};
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const SettingsApplicationAboutSidebar = ({
|
||||
actionButton,
|
||||
author,
|
||||
category,
|
||||
contentEntries,
|
||||
currentVersion,
|
||||
latestAvailableVersion,
|
||||
developerLinks,
|
||||
}: SettingsApplicationAboutSidebarProps) => {
|
||||
const isSafeUrl = (url: string | undefined): url is string => {
|
||||
if (!isNonEmptyString(url)) return false;
|
||||
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
|
||||
return ['http:', 'https:', 'mailto:'].includes(parsed.protocol);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const filteredContentEntries = (contentEntries ?? []).filter(
|
||||
(entry) => entry.count > 0,
|
||||
);
|
||||
|
||||
const hasDeveloperLinks =
|
||||
isDefined(developerLinks) &&
|
||||
(isNonEmptyString(developerLinks.websiteUrl) ||
|
||||
isNonEmptyString(developerLinks.termsUrl) ||
|
||||
isNonEmptyString(developerLinks.emailSupport) ||
|
||||
isNonEmptyString(developerLinks.issueReportUrl) ||
|
||||
isNonEmptyString(developerLinks.sourcePackageUrl));
|
||||
|
||||
return (
|
||||
<StyledSidebar>
|
||||
{isDefined(actionButton) && (
|
||||
<StyledSidebarSection>{actionButton}</StyledSidebarSection>
|
||||
)}
|
||||
|
||||
{isDefined(author) && (
|
||||
<StyledSidebarSection>
|
||||
<StyledSidebarLabel>{t`Created by`}</StyledSidebarLabel>
|
||||
<StyledSidebarValue>{author}</StyledSidebarValue>
|
||||
</StyledSidebarSection>
|
||||
)}
|
||||
|
||||
{isDefined(category) && (
|
||||
<StyledSidebarSection>
|
||||
<StyledSidebarLabel>{t`Category`}</StyledSidebarLabel>
|
||||
<StyledSidebarValue>{category}</StyledSidebarValue>
|
||||
</StyledSidebarSection>
|
||||
)}
|
||||
|
||||
{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>
|
||||
))}
|
||||
</StyledSidebarSection>
|
||||
)}
|
||||
|
||||
{isDefined(currentVersion) && (
|
||||
<StyledSidebarSection>
|
||||
<StyledSidebarLabel>{t`Current`}</StyledSidebarLabel>
|
||||
<StyledSidebarValue>{currentVersion}</StyledSidebarValue>
|
||||
</StyledSidebarSection>
|
||||
)}
|
||||
|
||||
{isDefined(latestAvailableVersion) && (
|
||||
<StyledSidebarSection>
|
||||
<StyledSidebarLabel>{t`Latest`}</StyledSidebarLabel>
|
||||
<StyledSidebarValue>{latestAvailableVersion}</StyledSidebarValue>
|
||||
</StyledSidebarSection>
|
||||
)}
|
||||
|
||||
{hasDeveloperLinks && (
|
||||
<StyledSidebarSection>
|
||||
<StyledSidebarLabel>{t`Developers links`}</StyledSidebarLabel>
|
||||
{isSafeUrl(developerLinks.websiteUrl) && (
|
||||
<StyledLink
|
||||
href={developerLinks.websiteUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<IconWorld size={16} />
|
||||
{t`Website`}
|
||||
</StyledLink>
|
||||
)}
|
||||
{isSafeUrl(developerLinks.termsUrl) && (
|
||||
<StyledLink
|
||||
href={developerLinks.termsUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<IconLink size={16} />
|
||||
{t`Terms / Privacy`}
|
||||
</StyledLink>
|
||||
)}
|
||||
{isNonEmptyString(developerLinks.emailSupport) && (
|
||||
<StyledLink
|
||||
href={`mailto:${developerLinks.emailSupport}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<IconMail size={16} />
|
||||
{t`Email support`}
|
||||
</StyledLink>
|
||||
)}
|
||||
{isSafeUrl(developerLinks.issueReportUrl) && (
|
||||
<StyledLink
|
||||
href={developerLinks.issueReportUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<IconAlertTriangle size={16} />
|
||||
{t`Report an issue`}
|
||||
</StyledLink>
|
||||
)}
|
||||
{isSafeUrl(developerLinks.sourcePackageUrl) && (
|
||||
<StyledLink
|
||||
href={developerLinks.sourcePackageUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<IconBrandNpm size={16} />
|
||||
{t`Npm package`}
|
||||
</StyledLink>
|
||||
)}
|
||||
</StyledSidebarSection>
|
||||
)}
|
||||
</StyledSidebar>
|
||||
);
|
||||
};
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useState } from 'react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type SettingsApplicationScreenshotGalleryProps = {
|
||||
screenshots: string[];
|
||||
displayName: string;
|
||||
};
|
||||
|
||||
const StyledScreenshotsContainer = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${themeCssVariables.background.secondary};
|
||||
border: 1px solid ${themeCssVariables.border.color.medium};
|
||||
border-radius: ${themeCssVariables.border.radius.md};
|
||||
display: flex;
|
||||
height: 300px;
|
||||
justify-content: center;
|
||||
margin-bottom: ${themeCssVariables.spacing[2]};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledScreenshotImage = styled.img`
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledScreenshotThumbnails = styled.div`
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
margin-bottom: ${themeCssVariables.spacing[6]};
|
||||
`;
|
||||
|
||||
const StyledThumbnail = styled.div<{ isSelected?: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${themeCssVariables.background.secondary};
|
||||
border: 1px solid
|
||||
${({ isSelected }) =>
|
||||
isSelected
|
||||
? themeCssVariables.color.blue
|
||||
: themeCssVariables.border.color.medium};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 60px;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
border-color: ${themeCssVariables.color.blue};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledThumbnailImage = styled.img`
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const SettingsApplicationScreenshotGallery = ({
|
||||
screenshots,
|
||||
displayName,
|
||||
}: SettingsApplicationScreenshotGalleryProps) => {
|
||||
const [selectedScreenshotIndex, setSelectedScreenshotIndex] = useState(0);
|
||||
|
||||
if (screenshots.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const safeIndex = Math.min(selectedScreenshotIndex, screenshots.length - 1);
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledScreenshotsContainer>
|
||||
<StyledScreenshotImage
|
||||
src={screenshots[safeIndex]}
|
||||
alt={`${displayName} screenshot ${safeIndex + 1}`}
|
||||
/>
|
||||
</StyledScreenshotsContainer>
|
||||
<StyledScreenshotThumbnails>
|
||||
{screenshots.slice(0, 6).map((screenshot, index) => (
|
||||
<StyledThumbnail
|
||||
key={index}
|
||||
isSelected={index === selectedScreenshotIndex}
|
||||
onClick={() => setSelectedScreenshotIndex(index)}
|
||||
>
|
||||
<StyledThumbnailImage
|
||||
src={screenshot}
|
||||
alt={`${displayName} thumbnail ${index + 1}`}
|
||||
/>
|
||||
</StyledThumbnail>
|
||||
))}
|
||||
</StyledScreenshotThumbnails>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { useObjectAndFieldRows } from '@/settings/applications/hooks/useObjectAndFieldRows';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { getTestEnrichedObjectMetadataItemsMock } from '~/testing/utils/getTestEnrichedObjectMetadataItemsMock';
|
||||
|
||||
const mockObjectMetadataItems = getTestEnrichedObjectMetadataItemsMock();
|
||||
|
||||
const personObject = mockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'person',
|
||||
)!;
|
||||
const companyObject = mockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'company',
|
||||
)!;
|
||||
|
||||
const APP_ID = 'test-app-id';
|
||||
|
||||
const wrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: [],
|
||||
});
|
||||
|
||||
describe('useObjectAndFieldRows', () => {
|
||||
describe('with installed application', () => {
|
||||
it('should return object rows for installed application objects', () => {
|
||||
const installedApplication = {
|
||||
id: APP_ID,
|
||||
objects: [{ id: personObject.id }],
|
||||
name: 'Test App',
|
||||
canBeUninstalled: true,
|
||||
availablePackages: {},
|
||||
applicationVariables: [],
|
||||
agents: [],
|
||||
logicFunctions: [],
|
||||
frontComponents: [],
|
||||
};
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useObjectAndFieldRows({
|
||||
applicationId: APP_ID,
|
||||
installedApplication,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.objectRows).toHaveLength(1);
|
||||
expect(result.current.objectRows[0].key).toBe(personObject.nameSingular);
|
||||
expect(result.current.objectRows[0].labelPlural).toBe(
|
||||
personObject.labelPlural,
|
||||
);
|
||||
expect(result.current.objectRows[0].fieldsCount).toBeGreaterThan(0);
|
||||
expect(result.current.objectRows[0].link).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return empty object rows when application has no objects', () => {
|
||||
const installedApplication = {
|
||||
id: APP_ID,
|
||||
objects: [],
|
||||
name: 'Test App',
|
||||
canBeUninstalled: true,
|
||||
availablePackages: {},
|
||||
applicationVariables: [],
|
||||
agents: [],
|
||||
logicFunctions: [],
|
||||
frontComponents: [],
|
||||
};
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useObjectAndFieldRows({
|
||||
applicationId: APP_ID,
|
||||
installedApplication,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.objectRows).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should return field group rows for fields added to other objects', () => {
|
||||
const fieldBelongingToApp = companyObject.fields[0];
|
||||
|
||||
const installedApplication = {
|
||||
id: fieldBelongingToApp.applicationId ?? APP_ID,
|
||||
objects: [{ id: personObject.id }],
|
||||
name: 'Test App',
|
||||
canBeUninstalled: true,
|
||||
availablePackages: {},
|
||||
applicationVariables: [],
|
||||
agents: [],
|
||||
logicFunctions: [],
|
||||
frontComponents: [],
|
||||
};
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useObjectAndFieldRows({
|
||||
applicationId: installedApplication.id,
|
||||
installedApplication,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Field group rows should not include the app's own objects
|
||||
const hasOwnObject = result.current.fieldGroupRows.some(
|
||||
(row) => row.key === personObject.nameSingular,
|
||||
);
|
||||
expect(hasOwnObject).toBe(false);
|
||||
});
|
||||
|
||||
it('should exclude deny-listed objects from field group rows', () => {
|
||||
const installedApplication = {
|
||||
id: APP_ID,
|
||||
objects: [],
|
||||
name: 'Test App',
|
||||
canBeUninstalled: true,
|
||||
availablePackages: {},
|
||||
applicationVariables: [],
|
||||
agents: [],
|
||||
logicFunctions: [],
|
||||
frontComponents: [],
|
||||
};
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useObjectAndFieldRows({
|
||||
applicationId: APP_ID,
|
||||
installedApplication,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
const hasDeniedObject = result.current.fieldGroupRows.some(
|
||||
(row) => row.key === 'timelineActivity' || row.key === 'favorite',
|
||||
);
|
||||
expect(hasDeniedObject).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with manifest content', () => {
|
||||
it('should return object rows from manifest objects', () => {
|
||||
const manifestContent = {
|
||||
objects: [
|
||||
{
|
||||
universalIdentifier: 'uid-1',
|
||||
nameSingular: 'customObject',
|
||||
namePlural: 'customObjects',
|
||||
labelSingular: 'Custom Object',
|
||||
labelPlural: 'Custom Objects',
|
||||
icon: 'IconBox',
|
||||
fields: [{ name: 'field1' }, { name: 'field2' }],
|
||||
},
|
||||
],
|
||||
fields: [],
|
||||
} as unknown as Manifest;
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useObjectAndFieldRows({
|
||||
applicationId: 'app-uid',
|
||||
manifestContent,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.objectRows).toHaveLength(1);
|
||||
expect(result.current.objectRows[0].key).toBe('customObject');
|
||||
expect(result.current.objectRows[0].labelPlural).toBe('Custom Objects');
|
||||
expect(result.current.objectRows[0].fieldsCount).toBe(2);
|
||||
expect(result.current.objectRows[0].tagItem.applicationId).toBe(
|
||||
'app-uid',
|
||||
);
|
||||
});
|
||||
|
||||
it('should return field group rows grouped by object from manifest fields', () => {
|
||||
const manifestContent = {
|
||||
objects: [
|
||||
{
|
||||
universalIdentifier: 'custom-obj-uid',
|
||||
nameSingular: 'customObj',
|
||||
namePlural: 'customObjs',
|
||||
labelSingular: 'Custom',
|
||||
labelPlural: 'Customs',
|
||||
icon: 'IconBox',
|
||||
fields: [],
|
||||
},
|
||||
],
|
||||
fields: [
|
||||
{
|
||||
objectUniversalIdentifier: 'custom-obj-uid',
|
||||
name: 'field1',
|
||||
},
|
||||
{
|
||||
objectUniversalIdentifier: 'custom-obj-uid',
|
||||
name: 'field2',
|
||||
},
|
||||
{
|
||||
objectUniversalIdentifier: 'custom-obj-uid',
|
||||
name: 'field3',
|
||||
},
|
||||
],
|
||||
} as unknown as Manifest;
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useObjectAndFieldRows({
|
||||
applicationId: 'app-uid',
|
||||
manifestContent,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.fieldGroupRows).toHaveLength(1);
|
||||
expect(result.current.fieldGroupRows[0].key).toBe('customObj');
|
||||
expect(result.current.fieldGroupRows[0].fieldsCount).toBe(3);
|
||||
});
|
||||
|
||||
it('should return empty field group rows when manifest has no fields', () => {
|
||||
const manifestContent = {
|
||||
objects: [],
|
||||
fields: [],
|
||||
} as unknown as Manifest;
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useObjectAndFieldRows({
|
||||
applicationId: 'app-uid',
|
||||
manifestContent,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.fieldGroupRows).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should return empty rows when no data is provided', () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useObjectAndFieldRows({
|
||||
applicationId: 'app-uid',
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
expect(result.current.objectRows).toHaveLength(0);
|
||||
expect(result.current.fieldGroupRows).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('data source priority', () => {
|
||||
it('should use installed application data when both sources are provided', () => {
|
||||
const installedApplication = {
|
||||
id: APP_ID,
|
||||
objects: [{ id: personObject.id }],
|
||||
name: 'Test App',
|
||||
canBeUninstalled: true,
|
||||
availablePackages: {},
|
||||
applicationVariables: [],
|
||||
agents: [],
|
||||
logicFunctions: [],
|
||||
frontComponents: [],
|
||||
};
|
||||
|
||||
const manifestContent = {
|
||||
objects: [
|
||||
{
|
||||
universalIdentifier: 'uid-1',
|
||||
nameSingular: 'manifestObj',
|
||||
namePlural: 'manifestObjs',
|
||||
labelSingular: 'Manifest',
|
||||
labelPlural: 'Manifests',
|
||||
icon: 'IconBox',
|
||||
fields: [],
|
||||
},
|
||||
],
|
||||
fields: [],
|
||||
} as unknown as Manifest;
|
||||
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useObjectAndFieldRows({
|
||||
applicationId: APP_ID,
|
||||
installedApplication,
|
||||
manifestContent,
|
||||
}),
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
// Should use installed data, not manifest
|
||||
expect(result.current.objectRows[0].key).toBe(personObject.nameSingular);
|
||||
expect(
|
||||
result.current.objectRows.some((r) => r.key === 'manifestObj'),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
+179
@@ -0,0 +1,179 @@
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useMemo } from 'react';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { type Application } from '~/generated-metadata/graphql';
|
||||
import { type ApplicationDataTableRow } from '~/pages/settings/applications/components/SettingsApplicationDataTable';
|
||||
import { findObjectNameByUniversalIdentifier } from '~/pages/settings/applications/utils/findObjectNameByUniversalIdentifier';
|
||||
|
||||
type InstalledApplicationForObjectRows = Omit<
|
||||
Application,
|
||||
'objects' | 'universalIdentifier' | 'frontComponents'
|
||||
> & {
|
||||
objects: { id: string }[];
|
||||
};
|
||||
|
||||
export const useObjectAndFieldRows = ({
|
||||
applicationId,
|
||||
installedApplication,
|
||||
manifestContent,
|
||||
}: {
|
||||
applicationId: string;
|
||||
installedApplication?: InstalledApplicationForObjectRows;
|
||||
manifestContent?: Manifest;
|
||||
}) => {
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
|
||||
const installedObjectIds = useMemo(
|
||||
() => installedApplication?.objects.map((object) => object.id) ?? [],
|
||||
[installedApplication?.objects],
|
||||
);
|
||||
|
||||
const objectRows = useMemo((): ApplicationDataTableRow[] => {
|
||||
if (isDefined(installedApplication)) {
|
||||
if (installedApplication.objects.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return objectMetadataItems
|
||||
.filter((item) => installedObjectIds.includes(item.id))
|
||||
.map((item) => ({
|
||||
key: item.nameSingular,
|
||||
labelPlural: item.labelPlural,
|
||||
icon: item.icon ?? undefined,
|
||||
fieldsCount: item.fields.filter((f) => !isHiddenSystemField(f))
|
||||
.length,
|
||||
link: getSettingsPath(SettingsPath.ObjectDetail, {
|
||||
objectNamePlural: item.namePlural,
|
||||
}),
|
||||
tagItem: {
|
||||
isCustom: item.isCustom,
|
||||
isRemote: item.isRemote,
|
||||
applicationId: item.applicationId,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
return (manifestContent?.objects ?? []).map((appObject) => ({
|
||||
key: appObject.nameSingular,
|
||||
labelPlural: appObject.labelPlural,
|
||||
icon: appObject.icon ?? undefined,
|
||||
fieldsCount: appObject.fields.length,
|
||||
tagItem: { applicationId },
|
||||
}));
|
||||
}, [
|
||||
installedApplication,
|
||||
manifestContent?.objects,
|
||||
objectMetadataItems,
|
||||
installedObjectIds,
|
||||
applicationId,
|
||||
]);
|
||||
|
||||
const fieldGroupRows = useMemo((): ApplicationDataTableRow[] => {
|
||||
if (isDefined(installedApplication)) {
|
||||
const FIELD_GROUP_DENY_LIST = ['timelineActivity', 'favorite'];
|
||||
|
||||
return objectMetadataItems
|
||||
.filter((item) => {
|
||||
if (installedObjectIds.includes(item.id)) return false;
|
||||
if (FIELD_GROUP_DENY_LIST.includes(item.nameSingular)) return false;
|
||||
|
||||
return item.fields.some(
|
||||
(field) => field.applicationId === installedApplication.id,
|
||||
);
|
||||
})
|
||||
.map((item) => ({
|
||||
key: item.nameSingular,
|
||||
labelPlural: item.labelPlural,
|
||||
icon: item.icon ?? undefined,
|
||||
fieldsCount: item.fields.filter(
|
||||
(field) => field.applicationId === installedApplication.id,
|
||||
).length,
|
||||
link: getSettingsPath(SettingsPath.ObjectDetail, {
|
||||
objectNamePlural: item.namePlural,
|
||||
}),
|
||||
tagItem: {
|
||||
isCustom: item.isCustom,
|
||||
isRemote: item.isRemote,
|
||||
applicationId: item.applicationId,
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
const manifestFields = manifestContent?.fields ?? [];
|
||||
const manifestObjects = manifestContent?.objects ?? [];
|
||||
|
||||
if (manifestFields.length === 0) return [];
|
||||
|
||||
const groupMap = new Map<
|
||||
string,
|
||||
{ objectUniversalIdentifier: string; count: number }
|
||||
>();
|
||||
|
||||
for (const field of manifestFields) {
|
||||
const objectUid = field.objectUniversalIdentifier;
|
||||
const existing = groupMap.get(objectUid);
|
||||
|
||||
if (isDefined(existing)) {
|
||||
existing.count++;
|
||||
} else {
|
||||
groupMap.set(objectUid, {
|
||||
objectUniversalIdentifier: objectUid,
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(groupMap.values())
|
||||
.map((group) => {
|
||||
const appObject = manifestObjects.find(
|
||||
(obj) => obj.universalIdentifier === group.objectUniversalIdentifier,
|
||||
);
|
||||
|
||||
if (isDefined(appObject)) {
|
||||
return {
|
||||
key: appObject.nameSingular,
|
||||
labelPlural: appObject.labelPlural,
|
||||
icon: appObject.icon ?? undefined,
|
||||
fieldsCount: group.count,
|
||||
tagItem: { applicationId },
|
||||
};
|
||||
}
|
||||
|
||||
const standardObjectName = findObjectNameByUniversalIdentifier(
|
||||
group.objectUniversalIdentifier,
|
||||
);
|
||||
|
||||
const objectMetadataItem = isDefined(standardObjectName)
|
||||
? objectMetadataItems.find(
|
||||
(item) => item.nameSingular === standardObjectName,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
key: objectMetadataItem.nameSingular,
|
||||
labelPlural: objectMetadataItem.labelPlural,
|
||||
icon: objectMetadataItem.icon ?? undefined,
|
||||
fieldsCount: group.count,
|
||||
tagItem: {},
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
}, [
|
||||
installedApplication,
|
||||
manifestContent?.fields,
|
||||
manifestContent?.objects,
|
||||
objectMetadataItems,
|
||||
installedObjectIds,
|
||||
applicationId,
|
||||
]);
|
||||
|
||||
return { objectRows, fieldGroupRows };
|
||||
};
|
||||
+17
-43
@@ -1,13 +1,15 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { type LogicFunction } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
type LogicFunctionTableRow,
|
||||
StyledTableRow,
|
||||
} from '@/settings/logic-functions/components/SettingsLogicFunctionsTable';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import {
|
||||
IconChevronRight,
|
||||
IconCode,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui/display';
|
||||
import { StyledTableRow } from '@/settings/logic-functions/components/SettingsLogicFunctionsTable';
|
||||
import { useContext } from 'react';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledIconContainer = styled.span`
|
||||
@@ -21,43 +23,13 @@ const StyledIconChevronRightContainer = styled(StyledIconContainer)`
|
||||
|
||||
export const SettingsLogicFunctionsFieldItemTableRow = ({
|
||||
logicFunction,
|
||||
to,
|
||||
}: {
|
||||
logicFunction: LogicFunction;
|
||||
to: string;
|
||||
logicFunction: LogicFunctionTableRow;
|
||||
}) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const computeTrigger = () => {
|
||||
const cronTrigger = logicFunction.cronTriggerSettings;
|
||||
|
||||
const routeTrigger = logicFunction.httpRouteTriggerSettings;
|
||||
|
||||
const databaseEventTriggerSettings =
|
||||
logicFunction.databaseEventTriggerSettings;
|
||||
|
||||
const isTool = logicFunction.isTool;
|
||||
|
||||
if (isTool) {
|
||||
return 'Tool';
|
||||
}
|
||||
|
||||
if (cronTrigger) {
|
||||
return 'Cron';
|
||||
}
|
||||
|
||||
if (routeTrigger) {
|
||||
return 'Route';
|
||||
}
|
||||
|
||||
if (databaseEventTriggerSettings) {
|
||||
return databaseEventTriggerSettings.eventName;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
return (
|
||||
<StyledTableRow to={to}>
|
||||
<StyledTableRow to={logicFunction.link}>
|
||||
<TableCell
|
||||
color={themeCssVariables.font.color.primary}
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
@@ -74,18 +46,20 @@ export const SettingsLogicFunctionsFieldItemTableRow = ({
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
>
|
||||
<OverflowingTextWithTooltip text={computeTrigger()} />
|
||||
<OverflowingTextWithTooltip text={logicFunction.trigger} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align="center"
|
||||
padding={`0 ${themeCssVariables.spacing[1]} 0 ${themeCssVariables.spacing[2]}`}
|
||||
>
|
||||
<StyledIconChevronRightContainer>
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledIconChevronRightContainer>
|
||||
{logicFunction.link && (
|
||||
<StyledIconChevronRightContainer>
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledIconChevronRightContainer>
|
||||
)}
|
||||
</TableCell>
|
||||
</StyledTableRow>
|
||||
);
|
||||
|
||||
+10
-13
@@ -4,14 +4,17 @@ import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { styled } from '@linaria/react';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { type LogicFunction } from '~/generated-metadata/graphql';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import React from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
export type LogicFunctionTableRow = {
|
||||
key: string;
|
||||
name: string;
|
||||
trigger: string;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
export const StyledTableRow = (
|
||||
props: React.ComponentProps<typeof TableRow>,
|
||||
) => (
|
||||
@@ -29,10 +32,8 @@ const StyledTableBodyContainer = styled.div`
|
||||
export const SettingsLogicFunctionsTable = ({
|
||||
logicFunctions,
|
||||
}: {
|
||||
logicFunctions: LogicFunction[];
|
||||
logicFunctions: LogicFunctionTableRow[];
|
||||
}) => {
|
||||
const { applicationId = '' } = useParams();
|
||||
|
||||
const { t } = useLingui();
|
||||
|
||||
if (logicFunctions.length === 0) {
|
||||
@@ -48,14 +49,10 @@ export const SettingsLogicFunctionsTable = ({
|
||||
</StyledTableRow>
|
||||
<StyledTableBodyContainer>
|
||||
<TableBody>
|
||||
{logicFunctions.map((logicFunction: LogicFunction) => (
|
||||
{logicFunctions.map((logicFunction) => (
|
||||
<SettingsLogicFunctionsFieldItemTableRow
|
||||
key={logicFunction.id}
|
||||
key={logicFunction.key}
|
||||
logicFunction={logicFunction}
|
||||
to={getSettingsPath(SettingsPath.ApplicationLogicFunctionDetail, {
|
||||
applicationId,
|
||||
logicFunctionId: logicFunction.id,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
|
||||
+201
-29
@@ -1,29 +1,48 @@
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { FindOneApplicationDocument } from '~/generated-metadata/graphql';
|
||||
import { useUpgradeApplication } from '@/marketplace/hooks/useUpgradeApplication';
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import type { SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconApps,
|
||||
IconBox,
|
||||
IconCommand,
|
||||
IconGraph,
|
||||
IconInfoCircle,
|
||||
IconLego,
|
||||
IconListDetails,
|
||||
IconLock,
|
||||
IconSettings,
|
||||
} from 'twenty-ui/display';
|
||||
import {
|
||||
ApplicationRegistrationSourceType,
|
||||
FindMarketplaceAppDetailDocument,
|
||||
FindOneApplicationDocument,
|
||||
PermissionFlagType,
|
||||
UninstallApplicationDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { SettingsApplicationDetailSkeletonLoader } from '~/pages/settings/applications/components/SettingsApplicationDetailSkeletonLoader';
|
||||
import { SettingsApplicationDetailTitle } from '~/pages/settings/applications/components/SettingsApplicationDetailTitle';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { SettingsApplicationDetailContentTab } from '~/pages/settings/applications/tabs/SettingsApplicationDetailContentTab';
|
||||
import { SettingsApplicationCustomTab } from '~/pages/settings/applications/tabs/SettingsApplicationCustomTab';
|
||||
import { SettingsApplicationDetailAboutTab } from '~/pages/settings/applications/tabs/SettingsApplicationDetailAboutTab';
|
||||
import { SettingsApplicationDetailContentTab } from '~/pages/settings/applications/tabs/SettingsApplicationDetailContentTab';
|
||||
import { SettingsApplicationDetailSettingsTab } from '~/pages/settings/applications/tabs/SettingsApplicationDetailSettingsTab';
|
||||
import { SettingsApplicationPermissionsTab } from '~/pages/settings/applications/tabs/SettingsApplicationPermissionsTab';
|
||||
import { SettingsApplicationCustomTab } from '~/pages/settings/applications/tabs/SettingsApplicationCustomTab';
|
||||
import type { SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
|
||||
import { isNewerSemver } from '~/pages/settings/applications/utils/isNewerSemver';
|
||||
|
||||
const APPLICATION_DETAIL_ID = 'application-detail-id';
|
||||
|
||||
@@ -42,14 +61,133 @@ export const SettingsApplicationDetails = () => {
|
||||
|
||||
const application = data?.findOneApplication;
|
||||
|
||||
const applicationName = application?.name ?? t`Application details`;
|
||||
const applicationDescription = application?.description ?? undefined;
|
||||
const applicationLogoUrl =
|
||||
application?.applicationRegistration?.logoUrl ?? undefined;
|
||||
const { data: detailData } = useQuery(FindMarketplaceAppDetailDocument, {
|
||||
variables: { universalIdentifier: application?.universalIdentifier ?? '' },
|
||||
skip: !application?.universalIdentifier,
|
||||
});
|
||||
|
||||
const detail = detailData?.findMarketplaceAppDetail;
|
||||
const manifest = detail?.manifest as Manifest | undefined;
|
||||
const app = manifest?.application;
|
||||
|
||||
const displayName =
|
||||
app?.displayName ?? application?.name ?? t`Application details`;
|
||||
const description = app?.description ?? application?.description ?? undefined;
|
||||
const logoUrl =
|
||||
app?.logoUrl ?? application?.applicationRegistration?.logoUrl ?? undefined;
|
||||
|
||||
const settingsCustomTabFrontComponentId =
|
||||
application?.settingsCustomTabFrontComponentId;
|
||||
|
||||
const { upgrade, isUpgrading } = useUpgradeApplication();
|
||||
|
||||
const canInstallMarketplaceApps = useHasPermissionFlag(
|
||||
PermissionFlagType.MARKETPLACE_APPS,
|
||||
);
|
||||
|
||||
const sourceType = application?.applicationRegistration?.sourceType;
|
||||
const isNpmApp = sourceType === ApplicationRegistrationSourceType.NPM;
|
||||
const registrationId = detail?.id ?? application?.applicationRegistration?.id;
|
||||
const currentVersion = application?.version;
|
||||
const latestAvailableVersion =
|
||||
detail?.latestAvailableVersion ??
|
||||
application?.applicationRegistration?.latestAvailableVersion;
|
||||
|
||||
const hasUpdate =
|
||||
isNpmApp &&
|
||||
isDefined(latestAvailableVersion) &&
|
||||
isDefined(currentVersion) &&
|
||||
isNewerSemver(latestAvailableVersion, currentVersion);
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
if (!isDefined(registrationId) || !isDefined(latestAvailableVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await upgrade({
|
||||
appRegistrationId: registrationId,
|
||||
targetVersion: latestAvailableVersion,
|
||||
});
|
||||
};
|
||||
|
||||
const [uninstallApplication] = useMutation(UninstallApplicationDocument);
|
||||
const [isUninstalling, setIsUninstalling] = useState(false);
|
||||
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
|
||||
const navigate = useNavigateSettings();
|
||||
|
||||
const handleUninstall = async () => {
|
||||
if (!isDefined(application)) return;
|
||||
|
||||
setIsUninstalling(true);
|
||||
try {
|
||||
await uninstallApplication({
|
||||
variables: { universalIdentifier: application.universalIdentifier },
|
||||
});
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Application successfully uninstalled.`,
|
||||
});
|
||||
navigate(SettingsPath.Applications);
|
||||
} catch {
|
||||
enqueueErrorSnackBar({ message: t`Error uninstalling application.` });
|
||||
} finally {
|
||||
setIsUninstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
|
||||
const applicationObjectIds = useMemo(
|
||||
() => new Set((application?.objects ?? []).map((obj) => obj.id)),
|
||||
[application?.objects],
|
||||
);
|
||||
|
||||
const appFieldExtensionsCount = useMemo(() => {
|
||||
if (!isDefined(application)) return 0;
|
||||
|
||||
return objectMetadataItems
|
||||
.filter((item) => !applicationObjectIds.has(item.id))
|
||||
.reduce(
|
||||
(total, item) =>
|
||||
total +
|
||||
item.fields.filter((field) => field.applicationId === application.id)
|
||||
.length,
|
||||
0,
|
||||
);
|
||||
}, [objectMetadataItems, applicationObjectIds, application]);
|
||||
|
||||
const contentEntries = [
|
||||
{
|
||||
icon: IconBox,
|
||||
count: (application?.objects ?? []).length,
|
||||
one: t`object`,
|
||||
many: t`objects`,
|
||||
},
|
||||
{
|
||||
icon: IconListDetails,
|
||||
count: appFieldExtensionsCount,
|
||||
one: t`field`,
|
||||
many: t`fields`,
|
||||
},
|
||||
{
|
||||
icon: IconCommand,
|
||||
count: (application?.logicFunctions ?? []).length,
|
||||
one: t`logic function`,
|
||||
many: t`logic functions`,
|
||||
},
|
||||
{
|
||||
icon: IconGraph,
|
||||
count: (application?.frontComponents ?? []).length,
|
||||
one: t`front component`,
|
||||
many: t`front components`,
|
||||
},
|
||||
{
|
||||
icon: IconLego,
|
||||
count: (application?.agents ?? []).length,
|
||||
one: t`agent`,
|
||||
many: t`agents`,
|
||||
},
|
||||
];
|
||||
|
||||
const tabs: SingleTabProps[] = [
|
||||
{ id: 'about', title: t`About`, Icon: IconInfoCircle },
|
||||
{ id: 'content', title: t`Content`, Icon: IconBox },
|
||||
@@ -78,17 +216,55 @@ export const SettingsApplicationDetails = () => {
|
||||
];
|
||||
|
||||
const renderActiveTabContent = () => {
|
||||
if (!isDefined(application)) {
|
||||
return <SettingsApplicationDetailSkeletonLoader />;
|
||||
}
|
||||
|
||||
switch (activeTabId) {
|
||||
case 'about':
|
||||
return <SettingsApplicationDetailAboutTab application={application} />;
|
||||
return (
|
||||
<SettingsApplicationDetailAboutTab
|
||||
displayName={displayName}
|
||||
description={description}
|
||||
aboutDescription={app?.aboutDescription}
|
||||
screenshots={app?.screenshots}
|
||||
author={app?.author}
|
||||
category={app?.category}
|
||||
contentEntries={contentEntries}
|
||||
currentVersion={currentVersion ?? undefined}
|
||||
latestAvailableVersion={latestAvailableVersion ?? undefined}
|
||||
developerLinks={
|
||||
isDefined(app)
|
||||
? {
|
||||
websiteUrl: app.websiteUrl,
|
||||
termsUrl: app.termsUrl,
|
||||
emailSupport: app.emailSupport,
|
||||
issueReportUrl: app.issueReportUrl,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
isInstalled={true}
|
||||
canInstallMarketplaceApps={canInstallMarketplaceApps}
|
||||
hasUpdate={hasUpdate}
|
||||
onUpgrade={handleUpgrade}
|
||||
isUpgrading={isUpgrading}
|
||||
canBeUninstalled={application.canBeUninstalled}
|
||||
onUninstall={handleUninstall}
|
||||
isUninstalling={isUninstalling}
|
||||
/>
|
||||
);
|
||||
case 'content':
|
||||
return (
|
||||
<SettingsApplicationDetailContentTab application={application} />
|
||||
<SettingsApplicationDetailContentTab
|
||||
applicationId={application.id}
|
||||
installedApplication={application}
|
||||
manifestContent={manifest}
|
||||
/>
|
||||
);
|
||||
case 'permissions':
|
||||
return (
|
||||
<SettingsApplicationPermissionsTab
|
||||
defaultRoleId={application?.defaultRoleId}
|
||||
defaultRoleId={application.defaultRoleId}
|
||||
/>
|
||||
);
|
||||
case 'settings':
|
||||
@@ -114,9 +290,9 @@ export const SettingsApplicationDetails = () => {
|
||||
<SubMenuTopBarContainer
|
||||
title={
|
||||
<SettingsApplicationDetailTitle
|
||||
displayName={applicationName}
|
||||
description={applicationDescription}
|
||||
logoUrl={applicationLogoUrl}
|
||||
displayName={displayName}
|
||||
description={description}
|
||||
logoUrl={logoUrl}
|
||||
/>
|
||||
}
|
||||
links={[
|
||||
@@ -128,16 +304,12 @@ export const SettingsApplicationDetails = () => {
|
||||
children: t`Applications`,
|
||||
href: getSettingsPath(SettingsPath.Applications),
|
||||
},
|
||||
{ children: applicationName },
|
||||
{ children: displayName },
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<TabList tabs={tabs} componentInstanceId={APPLICATION_DETAIL_ID} />
|
||||
{!isDefined(application) ? (
|
||||
<SettingsApplicationDetailSkeletonLoader />
|
||||
) : (
|
||||
renderActiveTabContent()
|
||||
)}
|
||||
{renderActiveTabContent()}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
|
||||
+49
-368
@@ -1,192 +1,49 @@
|
||||
import { LazyMarkdownRenderer } from '@/ai/components/LazyMarkdownRenderer';
|
||||
import { useInstallMarketplaceApp } from '@/marketplace/hooks/useInstallMarketplaceApp';
|
||||
import { useUpgradeApplication } from '@/marketplace/hooks/useUpgradeApplication';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconBook,
|
||||
IconBox,
|
||||
IconBrandNpm,
|
||||
IconCheck,
|
||||
IconCommand,
|
||||
IconDownload,
|
||||
IconGraph,
|
||||
IconInfoCircle,
|
||||
IconLego,
|
||||
IconLink,
|
||||
IconListDetails,
|
||||
IconLock,
|
||||
IconMail,
|
||||
IconSettings,
|
||||
IconShield,
|
||||
IconUpload,
|
||||
IconWorld,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import {
|
||||
PermissionFlagType,
|
||||
FindOneApplicationByUniversalIdentifierDocument,
|
||||
FindMarketplaceAppDetailDocument,
|
||||
ApplicationRegistrationSourceType,
|
||||
FindMarketplaceAppDetailDocument,
|
||||
FindOneApplicationByUniversalIdentifierDocument,
|
||||
PermissionFlagType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { SettingsApplicationPermissionsTab } from '~/pages/settings/applications/tabs/SettingsApplicationPermissionsTab';
|
||||
import { SettingsAvailableApplicationDetailContentTab } from '~/pages/settings/applications/tabs/SettingsAvailableApplicationDetailContentTab';
|
||||
import { SettingsApplicationDetailTitle } from '~/pages/settings/applications/components/SettingsApplicationDetailTitle';
|
||||
import { SettingsApplicationDetailAboutTab } from '~/pages/settings/applications/tabs/SettingsApplicationDetailAboutTab';
|
||||
import { SettingsApplicationDetailContentTab } from '~/pages/settings/applications/tabs/SettingsApplicationDetailContentTab';
|
||||
import { SettingsApplicationPermissionsTab } from '~/pages/settings/applications/tabs/SettingsApplicationPermissionsTab';
|
||||
import { isNewerSemver } from '~/pages/settings/applications/utils/isNewerSemver';
|
||||
import { useUpgradeApplication } from '@/marketplace/hooks/useUpgradeApplication';
|
||||
import { SettingsApplicationDetailSettingsTab } from '~/pages/settings/applications/tabs/SettingsApplicationDetailSettingsTab';
|
||||
|
||||
const AVAILABLE_APPLICATION_DETAIL_ID = 'available-application-detail';
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const StyledMainContent = styled.div`
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledSidebar = styled.div`
|
||||
flex-shrink: 0;
|
||||
width: 140px;
|
||||
`;
|
||||
|
||||
const StyledSidebarSection = styled.div`
|
||||
padding: ${themeCssVariables.spacing[3]} 0;
|
||||
|
||||
&:first-of-type {
|
||||
padding-top: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSidebarLabel = styled.div`
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
margin-bottom: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledSidebarValue = styled.div`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledContentItem = styled.div`
|
||||
align-items: center;
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
display: flex;
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
margin-bottom: ${themeCssVariables.spacing[2]};
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLink = styled.a`
|
||||
align-items: center;
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
display: flex;
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
margin-bottom: ${themeCssVariables.spacing[2]};
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledScreenshotsContainer = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${themeCssVariables.background.secondary};
|
||||
border: 1px solid ${themeCssVariables.border.color.medium};
|
||||
border-radius: ${themeCssVariables.border.radius.md};
|
||||
display: flex;
|
||||
height: 300px;
|
||||
justify-content: center;
|
||||
margin-bottom: ${themeCssVariables.spacing[2]};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledScreenshotImage = styled.img`
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledScreenshotThumbnails = styled.div`
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
margin-bottom: ${themeCssVariables.spacing[6]};
|
||||
`;
|
||||
|
||||
const StyledThumbnail = styled.div<{ isSelected?: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${themeCssVariables.background.secondary};
|
||||
border: 1px solid
|
||||
${({ isSelected }) =>
|
||||
isSelected
|
||||
? themeCssVariables.color.blue
|
||||
: themeCssVariables.border.color.medium};
|
||||
border-radius: ${themeCssVariables.border.radius.sm};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 60px;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
border-color: ${themeCssVariables.color.blue};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledThumbnailImage = styled.img`
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledSectionTitle = styled.h2`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-size: ${themeCssVariables.font.size.xl};
|
||||
font-weight: ${themeCssVariables.font.weight.semiBold};
|
||||
margin: 0 0 ${themeCssVariables.spacing[3]} 0;
|
||||
`;
|
||||
|
||||
const StyledAboutContainer = styled.div``;
|
||||
|
||||
export const SettingsAvailableApplicationDetails = () => {
|
||||
const { availableApplicationId = '' } = useParams<{
|
||||
availableApplicationId: string;
|
||||
}>();
|
||||
|
||||
const [selectedScreenshotIndex, setSelectedScreenshotIndex] = useState(0);
|
||||
|
||||
const { install, isInstalling } = useInstallMarketplaceApp();
|
||||
const { upgrade, isUpgrading } = useUpgradeApplication();
|
||||
|
||||
const canInstallMarketplaceApps = useHasPermissionFlag(
|
||||
PermissionFlagType.MARKETPLACE_APPS,
|
||||
@@ -213,8 +70,6 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
|
||||
const displayName = app?.displayName ?? detail?.name ?? '';
|
||||
const description = app?.description ?? '';
|
||||
const screenshots = app?.screenshots ?? [];
|
||||
const aboutDescription = app?.aboutDescription;
|
||||
|
||||
const currentVersion = application?.version;
|
||||
const latestAvailableVersion = detail?.latestAvailableVersion;
|
||||
@@ -228,14 +83,18 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
: undefined;
|
||||
|
||||
const isUnlisted = isDefined(detail) && !detail.isListed;
|
||||
const installedApp = applicationData?.findOneApplication;
|
||||
const isAlreadyInstalled = isDefined(installedApp);
|
||||
const hasScreenshots = screenshots.length > 0;
|
||||
const isAlreadyInstalled = isDefined(application);
|
||||
|
||||
const defaultRole = manifest?.roles?.find(
|
||||
(r) => r.universalIdentifier === app?.defaultRoleUniversalIdentifier,
|
||||
);
|
||||
|
||||
const hasUpdate =
|
||||
isNpmApp &&
|
||||
isDefined(latestAvailableVersion) &&
|
||||
isDefined(currentVersion) &&
|
||||
isNewerSemver(latestAvailableVersion, currentVersion);
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (isDefined(detail)) {
|
||||
await install({
|
||||
@@ -244,14 +103,6 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const hasUpdate =
|
||||
isNpmApp &&
|
||||
isDefined(latestAvailableVersion) &&
|
||||
isDefined(currentVersion) &&
|
||||
isNewerSemver(latestAvailableVersion, currentVersion);
|
||||
|
||||
const { upgrade, isUpgrading } = useUpgradeApplication();
|
||||
|
||||
const handleUpgrade = async () => {
|
||||
if (!isDefined(registrationId) || !isDefined(latestAvailableVersion)) {
|
||||
return;
|
||||
@@ -263,55 +114,6 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const getActionButton = () => {
|
||||
if (!canInstallMarketplaceApps) {
|
||||
return null;
|
||||
}
|
||||
if (!isAlreadyInstalled) {
|
||||
return (
|
||||
<StyledSidebarSection>
|
||||
<Button
|
||||
Icon={IconDownload}
|
||||
title={isInstalling ? t`Installing...` : t`Install`}
|
||||
variant={'primary'}
|
||||
accent={'blue'}
|
||||
onClick={handleInstall}
|
||||
disabled={isInstalling}
|
||||
/>
|
||||
</StyledSidebarSection>
|
||||
);
|
||||
}
|
||||
if (hasUpdate && isDefined(registrationId)) {
|
||||
return (
|
||||
<StyledSidebarSection>
|
||||
<Button
|
||||
Icon={IconUpload}
|
||||
title={
|
||||
isUpgrading
|
||||
? t`Upgrading...`
|
||||
: t`Upgrade to ${latestAvailableVersion}`
|
||||
}
|
||||
variant={'secondary'}
|
||||
accent={'blue'}
|
||||
onClick={handleUpgrade}
|
||||
disabled={isUpgrading}
|
||||
/>
|
||||
</StyledSidebarSection>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<StyledSidebarSection>
|
||||
<Button
|
||||
Icon={IconCheck}
|
||||
title={t`Installed`}
|
||||
variant={'secondary'}
|
||||
accent={'default'}
|
||||
disabled={isAlreadyInstalled}
|
||||
/>
|
||||
</StyledSidebarSection>
|
||||
);
|
||||
};
|
||||
|
||||
const contentEntries = [
|
||||
{
|
||||
icon: IconBox,
|
||||
@@ -373,7 +175,7 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
one: t`agent`,
|
||||
many: t`agents`,
|
||||
},
|
||||
].filter((entry) => entry.count > 0);
|
||||
];
|
||||
|
||||
const activeTabId = useAtomComponentStateValue(
|
||||
activeTabIdComponentState,
|
||||
@@ -384,7 +186,6 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
{ id: 'about', title: t`About`, Icon: IconInfoCircle },
|
||||
{ id: 'content', title: t`Content`, Icon: IconBox },
|
||||
{ id: 'permissions', title: t`Permissions`, Icon: IconLock },
|
||||
{ id: 'settings', title: t`Settings`, Icon: IconSettings },
|
||||
];
|
||||
|
||||
const renderActiveTabContent = () => {
|
||||
@@ -393,158 +194,41 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
switch (activeTabId) {
|
||||
case 'about':
|
||||
return (
|
||||
<>
|
||||
{hasScreenshots && (
|
||||
<StyledAboutContainer>
|
||||
<StyledScreenshotsContainer>
|
||||
<StyledScreenshotImage
|
||||
src={screenshots[selectedScreenshotIndex]}
|
||||
alt={`${displayName} screenshot ${selectedScreenshotIndex + 1}`}
|
||||
/>
|
||||
</StyledScreenshotsContainer>
|
||||
<StyledScreenshotThumbnails>
|
||||
{screenshots.slice(0, 6).map((screenshot, index) => (
|
||||
<StyledThumbnail
|
||||
key={index}
|
||||
isSelected={index === selectedScreenshotIndex}
|
||||
onClick={() => setSelectedScreenshotIndex(index)}
|
||||
>
|
||||
<StyledThumbnailImage
|
||||
src={screenshot}
|
||||
alt={`${displayName} thumbnail ${index + 1}`}
|
||||
/>
|
||||
</StyledThumbnail>
|
||||
))}
|
||||
</StyledScreenshotThumbnails>
|
||||
</StyledAboutContainer>
|
||||
)}
|
||||
|
||||
<StyledContentContainer>
|
||||
<StyledMainContent>
|
||||
<Section>
|
||||
<StyledSectionTitle>{t`About`}</StyledSectionTitle>
|
||||
<LazyMarkdownRenderer
|
||||
text={
|
||||
aboutDescription
|
||||
? aboutDescription
|
||||
: t`No description available for this application`
|
||||
}
|
||||
/>
|
||||
</Section>
|
||||
</StyledMainContent>
|
||||
|
||||
<StyledSidebar>
|
||||
{getActionButton()}
|
||||
<StyledSidebarSection>
|
||||
<StyledSidebarLabel>{t`Created by`}</StyledSidebarLabel>
|
||||
<StyledSidebarValue>
|
||||
{app?.author ?? 'Unknown'}
|
||||
</StyledSidebarValue>
|
||||
</StyledSidebarSection>
|
||||
|
||||
{app?.category && (
|
||||
<StyledSidebarSection>
|
||||
<StyledSidebarLabel>{t`Category`}</StyledSidebarLabel>
|
||||
<StyledSidebarValue>{app.category}</StyledSidebarValue>
|
||||
</StyledSidebarSection>
|
||||
)}
|
||||
|
||||
{contentEntries.length > 0 && (
|
||||
<StyledSidebarSection>
|
||||
<StyledSidebarLabel>{t`Content`}</StyledSidebarLabel>
|
||||
{contentEntries.map((entry) => (
|
||||
<StyledContentItem key={entry.one}>
|
||||
<entry.icon size={16} />
|
||||
{entry.count}{' '}
|
||||
{entry.count === 1 ? entry.one : entry.many}
|
||||
</StyledContentItem>
|
||||
))}
|
||||
</StyledSidebarSection>
|
||||
)}
|
||||
|
||||
{isAlreadyInstalled && (
|
||||
<StyledSidebarSection>
|
||||
<StyledSidebarLabel>{t`Current`}</StyledSidebarLabel>
|
||||
<StyledSidebarValue>
|
||||
{installedApp?.version ?? t`Unknown`}
|
||||
</StyledSidebarValue>
|
||||
</StyledSidebarSection>
|
||||
)}
|
||||
|
||||
<StyledSidebarSection>
|
||||
<StyledSidebarLabel>{t`Latest`}</StyledSidebarLabel>
|
||||
<StyledSidebarValue>
|
||||
{detail.latestAvailableVersion ?? '0.0.0'}
|
||||
</StyledSidebarValue>
|
||||
</StyledSidebarSection>
|
||||
|
||||
{(app?.websiteUrl ||
|
||||
app?.termsUrl ||
|
||||
app?.emailSupport ||
|
||||
app?.issueReportUrl) && (
|
||||
<StyledSidebarSection>
|
||||
<StyledSidebarLabel>{t`Developers links`}</StyledSidebarLabel>
|
||||
{app?.websiteUrl && (
|
||||
<StyledLink
|
||||
href={app.websiteUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<IconWorld size={16} />
|
||||
{t`Website`}
|
||||
</StyledLink>
|
||||
)}
|
||||
{app?.termsUrl && (
|
||||
<StyledLink
|
||||
href={app.termsUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<IconLink size={16} />
|
||||
{t`Terms / Privacy`}
|
||||
</StyledLink>
|
||||
)}
|
||||
{app?.emailSupport && (
|
||||
<StyledLink
|
||||
href={`mailto:${app.emailSupport}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<IconMail size={16} />
|
||||
{t`Email support`}
|
||||
</StyledLink>
|
||||
)}
|
||||
{app?.issueReportUrl && (
|
||||
<StyledLink
|
||||
href={app.issueReportUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<IconAlertTriangle size={16} />
|
||||
{t`Report and issue`}
|
||||
</StyledLink>
|
||||
)}
|
||||
{sourcePackageUrl && (
|
||||
<StyledLink
|
||||
href={sourcePackageUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<IconBrandNpm size={16} />
|
||||
{t`Npm package`}
|
||||
</StyledLink>
|
||||
)}
|
||||
</StyledSidebarSection>
|
||||
)}
|
||||
</StyledSidebar>
|
||||
</StyledContentContainer>
|
||||
</>
|
||||
<SettingsApplicationDetailAboutTab
|
||||
displayName={displayName}
|
||||
description={description}
|
||||
aboutDescription={app?.aboutDescription}
|
||||
screenshots={app?.screenshots}
|
||||
author={app?.author ?? 'Unknown'}
|
||||
category={app?.category}
|
||||
contentEntries={contentEntries}
|
||||
currentVersion={
|
||||
isAlreadyInstalled
|
||||
? (application.version ?? undefined)
|
||||
: undefined
|
||||
}
|
||||
latestAvailableVersion={detail.latestAvailableVersion ?? '0.0.0'}
|
||||
developerLinks={{
|
||||
websiteUrl: app?.websiteUrl,
|
||||
termsUrl: app?.termsUrl,
|
||||
emailSupport: app?.emailSupport,
|
||||
issueReportUrl: app?.issueReportUrl,
|
||||
sourcePackageUrl,
|
||||
}}
|
||||
isInstalled={isAlreadyInstalled}
|
||||
canInstallMarketplaceApps={canInstallMarketplaceApps}
|
||||
onInstall={handleInstall}
|
||||
isInstalling={isInstalling}
|
||||
hasUpdate={hasUpdate}
|
||||
onUpgrade={handleUpgrade}
|
||||
isUpgrading={isUpgrading}
|
||||
/>
|
||||
);
|
||||
case 'content':
|
||||
return (
|
||||
<SettingsAvailableApplicationDetailContentTab
|
||||
<SettingsApplicationDetailContentTab
|
||||
applicationId={detail.universalIdentifier}
|
||||
content={manifest}
|
||||
manifestContent={manifest}
|
||||
/>
|
||||
);
|
||||
case 'permissions':
|
||||
@@ -554,10 +238,7 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
marketplaceAppObjects={manifest?.objects}
|
||||
/>
|
||||
);
|
||||
case 'settings':
|
||||
return (
|
||||
<SettingsApplicationDetailSettingsTab application={application} />
|
||||
);
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
+7
-5
@@ -37,11 +37,13 @@ export const SettingsApplicationDataTableRow = ({
|
||||
</TableCell>
|
||||
<TableCell align="right">{row.fieldsCount}</TableCell>
|
||||
<StyledActionTableCell>
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
{row.link && (
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
)}
|
||||
</StyledActionTableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
||||
+4
-3
@@ -8,7 +8,8 @@ import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type SettingsApplicationNameDescriptionTableItem = {
|
||||
export type ApplicationNameDescriptionTableRow = {
|
||||
key: string;
|
||||
name: string;
|
||||
description?: string | null;
|
||||
};
|
||||
@@ -22,7 +23,7 @@ export const SettingsApplicationNameDescriptionTable = ({
|
||||
title: string;
|
||||
description: string;
|
||||
sectionTitle: string;
|
||||
items: SettingsApplicationNameDescriptionTableItem[];
|
||||
items: ApplicationNameDescriptionTableRow[];
|
||||
}) => {
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
@@ -38,7 +39,7 @@ export const SettingsApplicationNameDescriptionTable = ({
|
||||
</TableRow>
|
||||
<TableSection title={sectionTitle}>
|
||||
{items.map((item) => (
|
||||
<TableRow key={item.name} gridAutoColumns="180px 1fr">
|
||||
<TableRow key={item.key} gridAutoColumns="180px 1fr">
|
||||
<TableCell
|
||||
color={themeCssVariables.font.color.primary}
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
|
||||
+4
-1
@@ -24,7 +24,10 @@ export const SettingsApplicationVersionContainer = ({
|
||||
latestAvailableVersion,
|
||||
appRegistrationId,
|
||||
}: {
|
||||
application?: Omit<Application, 'objects' | 'universalIdentifier'> & {
|
||||
application?: Omit<
|
||||
Application,
|
||||
'objects' | 'universalIdentifier' | 'frontComponents'
|
||||
> & {
|
||||
objects: { id: string }[];
|
||||
};
|
||||
latestAvailableVersion?: string | null;
|
||||
|
||||
+163
-87
@@ -1,106 +1,182 @@
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { H2Title, IconTrash, AppTooltip } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { SettingsApplicationVersionContainer } from '~/pages/settings/applications/components/SettingsApplicationVersionContainer';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { LazyMarkdownRenderer } from '@/ai/components/LazyMarkdownRenderer';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { useState } from 'react';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type Application,
|
||||
UninstallApplicationDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
IconCheck,
|
||||
IconDownload,
|
||||
IconTrash,
|
||||
IconUpload,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
type ContentEntry,
|
||||
type DeveloperLinks,
|
||||
SettingsApplicationAboutSidebar,
|
||||
} from '@/settings/applications/components/SettingsApplicationAboutSidebar';
|
||||
import { SettingsApplicationScreenshotGallery } from '@/settings/applications/components/SettingsApplicationScreenshotGallery';
|
||||
|
||||
const UNINSTALL_APPLICATION_MODAL_ID = 'uninstall-application-modal';
|
||||
|
||||
type SettingsApplicationDetailAboutTabProps = {
|
||||
displayName: string;
|
||||
description?: string;
|
||||
aboutDescription?: string;
|
||||
screenshots?: string[];
|
||||
author?: string;
|
||||
category?: string;
|
||||
contentEntries?: ContentEntry[];
|
||||
currentVersion?: string;
|
||||
latestAvailableVersion?: string;
|
||||
developerLinks?: DeveloperLinks;
|
||||
isInstalled: boolean;
|
||||
canInstallMarketplaceApps?: boolean;
|
||||
onInstall?: () => void;
|
||||
isInstalling?: boolean;
|
||||
hasUpdate?: boolean;
|
||||
onUpgrade?: () => void;
|
||||
isUpgrading?: boolean;
|
||||
canBeUninstalled?: boolean;
|
||||
onUninstall?: () => void;
|
||||
isUninstalling?: boolean;
|
||||
};
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const StyledMainContent = styled.div`
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const SettingsApplicationDetailAboutTab = ({
|
||||
application,
|
||||
}: {
|
||||
application?: Omit<Application, 'objects'> & {
|
||||
objects: { id: string }[];
|
||||
};
|
||||
}) => {
|
||||
displayName,
|
||||
description,
|
||||
aboutDescription,
|
||||
screenshots,
|
||||
author,
|
||||
category,
|
||||
contentEntries,
|
||||
currentVersion,
|
||||
latestAvailableVersion,
|
||||
developerLinks,
|
||||
isInstalled,
|
||||
canInstallMarketplaceApps,
|
||||
onInstall,
|
||||
isInstalling,
|
||||
hasUpdate,
|
||||
onUpgrade,
|
||||
isUpgrading,
|
||||
canBeUninstalled,
|
||||
onUninstall,
|
||||
isUninstalling,
|
||||
}: SettingsApplicationDetailAboutTabProps) => {
|
||||
const { openModal } = useModal();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const hasScreenshots = isDefined(screenshots) && screenshots.length > 0;
|
||||
|
||||
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
|
||||
const markdownText =
|
||||
aboutDescription ??
|
||||
description ??
|
||||
t`No description available for this application`;
|
||||
|
||||
const [uninstallApplication] = useMutation(UninstallApplicationDocument);
|
||||
|
||||
const navigate = useNavigateSettings();
|
||||
|
||||
const registrationId = application?.applicationRegistrationId;
|
||||
|
||||
const latestAvailableVersion =
|
||||
application?.applicationRegistration?.latestAvailableVersion ?? null;
|
||||
|
||||
if (!isDefined(application)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleUninstallApplication = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await uninstallApplication({
|
||||
variables: { universalIdentifier: application.universalIdentifier },
|
||||
});
|
||||
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Application successfully uninstalled.`,
|
||||
});
|
||||
navigate(SettingsPath.Applications);
|
||||
} catch {
|
||||
enqueueErrorSnackBar({ message: t`Error uninstalling application.` });
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
const getActionButton = () => {
|
||||
if (!canInstallMarketplaceApps) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isInstalled) {
|
||||
return (
|
||||
<Button
|
||||
Icon={IconDownload}
|
||||
title={isInstalling ? t`Installing...` : t`Install`}
|
||||
variant={'primary'}
|
||||
accent={'blue'}
|
||||
onClick={onInstall}
|
||||
disabled={isInstalling}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (hasUpdate) {
|
||||
return (
|
||||
<Button
|
||||
Icon={IconUpload}
|
||||
title={
|
||||
isUpgrading
|
||||
? t`Upgrading...`
|
||||
: t`Upgrade to ${latestAvailableVersion}`
|
||||
}
|
||||
variant={'secondary'}
|
||||
accent={'blue'}
|
||||
onClick={onUpgrade}
|
||||
disabled={isUpgrading}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (canBeUninstalled) {
|
||||
return (
|
||||
<Button
|
||||
Icon={IconTrash}
|
||||
title={isUninstalling ? t`Uninstalling...` : t`Uninstall`}
|
||||
variant={'secondary'}
|
||||
accent={'danger'}
|
||||
onClick={() => openModal(UNINSTALL_APPLICATION_MODAL_ID)}
|
||||
disabled={isUninstalling}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button
|
||||
Icon={IconCheck}
|
||||
title={t`Installed`}
|
||||
variant={'secondary'}
|
||||
accent={'default'}
|
||||
disabled={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const confirmationValue = t`yes`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<SettingsApplicationVersionContainer
|
||||
application={application}
|
||||
latestAvailableVersion={latestAvailableVersion}
|
||||
appRegistrationId={registrationId}
|
||||
{hasScreenshots && (
|
||||
<SettingsApplicationScreenshotGallery
|
||||
screenshots={screenshots}
|
||||
displayName={displayName}
|
||||
/>
|
||||
</Section>
|
||||
<>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Manage your app`}
|
||||
description={t`Uninstall this application`}
|
||||
/>
|
||||
<Button
|
||||
accent="danger"
|
||||
id={'uninstall-button-anchor'}
|
||||
variant="secondary"
|
||||
title={t`Uninstall`}
|
||||
Icon={IconTrash}
|
||||
disabled={!application.canBeUninstalled}
|
||||
onClick={() =>
|
||||
application.canBeUninstalled
|
||||
? openModal(UNINSTALL_APPLICATION_MODAL_ID)
|
||||
: null
|
||||
}
|
||||
/>
|
||||
{!application.canBeUninstalled && (
|
||||
<AppTooltip
|
||||
anchorSelect={`#uninstall-button-anchor`}
|
||||
content={t`This application is required for your workspace to function properly and cannot be uninstalled.`}
|
||||
place="bottom-start"
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<StyledContentContainer>
|
||||
<StyledMainContent>
|
||||
<Section>
|
||||
<LazyMarkdownRenderer text={markdownText} />
|
||||
</Section>
|
||||
</StyledMainContent>
|
||||
|
||||
<SettingsApplicationAboutSidebar
|
||||
actionButton={getActionButton()}
|
||||
author={author}
|
||||
category={category}
|
||||
contentEntries={contentEntries}
|
||||
currentVersion={currentVersion}
|
||||
latestAvailableVersion={latestAvailableVersion}
|
||||
developerLinks={developerLinks}
|
||||
/>
|
||||
</StyledContentContainer>
|
||||
|
||||
{canBeUninstalled && isDefined(onUninstall) && (
|
||||
<ConfirmationModal
|
||||
confirmationPlaceholder={confirmationValue}
|
||||
confirmationValue={confirmationValue}
|
||||
@@ -112,11 +188,11 @@ export const SettingsApplicationDetailAboutTab = ({
|
||||
uninstall this application.
|
||||
</Trans>
|
||||
}
|
||||
onConfirmClick={handleUninstallApplication}
|
||||
onConfirmClick={onUninstall}
|
||||
confirmButtonText={t`Uninstall`}
|
||||
loading={isLoading}
|
||||
loading={isUninstalling}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+86
-114
@@ -1,122 +1,97 @@
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { isHiddenSystemField } from '@/object-metadata/utils/isHiddenSystemField';
|
||||
import { SettingsLogicFunctionsTable } from '@/settings/logic-functions/components/SettingsLogicFunctionsTable';
|
||||
import {
|
||||
type LogicFunctionTableRow,
|
||||
SettingsLogicFunctionsTable,
|
||||
} from '@/settings/logic-functions/components/SettingsLogicFunctionsTable';
|
||||
import { useObjectAndFieldRows } from '@/settings/applications/hooks/useObjectAndFieldRows';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useMemo } from 'react';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { type Application } from '~/generated-metadata/graphql';
|
||||
import { SettingsAiAgentsTable } from '~/pages/settings/ai/components/SettingsAiAgentsTable';
|
||||
import { SettingsApplicationDataTable } from '~/pages/settings/applications/components/SettingsApplicationDataTable';
|
||||
import {
|
||||
SettingsApplicationDataTable,
|
||||
type ApplicationDataTableRow,
|
||||
} from '~/pages/settings/applications/components/SettingsApplicationDataTable';
|
||||
type ApplicationNameDescriptionTableRow,
|
||||
SettingsApplicationNameDescriptionTable,
|
||||
} from '~/pages/settings/applications/components/SettingsApplicationNameDescriptionTable';
|
||||
|
||||
type InstalledApplicationForContentTab = Omit<
|
||||
Application,
|
||||
'objects' | 'universalIdentifier' | 'frontComponents'
|
||||
> & {
|
||||
objects: { id: string }[];
|
||||
frontComponents?: { name: string; description?: string | null }[];
|
||||
};
|
||||
|
||||
type SettingsApplicationDetailContentTabProps = {
|
||||
applicationId: string;
|
||||
installedApplication?: InstalledApplicationForContentTab;
|
||||
manifestContent?: Manifest;
|
||||
};
|
||||
|
||||
export const SettingsApplicationDetailContentTab = ({
|
||||
application,
|
||||
}: {
|
||||
application?: Omit<Application, 'objects' | 'universalIdentifier'> & {
|
||||
objects: { id: string }[];
|
||||
};
|
||||
}) => {
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
applicationId,
|
||||
installedApplication,
|
||||
manifestContent,
|
||||
}: SettingsApplicationDetailContentTabProps) => {
|
||||
const { objectRows, fieldGroupRows } = useObjectAndFieldRows({
|
||||
applicationId,
|
||||
installedApplication,
|
||||
manifestContent,
|
||||
});
|
||||
|
||||
const applicationObjectIds = useMemo(
|
||||
() => application?.objects.map((object) => object.id) ?? [],
|
||||
[application?.objects],
|
||||
);
|
||||
const logicFunctionRows = useMemo((): LogicFunctionTableRow[] => {
|
||||
const computeTrigger = (lf: {
|
||||
isTool?: boolean;
|
||||
cronTriggerSettings?: unknown;
|
||||
httpRouteTriggerSettings?: unknown;
|
||||
databaseEventTriggerSettings?: { eventName?: string } | null;
|
||||
}): string => {
|
||||
if (lf.isTool) return 'Tool';
|
||||
if (lf.cronTriggerSettings) return 'Cron';
|
||||
if (lf.httpRouteTriggerSettings) return 'Route';
|
||||
if (lf.databaseEventTriggerSettings)
|
||||
return lf.databaseEventTriggerSettings.eventName ?? '';
|
||||
return '';
|
||||
};
|
||||
|
||||
const objectRows = useMemo((): ApplicationDataTableRow[] => {
|
||||
if (!isDefined(application) || application.objects.length === 0) {
|
||||
return [];
|
||||
if (isDefined(installedApplication)) {
|
||||
return (installedApplication.logicFunctions ?? []).map((lf) => ({
|
||||
key: lf.id,
|
||||
name: lf.name,
|
||||
trigger: computeTrigger(lf),
|
||||
link: getSettingsPath(SettingsPath.ApplicationLogicFunctionDetail, {
|
||||
applicationId,
|
||||
logicFunctionId: lf.id,
|
||||
}),
|
||||
}));
|
||||
}
|
||||
|
||||
return objectMetadataItems
|
||||
.filter((objectMetadataItem) =>
|
||||
applicationObjectIds.includes(objectMetadataItem.id),
|
||||
)
|
||||
.map((objectMetadataItem) => {
|
||||
const nonSystemFields = objectMetadataItem.fields.filter(
|
||||
(field) => !isHiddenSystemField(field),
|
||||
);
|
||||
return (manifestContent?.logicFunctions ?? []).map((lf) => ({
|
||||
key: lf.universalIdentifier,
|
||||
name: lf.name ?? lf.universalIdentifier,
|
||||
trigger: computeTrigger(lf),
|
||||
}));
|
||||
}, [installedApplication, manifestContent?.logicFunctions, applicationId]);
|
||||
|
||||
return {
|
||||
key: objectMetadataItem.nameSingular,
|
||||
labelPlural: objectMetadataItem.labelPlural,
|
||||
icon: objectMetadataItem.icon ?? undefined,
|
||||
fieldsCount: nonSystemFields.length,
|
||||
link: getSettingsPath(SettingsPath.ObjectDetail, {
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
}),
|
||||
tagItem: {
|
||||
isCustom: objectMetadataItem.isCustom,
|
||||
isRemote: objectMetadataItem.isRemote,
|
||||
applicationId: objectMetadataItem.applicationId,
|
||||
},
|
||||
};
|
||||
});
|
||||
}, [application, objectMetadataItems, applicationObjectIds]);
|
||||
const frontComponentRows =
|
||||
useMemo((): ApplicationNameDescriptionTableRow[] => {
|
||||
if (isDefined(installedApplication)) {
|
||||
return (installedApplication.frontComponents ?? []).map((fc) => ({
|
||||
key: fc.name,
|
||||
name: fc.name,
|
||||
description: fc.description,
|
||||
}));
|
||||
}
|
||||
|
||||
const fieldGroupRows = useMemo((): ApplicationDataTableRow[] => {
|
||||
if (!isDefined(application)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const FIELD_GROUP_DENY_LIST = ['timelineActivity', 'favorite'];
|
||||
|
||||
return objectMetadataItems
|
||||
.filter((objectMetadataItem) => {
|
||||
if (applicationObjectIds.includes(objectMetadataItem.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (FIELD_GROUP_DENY_LIST.includes(objectMetadataItem.nameSingular)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const appFields = objectMetadataItem.fields.filter(
|
||||
(field) => field.applicationId === application.id,
|
||||
);
|
||||
|
||||
return appFields.length > 0;
|
||||
})
|
||||
.map((objectMetadataItem) => {
|
||||
const appFieldsCount = objectMetadataItem.fields.filter(
|
||||
(field) => field.applicationId === application.id,
|
||||
).length;
|
||||
|
||||
return {
|
||||
key: objectMetadataItem.nameSingular,
|
||||
labelPlural: objectMetadataItem.labelPlural,
|
||||
icon: objectMetadataItem.icon ?? undefined,
|
||||
fieldsCount: appFieldsCount,
|
||||
link: getSettingsPath(SettingsPath.ObjectDetail, {
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
}),
|
||||
tagItem: {
|
||||
isCustom: objectMetadataItem.isCustom,
|
||||
isRemote: objectMetadataItem.isRemote,
|
||||
applicationId: objectMetadataItem.applicationId,
|
||||
},
|
||||
};
|
||||
});
|
||||
}, [objectMetadataItems, applicationObjectIds, application]);
|
||||
|
||||
if (!isDefined(application)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { logicFunctions } = application;
|
||||
|
||||
const shouldDisplayLogicFunctions =
|
||||
isDefined(logicFunctions) && logicFunctions?.length > 0;
|
||||
|
||||
// TODO: uncomment when adding back agents in application settings
|
||||
// const shouldDisplayAgents = isDefined(agents) && agents.length > 0;
|
||||
const shouldDisplayAgents = false;
|
||||
return (manifestContent?.frontComponents ?? []).map((fc) => ({
|
||||
key: fc.universalIdentifier,
|
||||
name: fc.name ?? fc.universalIdentifier,
|
||||
description: fc.description,
|
||||
}));
|
||||
}, [installedApplication, manifestContent?.frontComponents]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -124,24 +99,21 @@ export const SettingsApplicationDetailContentTab = ({
|
||||
objectRows={objectRows}
|
||||
fieldGroupRows={fieldGroupRows}
|
||||
/>
|
||||
{shouldDisplayLogicFunctions && (
|
||||
{logicFunctionRows.length > 0 && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Logic`}
|
||||
description={t`Logic functions powering this app`}
|
||||
/>
|
||||
<SettingsLogicFunctionsTable logicFunctions={logicFunctions} />
|
||||
</Section>
|
||||
)}
|
||||
{shouldDisplayAgents && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Agents`}
|
||||
description={t`Agents powering this app`}
|
||||
/>
|
||||
<SettingsAiAgentsTable />
|
||||
<SettingsLogicFunctionsTable logicFunctions={logicFunctionRows} />
|
||||
</Section>
|
||||
)}
|
||||
<SettingsApplicationNameDescriptionTable
|
||||
title={t`Front components`}
|
||||
description={t`UI components provided by this app`}
|
||||
sectionTitle={t`Front components`}
|
||||
items={frontComponentRows}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
-196
@@ -1,196 +0,0 @@
|
||||
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useMemo } from 'react';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
type ApplicationDataTableRow,
|
||||
SettingsApplicationDataTable,
|
||||
} from '~/pages/settings/applications/components/SettingsApplicationDataTable';
|
||||
import { SettingsApplicationNameDescriptionTable } from '~/pages/settings/applications/components/SettingsApplicationNameDescriptionTable';
|
||||
import { findObjectNameByUniversalIdentifier } from '~/pages/settings/applications/utils/findObjectNameByUniversalIdentifier';
|
||||
|
||||
export const SettingsAvailableApplicationDetailContentTab = ({
|
||||
applicationId,
|
||||
content,
|
||||
}: {
|
||||
applicationId: string;
|
||||
content?: Manifest;
|
||||
}) => {
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
|
||||
const objects = useMemo(() => content?.objects ?? [], [content?.objects]);
|
||||
const fields = useMemo(() => content?.fields ?? [], [content?.fields]);
|
||||
const logicFunctions = content?.logicFunctions ?? [];
|
||||
const frontComponents = content?.frontComponents ?? [];
|
||||
|
||||
const objectRows = useMemo(
|
||||
(): ApplicationDataTableRow[] =>
|
||||
objects.map((appObject) => ({
|
||||
key: appObject.nameSingular,
|
||||
labelPlural: appObject.labelPlural,
|
||||
icon: appObject.icon ?? undefined,
|
||||
fieldsCount: appObject.fields.length,
|
||||
tagItem: { applicationId },
|
||||
})),
|
||||
[objects, applicationId],
|
||||
);
|
||||
|
||||
const fieldGroupRows = useMemo((): ApplicationDataTableRow[] => {
|
||||
if (fields.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const groupMap = new Map<
|
||||
string,
|
||||
{
|
||||
objectUniversalIdentifier: string;
|
||||
count: number;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const field of fields) {
|
||||
const objectUid = field.objectUniversalIdentifier;
|
||||
const existing = groupMap.get(objectUid);
|
||||
|
||||
if (isDefined(existing)) {
|
||||
existing.count++;
|
||||
} else {
|
||||
groupMap.set(objectUid, {
|
||||
objectUniversalIdentifier: objectUid,
|
||||
count: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(groupMap.values())
|
||||
.map((group) => {
|
||||
const appObject = objects.find(
|
||||
(obj) => obj.universalIdentifier === group.objectUniversalIdentifier,
|
||||
);
|
||||
|
||||
if (isDefined(appObject)) {
|
||||
return {
|
||||
key: appObject.nameSingular,
|
||||
labelPlural: appObject.labelPlural,
|
||||
icon: appObject.icon ?? undefined,
|
||||
fieldsCount: group.count,
|
||||
tagItem: { applicationId },
|
||||
};
|
||||
}
|
||||
|
||||
const standardObjectName = findObjectNameByUniversalIdentifier(
|
||||
group.objectUniversalIdentifier,
|
||||
);
|
||||
|
||||
const objectMetadataItem = isDefined(standardObjectName)
|
||||
? objectMetadataItems.find(
|
||||
(item) => item.nameSingular === standardObjectName,
|
||||
)
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(objectMetadataItem)) {
|
||||
return;
|
||||
}
|
||||
|
||||
return {
|
||||
key: objectMetadataItem.nameSingular,
|
||||
labelPlural: objectMetadataItem.labelPlural,
|
||||
icon: objectMetadataItem.icon ?? undefined,
|
||||
fieldsCount: group.count,
|
||||
link: getSettingsPath(SettingsPath.ObjectDetail, {
|
||||
objectNamePlural: objectMetadataItem.namePlural,
|
||||
}),
|
||||
tagItem: {},
|
||||
};
|
||||
})
|
||||
.filter(isDefined);
|
||||
}, [fields, objectMetadataItems, objects, applicationId]);
|
||||
|
||||
const roles = content?.roles ?? [];
|
||||
const skills = content?.skills ?? [];
|
||||
const agents = content?.agents ?? [];
|
||||
const views = content?.views ?? [];
|
||||
const navigationMenuItems = content?.navigationMenuItems ?? [];
|
||||
const pageLayouts = content?.pageLayouts ?? [];
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsApplicationDataTable
|
||||
objectRows={objectRows}
|
||||
fieldGroupRows={fieldGroupRows}
|
||||
/>
|
||||
<SettingsApplicationNameDescriptionTable
|
||||
title={t`Logic functions`}
|
||||
description={t`Logic functions provided by this app`}
|
||||
sectionTitle={t`Logic functions`}
|
||||
items={logicFunctions.map((lf) => ({
|
||||
name: lf.name ?? lf.universalIdentifier,
|
||||
description: lf.description,
|
||||
}))}
|
||||
/>
|
||||
<SettingsApplicationNameDescriptionTable
|
||||
title={t`Front components`}
|
||||
description={t`UI components provided by this app`}
|
||||
sectionTitle={t`Front components`}
|
||||
items={frontComponents.map((fc) => ({
|
||||
name: fc.name ?? fc.universalIdentifier,
|
||||
description: fc.description,
|
||||
}))}
|
||||
/>
|
||||
<SettingsApplicationNameDescriptionTable
|
||||
title={t`Roles`}
|
||||
description={t`Roles defined by this app`}
|
||||
sectionTitle={t`Roles`}
|
||||
items={roles.map((role) => ({
|
||||
name: role.label,
|
||||
description: role.description,
|
||||
}))}
|
||||
/>
|
||||
<SettingsApplicationNameDescriptionTable
|
||||
title={t`Skills`}
|
||||
description={t`Skills provided by this app`}
|
||||
sectionTitle={t`Skills`}
|
||||
items={skills.map((skill) => ({
|
||||
name: skill.label ?? skill.name,
|
||||
description: skill.description,
|
||||
}))}
|
||||
/>
|
||||
<SettingsApplicationNameDescriptionTable
|
||||
title={t`Agents`}
|
||||
description={t`Agents provided by this app`}
|
||||
sectionTitle={t`Agents`}
|
||||
items={agents.map((agent) => ({
|
||||
name: agent.label ?? agent.name,
|
||||
description: agent.description,
|
||||
}))}
|
||||
/>
|
||||
<SettingsApplicationNameDescriptionTable
|
||||
title={t`Views`}
|
||||
description={t`Views created by this app`}
|
||||
sectionTitle={t`Views`}
|
||||
items={views.map((view) => ({
|
||||
name: view.name,
|
||||
}))}
|
||||
/>
|
||||
<SettingsApplicationNameDescriptionTable
|
||||
title={t`Navigation menu items`}
|
||||
description={t`Navigation items added by this app`}
|
||||
sectionTitle={t`Navigation items`}
|
||||
items={navigationMenuItems.map((item) => ({
|
||||
name: item.name ?? item.universalIdentifier,
|
||||
}))}
|
||||
/>
|
||||
<SettingsApplicationNameDescriptionTable
|
||||
title={t`Page layouts`}
|
||||
description={t`Page layouts defined by this app`}
|
||||
sectionTitle={t`Page layouts`}
|
||||
items={pageLayouts.map((layout) => ({
|
||||
name: layout.name,
|
||||
}))}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-sdk",
|
||||
"version": "1.23.0-canary.1",
|
||||
"version": "1.23.0-canary.2",
|
||||
"sideEffects": false,
|
||||
"bin": {
|
||||
"twenty": "dist/cli.cjs"
|
||||
|
||||
@@ -20,6 +20,7 @@ import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/appli
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { ApplicationVariableEntity } from 'src/engine/core-modules/application/application-variable/application-variable.entity';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { FrontComponentEntity } from 'src/engine/metadata-modules/front-component/entities/front-component.entity';
|
||||
import { LogicFunctionEntity } from 'src/engine/metadata-modules/logic-function/logic-function.entity';
|
||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||
@@ -129,6 +130,15 @@ export class ApplicationEntity extends WorkspaceRelatedEntity {
|
||||
})
|
||||
objects: Relation<ObjectMetadataEntity[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => FrontComponentEntity,
|
||||
(frontComponent) => frontComponent.application,
|
||||
{
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
)
|
||||
frontComponents: Relation<FrontComponentEntity[]>;
|
||||
|
||||
@OneToMany(
|
||||
() => ApplicationVariableEntity,
|
||||
(applicationVariable) => applicationVariable.application,
|
||||
|
||||
@@ -122,6 +122,7 @@ export class ApplicationService {
|
||||
relations: [
|
||||
'logicFunctions',
|
||||
'agents',
|
||||
'frontComponents',
|
||||
'objects',
|
||||
'applicationVariables',
|
||||
'packageJsonFile',
|
||||
@@ -157,6 +158,7 @@ export class ApplicationService {
|
||||
relations: [
|
||||
'logicFunctions',
|
||||
'agents',
|
||||
'frontComponents',
|
||||
'objects',
|
||||
'applicationVariables',
|
||||
'packageJsonFile',
|
||||
|
||||
+1
@@ -3,6 +3,7 @@ import { type ApplicationEntity } from 'src/engine/core-modules/application/appl
|
||||
export const APPLICATION_ENTITY_RELATION_PROPERTIES = [
|
||||
'workspace',
|
||||
'agents',
|
||||
'frontComponents',
|
||||
'logicFunctions',
|
||||
'objects',
|
||||
'applicationVariables',
|
||||
|
||||
@@ -13,6 +13,7 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/
|
||||
import { ApplicationRegistrationSummaryDTO } from 'src/engine/core-modules/application/application-registration/dtos/application-registration-summary.dto';
|
||||
import { ApplicationVariableEntityDTO } from 'src/engine/core-modules/application/application-variable/dtos/application-variable.dto';
|
||||
import { AgentDTO } from 'src/engine/metadata-modules/ai/ai-agent/dtos/agent.dto';
|
||||
import { FrontComponentDTO } from 'src/engine/metadata-modules/front-component/dtos/front-component.dto';
|
||||
import { LogicFunctionDTO } from 'src/engine/metadata-modules/logic-function/dtos/logic-function.dto';
|
||||
import { ObjectMetadataDTO } from 'src/engine/metadata-modules/object-metadata/dtos/object-metadata.dto';
|
||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||
@@ -91,6 +92,9 @@ export class ApplicationDTO {
|
||||
@Field(() => [AgentDTO])
|
||||
agents?: AgentDTO[];
|
||||
|
||||
@Field(() => [FrontComponentDTO])
|
||||
frontComponents?: FrontComponentDTO[];
|
||||
|
||||
@Field(() => [LogicFunctionDTO])
|
||||
logicFunctions?: LogicFunctionDTO[];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user