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">``<img alt="Review in cubic" src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg">``</a>
This commit is contained in:
@@ -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
+39
-1
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
+10
@@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
+36
@@ -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,
|
||||
})),
|
||||
});
|
||||
+20
@@ -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;
|
||||
};
|
||||
+20
-13
@@ -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
|
||||
}
|
||||
|
||||
+26
-21
@@ -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}
|
||||
|
||||
+5
-10
@@ -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}
|
||||
|
||||
+4
-4
@@ -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 ?? '',
|
||||
|
||||
+63
-1
@@ -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;
|
||||
}
|
||||
|
||||
+129
@@ -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[];
|
||||
}
|
||||
+76
-1
@@ -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,
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user