Skip to content

[Apps] Content + permission tabs for marketplace and installed apps#17888

Merged
ijreilly merged 10 commits intomainfrom
ft--marketplace-2
Feb 12, 2026
Merged

[Apps] Content + permission tabs for marketplace and installed apps#17888
ijreilly merged 10 commits intomainfrom
ft--marketplace-2

Conversation

@ijreilly
Copy link
Copy Markdown
Contributor

@ijreilly ijreilly commented Feb 12, 2026

In this PR

  • Content tab for marketplace apps and installed apps: complies with figma; addition of field section showing fields added to standard objects; initiative: arrow opens a sub-table of fields rows. (suggested because 1/ for marketplace apps we cannot redirect to the actual object page in settings since the object does not exist yet in the workspace 2/ since we dont have an quick "go back to application page" option, it can be annoying to be redirected to a different setting page when we just want to look at the content of the app objects)
  • Permission tab for marketplace apps and installed apps
  • left to do - settings tab (in another PR)

There are breaking changes but it's behind a feature flag not exposed (access to marketplace) so not problematic

marketplace apps
https://github.com/user-attachments/assets/4c660101-50fc-47ce-b90a-8d6f17db5e74

installed apps
https://github.com/user-attachments/assets/c9229ee1-e75f-4cad-8766-758b2c5b37b4

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 26 files

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/twenty-front/src/pages/settings/applications/tabs/SettingsAvailableApplicationDetailContentTab.tsx">

<violation number="1" location="packages/twenty-front/src/pages/settings/applications/tabs/SettingsAvailableApplicationDetailContentTab.tsx:109">
P1: Avoid throwing during render when object metadata is still loading; the state defaults to an empty array so this can crash the page before metadata arrives. Handle the missing metadata gracefully (e.g., return an empty list or skip the row until metadata is loaded).</violation>
</file>

<file name="packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationPermissionsTab.tsx">

