[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
This commit is contained in:
Marie
2026-01-30 18:32:32 +01:00
committed by GitHub
parent 36cf388c81
commit 0001c2e7d0
98 changed files with 1713 additions and 280 deletions
@@ -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();
@@ -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={<SettingsApplicationDetails />}
/>
<Route
path={SettingsPath.AvailableApplicationDetail}
element={<SettingsAvailableApplicationDetails />}
/>
<Route
path={SettingsPath.ApplicationLogicFunctionDetail}
element={<SettingsLogicFunctionDetail />}
@@ -0,0 +1,7 @@
import gql from 'graphql-tag';
export const INSTALL_MARKETPLACE_APP = gql`
mutation InstallMarketplaceApp() {
installMarketplaceApp()
}
`;
@@ -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
}
}
`;
@@ -0,0 +1,9 @@
export const useInstallMarketplaceApp = () => {
const install = async () => {
return true;
};
return {
install,
};
};
@@ -2,16 +2,19 @@ import styled from '@emotion/styled';
type StyledCardContentProps = {
disabled?: boolean;
alignItems?: 'center' | 'flex-start';
fullHeight?: boolean;
};
export const StyledSettingsOptionCardContent = styled.div<StyledCardContentProps>`
align-items: center;
export const StyledSettingsCardContent = styled.div<StyledCardContentProps>`
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)};
`;
@@ -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 (
<StyledSettingsOptionCardContent disabled={disabled}>
<StyledSettingsCardContent disabled={disabled}>
{Icon && (
<StyledSettingsOptionCardIcon>
<StyledSettingsCardIcon>
<SettingsOptionIconCustomizer Icon={Icon} />
</StyledSettingsOptionCardIcon>
</StyledSettingsCardIcon>
)}
<div>
<StyledSettingsOptionCardTitle>{title}</StyledSettingsOptionCardTitle>
<StyledSettingsOptionCardDescription>
<StyledSettingsCardTitle>{title}</StyledSettingsCardTitle>
<StyledSettingsCardDescription>
{description}
</StyledSettingsOptionCardDescription>
</StyledSettingsCardDescription>
</div>
{Button && <StyledButtonContainer>{Button}</StyledButtonContainer>}
</StyledSettingsOptionCardContent>
</StyledSettingsCardContent>
);
};
@@ -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 (
<StyledSettingsOptionCardContent disabled={disabled}>
<StyledSettingsCardContent disabled={disabled}>
{Icon && (
<StyledSettingsOptionCardIcon>
<StyledSettingsCardIcon>
<SettingsOptionIconCustomizer Icon={Icon} />
</StyledSettingsOptionCardIcon>
</StyledSettingsCardIcon>
)}
<div>
<StyledSettingsOptionCardTitle>{title}</StyledSettingsOptionCardTitle>
<StyledSettingsCardTitle>{title}</StyledSettingsCardTitle>
{description && (
<StyledSettingsOptionCardDescription>
<StyledSettingsCardDescription>
{description}
</StyledSettingsOptionCardDescription>
</StyledSettingsCardDescription>
)}
</div>
<SettingsCounter
@@ -54,6 +54,6 @@ export const SettingsOptionCardContentCounter = ({
disabled={disabled}
showButtons={showButtons}
/>
</StyledSettingsOptionCardContent>
</StyledSettingsCardContent>
);
};
@@ -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 (
<StyledSettingsOptionCardContent disabled={disabled}>
<StyledSettingsCardContent disabled={disabled}>
{Icon && (
<StyledSettingsOptionCardIcon>
<StyledSettingsCardIcon>
<SettingsOptionIconCustomizer Icon={Icon} />
</StyledSettingsOptionCardIcon>
</StyledSettingsCardIcon>
)}
<div>
<StyledSettingsOptionCardTitle>{title}</StyledSettingsOptionCardTitle>
<StyledSettingsOptionCardDescription>
<StyledSettingsCardTitle>{title}</StyledSettingsCardTitle>
<StyledSettingsCardDescription>
{description}
</StyledSettingsOptionCardDescription>
</StyledSettingsCardDescription>
</div>
<StyledSelectContainer>{children}</StyledSelectContainer>
</StyledSettingsOptionCardContent>
</StyledSettingsCardContent>
);
};
@@ -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 (
<>
<StyledSettingsOptionCardToggleContent disabled={disabled}>
<StyledSettingsCardToggleContent disabled={disabled}>
{Icon && (
<StyledSettingsOptionCardIcon>
<StyledSettingsCardIcon>
<SettingsOptionIconCustomizer Icon={Icon} />
</StyledSettingsOptionCardIcon>
</StyledSettingsCardIcon>
)}
<div>
<StyledSettingsOptionCardTitle>
<StyledSettingsCardTitle>
<label htmlFor={toggleId}>
{title}
<StyledSettingsOptionCardToggleCover />
<StyledSettingsCardToggleCover />
</label>
</StyledSettingsOptionCardTitle>
<StyledSettingsOptionCardDescription>
</StyledSettingsCardTitle>
<StyledSettingsCardDescription>
{description}
</StyledSettingsOptionCardDescription>
</StyledSettingsCardDescription>
</div>
<StyledSettingsOptionCardToggleButton
<StyledSettingsCardToggleButton
id={toggleId}
value={checked}
onChange={onChange}
@@ -92,7 +90,7 @@ export const SettingsOptionCardContentToggle = ({
color={advancedMode ? theme.color.yellow : theme.color.blue}
toggleCentered={toggleCentered}
/>
</StyledSettingsOptionCardToggleContent>
</StyledSettingsCardToggleContent>
{divider && <Separator />}
</>
);