feat: expose marketplace app detail fields explicitly and deprecate manifest blob (#22526)

Part of the application settings architecture work:
https://github.com/twentyhq/core-team-issues/issues/2456 — follow-up to
#22513.

`MarketplaceAppDetail` returned the entire `manifest` jsonb (100KB+)
over GraphQL and the front dug display fields and roles out of it. This
PR:

- Adds explicit fields to `MarketplaceAppDetail`: `description, author,
category, logo, websiteUrl, aboutDescription, termsUrl, emailSupport,
issueReportUrl, screenshots, defaultRoleUniversalIdentifier`, sourced
from the registration columns introduced in #22513, and `roles:
[MarketplaceAppRole!]` (full permission shape — the permissions tab and
install modal render object/field permissions), sourced from the
manifest at detail time.
- Marks the `manifest` field `@deprecated` (kept functional — removal
would be a breaking change).
- Front: the shared `marketplaceAppDetailFragment` no longer selects
`manifest`; display and role reads are flattened across
`SettingsAvailableApplicationDetails`, `SettingsApplicationDetails`, and
the share-link buttons. The three consumers that genuinely need deep
manifest structure (content-tab counts/`manifestContent`, permissions
objects, `useApplicationManifest` page-layout/view reads) use a scoped
`FindMarketplaceAppManifest` query until the manifest demotion PR
removes that need.
- Codegen regenerated where the documents live: front metadata config +
twenty-client-sdk metadata client (data/admin configs verified
untouched).

Verified: server+front typecheck, lint (0 warnings), server marketplace
suite 10/10, front marketplace/applications suites 41/41, live schema
introspection confirms the new fields and the deprecation.

<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22526?utm_source=github"
rel="nofollow noreferrer noopener" target="_blank">``&lt;img alt="Review
in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"&gt;``</a>
This commit is contained in:
martmull
2026-07-04 23:26:53 +02:00
committed by GitHub
parent 3db09e4423
commit ea851f7d9c
16 changed files with 1098 additions and 371 deletions
@@ -2423,6 +2423,39 @@ type MarketplaceApp {
isFeatured: Boolean!
}
type MarketplaceAppRoleObjectPermission {
universalIdentifier: String!
objectUniversalIdentifier: String!
canReadObjectRecords: Boolean
canUpdateObjectRecords: Boolean
canSoftDeleteObjectRecords: Boolean
canDestroyObjectRecords: Boolean
}
type MarketplaceAppRoleFieldPermission {
universalIdentifier: String!
objectUniversalIdentifier: String!
fieldUniversalIdentifier: String!
canReadFieldValue: Boolean
canUpdateFieldValue: Boolean
}
type MarketplaceAppRole {
universalIdentifier: String!
label: String!
description: String
icon: String
canUpdateAllSettings: Boolean
canAccessAllTools: Boolean
canReadAllObjectRecords: Boolean
canUpdateAllObjectRecords: Boolean
canSoftDeleteAllObjectRecords: Boolean
canDestroyAllObjectRecords: Boolean
permissionFlagUniversalIdentifiers: [String!]
objectPermissions: [MarketplaceAppRoleObjectPermission!]
fieldPermissions: [MarketplaceAppRoleFieldPermission!]
}
type MarketplaceAppDetail {
universalIdentifier: String!
id: String!
@@ -2432,7 +2465,19 @@ type MarketplaceAppDetail {
latestAvailableVersion: String
isListed: Boolean!
isFeatured: Boolean!
manifest: JSON
description: String
author: String
category: String
logo: String
websiteUrl: String
aboutDescription: String
termsUrl: String
emailSupport: String
issueReportUrl: String
screenshots: [String!]!
defaultRoleUniversalIdentifier: String
roles: [MarketplaceAppRole!]
manifest: JSON @deprecated(reason: "Use the explicit MarketplaceAppDetail fields (description, author, roles, ...) instead")
}
type PublicDomain {
@@ -2104,6 +2104,42 @@ export interface MarketplaceApp {
__typename: 'MarketplaceApp'
}
export interface MarketplaceAppRoleObjectPermission {
universalIdentifier: Scalars['String']
objectUniversalIdentifier: Scalars['String']
canReadObjectRecords?: Scalars['Boolean']
canUpdateObjectRecords?: Scalars['Boolean']
canSoftDeleteObjectRecords?: Scalars['Boolean']
canDestroyObjectRecords?: Scalars['Boolean']
__typename: 'MarketplaceAppRoleObjectPermission'
}
export interface MarketplaceAppRoleFieldPermission {
universalIdentifier: Scalars['String']
objectUniversalIdentifier: Scalars['String']
fieldUniversalIdentifier: Scalars['String']
canReadFieldValue?: Scalars['Boolean']
canUpdateFieldValue?: Scalars['Boolean']
__typename: 'MarketplaceAppRoleFieldPermission'
}
export interface MarketplaceAppRole {
universalIdentifier: Scalars['String']
label: Scalars['String']
description?: Scalars['String']
icon?: Scalars['String']
canUpdateAllSettings?: Scalars['Boolean']
canAccessAllTools?: Scalars['Boolean']
canReadAllObjectRecords?: Scalars['Boolean']
canUpdateAllObjectRecords?: Scalars['Boolean']
canSoftDeleteAllObjectRecords?: Scalars['Boolean']
canDestroyAllObjectRecords?: Scalars['Boolean']
permissionFlagUniversalIdentifiers?: Scalars['String'][]
objectPermissions?: MarketplaceAppRoleObjectPermission[]
fieldPermissions?: MarketplaceAppRoleFieldPermission[]
__typename: 'MarketplaceAppRole'
}
export interface MarketplaceAppDetail {
universalIdentifier: Scalars['String']
id: Scalars['String']
@@ -2113,6 +2149,19 @@ export interface MarketplaceAppDetail {
latestAvailableVersion?: Scalars['String']
isListed: Scalars['Boolean']
isFeatured: Scalars['Boolean']
description?: Scalars['String']
author?: Scalars['String']
category?: Scalars['String']
logo?: Scalars['String']
websiteUrl?: Scalars['String']
aboutDescription?: Scalars['String']
termsUrl?: Scalars['String']
emailSupport?: Scalars['String']
issueReportUrl?: Scalars['String']
screenshots: Scalars['String'][]
defaultRoleUniversalIdentifier?: Scalars['String']
roles?: MarketplaceAppRole[]
/** @deprecated Use the explicit MarketplaceAppDetail fields (description, author, roles, ...) instead */
manifest?: Scalars['JSON']
__typename: 'MarketplaceAppDetail'
}
@@ -5232,6 +5281,45 @@ export interface MarketplaceAppGenqlSelection{
__scalar?: boolean | number
}
export interface MarketplaceAppRoleObjectPermissionGenqlSelection{
universalIdentifier?: boolean | number
objectUniversalIdentifier?: boolean | number
canReadObjectRecords?: boolean | number
canUpdateObjectRecords?: boolean | number
canSoftDeleteObjectRecords?: boolean | number
canDestroyObjectRecords?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppRoleFieldPermissionGenqlSelection{
universalIdentifier?: boolean | number
objectUniversalIdentifier?: boolean | number
fieldUniversalIdentifier?: boolean | number
canReadFieldValue?: boolean | number
canUpdateFieldValue?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppRoleGenqlSelection{
universalIdentifier?: boolean | number
label?: boolean | number
description?: boolean | number
icon?: boolean | number
canUpdateAllSettings?: boolean | number
canAccessAllTools?: boolean | number
canReadAllObjectRecords?: boolean | number
canUpdateAllObjectRecords?: boolean | number
canSoftDeleteAllObjectRecords?: boolean | number
canDestroyAllObjectRecords?: boolean | number
permissionFlagUniversalIdentifiers?: boolean | number
objectPermissions?: MarketplaceAppRoleObjectPermissionGenqlSelection
fieldPermissions?: MarketplaceAppRoleFieldPermissionGenqlSelection
__typename?: boolean | number
__scalar?: boolean | number
}
export interface MarketplaceAppDetailGenqlSelection{
universalIdentifier?: boolean | number
id?: boolean | number
@@ -5241,6 +5329,19 @@ export interface MarketplaceAppDetailGenqlSelection{
latestAvailableVersion?: boolean | number
isListed?: boolean | number
isFeatured?: boolean | number
description?: boolean | number
author?: boolean | number
category?: boolean | number
logo?: boolean | number
websiteUrl?: boolean | number
aboutDescription?: boolean | number
termsUrl?: boolean | number
emailSupport?: boolean | number
issueReportUrl?: boolean | number
screenshots?: boolean | number
defaultRoleUniversalIdentifier?: boolean | number
roles?: MarketplaceAppRoleGenqlSelection
/** @deprecated Use the explicit MarketplaceAppDetail fields (description, author, roles, ...) instead */
manifest?: boolean | number
__typename?: boolean | number
__scalar?: boolean | number
@@ -8184,6 +8285,30 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
const MarketplaceAppRoleObjectPermission_possibleTypes: string[] = ['MarketplaceAppRoleObjectPermission']
export const isMarketplaceAppRoleObjectPermission = (obj?: { __typename?: any } | null): obj is MarketplaceAppRoleObjectPermission => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppRoleObjectPermission"')
return MarketplaceAppRoleObjectPermission_possibleTypes.includes(obj.__typename)
}
const MarketplaceAppRoleFieldPermission_possibleTypes: string[] = ['MarketplaceAppRoleFieldPermission']
export const isMarketplaceAppRoleFieldPermission = (obj?: { __typename?: any } | null): obj is MarketplaceAppRoleFieldPermission => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppRoleFieldPermission"')
return MarketplaceAppRoleFieldPermission_possibleTypes.includes(obj.__typename)
}
const MarketplaceAppRole_possibleTypes: string[] = ['MarketplaceAppRole']
export const isMarketplaceAppRole = (obj?: { __typename?: any } | null): obj is MarketplaceAppRole => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppRole"')
return MarketplaceAppRole_possibleTypes.includes(obj.__typename)
}
const MarketplaceAppDetail_possibleTypes: string[] = ['MarketplaceAppDetail']
export const isMarketplaceAppDetail = (obj?: { __typename?: any } | null): obj is MarketplaceAppDetail => {
if (!obj?.__typename) throw new Error('__typename is missing in "isMarketplaceAppDetail"')
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
@@ -10,6 +10,44 @@ export const MARKETPLACE_APP_DETAIL_FRAGMENT = gql`
latestAvailableVersion
isListed
isFeatured
manifest
description
author
category
logo
websiteUrl
aboutDescription
termsUrl
emailSupport
issueReportUrl
screenshots
defaultRoleUniversalIdentifier
roles {
universalIdentifier
label
description
icon
canUpdateAllSettings
canAccessAllTools
canReadAllObjectRecords
canUpdateAllObjectRecords
canSoftDeleteAllObjectRecords
canDestroyAllObjectRecords
permissionFlagUniversalIdentifiers
objectPermissions {
universalIdentifier
objectUniversalIdentifier
canReadObjectRecords
canUpdateObjectRecords
canSoftDeleteObjectRecords
canDestroyObjectRecords
}
fieldPermissions {
universalIdentifier
objectUniversalIdentifier
fieldUniversalIdentifier
canReadFieldValue
canUpdateFieldValue
}
}
}
`;
@@ -0,0 +1,10 @@
import gql from 'graphql-tag';
export const FIND_MARKETPLACE_APP_MANIFEST = gql`
query FindMarketplaceAppManifest($universalIdentifier: String!) {
findMarketplaceAppDetail(universalIdentifier: $universalIdentifier) {
id
manifest
}
}
`;
@@ -0,0 +1,36 @@
import { type RoleManifest } from 'twenty-shared/application';
import { type MarketplaceAppRole } from '~/generated-metadata/graphql';
export const buildRoleManifestFromMarketplaceAppRole = (
role: MarketplaceAppRole,
): RoleManifest => ({
universalIdentifier: role.universalIdentifier,
label: role.label,
description: role.description ?? undefined,
icon: role.icon ?? undefined,
canUpdateAllSettings: role.canUpdateAllSettings ?? undefined,
canAccessAllTools: role.canAccessAllTools ?? undefined,
canReadAllObjectRecords: role.canReadAllObjectRecords ?? undefined,
canUpdateAllObjectRecords: role.canUpdateAllObjectRecords ?? undefined,
canSoftDeleteAllObjectRecords:
role.canSoftDeleteAllObjectRecords ?? undefined,
canDestroyAllObjectRecords: role.canDestroyAllObjectRecords ?? undefined,
permissionFlagUniversalIdentifiers:
role.permissionFlagUniversalIdentifiers ?? undefined,
objectPermissions: role.objectPermissions?.map((permission) => ({
universalIdentifier: permission.universalIdentifier,
objectUniversalIdentifier: permission.objectUniversalIdentifier,
canReadObjectRecords: permission.canReadObjectRecords ?? undefined,
canUpdateObjectRecords: permission.canUpdateObjectRecords ?? undefined,
canSoftDeleteObjectRecords:
permission.canSoftDeleteObjectRecords ?? undefined,
canDestroyObjectRecords: permission.canDestroyObjectRecords ?? undefined,
})),
fieldPermissions: role.fieldPermissions?.map((permission) => ({
universalIdentifier: permission.universalIdentifier,
objectUniversalIdentifier: permission.objectUniversalIdentifier,
fieldUniversalIdentifier: permission.fieldUniversalIdentifier,
canReadFieldValue: permission.canReadFieldValue ?? undefined,
canUpdateFieldValue: permission.canUpdateFieldValue ?? undefined,
})),
});
@@ -0,0 +1,20 @@
import { buildRoleManifestFromMarketplaceAppRole } from '@/marketplace/utils/buildRoleManifestFromMarketplaceAppRole';
import { type RoleManifest } from 'twenty-shared/application';
import { isDefined } from 'twenty-shared/utils';
import { type MarketplaceAppDetail } from '~/generated-metadata/graphql';
export const getMarketplaceAppDefaultRoleManifest = (
detail:
| Pick<MarketplaceAppDetail, 'roles' | 'defaultRoleUniversalIdentifier'>
| null
| undefined,
): RoleManifest | undefined => {
const defaultRole = detail?.roles?.find(
(role) =>
role.universalIdentifier === detail?.defaultRoleUniversalIdentifier,
);
return isDefined(defaultRole)
? buildRoleManifestFromMarketplaceAppRole(defaultRole)
: undefined;
};
@@ -35,6 +35,7 @@ import {
import {
ApplicationRegistrationSourceType,
FindMarketplaceAppDetailDocument,
FindMarketplaceAppManifestDocument,
FindOneApplicationDocument,
PermissionFlagType,
UninstallApplicationDocument,
@@ -76,9 +77,15 @@ export const SettingsApplicationDetails = () => {
skip: !application?.universalIdentifier,
});
const { data: manifestData } = useQuery(FindMarketplaceAppManifestDocument, {
variables: { universalIdentifier: application?.universalIdentifier ?? '' },
skip: !application?.universalIdentifier,
});
const detail = detailData?.findMarketplaceAppDetail;
const manifest = detail?.manifest as Manifest | undefined;
const app = manifest?.application;
const manifest = manifestData?.findMarketplaceAppDetail?.manifest as
| Manifest
| undefined;
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const isStandardApplication = isTwentyStandardApplication(application);
const isCustomApplication = isWorkspaceCustomApplication(
@@ -89,11 +96,11 @@ export const SettingsApplicationDetails = () => {
const resolvedDescription = useResolvedApplicationDescription(application);
const displayName =
app?.displayName ?? application?.name ?? t`Application details`;
const description = app?.description ?? resolvedDescription;
detail?.name ?? application?.name ?? t`Application details`;
const description = detail?.description ?? resolvedDescription;
const getScreenshots = () => {
if (app?.screenshots?.length) return app.screenshots;
if (detail?.screenshots?.length) return detail.screenshots;
if (isStandardApplication) return STANDARD_APPLICATION_ILLUSTRATIONS;
if (isCustomApplication) return CUSTOM_APPLICATION_ILLUSTRATIONS;
return undefined;
@@ -254,20 +261,20 @@ export const SettingsApplicationDetails = () => {
<SettingsApplicationDetailAboutTab
displayName={displayName}
description={description}
aboutDescription={app?.aboutDescription}
aboutDescription={detail?.aboutDescription ?? undefined}
screenshots={screenshots}
author={app?.author}
category={app?.category}
author={detail?.author ?? undefined}
category={detail?.category ?? undefined}
contentEntries={contentEntries}
currentVersion={currentVersion ?? undefined}
latestAvailableVersion={latestAvailableVersion ?? undefined}
developerLinks={
isDefined(app)
isDefined(detail)
? {
websiteUrl: app.websiteUrl,
termsUrl: app.termsUrl,
emailSupport: app.emailSupport,
issueReportUrl: app.issueReportUrl,
websiteUrl: detail.websiteUrl ?? undefined,
termsUrl: detail.termsUrl ?? undefined,
emailSupport: detail.emailSupport ?? undefined,
issueReportUrl: detail.issueReportUrl ?? undefined,
}
: undefined
}
@@ -3,6 +3,7 @@ import { AppChip } from '@/applications/components/AppChip';
import { SettingsApplicationInstallPermissionValidationModal } from '@/marketplace/components/SettingsApplicationInstallPermissionValidationModal';
import { useInstallMarketplaceAppWithPermissionValidation } from '@/marketplace/hooks/useInstallMarketplaceAppWithPermissionValidation';
import { useUpgradeApplication } from '@/marketplace/hooks/useUpgradeApplication';
import { getMarketplaceAppDefaultRoleManifest } from '@/marketplace/utils/getMarketplaceAppDefaultRoleManifest';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
@@ -32,6 +33,7 @@ import {
import {
ApplicationRegistrationSourceType,
FindMarketplaceAppDetailDocument,
FindMarketplaceAppManifestDocument,
FindOneApplicationByUniversalIdentifierDocument,
PermissionFlagType,
} from '~/generated-metadata/graphql';
@@ -69,14 +71,20 @@ export const SettingsAvailableApplicationDetails = () => {
skip: !availableApplicationId,
});
const { data: manifestData } = useQuery(FindMarketplaceAppManifestDocument, {
variables: { universalIdentifier: availableApplicationId },
skip: !availableApplicationId,
});
const application = applicationData?.findOneApplication;
const detail = detailData?.findMarketplaceAppDetail;
const manifest = detail?.manifest as Manifest | undefined;
const app = manifest?.application;
const manifest = manifestData?.findMarketplaceAppDetail?.manifest as
| Manifest
| undefined;
const displayName = app?.displayName ?? detail?.name ?? '';
const description = app?.description ?? '';
const displayName = detail?.name ?? '';
const description = detail?.description ?? '';
const currentVersion = application?.version;
const latestAvailableVersion = detail?.latestAvailableVersion;
@@ -92,9 +100,7 @@ export const SettingsAvailableApplicationDetails = () => {
const isUnlisted = isDefined(detail) && !detail.isListed;
const isAlreadyInstalled = isDefined(application);
const defaultRole = manifest?.roles?.find(
(r) => r.universalIdentifier === app?.defaultRoleUniversalIdentifier,
);
const defaultRole = getMarketplaceAppDefaultRoleManifest(detail);
const hasUpdate =
isNpmApp &&
@@ -173,10 +179,9 @@ export const SettingsAvailableApplicationDetails = () => {
},
{
icon: IconShield,
count: (manifest?.roles ?? []).filter(
count: (detail?.roles ?? []).filter(
(role) =>
role.universalIdentifier !==
manifest?.application.defaultRoleUniversalIdentifier,
role.universalIdentifier !== detail?.defaultRoleUniversalIdentifier,
).length,
one: t`role`,
many: t`roles`,
@@ -215,10 +220,10 @@ export const SettingsAvailableApplicationDetails = () => {
<SettingsApplicationDetailAboutTab
displayName={displayName}
description={description}
aboutDescription={app?.aboutDescription}
screenshots={app?.screenshots}
author={app?.author ?? 'Unknown'}
category={app?.category}
aboutDescription={detail.aboutDescription ?? undefined}
screenshots={detail.screenshots}
author={detail.author ?? 'Unknown'}
category={detail.category ?? undefined}
contentEntries={contentEntries}
currentVersion={
isAlreadyInstalled
@@ -227,10 +232,10 @@ export const SettingsAvailableApplicationDetails = () => {
}
latestAvailableVersion={detail.latestAvailableVersion ?? '0.0.0'}
developerLinks={{
websiteUrl: app?.websiteUrl,
termsUrl: app?.termsUrl,
emailSupport: app?.emailSupport,
issueReportUrl: app?.issueReportUrl,
websiteUrl: detail.websiteUrl ?? undefined,
termsUrl: detail.termsUrl ?? undefined,
emailSupport: detail.emailSupport ?? undefined,
issueReportUrl: detail.issueReportUrl ?? undefined,
sourcePackageUrl,
}}
isInstalled={isAlreadyInstalled}
@@ -249,7 +254,7 @@ export const SettingsAvailableApplicationDetails = () => {
manifestContent={manifest}
applicationInfo={{
name: displayName,
logo: app?.logoUrl,
logo: detail.logo,
universalIdentifier: detail.universalIdentifier,
}}
/>
@@ -290,7 +295,7 @@ export const SettingsAvailableApplicationDetails = () => {
<AppChip
applicationId={application?.id}
fallbackApplicationData={{
logo: app?.logoUrl,
logo: detail?.logo,
name: displayName,
}}
size="md"
@@ -315,7 +320,7 @@ export const SettingsAvailableApplicationDetails = () => {
<SettingsApplicationInstallPermissionValidationModal
modalInstanceId={modalInstanceId}
appDisplayName={displayName}
appLogoUrl={app?.logoUrl}
appLogoUrl={detail?.logo ?? undefined}
defaultRole={defaultRole}
onAuthorize={handleInstall}
isInstalling={isInstalling}
@@ -1,9 +1,9 @@
import { SettingsApplicationInstallPermissionValidationModal } from '@/marketplace/components/SettingsApplicationInstallPermissionValidationModal';
import { useInstallMarketplaceAppWithPermissionValidation } from '@/marketplace/hooks/useInstallMarketplaceAppWithPermissionValidation';
import { getMarketplaceAppDefaultRoleManifest } from '@/marketplace/utils/getMarketplaceAppDefaultRoleManifest';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useQuery } from '@apollo/client/react';
import { type Manifest } from 'twenty-shared/application';
import { isDefined } from 'twenty-shared/utils';
import {
IconArrowUpRight,
@@ -49,15 +49,10 @@ export const SettingsApplicationRegistrationShareLinkButtons = ({
skip: !installable || !isDefined(universalIdentifier),
});
const manifest = detailData?.findMarketplaceAppDetail?.manifest as
| Manifest
| undefined;
const app = manifest?.application;
const displayName = app?.displayName ?? '';
const detail = detailData?.findMarketplaceAppDetail;
const displayName = detail?.name ?? '';
const defaultRole = manifest?.roles?.find(
(r) => r.universalIdentifier === app?.defaultRoleUniversalIdentifier,
);
const defaultRole = getMarketplaceAppDefaultRoleManifest(detail);
const handleInstall = async () => {
if (installable) {
@@ -79,7 +74,7 @@ export const SettingsApplicationRegistrationShareLinkButtons = ({
<SettingsApplicationInstallPermissionValidationModal
modalInstanceId={modalInstanceId}
appDisplayName={displayName}
appLogoUrl={app?.logoUrl}
appLogoUrl={detail?.logo ?? undefined}
defaultRole={defaultRole}
onAuthorize={handleInstall}
isInstalling={isInstalling}
@@ -2,7 +2,7 @@ import { renderHook, waitFor } from '@testing-library/react';
import { type MockedResponse } from '@apollo/client/testing';
import { useApplicationManifest } from '~/pages/settings/layout/hooks/useApplicationManifest';
import {
FindMarketplaceAppDetailDocument,
FindMarketplaceAppManifestDocument,
FindOneApplicationDocument,
} from '~/generated-metadata/graphql';
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
@@ -20,11 +20,11 @@ const findOneApplicationMock = (
result: { data: { findOneApplication: application } },
});
const findMarketplaceAppDetailMock = (
const findMarketplaceAppManifestMock = (
manifest: object | null,
): MockedResponse => ({
request: {
query: FindMarketplaceAppDetailDocument,
query: FindMarketplaceAppManifestDocument,
variables: { universalIdentifier: APP_UID },
},
result: {
@@ -52,7 +52,7 @@ describe('useApplicationManifest', () => {
universalIdentifier: APP_UID,
name: 'My App',
}),
findMarketplaceAppDetailMock(null),
findMarketplaceAppManifestMock(null),
],
});
@@ -1,7 +1,7 @@
import { useQuery } from '@apollo/client/react';
import { type Manifest } from 'twenty-shared/application';
import {
FindMarketplaceAppDetailDocument,
FindMarketplaceAppManifestDocument,
FindOneApplicationDocument,
} from '~/generated-metadata/graphql';
@@ -20,7 +20,7 @@ export const useApplicationManifest = (applicationId: string) => {
const application = appData?.findOneApplication;
const { data: detailData, loading: detailLoading } = useQuery(
FindMarketplaceAppDetailDocument,
FindMarketplaceAppManifestDocument,
{
variables: {
universalIdentifier: application?.universalIdentifier ?? '',
@@ -4,6 +4,7 @@ import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
import { GraphQLJSON } from 'graphql-type-json';
import { type Manifest } from 'twenty-shared/application';
import { MarketplaceAppRoleDTO } from 'src/engine/core-modules/application/application-marketplace/dtos/marketplace-app-role.dto';
import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/application/application-registration/enums/application-registration-source-type.enum';
@ObjectType('MarketplaceAppDetail')
@@ -44,6 +45,67 @@ export class MarketplaceAppDetailDTO {
@Field(() => Boolean)
isFeatured: boolean;
@Field(() => GraphQLJSON, { nullable: true })
@IsOptional()
@IsString()
@Field({ nullable: true })
description?: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
author?: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
category?: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
logo?: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
websiteUrl?: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
aboutDescription?: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
termsUrl?: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
emailSupport?: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
issueReportUrl?: string;
@Field(() => [String])
screenshots: string[];
@IsOptional()
@IsString()
@Field({ nullable: true })
defaultRoleUniversalIdentifier?: string;
@IsOptional()
@Field(() => [MarketplaceAppRoleDTO], { nullable: true })
roles?: MarketplaceAppRoleDTO[];
@Field(() => GraphQLJSON, {
nullable: true,
deprecationReason:
'Use the explicit MarketplaceAppDetail fields (description, author, roles, ...) instead',
})
manifest?: Manifest;
}
@@ -0,0 +1,129 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { IsBoolean, IsNotEmpty, IsOptional, IsString } from 'class-validator';
@ObjectType('MarketplaceAppRoleObjectPermission')
export class MarketplaceAppRoleObjectPermissionDTO {
@IsString()
@IsNotEmpty()
@Field()
universalIdentifier: string;
@IsString()
@IsNotEmpty()
@Field()
objectUniversalIdentifier: string;
@IsOptional()
@IsBoolean()
@Field(() => Boolean, { nullable: true })
canReadObjectRecords?: boolean;
@IsOptional()
@IsBoolean()
@Field(() => Boolean, { nullable: true })
canUpdateObjectRecords?: boolean;
@IsOptional()
@IsBoolean()
@Field(() => Boolean, { nullable: true })
canSoftDeleteObjectRecords?: boolean;
@IsOptional()
@IsBoolean()
@Field(() => Boolean, { nullable: true })
canDestroyObjectRecords?: boolean;
}
@ObjectType('MarketplaceAppRoleFieldPermission')
export class MarketplaceAppRoleFieldPermissionDTO {
@IsString()
@IsNotEmpty()
@Field()
universalIdentifier: string;
@IsString()
@IsNotEmpty()
@Field()
objectUniversalIdentifier: string;
@IsString()
@IsNotEmpty()
@Field()
fieldUniversalIdentifier: string;
@IsOptional()
@IsBoolean()
@Field(() => Boolean, { nullable: true })
canReadFieldValue?: boolean;
@IsOptional()
@IsBoolean()
@Field(() => Boolean, { nullable: true })
canUpdateFieldValue?: boolean;
}
@ObjectType('MarketplaceAppRole')
export class MarketplaceAppRoleDTO {
@IsString()
@IsNotEmpty()
@Field()
universalIdentifier: string;
@IsString()
@IsNotEmpty()
@Field()
label: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
description?: string;
@IsOptional()
@IsString()
@Field({ nullable: true })
icon?: string;
@IsOptional()
@IsBoolean()
@Field(() => Boolean, { nullable: true })
canUpdateAllSettings?: boolean;
@IsOptional()
@IsBoolean()
@Field(() => Boolean, { nullable: true })
canAccessAllTools?: boolean;
@IsOptional()
@IsBoolean()
@Field(() => Boolean, { nullable: true })
canReadAllObjectRecords?: boolean;
@IsOptional()
@IsBoolean()
@Field(() => Boolean, { nullable: true })
canUpdateAllObjectRecords?: boolean;
@IsOptional()
@IsBoolean()
@Field(() => Boolean, { nullable: true })
canSoftDeleteAllObjectRecords?: boolean;
@IsOptional()
@IsBoolean()
@Field(() => Boolean, { nullable: true })
canDestroyAllObjectRecords?: boolean;
@IsOptional()
@Field(() => [String], { nullable: true })
permissionFlagUniversalIdentifiers?: string[];
@IsOptional()
@Field(() => [MarketplaceAppRoleObjectPermissionDTO], { nullable: true })
objectPermissions?: MarketplaceAppRoleObjectPermissionDTO[];
@IsOptional()
@Field(() => [MarketplaceAppRoleFieldPermissionDTO], { nullable: true })
fieldPermissions?: MarketplaceAppRoleFieldPermissionDTO[];
}
@@ -1,6 +1,7 @@
import { Injectable, Logger } from '@nestjs/common';
import { isDefined } from 'twenty-shared/utils';
import { type RoleManifest } from 'twenty-shared/application';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
import { type ApplicationRegistrationEntity } from 'src/engine/core-modules/application/application-registration/application-registration.entity';
import {
@@ -15,6 +16,7 @@ import {
import { MarketplaceCatalogSyncCronJob } from 'src/engine/core-modules/application/application-marketplace/crons/marketplace-catalog-sync.cron.job';
import { MarketplaceAppDTO } from 'src/engine/core-modules/application/application-marketplace/dtos/marketplace-app.dto';
import { MarketplaceAppDetailDTO } from 'src/engine/core-modules/application/application-marketplace/dtos/marketplace-app-detail.dto';
import { MarketplaceAppRoleDTO } from 'src/engine/core-modules/application/application-marketplace/dtos/marketplace-app-role.dto';
import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator';
import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants';
import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service';
@@ -115,7 +117,80 @@ export class MarketplaceQueryService {
latestAvailableVersion: registration.latestAvailableVersion ?? undefined,
isListed: registration.isListed,
isFeatured: registration.isFeatured,
description:
registration.description ??
registration.manifest?.application?.description ??
undefined,
author:
registration.author ??
registration.manifest?.application?.author ??
undefined,
category:
registration.category ??
registration.manifest?.application?.category ??
undefined,
logo: registration.logoUrl ?? undefined,
websiteUrl:
registration.websiteUrl ??
registration.manifest?.application?.websiteUrl ??
undefined,
aboutDescription:
registration.aboutDescription ??
registration.manifest?.application?.aboutDescription ??
undefined,
termsUrl:
registration.termsUrl ??
registration.manifest?.application?.termsUrl ??
undefined,
emailSupport:
registration.emailSupport ??
registration.manifest?.application?.emailSupport ??
undefined,
issueReportUrl:
registration.issueReportUrl ??
registration.manifest?.application?.issueReportUrl ??
undefined,
screenshots: isNonEmptyArray(registration.screenshots)
? registration.screenshots
: (registration.manifest?.application?.screenshots ?? []),
defaultRoleUniversalIdentifier:
registration.manifest?.application?.defaultRoleUniversalIdentifier,
roles: registration.manifest?.roles?.map((role) =>
this.toMarketplaceAppRoleDTO(role),
),
manifest: registration.manifest ?? undefined,
};
}
private toMarketplaceAppRoleDTO(role: RoleManifest): MarketplaceAppRoleDTO {
return {
universalIdentifier: role.universalIdentifier,
label: role.label,
description: role.description,
icon: role.icon,
canUpdateAllSettings: role.canUpdateAllSettings,
canAccessAllTools: role.canAccessAllTools,
canReadAllObjectRecords: role.canReadAllObjectRecords,
canUpdateAllObjectRecords: role.canUpdateAllObjectRecords,
canSoftDeleteAllObjectRecords: role.canSoftDeleteAllObjectRecords,
canDestroyAllObjectRecords: role.canDestroyAllObjectRecords,
permissionFlagUniversalIdentifiers:
role.permissionFlagUniversalIdentifiers,
objectPermissions: role.objectPermissions?.map((permission) => ({
universalIdentifier: permission.universalIdentifier,
objectUniversalIdentifier: permission.objectUniversalIdentifier,
canReadObjectRecords: permission.canReadObjectRecords,
canUpdateObjectRecords: permission.canUpdateObjectRecords,
canSoftDeleteObjectRecords: permission.canSoftDeleteObjectRecords,
canDestroyObjectRecords: permission.canDestroyObjectRecords,
})),
fieldPermissions: role.fieldPermissions?.map((permission) => ({
universalIdentifier: permission.universalIdentifier,
objectUniversalIdentifier: permission.objectUniversalIdentifier,
fieldUniversalIdentifier: permission.fieldUniversalIdentifier,
canReadFieldValue: permission.canReadFieldValue,
canUpdateFieldValue: permission.canUpdateFieldValue,
})),
};
}
}