<violation number="1" location="packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationPermissionsTab.tsx:139">
P2: `uuidv4()` inside `useMemo` generates new IDs on every recomputation, making the memoized result referentially unstable in its nested properties. Consider generating stable IDs outside the memo (e.g., using a deterministic hash of the permission's universal identifiers) or caching them with `useRef` to avoid downstream re-render cascades.</violation>
</file>

<file name="packages/twenty-server/src/engine/core-modules/application/services/mocked-marketplace-app.constant.ts">

<violation number="1" location="packages/twenty-server/src/engine/core-modules/application/services/mocked-marketplace-app.constant.ts:22">
P2: Several different field universal identifiers are set to the same UUID, which will collide and make field metadata/permissions ambiguous. Each field should have a unique universalIdentifier.</violation>

<violation number="2" location="packages/twenty-server/src/engine/core-modules/application/services/mocked-marketplace-app.constant.ts:193">
P2: fieldPermissions should reference the field’s universalIdentifier, not its name string. This entry won’t match the jobTitle field and will make permissions ineffective.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

: undefined;

if (!isDefined(objectMetadataItem)) {
throw new Error(
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Avoid throwing during render when object metadata is still loading; the state defaults to an empty array so this can crash the page before metadata arrives. Handle the missing metadata gracefully (e.g., return an empty list or skip the row until metadata is loaded).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-front/src/pages/settings/applications/tabs/SettingsAvailableApplicationDetailContentTab.tsx, line 109:

<comment>Avoid throwing during render when object metadata is still loading; the state defaults to an empty array so this can crash the page before metadata arrives. Handle the missing metadata gracefully (e.g., return an empty list or skip the row until metadata is loaded).</comment>

<file context>
@@ -0,0 +1,148 @@
+        : undefined;
+
+      if (!isDefined(objectMetadataItem)) {
+        throw new Error(
+          `Could not resolve object for universalIdentifier: ${group.objectUniversalIdentifier}`,
+        );
</file context>
Fix with Cubic

})),
fieldPermissions: defaultRole.fieldPermissions.map((permission) => ({
__typename: 'FieldPermission' as const,
id: uuidv4(),
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: uuidv4() inside useMemo generates new IDs on every recomputation, making the memoized result referentially unstable in its nested properties. Consider generating stable IDs outside the memo (e.g., using a deterministic hash of the permission's universal identifiers) or caching them with useRef to avoid downstream re-render cascades.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationPermissionsTab.tsx, line 139:

<comment>`uuidv4()` inside `useMemo` generates new IDs on every recomputation, making the memoized result referentially unstable in its nested properties. Consider generating stable IDs outside the memo (e.g., using a deterministic hash of the permission's universal identifiers) or caching them with `useRef` to avoid downstream re-render cascades.</comment>

<file context>
@@ -0,0 +1,387 @@
+  })),
+  fieldPermissions: defaultRole.fieldPermissions.map((permission) => ({
+    __typename: 'FieldPermission' as const,
+    id: uuidv4(),
+    roleId: defaultRole.id,
+    objectMetadataId:
</file context>
Fix with Cubic

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Feb 12, 2026

Greptile Overview

Greptile Summary

added comprehensive Content and Permissions tabs for marketplace and installed apps in the Twenty CRM settings interface

Major changes:

  • Backend: created complete marketplace app DTOs with nested objects, fields, permissions, logic functions, and front components; implemented MarketplaceService with GitHub integration, 1-hour caching, and manifest parsing
  • Frontend: built expandable data table showing app objects and fields with search; created permissions tab that resolves universal identifiers to workspace IDs and renders synthetic roles for preview
  • Reusable components: added TableSubRow for nested table rows, SettingsApplicationDataTable for object/field display, and utility for mapping universal identifiers to standard objects

Issues found:

  • Mock data has duplicate field universal identifiers (all using the same value) which will cause identifier conflicts
  • Hardcoded string 'jobTitle' used instead of constant in field permission
  • Error thrown instead of graceful degradation when universal identifier resolution fails
  • Minor styling issue with template literal usage in theme spacing

The architecture properly separates concerns between marketplace apps (not yet installed) and installed apps, with the permissions tab intelligently handling both cases.

Confidence Score: 3/5

  • safe to merge after fixing duplicate field identifiers in mocked data
  • the implementation is well-structured with proper DTOs, service layer, and comprehensive frontend components, but contains critical bugs in mock data (duplicate universal identifiers) and error handling that throws instead of gracefully degrading
  • mocked-marketplace-app.constant.ts needs unique field identifiers, and SettingsAvailableApplicationDetailContentTab.tsx needs graceful error handling

Important Files Changed

Filename Overview
packages/twenty-server/src/engine/core-modules/application/dtos/marketplace-app.dto.ts comprehensive DTOs added for marketplace app structure with proper validation decorators
packages/twenty-server/src/engine/core-modules/application/services/marketplace.service.ts marketplace service with GitHub integration, caching, and manifest parsing - well structured
packages/twenty-server/src/engine/core-modules/application/services/mocked-marketplace-app.constant.ts mock data with duplicate field universal identifiers and inconsistent constant usage
packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationPermissionsTab.tsx permissions tab with complex role resolution logic for marketplace and installed apps
packages/twenty-front/src/pages/settings/applications/tabs/SettingsAvailableApplicationDetailContentTab.tsx content tab for marketplace apps with error thrown when universal identifier resolution fails
packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationDataTable.tsx expandable table component for displaying objects and fields with search functionality

Sequence Diagram

sequenceDiagram
    participant User
    participant Frontend
    participant GraphQL
    participant MarketplaceService
    participant GitHub

    User->>Frontend: Navigate to Marketplace Apps
    Frontend->>GraphQL: Query findManyMarketplaceApps
    GraphQL->>MarketplaceService: findAllMarketplaceApps()
    
    alt Cache Valid
        MarketplaceService-->>GraphQL: Return cached apps
    else Cache Expired
        MarketplaceService->>GitHub: Fetch app directories (API)
        GitHub-->>MarketplaceService: Return directory list
        loop For each app
            MarketplaceService->>GitHub: Fetch manifest.json (raw)
            GitHub-->>MarketplaceService: Return manifest
            MarketplaceService->>GitHub: Fetch package.json (raw)
            GitHub-->>MarketplaceService: Return package.json
            MarketplaceService->>MarketplaceService: Parse & build MarketplaceAppDTO
        end
        MarketplaceService->>MarketplaceService: Cache apps (1 hour TTL)
        MarketplaceService-->>GraphQL: Return apps with objects/fields/permissions
    end
    
    GraphQL-->>Frontend: Return MarketplaceApp[]
    Frontend->>Frontend: Render Content Tab (objects, fields, functions)
    
    User->>Frontend: Click on Permissions Tab
    Frontend->>Frontend: resolvePermissionIds (universal→workspace IDs)
    Frontend->>Frontend: buildSyntheticRole (create mock role)
    Frontend->>Frontend: buildobjectMetadataItemsFromMarketplaceApp
    Frontend->>Frontend: Render SettingsRolePermissions
Loading

Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

26 files reviewed, 4 comments

Edit Code Review Agent Settings | Greptile

},
{
objectUniversalIdentifier: MOCK_ENRICHMENT_JOB_UNIVERSAL_ID,
fieldUniversalIdentifier: 'jobTitle',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

using string 'jobTitle' instead of the constant PERSON_JOB_TITLE_FIELD_UNIVERSAL_ID

Suggested change
fieldUniversalIdentifier: 'jobTitle',
fieldUniversalIdentifier: PERSON_JOB_TITLE_FIELD_UNIVERSAL_ID,

Comment on lines +108 to +111
if (!isDefined(objectMetadataItem)) {
throw new Error(
`Could not resolve object for universalIdentifier: ${group.objectUniversalIdentifier}`,
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

throwing an error will crash the UI if the universal identifier cannot be resolved - consider graceful degradation (skip the field group or show a placeholder) instead

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

import { TableRow } from '@/ui/layout/table/components/TableRow';

const StyledTableSubRow = styled(TableRow)`
padding-left: ${({ theme }) => theme.spacing(4)};
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

prefer direct theme spacing instead of template literals

Suggested change
padding-left: ${({ theme }) => theme.spacing(4)};
padding-left: ${({ theme }) => theme.spacing(4)};

Context Used: Context from dashboard - When using styled components, prefer to use theme spacing directly in calculations instead of wrappi... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +22 to +29
const COMPANY_INDUSTRY_FIELD_UNIVERSAL_ID =
'20202020-4dc2-47c9-bb15-6e6f19ba9e46';
const COMPANY_EMPLOYEE_COUNT_FIELD_UNIVERSAL_ID =
'20202020-4dc2-47c9-bb15-6e6f19ba9e46';
const PERSON_LINKEDIN_URL_FIELD_UNIVERSAL_ID =
'20202020-4dc2-47c9-bb15-6e6f19ba9e46';
const PERSON_JOB_TITLE_FIELD_UNIVERSAL_ID =
'20202020-4dc2-47c9-bb15-6e6f19ba9e46';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all four field constants share identical universal identifier values, causing field identifier conflicts

Suggested change
const COMPANY_INDUSTRY_FIELD_UNIVERSAL_ID =
'20202020-4dc2-47c9-bb15-6e6f19ba9e46';
const COMPANY_EMPLOYEE_COUNT_FIELD_UNIVERSAL_ID =
'20202020-4dc2-47c9-bb15-6e6f19ba9e46';
const PERSON_LINKEDIN_URL_FIELD_UNIVERSAL_ID =
'20202020-4dc2-47c9-bb15-6e6f19ba9e46';
const PERSON_JOB_TITLE_FIELD_UNIVERSAL_ID =
'20202020-4dc2-47c9-bb15-6e6f19ba9e46';
const COMPANY_INDUSTRY_FIELD_UNIVERSAL_ID =
'20202020-ind1-ind1-ind1-industry1111';
const COMPANY_EMPLOYEE_COUNT_FIELD_UNIVERSAL_ID =
'20202020-emp1-emp1-emp1-employee1111';
const PERSON_LINKEDIN_URL_FIELD_UNIVERSAL_ID =
'20202020-lin1-lin1-lin1-linkedin1111';
const PERSON_JOB_TITLE_FIELD_UNIVERSAL_ID =
'20202020-job1-job1-job1-jobtitle1111';

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Feb 12, 2026

🚀 Preview Environment Ready!

Your preview environment is available at: http://bore.pub:29657

This environment will automatically shut down when the PR is closed or after 5 hours.

@@ -16,6 +16,65 @@ export const FIND_MANY_MARKETPLACE_APPS = gql`
providers
websiteUrl
termsUrl
objects {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't this can be extracted from APPLICATION_FRAGMENT?

{
...marketplaceRelatedFields
...ApplicationFields
}

@Field(() => [String])
permissionFlags: string[];
}

@ObjectType('MarketplaceApp')
export class MarketplaceAppDTO {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i guess you might extend a lot from ApplicationDTO isn't it?

Copy link
Copy Markdown
Contributor Author

@ijreilly ijreilly Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wouldn't say extend as there ApplicationDTO stuff that marketplaceApplicationDTO don't have: yarnlock and packageJson files and checksums, etc.
Their objects and fields also are different; marketplaceApplicationDTO don't have ids.
I think it's safe to keep two different types

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 6 files (changes from recent commits).

Prompt for AI agents (all issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/twenty-server/src/engine/core-modules/application/services/mocked-marketplace-app.constant.ts">

<violation number="1" location="packages/twenty-server/src/engine/core-modules/application/services/mocked-marketplace-app.constant.ts:197">
P2: Field permission references the Person jobTitle field while the object is Enrichment Job, so the permission won’t match any field on that object. Align the objectUniversalIdentifier with the field’s actual object (Person) or use an enrichment job field identifier instead.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

const hasFields = isDefined(row.fields) && row.fields.length > 0;

return (
<div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you need div here? If you absolutelly need it, you need to use an empty

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needed because there is another element at the same level as TableRow

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in that case you might just need <> </>

@ijreilly ijreilly added this pull request to the merge queue Feb 12, 2026
Merged via the queue into main with commit 90a3026 Feb 12, 2026
75 checks passed
@ijreilly ijreilly deleted the ft--marketplace-2 branch February 12, 2026 17:55
@twenty-eng-sync
Copy link
Copy Markdown

Hey @ijreilly! After you've done the QA of your Pull Request, you can mark it as done here. Thank you!

1 similar comment
@twenty-eng-sync
Copy link
Copy Markdown

Hey @ijreilly! After you've done the QA of your Pull Request, you can mark it as done here. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants