From 0001c2e7d06d2d7a6d93ca9254238d97e09594cd Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Fri, 30 Jan 2026 18:32:32 +0100 Subject: [PATCH] [Apps] Apps marketplace (first draft) (#17562) https://github.com/user-attachments/assets/c4e63edb-6e98-44bc-841f-ee110ae712d4 How it works - `manifest.json` files are now committed when apps are published to our repo - to display available apps, from the server, we read into our github repo, using a pod-scoped cache - feature flagged - app installation will be behind permission gate MARKETPLACE_APPS Limitations and what is yet to develop - content and settings tabs - installed apps tab - app installation - test and potentially fix reading from .manifest.json once [Reshape manifest structure](https://github.com/twentyhq/core-team-issues/issues/2183) is done. additional work is expected on assets notably. (couldnt properly do it here as manifest.json will only be committed after this pr) - we only read in community/ folder for now - we may want tochange that - the cache is rather artisanal for now and scoped by pod - we may want to change that --- .../src/utils/app-template.ts | 4 +- packages/twenty-front/codegen.cjs | 1 + .../src/generated-metadata/graphql.ts | 21 + .../twenty-front/src/generated/graphql.ts | 72 +++ .../modules/ai/hooks/useAIChatFileUpload.ts | 6 +- .../modules/app/components/SettingsRoutes.tsx | 12 + .../mutations/installMarketplaceApp.ts | 7 + .../queries/findManyMarketplaceApps.ts | 21 + .../hooks/useInstallMarketplaceApp.ts | 9 + ...ntBase.tsx => SettingsCardContentBase.tsx} | 23 +- .../SettingsOptionCardContentButton.tsx | 24 +- .../SettingsOptionCardContentCounter.tsx | 24 +- .../SettingsOptionCardContentSelect.tsx | 24 +- .../SettingsOptionCardContentToggle.tsx | 38 +- .../components/SettingsAIRouterSettings.tsx | 46 +- .../applications/SettingsApplications.tsx | 122 ++--- .../SettingsAvailableApplicationDetails.tsx | 467 ++++++++++++++++++ .../SettingsAvailableApplicationCard.tsx | 63 +++ .../applications/hooks/useMarketplaceApps.ts | 34 ++ .../tabs/SettingsApplicationsAvailableTab.tsx | 81 +++ .../tabs/SettingsApplicationsCreateTab.tsx | 68 +++ .../tabs/SettingsApplicationsInstalledTab.tsx | 14 + .../types/availableApplication.ts | 20 + .../data-model/SettingsObjectTable.tsx | 101 ++-- ...ge-layout-universal-identifiers.command.ts | 2 +- .../1-15-backfill-updated-by-field.command.ts | 2 +- ...ackfill-opportunity-owner-field.command.ts | 2 +- ...-backfill-standard-page-layouts.command.ts | 2 +- .../1-16-identify-agent-metadata.command.ts | 2 +- .../1-16-identify-field-metadata.command.ts | 2 +- .../1-16-identify-index-metadata.command.ts | 2 +- .../1-16-identify-object-metadata.command.ts | 2 +- ...ify-remaining-entities-metadata.command.ts | 2 +- .../1-16-identify-role-metadata.command.ts | 2 +- ...16-identify-view-field-metadata.command.ts | 2 +- ...6-identify-view-filter-metadata.command.ts | 2 +- ...16-identify-view-group-metadata.command.ts | 2 +- .../1-16-identify-view-metadata.command.ts | 2 +- .../1-17-identify-webhook-metadata.command.ts | 2 +- .../application/application-sync.module.ts | 10 +- .../application/application.module.ts | 15 +- .../application/dtos/marketplace-app.dto.ts | 70 +++ .../{ => resolvers}/application.resolver.ts | 4 +- .../resolvers/marketplace.resolver.ts | 29 ++ .../application-sync.service.ts | 14 +- .../{ => services}/application.service.ts | 0 .../services/marketplace.service.ts | 250 ++++++++++ .../auth/services/sign-in-up.service.ts | 2 +- .../enums/feature-flag-key.enum.ts | 1 + .../mocks/data-enrichment-logo.svg | 14 + .../marketplace/mocks/mock-manifest.json | 37 ++ .../user/services/user.service.spec.ts | 2 +- .../workspace/workspace.resolver.ts | 2 +- .../ai/ai-agent/agent.service.ts | 2 +- .../command-menu-item.service.ts | 2 +- .../services/field-metadata.service.ts | 2 +- .../front-component.service.ts | 2 +- .../logic-function/logic-function.service.ts | 2 +- .../services/logic-function-v2.service.ts | 2 +- .../navigation-menu-item.service.ts | 2 +- .../object-metadata.service.ts | 2 +- .../services/page-layout-tab.service.ts | 2 +- .../services/page-layout-widget.service.ts | 2 +- .../page-layout-duplication.service.ts | 2 +- .../services/page-layout-update.service.ts | 2 +- .../services/page-layout.service.ts | 2 +- .../__tests__/permissions.service.spec.ts | 2 +- .../permissions/permissions.service.ts | 3 +- .../services/role-target.service.ts | 2 +- .../metadata-modules/role/role.resolver.ts | 2 +- .../row-level-permission-predicate.service.ts | 2 +- .../metadata-modules/skill/skill.service.ts | 2 +- .../services/view-field-v2.service.ts | 2 +- .../tests/view-filter-group.service.spec.ts | 2 +- .../services/view-filter-group.service.ts | 2 +- .../services/view-filter.service.ts | 2 +- .../view-group/services/view-group.service.ts | 2 +- .../services/tests/view-sort.service.spec.ts | 2 +- .../view-sort/services/view-sort.service.ts | 2 +- .../view/services/view.service.ts | 2 +- .../webhook/webhook.service.ts | 4 +- .../workspace-entity-manager.spec.ts | 1 + .../core/utils/seed-core-schema.util.ts | 2 +- .../core/utils/seed-feature-flags.util.ts | 5 + .../dev-seeder/services/dev-seeder.service.ts | 2 +- .../twenty-standard-application.service.ts | 2 +- .../workspace-manager.service.ts | 2 +- .../dashboard-tool.workspace-service.ts | 2 +- .../types/dashboard-tool-dependencies.type.ts | 2 +- .../workflow-execution-context.service.ts | 2 +- .../src/application/applicationType.ts | 12 + .../twenty-shared/src/application/index.ts | 5 +- .../src/constants/PermissionFlagType.ts | 1 + .../twenty-shared/src/types/SettingsPath.ts | 1 + .../display/icon/components/TablerIcons.ts | 3 + packages/twenty-ui/src/display/index.ts | 3 + .../src/input/components/SearchInput.tsx | 105 ++++ packages/twenty-ui/src/input/index.ts | 2 + 98 files changed, 1713 insertions(+), 280 deletions(-) create mode 100644 packages/twenty-front/src/modules/marketplace/graphql/mutations/installMarketplaceApp.ts create mode 100644 packages/twenty-front/src/modules/marketplace/graphql/queries/findManyMarketplaceApps.ts create mode 100644 packages/twenty-front/src/modules/marketplace/hooks/useInstallMarketplaceApp.ts rename packages/twenty-front/src/modules/settings/components/SettingsOptions/{SettingsOptionCardContentBase.tsx => SettingsCardContentBase.tsx} (57%) create mode 100644 packages/twenty-front/src/pages/settings/applications/SettingsAvailableApplicationDetails.tsx create mode 100644 packages/twenty-front/src/pages/settings/applications/components/SettingsAvailableApplicationCard.tsx create mode 100644 packages/twenty-front/src/pages/settings/applications/hooks/useMarketplaceApps.ts create mode 100644 packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationsAvailableTab.tsx create mode 100644 packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationsCreateTab.tsx create mode 100644 packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationsInstalledTab.tsx create mode 100644 packages/twenty-front/src/pages/settings/applications/types/availableApplication.ts create mode 100644 packages/twenty-server/src/engine/core-modules/application/dtos/marketplace-app.dto.ts rename packages/twenty-server/src/engine/core-modules/application/{ => resolvers}/application.resolver.ts (98%) create mode 100644 packages/twenty-server/src/engine/core-modules/application/resolvers/marketplace.resolver.ts rename packages/twenty-server/src/engine/core-modules/application/{ => services}/application-sync.service.ts (99%) rename packages/twenty-server/src/engine/core-modules/application/{ => services}/application.service.ts (100%) create mode 100644 packages/twenty-server/src/engine/core-modules/application/services/marketplace.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/marketplace/mocks/data-enrichment-logo.svg create mode 100644 packages/twenty-server/src/engine/core-modules/marketplace/mocks/mock-manifest.json create mode 100644 packages/twenty-ui/src/input/components/SearchInput.tsx diff --git a/packages/create-twenty-app/src/utils/app-template.ts b/packages/create-twenty-app/src/utils/app-template.ts index b5c3314e658..7e7757fe558 100644 --- a/packages/create-twenty-app/src/utils/app-template.ts +++ b/packages/create-twenty-app/src/utils/app-template.ts @@ -78,7 +78,9 @@ generated # dev /dist/ -.twenty + +.twenty/* +!.twenty/output/ # production /build diff --git a/packages/twenty-front/codegen.cjs b/packages/twenty-front/codegen.cjs index 262769bd9cf..ba3a56adfd7 100644 --- a/packages/twenty-front/codegen.cjs +++ b/packages/twenty-front/codegen.cjs @@ -15,6 +15,7 @@ module.exports = { './src/modules/favorites/graphql/**/*.{ts,tsx}', './src/modules/spreadsheet-import/graphql/**/*.{ts,tsx}', './src/modules/command-menu/graphql/**/*.{ts,tsx}', + './src/modules/marketplace/graphql/**/*.{ts,tsx}', './src/modules/prefetch/graphql/**/*.{ts,tsx}', './src/modules/subscription/graphql/**/*.{ts,tsx}', diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 983075af308..11157fd15a9 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -1414,6 +1414,7 @@ export enum FeatureFlagKey { IS_FILES_FIELD_ENABLED = 'IS_FILES_FIELD_ENABLED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED', + IS_MARKETPLACE_ENABLED = 'IS_MARKETPLACE_ENABLED', IS_NAVIGATION_MENU_ITEM_ENABLED = 'IS_NAVIGATION_MENU_ITEM_ENABLED', IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED', IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED', @@ -1978,6 +1979,23 @@ export type LoginTokenOutput = { loginToken: AuthToken; }; +export type MarketplaceApp = { + __typename?: 'MarketplaceApp'; + aboutDescription: Scalars['String']; + author: Scalars['String']; + category: Scalars['String']; + description: Scalars['String']; + icon: Scalars['String']; + id: Scalars['String']; + logo?: Maybe; + name: Scalars['String']; + providers: Array; + screenshots: Array; + termsUrl?: Maybe; + version: Scalars['String']; + websiteUrl?: Maybe; +}; + export enum MessageChannelVisibility { METADATA = 'METADATA', SHARE_EVERYTHING = 'SHARE_EVERYTHING', @@ -2106,6 +2124,7 @@ export type Mutation = { initiateOTPProvisioning: InitiateTwoFactorAuthenticationProvisioningOutput; initiateOTPProvisioningForAuthenticatedUser: InitiateTwoFactorAuthenticationProvisioningOutput; installApplication: Scalars['Boolean']; + installMarketplaceApp: Scalars['Boolean']; removeQueryFromEventStream: Scalars['Boolean']; removeRoleFromAgent: Scalars['Boolean']; renewToken: AuthTokens; @@ -3415,6 +3434,7 @@ export enum PermissionFlagType { IMPERSONATE = 'IMPERSONATE', IMPORT_CSV = 'IMPORT_CSV', LAYOUTS = 'LAYOUTS', + MARKETPLACE_APPS = 'MARKETPLACE_APPS', PROFILE_INFORMATION = 'PROFILE_INFORMATION', ROLES = 'ROLES', SECURITY = 'SECURITY', @@ -3538,6 +3558,7 @@ export type Query = { findManyAgents: Array; findManyApplications: Array; findManyLogicFunctions: Array; + findManyMarketplaceApps: Array; findManyPublicDomains: Array; findOneAgent: Agent; findOneApplication: Application; diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 96f99986bca..702fe4a5bba 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -1386,6 +1386,7 @@ export enum FeatureFlagKey { IS_FILES_FIELD_ENABLED = 'IS_FILES_FIELD_ENABLED', IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED', IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED', + IS_MARKETPLACE_ENABLED = 'IS_MARKETPLACE_ENABLED', IS_NAVIGATION_MENU_ITEM_ENABLED = 'IS_NAVIGATION_MENU_ITEM_ENABLED', IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED', IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED', @@ -1950,6 +1951,23 @@ export type LoginTokenOutput = { loginToken: AuthToken; }; +export type MarketplaceApp = { + __typename?: 'MarketplaceApp'; + aboutDescription: Scalars['String']; + author: Scalars['String']; + category: Scalars['String']; + description: Scalars['String']; + icon: Scalars['String']; + id: Scalars['String']; + logo?: Maybe; + name: Scalars['String']; + providers: Array; + screenshots: Array; + termsUrl?: Maybe; + version: Scalars['String']; + websiteUrl?: Maybe; +}; + export enum MessageChannelVisibility { METADATA = 'METADATA', SHARE_EVERYTHING = 'SHARE_EVERYTHING', @@ -2070,6 +2088,7 @@ export type Mutation = { initiateOTPProvisioning: InitiateTwoFactorAuthenticationProvisioningOutput; initiateOTPProvisioningForAuthenticatedUser: InitiateTwoFactorAuthenticationProvisioningOutput; installApplication: Scalars['Boolean']; + installMarketplaceApp: Scalars['Boolean']; removeQueryFromEventStream: Scalars['Boolean']; removeRoleFromAgent: Scalars['Boolean']; renewToken: AuthTokens; @@ -3320,6 +3339,7 @@ export enum PermissionFlagType { IMPERSONATE = 'IMPERSONATE', IMPORT_CSV = 'IMPORT_CSV', LAYOUTS = 'LAYOUTS', + MARKETPLACE_APPS = 'MARKETPLACE_APPS', PROFILE_INFORMATION = 'PROFILE_INFORMATION', ROLES = 'ROLES', SECURITY = 'SECURITY', @@ -3439,6 +3459,7 @@ export type Query = { findManyAgents: Array; findManyApplications: Array; findManyLogicFunctions: Array; + findManyMarketplaceApps: Array; findManyPublicDomains: Array; findOneAgent: Agent; findOneApplication: Application; @@ -5208,6 +5229,11 @@ export type SearchQueryVariables = Exact<{ export type SearchQuery = { __typename?: 'Query', search: { __typename?: 'SearchResultConnection', edges: Array<{ __typename?: 'SearchResultEdge', cursor: string, node: { __typename?: 'SearchRecord', recordId: any, objectNameSingular: string, objectLabelSingular: string, label: string, imageUrl?: string | null, tsRankCD: number, tsRank: number } }>, pageInfo: { __typename?: 'SearchResultPageInfo', hasNextPage: boolean, endCursor?: string | null } } }; +export type FindManyMarketplaceAppsQueryVariables = Exact<{ [key: string]: never; }>; + + +export type FindManyMarketplaceAppsQuery = { __typename?: 'Query', findManyMarketplaceApps: Array<{ __typename?: 'MarketplaceApp', id: string, name: string, description: string, icon: string, version: string, author: string, category: string, logo?: string | null, screenshots: Array, aboutDescription: string, providers: Array, websiteUrl?: string | null, termsUrl?: string | null }> }; + export type PageLayoutWidgetFragmentFragment = { __typename?: 'PageLayoutWidget', id: any, title: string, type: WidgetType, objectMetadataId?: any | null, createdAt: string, updatedAt: string, deletedAt?: string | null, pageLayoutTabId: any, gridPosition: { __typename?: 'GridPosition', column: number, columnSpan: number, row: number, rowSpan: number }, configuration: { __typename?: 'AggregateChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, label?: string | null, displayDataLabel?: boolean | null, format?: string | null, description?: string | null, filter?: any | null, prefix?: string | null, suffix?: string | null, timezone?: string | null, firstDayOfTheWeek?: number | null, ratioAggregateConfig?: { __typename?: 'RatioAggregateConfig', fieldMetadataId: any, optionValue: string } | null } | { __typename?: 'BarChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, groupMode?: BarChartGroupMode | null, layout: BarChartLayout, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'CalendarConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'EmailsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldRichTextConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FieldsConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FilesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'FrontComponentConfiguration', configurationType: WidgetConfigurationType, frontComponentId: any } | { __typename?: 'GaugeChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, displayDataLabel?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'IframeConfiguration', configurationType: WidgetConfigurationType, url?: string | null } | { __typename?: 'LineChartConfiguration', configurationType: WidgetConfigurationType, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, primaryAxisGroupByFieldMetadataId: any, primaryAxisGroupBySubFieldName?: string | null, primaryAxisDateGranularity?: ObjectRecordGroupByDateGranularity | null, primaryAxisOrderBy?: GraphOrderBy | null, primaryAxisManualSortOrder?: Array | null, secondaryAxisGroupByFieldMetadataId?: any | null, secondaryAxisGroupBySubFieldName?: string | null, secondaryAxisGroupByDateGranularity?: ObjectRecordGroupByDateGranularity | null, secondaryAxisOrderBy?: GraphOrderBy | null, secondaryAxisManualSortOrder?: Array | null, omitNullValues?: boolean | null, axisNameDisplay?: AxisNameDisplay | null, displayDataLabel?: boolean | null, displayLegend?: boolean | null, rangeMin?: number | null, rangeMax?: number | null, color?: string | null, description?: string | null, filter?: any | null, isStacked?: boolean | null, isCumulative?: boolean | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'NotesConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'PieChartConfiguration', configurationType: WidgetConfigurationType, groupByFieldMetadataId: any, aggregateFieldMetadataId: any, aggregateOperation: AggregateOperations, groupBySubFieldName?: string | null, dateGranularity?: ObjectRecordGroupByDateGranularity | null, orderBy?: GraphOrderBy | null, manualSortOrder?: Array | null, displayDataLabel?: boolean | null, showCenterMetric?: boolean | null, displayLegend?: boolean | null, hideEmptyCategory?: boolean | null, color?: string | null, description?: string | null, filter?: any | null, timezone?: string | null, firstDayOfTheWeek?: number | null } | { __typename?: 'StandaloneRichTextConfiguration', configurationType: WidgetConfigurationType, body: { __typename?: 'RichTextV2Body', blocknote?: string | null, markdown?: string | null } } | { __typename?: 'TasksConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'TimelineConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'ViewConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowRunConfiguration', configurationType: WidgetConfigurationType } | { __typename?: 'WorkflowVersionConfiguration', configurationType: WidgetConfigurationType } }; export type UpdatePageLayoutWithTabsAndWidgetsMutationVariables = Exact<{ @@ -5844,6 +5870,52 @@ export function useSearchLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions; export type SearchLazyQueryHookResult = ReturnType; export type SearchQueryResult = Apollo.QueryResult; +export const FindManyMarketplaceAppsDocument = gql` + query FindManyMarketplaceApps { + findManyMarketplaceApps { + id + name + description + icon + version + author + category + logo + screenshots + aboutDescription + providers + websiteUrl + termsUrl + } +} + `; + +/** + * __useFindManyMarketplaceAppsQuery__ + * + * To run a query within a React component, call `useFindManyMarketplaceAppsQuery` and pass it any options that fit your needs. + * When your component renders, `useFindManyMarketplaceAppsQuery` returns an object from Apollo Client that contains loading, error, and data properties + * you can use to render your UI. + * + * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; + * + * @example + * const { data, loading, error } = useFindManyMarketplaceAppsQuery({ + * variables: { + * }, + * }); + */ +export function useFindManyMarketplaceAppsQuery(baseOptions?: Apollo.QueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useQuery(FindManyMarketplaceAppsDocument, options); + } +export function useFindManyMarketplaceAppsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { + const options = {...defaultOptions, ...baseOptions} + return Apollo.useLazyQuery(FindManyMarketplaceAppsDocument, options); + } +export type FindManyMarketplaceAppsQueryHookResult = ReturnType; +export type FindManyMarketplaceAppsLazyQueryHookResult = ReturnType; +export type FindManyMarketplaceAppsQueryResult = Apollo.QueryResult; export const UpdatePageLayoutWithTabsAndWidgetsDocument = gql` mutation UpdatePageLayoutWithTabsAndWidgets($id: String!, $input: UpdatePageLayoutWithTabsInput!) { updatePageLayoutWithTabsAndWidgets(id: $id, input: $input) { diff --git a/packages/twenty-front/src/modules/ai/hooks/useAIChatFileUpload.ts b/packages/twenty-front/src/modules/ai/hooks/useAIChatFileUpload.ts index 1afbd2ff8b8..92ea1688708 100644 --- a/packages/twenty-front/src/modules/ai/hooks/useAIChatFileUpload.ts +++ b/packages/twenty-front/src/modules/ai/hooks/useAIChatFileUpload.ts @@ -7,8 +7,10 @@ import { type FileUIPart } from 'ai'; import { useRecoilState } from 'recoil'; import { buildSignedPath, isDefined } from 'twenty-shared/utils'; import { REACT_APP_SERVER_BASE_URL } from '~/config'; -import { useUploadFileMutation } from '~/generated-metadata/graphql'; -import { FileFolder } from '~/generated/graphql'; +import { + FileFolder, + useUploadFileMutation, +} from '~/generated-metadata/graphql'; export const useAIChatFileUpload = () => { const coreClient = useApolloCoreClient(); diff --git a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 4f78f8ebe61..93dd5478270 100644 --- a/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -159,6 +159,14 @@ const SettingsApplicationDetails = lazy(() => ), ); +const SettingsAvailableApplicationDetails = lazy(() => + import( + '~/pages/settings/applications/SettingsAvailableApplicationDetails' + ).then((module) => ({ + default: module.SettingsAvailableApplicationDetails, + })), +); + const SettingsAgentForm = lazy(() => import('~/pages/settings/ai/SettingsAgentForm').then((module) => ({ default: module.SettingsAgentForm, @@ -581,6 +589,10 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => ( path={SettingsPath.ApplicationDetail} element={} /> + } + /> } diff --git a/packages/twenty-front/src/modules/marketplace/graphql/mutations/installMarketplaceApp.ts b/packages/twenty-front/src/modules/marketplace/graphql/mutations/installMarketplaceApp.ts new file mode 100644 index 00000000000..223be5ab51d --- /dev/null +++ b/packages/twenty-front/src/modules/marketplace/graphql/mutations/installMarketplaceApp.ts @@ -0,0 +1,7 @@ +import gql from 'graphql-tag'; + +export const INSTALL_MARKETPLACE_APP = gql` + mutation InstallMarketplaceApp() { + installMarketplaceApp() + } +`; diff --git a/packages/twenty-front/src/modules/marketplace/graphql/queries/findManyMarketplaceApps.ts b/packages/twenty-front/src/modules/marketplace/graphql/queries/findManyMarketplaceApps.ts new file mode 100644 index 00000000000..7103b80334f --- /dev/null +++ b/packages/twenty-front/src/modules/marketplace/graphql/queries/findManyMarketplaceApps.ts @@ -0,0 +1,21 @@ +import gql from 'graphql-tag'; + +export const FIND_MANY_MARKETPLACE_APPS = gql` + query FindManyMarketplaceApps { + findManyMarketplaceApps { + id + name + description + icon + version + author + category + logo + screenshots + aboutDescription + providers + websiteUrl + termsUrl + } + } +`; diff --git a/packages/twenty-front/src/modules/marketplace/hooks/useInstallMarketplaceApp.ts b/packages/twenty-front/src/modules/marketplace/hooks/useInstallMarketplaceApp.ts new file mode 100644 index 00000000000..c629422f20a --- /dev/null +++ b/packages/twenty-front/src/modules/marketplace/hooks/useInstallMarketplaceApp.ts @@ -0,0 +1,9 @@ +export const useInstallMarketplaceApp = () => { + const install = async () => { + return true; + }; + + return { + install, + }; +}; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentBase.tsx b/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsCardContentBase.tsx similarity index 57% rename from packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentBase.tsx rename to packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsCardContentBase.tsx index 4ebe90e9675..68938b8183a 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentBase.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsCardContentBase.tsx @@ -2,16 +2,19 @@ import styled from '@emotion/styled'; type StyledCardContentProps = { disabled?: boolean; + alignItems?: 'center' | 'flex-start'; + fullHeight?: boolean; }; -export const StyledSettingsOptionCardContent = styled.div` - align-items: center; +export const StyledSettingsCardContent = styled.div` + align-items: ${({ alignItems }) => alignItems ?? 'center'}; display: flex; gap: ${({ theme }) => theme.spacing(3)}; background-color: ${({ theme }) => theme.background.secondary}; + height: ${({ fullHeight }) => (fullHeight ? '100%' : 'auto')}; padding: ${({ theme }) => theme.spacing(4)}; `; -export const StyledSettingsOptionCardIcon = styled.div` +export const StyledSettingsCardIcon = styled.div` align-items: center; border: 2px solid ${({ theme }) => theme.border.color.light}; border-radius: ${({ theme }) => theme.border.radius.sm}; @@ -23,15 +26,17 @@ export const StyledSettingsOptionCardIcon = styled.div` min-width: ${({ theme }) => theme.icon.size.md}; `; -export const StyledSettingsOptionCardTitle = styled.div` +export const StyledSettingsCardTitle = styled.div` color: ${({ theme }) => theme.font.color.primary}; font-weight: ${({ theme }) => theme.font.weight.medium}; margin-bottom: ${({ theme }) => theme.spacing(1)}; `; -export const StyledSettingsOptionCardDescription = styled.div` - color: ${({ theme }) => theme.font.color.tertiary}; +export const StyledSettingsCardDescription = styled.div` + color: ${({ theme }) => theme.font.color.secondary}; font-size: ${({ theme }) => theme.font.size.sm}; + overflow: hidden; + line-height: 1.5; a { position: relative; @@ -39,3 +44,9 @@ export const StyledSettingsOptionCardDescription = styled.div` pointer-events: auto; } `; + +export const StyledSettingsCardThirdLine = styled.div` + color: ${({ theme }) => theme.font.color.light}; + font-size: ${({ theme }) => theme.font.size.xs}; + margin-top: ${({ theme }) => theme.spacing(1)}; +`; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentButton.tsx b/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentButton.tsx index 7641508179c..2fd576d013e 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentButton.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentButton.tsx @@ -1,9 +1,9 @@ import { - StyledSettingsOptionCardContent, - StyledSettingsOptionCardDescription, - StyledSettingsOptionCardIcon, - StyledSettingsOptionCardTitle, -} from '@/settings/components/SettingsOptions/SettingsOptionCardContentBase'; + StyledSettingsCardContent, + StyledSettingsCardDescription, + StyledSettingsCardIcon, + StyledSettingsCardTitle, +} from '@/settings/components/SettingsOptions/SettingsCardContentBase'; import { SettingsOptionIconCustomizer } from '@/settings/components/SettingsOptions/SettingsOptionIconCustomizer'; import styled from '@emotion/styled'; import { type IconComponent } from 'twenty-ui/display'; @@ -28,19 +28,19 @@ export const SettingsOptionCardContentButton = ({ Button, }: SettingsOptionCardContentButtonProps) => { return ( - + {Icon && ( - + - + )}
- {title} - + {title} + {description} - +
{Button && {Button}} -
+ ); }; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentCounter.tsx b/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentCounter.tsx index fbd5b32528d..b84d01e2c6b 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentCounter.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentCounter.tsx @@ -1,10 +1,10 @@ import { SettingsCounter } from '@/settings/components/SettingsCounter'; import { - StyledSettingsOptionCardContent, - StyledSettingsOptionCardDescription, - StyledSettingsOptionCardIcon, - StyledSettingsOptionCardTitle, -} from '@/settings/components/SettingsOptions/SettingsOptionCardContentBase'; + StyledSettingsCardContent, + StyledSettingsCardDescription, + StyledSettingsCardIcon, + StyledSettingsCardTitle, +} from '@/settings/components/SettingsOptions/SettingsCardContentBase'; import { SettingsOptionIconCustomizer } from '@/settings/components/SettingsOptions/SettingsOptionIconCustomizer'; import { type IconComponent } from 'twenty-ui/display'; @@ -32,18 +32,18 @@ export const SettingsOptionCardContentCounter = ({ showButtons = true, }: SettingsOptionCardContentCounterProps) => { return ( - + {Icon && ( - + - + )}
- {title} + {title} {description && ( - + {description} - + )}
-
+ ); }; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentSelect.tsx b/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentSelect.tsx index 4a596588bf2..d5402a16593 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentSelect.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentSelect.tsx @@ -1,9 +1,9 @@ import { - StyledSettingsOptionCardContent, - StyledSettingsOptionCardDescription, - StyledSettingsOptionCardIcon, - StyledSettingsOptionCardTitle, -} from '@/settings/components/SettingsOptions/SettingsOptionCardContentBase'; + StyledSettingsCardContent, + StyledSettingsCardDescription, + StyledSettingsCardIcon, + StyledSettingsCardTitle, +} from '@/settings/components/SettingsOptions/SettingsCardContentBase'; import { SettingsOptionIconCustomizer } from '@/settings/components/SettingsOptions/SettingsOptionIconCustomizer'; import styled from '@emotion/styled'; import { type IconComponent } from 'twenty-ui/display'; @@ -30,19 +30,19 @@ export const SettingsOptionCardContentSelect = ({ children, }: SettingsOptionCardContentSelectProps) => { return ( - + {Icon && ( - + - + )}
- {title} - + {title} + {description} - +
{children} -
+ ); }; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentToggle.tsx b/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentToggle.tsx index 31350c49464..dc2dfd99889 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentToggle.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsOptions/SettingsOptionCardContentToggle.tsx @@ -1,10 +1,10 @@ import { Separator } from '@/settings/components/Separator'; import { - StyledSettingsOptionCardContent, - StyledSettingsOptionCardDescription, - StyledSettingsOptionCardIcon, - StyledSettingsOptionCardTitle, -} from '@/settings/components/SettingsOptions/SettingsOptionCardContentBase'; + StyledSettingsCardContent, + StyledSettingsCardDescription, + StyledSettingsCardIcon, + StyledSettingsCardTitle, +} from '@/settings/components/SettingsOptions/SettingsCardContentBase'; import { SettingsOptionIconCustomizer } from '@/settings/components/SettingsOptions/SettingsOptionIconCustomizer'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; @@ -12,9 +12,7 @@ import { useId } from 'react'; import { type IconComponent } from 'twenty-ui/display'; import { Toggle } from 'twenty-ui/input'; -const StyledSettingsOptionCardToggleContent = styled( - StyledSettingsOptionCardContent, -)` +const StyledSettingsCardToggleContent = styled(StyledSettingsCardContent)` cursor: ${({ disabled }) => (disabled ? 'default' : 'pointer')}; position: relative; pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')}; @@ -24,7 +22,7 @@ const StyledSettingsOptionCardToggleContent = styled( } `; -const StyledSettingsOptionCardToggleButton = styled(Toggle)<{ +const StyledSettingsCardToggleButton = styled(Toggle)<{ toggleCentered?: boolean; }>` align-self: ${({ toggleCentered }) => @@ -32,7 +30,7 @@ const StyledSettingsOptionCardToggleButton = styled(Toggle)<{ margin-left: auto; `; -const StyledSettingsOptionCardToggleCover = styled.span` +const StyledSettingsCardToggleCover = styled.span` cursor: pointer; inset: 0; position: absolute; @@ -66,24 +64,24 @@ export const SettingsOptionCardContentToggle = ({ return ( <> - + {Icon && ( - + - + )}
- + - - + + {description} - +
- -
+ {divider && } ); diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAIRouterSettings.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAIRouterSettings.tsx index 213cead967f..dd435719cbe 100644 --- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAIRouterSettings.tsx +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAIRouterSettings.tsx @@ -10,11 +10,11 @@ import { import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { aiModelsState } from '@/client-config/states/aiModelsState'; import { - StyledSettingsOptionCardContent, - StyledSettingsOptionCardDescription, - StyledSettingsOptionCardIcon, - StyledSettingsOptionCardTitle, -} from '@/settings/components/SettingsOptions/SettingsOptionCardContentBase'; + StyledSettingsCardContent, + StyledSettingsCardDescription, + StyledSettingsCardIcon, + StyledSettingsCardTitle, +} from '@/settings/components/SettingsOptions/SettingsCardContentBase'; import { SettingsOptionIconCustomizer } from '@/settings/components/SettingsOptions/SettingsOptionIconCustomizer'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Select } from '@/ui/input/components/Select'; @@ -161,25 +161,23 @@ export const SettingsAIRouterSettings = () => { {noModelsAvailable ? ( - + {t`No models available. Please configure AI models in your workspace settings.`} - + ) : ( - - + + - +
- - {t`Fast Model`} - - + {t`Fast Model`} + {t`Quick model for routing decisions`} - +
{ selectSizeVariant="small" /> -
+
)} diff --git a/packages/twenty-front/src/pages/settings/applications/SettingsApplications.tsx b/packages/twenty-front/src/pages/settings/applications/SettingsApplications.tsx index 0bd7042a220..3848baff589 100644 --- a/packages/twenty-front/src/pages/settings/applications/SettingsApplications.tsx +++ b/packages/twenty-front/src/pages/settings/applications/SettingsApplications.tsx @@ -1,55 +1,78 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; 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 { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { useLingui } from '@lingui/react/macro'; import { SettingsPath } from 'twenty-shared/types'; import { getSettingsPath } from 'twenty-shared/utils'; +import { IconApps, IconCode, IconDownload } from 'twenty-ui/display'; import { useFindManyApplicationsQuery } from '~/generated-metadata/graphql'; +import { type FeatureFlagKey } from '~/generated/graphql'; import { SettingsApplicationsTable } from '~/pages/settings/applications/components/SettingsApplicationsTable'; -import { - H2Title, - CommandBlock, - IconCopy, - IconFileInfo, -} from 'twenty-ui/display'; -import { Section } from 'twenty-ui/layout'; -import { useCopyToClipboard } from '~/hooks/useCopyToClipboard'; -import { useLingui } from '@lingui/react/macro'; -import { Button } from 'twenty-ui/input'; -import styled from '@emotion/styled'; -import { useRecoilValue } from 'recoil'; -import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; -import { getDocumentationUrl } from '@/support/utils/getDocumentationUrl'; +import { SettingsApplicationsAvailableTab } from '~/pages/settings/applications/tabs/SettingsApplicationsAvailableTab'; +import { SettingsApplicationsCreateTab } from '~/pages/settings/applications/tabs/SettingsApplicationsCreateTab'; +import { SettingsApplicationsInstalledTab } from '~/pages/settings/applications/tabs/SettingsApplicationsInstalledTab'; -const StyledButtonContainer = styled.div` - margin: ${({ theme }) => theme.spacing(2)} 0; -`; +const APPLICATIONS_TAB_LIST_ID = 'applications-tab-list'; export const SettingsApplications = () => { const { t } = useLingui(); - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + const isMarketplaceEnabled = useIsFeatureEnabled( + 'IS_MARKETPLACE_ENABLED' as FeatureFlagKey, + ); + + const activeTabId = useRecoilComponentValue( + activeTabIdComponentState, + APPLICATIONS_TAB_LIST_ID, + ); const { data } = useFindManyApplicationsQuery(); - const { copyToClipboard } = useCopyToClipboard(); - const applications = data?.findManyApplications ?? []; - const commands = [ - // eslint-disable-next-line lingui/no-unlocalized-strings - 'npx create-twenty-app@latest my-twenty-app', - // eslint-disable-next-line lingui/no-unlocalized-strings - 'cd my-twenty-app', + if (!isMarketplaceEnabled) { + return ( + + + {applications.length > 0 && ( + + )} + + + + ); + } + + const tabs = [ + { id: 'available', title: t`Available`, Icon: IconDownload }, + { id: 'installed', title: t`Installed`, Icon: IconApps }, + { id: 'create', title: t`Create an app`, Icon: IconCode }, ]; - const button = ( -