[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:
@@ -78,7 +78,9 @@ generated
|
||||
|
||||
# dev
|
||||
/dist/
|
||||
.twenty
|
||||
|
||||
.twenty/*
|
||||
!.twenty/output/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
@@ -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}',
|
||||
|
||||
@@ -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<Scalars['String']>;
|
||||
name: Scalars['String'];
|
||||
providers: Array<Scalars['String']>;
|
||||
screenshots: Array<Scalars['String']>;
|
||||
termsUrl?: Maybe<Scalars['String']>;
|
||||
version: Scalars['String'];
|
||||
websiteUrl?: Maybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
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<Agent>;
|
||||
findManyApplications: Array<Application>;
|
||||
findManyLogicFunctions: Array<LogicFunction>;
|
||||
findManyMarketplaceApps: Array<MarketplaceApp>;
|
||||
findManyPublicDomains: Array<PublicDomain>;
|
||||
findOneAgent: Agent;
|
||||
findOneApplication: Application;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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 />}
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
import gql from 'graphql-tag';
|
||||
|
||||
export const INSTALL_MARKETPLACE_APP = gql`
|
||||
mutation InstallMarketplaceApp() {
|
||||
installMarketplaceApp()
|
||||
}
|
||||
`;
|
||||
+21
@@ -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,
|
||||
};
|
||||
};
|
||||
+17
-6
@@ -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)};
|
||||
`;
|
||||
+12
-12
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+12
-12
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+12
-12
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+18
-20
@@ -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 />}
|
||||
</>
|
||||
);
|
||||
|
||||
+22
-24
@@ -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 ? (
|
||||
<Card rounded>
|
||||
<StyledSettingsOptionCardContent>
|
||||
<StyledSettingsCardContent>
|
||||
<StyledErrorMessage>
|
||||
{t`No models available. Please configure AI models in your workspace settings.`}
|
||||
</StyledErrorMessage>
|
||||
</StyledSettingsOptionCardContent>
|
||||
</StyledSettingsCardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<Card rounded>
|
||||
<StyledSettingsOptionCardContent>
|
||||
<StyledSettingsOptionCardIcon>
|
||||
<StyledSettingsCardContent>
|
||||
<StyledSettingsCardIcon>
|
||||
<SettingsOptionIconCustomizer Icon={IconBolt} />
|
||||
</StyledSettingsOptionCardIcon>
|
||||
</StyledSettingsCardIcon>
|
||||
<div>
|
||||
<StyledSettingsOptionCardTitle>
|
||||
{t`Fast Model`}
|
||||
</StyledSettingsOptionCardTitle>
|
||||
<StyledSettingsOptionCardDescription>
|
||||
<StyledSettingsCardTitle>{t`Fast Model`}</StyledSettingsCardTitle>
|
||||
<StyledSettingsCardDescription>
|
||||
{t`Quick model for routing decisions`}
|
||||
</StyledSettingsOptionCardDescription>
|
||||
</StyledSettingsCardDescription>
|
||||
</div>
|
||||
<StyledSelectContainer>
|
||||
<Select
|
||||
@@ -190,19 +188,19 @@ export const SettingsAIRouterSettings = () => {
|
||||
selectSizeVariant="small"
|
||||
/>
|
||||
</StyledSelectContainer>
|
||||
</StyledSettingsOptionCardContent>
|
||||
</StyledSettingsCardContent>
|
||||
|
||||
<StyledSettingsOptionCardContent>
|
||||
<StyledSettingsOptionCardIcon>
|
||||
<StyledSettingsCardContent>
|
||||
<StyledSettingsCardIcon>
|
||||
<SettingsOptionIconCustomizer Icon={IconBrain} />
|
||||
</StyledSettingsOptionCardIcon>
|
||||
</StyledSettingsCardIcon>
|
||||
<div>
|
||||
<StyledSettingsOptionCardTitle>
|
||||
<StyledSettingsCardTitle>
|
||||
{t`Smart Model`}
|
||||
</StyledSettingsOptionCardTitle>
|
||||
<StyledSettingsOptionCardDescription>
|
||||
</StyledSettingsCardTitle>
|
||||
<StyledSettingsCardDescription>
|
||||
{t`Advanced model for complex planning`}
|
||||
</StyledSettingsOptionCardDescription>
|
||||
</StyledSettingsCardDescription>
|
||||
</div>
|
||||
<StyledSelectContainer>
|
||||
<Select
|
||||
@@ -213,7 +211,7 @@ export const SettingsAIRouterSettings = () => {
|
||||
selectSizeVariant="small"
|
||||
/>
|
||||
</StyledSelectContainer>
|
||||
</StyledSettingsOptionCardContent>
|
||||
</StyledSettingsCardContent>
|
||||
</Card>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
@@ -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 (
|
||||
<SubMenuTopBarContainer
|
||||
title={t`Applications`}
|
||||
links={[
|
||||
{
|
||||
children: t`Workspace`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{ children: t`Applications` },
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
{applications.length > 0 && (
|
||||
<SettingsApplicationsTable applications={applications} />
|
||||
)}
|
||||
<SettingsApplicationsCreateTab />
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
}
|
||||
|
||||
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 = (
|
||||
<Button
|
||||
onClick={() => {
|
||||
copyToClipboard(commands.join('\n'), t`Commands copied to clipboard`);
|
||||
}}
|
||||
ariaLabel={t`Copy commands`}
|
||||
Icon={IconCopy}
|
||||
/>
|
||||
);
|
||||
const renderActiveTabContent = () => {
|
||||
switch (activeTabId) {
|
||||
case 'available':
|
||||
return <SettingsApplicationsAvailableTab />;
|
||||
case 'installed':
|
||||
return <SettingsApplicationsInstalledTab />;
|
||||
case 'create':
|
||||
return <SettingsApplicationsCreateTab />;
|
||||
default:
|
||||
return <SettingsApplicationsAvailableTab />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
@@ -63,31 +86,12 @@ export const SettingsApplications = () => {
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
{applications.length > 0 && (
|
||||
<SettingsApplicationsTable applications={applications} />
|
||||
)}
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Create an application`}
|
||||
description={t`You can either create a private app or share it to others`}
|
||||
/>
|
||||
<CommandBlock commands={commands} button={button} />
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
Icon={IconFileInfo}
|
||||
title={t`Read documentation`}
|
||||
onClick={() =>
|
||||
window.open(
|
||||
getDocumentationUrl({
|
||||
locale: currentWorkspaceMember?.locale,
|
||||
path: '/developers/extend/capabilities/apps',
|
||||
}),
|
||||
'_blank',
|
||||
)
|
||||
}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
</Section>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
componentInstanceId={APPLICATIONS_TAB_LIST_ID}
|
||||
behaveAsLinks={false}
|
||||
/>
|
||||
{renderActiveTabContent()}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
|
||||
+467
@@ -0,0 +1,467 @@
|
||||
import { useInstallMarketplaceApp } from '@/marketplace/hooks/useInstallMarketplaceApp';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
|
||||
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 styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconApps,
|
||||
IconBox,
|
||||
IconColumns,
|
||||
IconCommand,
|
||||
IconDownload,
|
||||
IconFileText,
|
||||
IconInfoCircle,
|
||||
IconLayoutGrid,
|
||||
IconLock,
|
||||
IconSettings,
|
||||
IconWorld,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { PermissionFlagType } from '~/generated/graphql';
|
||||
import { useMarketplaceApps } from '~/pages/settings/applications/hooks/useMarketplaceApps';
|
||||
|
||||
const AVAILABLE_APPLICATION_DETAIL_ID = 'available-application-detail';
|
||||
|
||||
const StyledHeader = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
justify-content: space-between;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledHeaderLeft = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
`;
|
||||
|
||||
const StyledLogo = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.tertiary};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
height: 48px;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
width: 48px;
|
||||
`;
|
||||
|
||||
const StyledLogoImage = styled.img`
|
||||
height: 32px;
|
||||
object-fit: contain;
|
||||
width: 32px;
|
||||
`;
|
||||
|
||||
const StyledLogoPlaceholder = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.color.blue};
|
||||
border-radius: ${({ theme }) => theme.border.radius.xs};
|
||||
color: ${({ theme }) => theme.font.color.inverted};
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.lg};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
height: 32px;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
`;
|
||||
|
||||
const StyledHeaderInfo = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledAppName = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.lg};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
`;
|
||||
|
||||
const StyledAppDescription = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
`;
|
||||
|
||||
const StyledContentContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
const StyledMainContent = styled.div`
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const StyledSidebar = styled.div`
|
||||
flex-shrink: 0;
|
||||
width: 180px;
|
||||
`;
|
||||
|
||||
const StyledSidebarSection = styled.div`
|
||||
padding: ${({ theme }) => theme.spacing(3)} 0;
|
||||
|
||||
&:first-of-type {
|
||||
padding-top: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledSidebarLabel = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledSidebarValue = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledContentItem = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledLink = styled.a`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
display: flex;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledScreenshotsContainer = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
display: flex;
|
||||
height: 300px;
|
||||
justify-content: center;
|
||||
margin-bottom: ${({ theme }) => theme.spacing(4)};
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledScreenshotImage = styled.img`
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledScreenshotThumbnails = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
margin-bottom: ${({ theme }) => theme.spacing(6)};
|
||||
`;
|
||||
|
||||
const StyledThumbnail = styled.div<{ isSelected?: boolean }>`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid
|
||||
${({ theme, isSelected }) =>
|
||||
isSelected ? theme.color.blue : theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
height: 60px;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
&:hover {
|
||||
border-color: ${({ theme }) => theme.color.blue};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledThumbnailImage = styled.img`
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledSectionTitle = styled.h2`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-size: ${({ theme }) => theme.font.size.lg};
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
margin: 0 0 ${({ theme }) => theme.spacing(3)} 0;
|
||||
`;
|
||||
|
||||
const StyledAboutText = styled.p`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
line-height: 1.6;
|
||||
margin: 0 0 ${({ theme }) => theme.spacing(6)} 0;
|
||||
white-space: pre-line;
|
||||
`;
|
||||
|
||||
const StyledProvidersList = styled.ul`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
list-style-type: disc;
|
||||
margin: 0;
|
||||
padding-left: ${({ theme }) => theme.spacing(5)};
|
||||
`;
|
||||
|
||||
const StyledProviderItem = styled.li`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const SettingsAvailableApplicationDetails = () => {
|
||||
const { availableApplicationId = '' } = useParams<{
|
||||
availableApplicationId: string;
|
||||
}>();
|
||||
|
||||
const [selectedScreenshotIndex, setSelectedScreenshotIndex] = useState(0);
|
||||
|
||||
const { data: marketplaceApps } = useMarketplaceApps();
|
||||
const { install } = useInstallMarketplaceApp();
|
||||
const canInstallMarketplaceApps = useHasPermissionFlag(
|
||||
PermissionFlagType.MARKETPLACE_APPS,
|
||||
);
|
||||
|
||||
const application = useMemo(() => {
|
||||
return marketplaceApps?.find((app) => app.id === availableApplicationId);
|
||||
}, [availableApplicationId, marketplaceApps]);
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (isDefined(application)) {
|
||||
await install();
|
||||
}
|
||||
};
|
||||
|
||||
const activeTabId = useRecoilComponentValue(
|
||||
activeTabIdComponentState,
|
||||
AVAILABLE_APPLICATION_DETAIL_ID,
|
||||
);
|
||||
|
||||
const tabs = [
|
||||
{ id: 'about', title: t`About`, Icon: IconInfoCircle },
|
||||
{ id: 'content', title: t`Content`, Icon: IconBox },
|
||||
{ id: 'permissions', title: t`Permissions`, Icon: IconLock },
|
||||
{ id: 'settings', title: t`Settings`, Icon: IconSettings },
|
||||
];
|
||||
|
||||
const getInitials = (name: string) => {
|
||||
return name.charAt(0).toUpperCase();
|
||||
};
|
||||
|
||||
const hasScreenshots =
|
||||
application?.screenshots && application.screenshots.length > 0;
|
||||
|
||||
const renderActiveTabContent = () => {
|
||||
if (!application) return null;
|
||||
|
||||
switch (activeTabId) {
|
||||
case 'about':
|
||||
return (
|
||||
<>
|
||||
{hasScreenshots && (
|
||||
<>
|
||||
<StyledScreenshotsContainer>
|
||||
<StyledScreenshotImage
|
||||
src={application.screenshots[selectedScreenshotIndex]}
|
||||
alt={`${application.name} screenshot ${selectedScreenshotIndex + 1}`}
|
||||
/>
|
||||
</StyledScreenshotsContainer>
|
||||
<StyledScreenshotThumbnails>
|
||||
{application.screenshots
|
||||
.slice(0, 6)
|
||||
.map((screenshot, index) => (
|
||||
<StyledThumbnail
|
||||
key={index}
|
||||
isSelected={index === selectedScreenshotIndex}
|
||||
onClick={() => setSelectedScreenshotIndex(index)}
|
||||
>
|
||||
<StyledThumbnailImage
|
||||
src={screenshot}
|
||||
alt={`${application.name} thumbnail ${index + 1}`}
|
||||
/>
|
||||
</StyledThumbnail>
|
||||
))}
|
||||
</StyledScreenshotThumbnails>
|
||||
</>
|
||||
)}
|
||||
|
||||
<StyledContentContainer>
|
||||
<StyledMainContent>
|
||||
<Section>
|
||||
<StyledSectionTitle>{t`About`}</StyledSectionTitle>
|
||||
<StyledAboutText>
|
||||
{application.aboutDescription}
|
||||
</StyledAboutText>
|
||||
|
||||
<StyledSectionTitle>{t`Providers`}</StyledSectionTitle>
|
||||
<StyledProvidersList>
|
||||
{application.providers.map((provider) => (
|
||||
<StyledProviderItem key={provider}>
|
||||
{provider}
|
||||
</StyledProviderItem>
|
||||
))}
|
||||
</StyledProvidersList>
|
||||
</Section>
|
||||
</StyledMainContent>
|
||||
|
||||
<StyledSidebar>
|
||||
<StyledSidebarSection>
|
||||
<StyledSidebarLabel>{t`Created by`}</StyledSidebarLabel>
|
||||
<StyledSidebarValue>{application.author}</StyledSidebarValue>
|
||||
</StyledSidebarSection>
|
||||
|
||||
<StyledSidebarSection>
|
||||
<StyledSidebarLabel>{t`Category`}</StyledSidebarLabel>
|
||||
<StyledSidebarValue>
|
||||
{application.category}
|
||||
</StyledSidebarValue>
|
||||
</StyledSidebarSection>
|
||||
|
||||
<StyledSidebarSection>
|
||||
<StyledSidebarLabel>{t`Content`}</StyledSidebarLabel>
|
||||
<StyledContentItem>
|
||||
<IconLayoutGrid size={16} />
|
||||
{application.content.objects} {t`objects`}
|
||||
</StyledContentItem>
|
||||
<StyledContentItem>
|
||||
<IconColumns size={16} />
|
||||
{application.content.fields} {t`fields`}
|
||||
</StyledContentItem>
|
||||
<StyledContentItem>
|
||||
<IconApps size={16} />
|
||||
{application.content.frontComponents} {t`front components`}
|
||||
</StyledContentItem>
|
||||
<StyledContentItem>
|
||||
<IconCommand size={16} />
|
||||
{application.content.functions} {t`functions`}
|
||||
</StyledContentItem>
|
||||
</StyledSidebarSection>
|
||||
|
||||
<StyledSidebarSection>
|
||||
<StyledSidebarLabel>{t`Latest`}</StyledSidebarLabel>
|
||||
<StyledSidebarValue>{application.version}</StyledSidebarValue>
|
||||
</StyledSidebarSection>
|
||||
|
||||
<StyledSidebarSection>
|
||||
<StyledSidebarLabel>{t`Developers links`}</StyledSidebarLabel>
|
||||
<StyledLink
|
||||
href={application.websiteUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<IconWorld size={16} />
|
||||
{t`Website`}
|
||||
</StyledLink>
|
||||
<StyledLink
|
||||
href={application.termsUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<IconFileText size={16} />
|
||||
{t`Terms / Privacy`}
|
||||
</StyledLink>
|
||||
</StyledSidebarSection>
|
||||
</StyledSidebar>
|
||||
</StyledContentContainer>
|
||||
</>
|
||||
);
|
||||
case 'content':
|
||||
return <div>{t`Content tab`}</div>;
|
||||
case 'permissions':
|
||||
return <div>{t`Permissions tab`}</div>;
|
||||
case 'settings':
|
||||
return <div>{t`Settings tab`}</div>;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
if (!application) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={application.name}
|
||||
links={[
|
||||
{
|
||||
children: t`Workspace`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: t`Applications`,
|
||||
href: getSettingsPath(SettingsPath.Applications),
|
||||
},
|
||||
{ children: application.name },
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<StyledHeader>
|
||||
<StyledHeaderLeft>
|
||||
<StyledLogo>
|
||||
{application.logoPath ? (
|
||||
<StyledLogoImage
|
||||
src={application.logoPath}
|
||||
alt={application.name}
|
||||
/>
|
||||
) : (
|
||||
<StyledLogoPlaceholder>
|
||||
{getInitials(application.name)}
|
||||
</StyledLogoPlaceholder>
|
||||
)}
|
||||
</StyledLogo>
|
||||
<StyledHeaderInfo>
|
||||
<StyledAppName>{application.name}</StyledAppName>
|
||||
<StyledAppDescription>
|
||||
{application.description}
|
||||
</StyledAppDescription>
|
||||
</StyledHeaderInfo>
|
||||
</StyledHeaderLeft>
|
||||
{canInstallMarketplaceApps && (
|
||||
<Button
|
||||
Icon={IconDownload}
|
||||
title={t`Install`}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
onClick={handleInstall}
|
||||
/>
|
||||
)}
|
||||
</StyledHeader>
|
||||
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
componentInstanceId={AVAILABLE_APPLICATION_DETAIL_ID}
|
||||
behaveAsLinks={false}
|
||||
/>
|
||||
|
||||
{renderActiveTabContent()}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
StyledSettingsCardContent,
|
||||
StyledSettingsCardDescription,
|
||||
StyledSettingsCardThirdLine,
|
||||
StyledSettingsCardTitle,
|
||||
} from '@/settings/components/SettingsOptions/SettingsCardContentBase';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { Avatar } from 'twenty-ui/display';
|
||||
import { Card } from 'twenty-ui/layout';
|
||||
import { type AvailableApplication } from '~/pages/settings/applications/types/availableApplication';
|
||||
|
||||
type SettingsAvailableApplicationCardProps = {
|
||||
application: AvailableApplication;
|
||||
};
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
display: flex;
|
||||
height: 100%;
|
||||
text-decoration: none;
|
||||
`;
|
||||
|
||||
const StyledDescription = styled(StyledSettingsCardDescription)`
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
`;
|
||||
|
||||
export const SettingsAvailableApplicationCard = ({
|
||||
application,
|
||||
}: SettingsAvailableApplicationCardProps) => {
|
||||
return (
|
||||
<StyledLink
|
||||
to={getSettingsPath(SettingsPath.AvailableApplicationDetail, {
|
||||
availableApplicationId: application.id,
|
||||
})}
|
||||
>
|
||||
<Card rounded fullWidth>
|
||||
<StyledSettingsCardContent alignItems="flex-start" fullHeight>
|
||||
<Avatar
|
||||
avatarUrl={application.logoPath || null}
|
||||
placeholder={application.name}
|
||||
placeholderColorSeed={application.name}
|
||||
size="lg"
|
||||
type="squared"
|
||||
/>
|
||||
<div>
|
||||
<StyledSettingsCardTitle>
|
||||
{application.name}
|
||||
</StyledSettingsCardTitle>
|
||||
<StyledDescription>{application.description}</StyledDescription>
|
||||
<StyledSettingsCardThirdLine>
|
||||
{t`by {author}`} {application.author}
|
||||
</StyledSettingsCardThirdLine>
|
||||
</div>
|
||||
</StyledSettingsCardContent>
|
||||
</Card>
|
||||
</StyledLink>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useFindManyMarketplaceAppsQuery } from '~/generated/graphql';
|
||||
import { type AvailableApplication } from '~/pages/settings/applications/types/availableApplication';
|
||||
|
||||
export const useMarketplaceApps = () => {
|
||||
const { data, loading, error } = useFindManyMarketplaceAppsQuery();
|
||||
|
||||
const marketplaceApps: AvailableApplication[] =
|
||||
data?.findManyMarketplaceApps.map((app) => ({
|
||||
id: app.id,
|
||||
name: app.name,
|
||||
description: app.description,
|
||||
author: app.author,
|
||||
logoPath: app.logo ?? '',
|
||||
category: app.category,
|
||||
aboutDescription: app.aboutDescription,
|
||||
providers: app.providers,
|
||||
screenshots: app.screenshots,
|
||||
content: {
|
||||
objects: 0,
|
||||
fields: 0,
|
||||
functions: 0,
|
||||
frontComponents: 0,
|
||||
},
|
||||
version: app.version,
|
||||
websiteUrl: app.websiteUrl ?? '',
|
||||
termsUrl: app.termsUrl ?? '',
|
||||
})) ?? [];
|
||||
|
||||
return {
|
||||
data: marketplaceApps,
|
||||
isLoading: loading,
|
||||
error,
|
||||
};
|
||||
};
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { SearchInput } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { SettingsAvailableApplicationCard } from '~/pages/settings/applications/components/SettingsAvailableApplicationCard';
|
||||
import { useMarketplaceApps } from '~/pages/settings/applications/hooks/useMarketplaceApps';
|
||||
import { type AvailableApplication } from '~/pages/settings/applications/types/availableApplication';
|
||||
|
||||
const StyledSearchInputContainer = styled.div`
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledCardsGrid = styled.div`
|
||||
display: grid;
|
||||
gap: ${({ theme }) => theme.spacing(3)};
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
|
||||
@media (max-width: 800px) {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledEmptyState = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
padding: ${({ theme }) => theme.spacing(4)};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export const SettingsApplicationsAvailableTab = () => {
|
||||
const { t } = useLingui();
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const { data: marketplaceApps, isLoading } = useMarketplaceApps();
|
||||
|
||||
const applications = marketplaceApps;
|
||||
|
||||
const filteredApplications = useMemo(() => {
|
||||
return applications.filter(
|
||||
(application: AvailableApplication) =>
|
||||
application.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
application.description
|
||||
.toLowerCase()
|
||||
.includes(searchTerm.toLowerCase()) ||
|
||||
application.author.toLowerCase().includes(searchTerm.toLowerCase()),
|
||||
);
|
||||
}, [applications, searchTerm]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Section>
|
||||
<StyledEmptyState>{t`Loading applications...`}</StyledEmptyState>
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<StyledSearchInputContainer>
|
||||
<SearchInput
|
||||
placeholder={t`Search an application`}
|
||||
value={searchTerm}
|
||||
onChange={setSearchTerm}
|
||||
/>
|
||||
</StyledSearchInputContainer>
|
||||
|
||||
{filteredApplications.length === 0 ? (
|
||||
<StyledEmptyState>{t`No applications available`}</StyledEmptyState>
|
||||
) : (
|
||||
<StyledCardsGrid>
|
||||
{filteredApplications.map((application: AvailableApplication) => (
|
||||
<SettingsAvailableApplicationCard
|
||||
key={application.id}
|
||||
application={application}
|
||||
/>
|
||||
))}
|
||||
</StyledCardsGrid>
|
||||
)}
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { getDocumentationUrl } from '@/support/utils/getDocumentationUrl';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import {
|
||||
CommandBlock,
|
||||
H2Title,
|
||||
IconCopy,
|
||||
IconFileInfo,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
margin: ${({ theme }) => theme.spacing(2)} 0;
|
||||
`;
|
||||
|
||||
export const SettingsApplicationsCreateTab = () => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
|
||||
const { copyToClipboard } = useCopyToClipboard();
|
||||
|
||||
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',
|
||||
];
|
||||
|
||||
const button = (
|
||||
<Button
|
||||
onClick={() => {
|
||||
copyToClipboard(commands.join('\n'), t`Commands copied to clipboard`);
|
||||
}}
|
||||
ariaLabel={t`Copy commands`}
|
||||
Icon={IconCopy}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Create an application`}
|
||||
description={t`You can either create a private app or share it to others`}
|
||||
/>
|
||||
<CommandBlock commands={commands} button={button} />
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
Icon={IconFileInfo}
|
||||
title={t`Read documentation`}
|
||||
onClick={() =>
|
||||
window.open(
|
||||
getDocumentationUrl({
|
||||
locale: currentWorkspaceMember?.locale,
|
||||
path: '/developers/extend/capabilities/apps',
|
||||
}),
|
||||
'_blank',
|
||||
)
|
||||
}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { useFindManyApplicationsQuery } from '~/generated-metadata/graphql';
|
||||
import { SettingsApplicationsTable } from '~/pages/settings/applications/components/SettingsApplicationsTable';
|
||||
|
||||
export const SettingsApplicationsInstalledTab = () => {
|
||||
const { data } = useFindManyApplicationsQuery();
|
||||
|
||||
const applications = data?.findManyApplications ?? [];
|
||||
|
||||
if (applications.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <SettingsApplicationsTable applications={applications} />;
|
||||
};
|
||||
@@ -0,0 +1,20 @@
|
||||
export type AvailableApplication = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
author: string;
|
||||
logoPath: string;
|
||||
category: string;
|
||||
aboutDescription: string;
|
||||
providers: string[];
|
||||
screenshots: string[];
|
||||
content: {
|
||||
objects: number;
|
||||
fields: number;
|
||||
functions: number;
|
||||
frontComponents: number;
|
||||
};
|
||||
version: string;
|
||||
websiteUrl: string;
|
||||
termsUrl: string;
|
||||
};
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
} from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow';
|
||||
import { SettingsObjectInactiveMenuDropDown } from '@/settings/data-model/objects/components/SettingsObjectInactiveMenuDropDown';
|
||||
import { getItemTagInfo } from '@/settings/data-model/utils/getItemTagInfo';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
@@ -21,18 +20,12 @@ import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/st
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { type ReactNode, useMemo, useState } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import {
|
||||
IconArchive,
|
||||
IconChevronRight,
|
||||
IconFilter,
|
||||
IconSearch,
|
||||
IconSettings,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { IconArchive, IconChevronRight, IconSettings } from 'twenty-ui/display';
|
||||
import { SearchInput } from 'twenty-ui/input';
|
||||
import { MenuItemToggle } from 'twenty-ui/navigation';
|
||||
import { GET_SETTINGS_OBJECT_TABLE_METADATA } from '~/pages/settings/data-model/constants/SettingsObjectTableMetadata';
|
||||
import type { SettingsObjectTableItem } from '~/pages/settings/data-model/types/SettingsObjectTableItem';
|
||||
@@ -42,18 +35,10 @@ const StyledIconChevronRight = styled(IconChevronRight)`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
const StyledSearchAndFilterContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
align-items: center;
|
||||
const StyledSearchInputContainer = styled.div`
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledSearchInput = styled(SettingsTextInput)`
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const SettingsObjectTable = ({
|
||||
objectMetadataItems,
|
||||
withSearchBar = true,
|
||||
@@ -146,53 +131,47 @@ export const SettingsObjectTable = ({
|
||||
return (
|
||||
<>
|
||||
{withSearchBar && (
|
||||
<StyledSearchAndFilterContainer>
|
||||
<StyledSearchInput
|
||||
instanceId="settings-objects-search"
|
||||
LeftIcon={IconSearch}
|
||||
<StyledSearchInputContainer>
|
||||
<SearchInput
|
||||
placeholder={t`Search for an object...`}
|
||||
value={searchTerm}
|
||||
onChange={setSearchTerm}
|
||||
/>
|
||||
<Dropdown
|
||||
dropdownId="settings-objects-filter-dropdown"
|
||||
dropdownPlacement="bottom-end"
|
||||
dropdownOffset={{ x: 0, y: 8 }}
|
||||
clickableComponent={
|
||||
<Button
|
||||
Icon={IconFilter}
|
||||
size="medium"
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
ariaLabel={t`Filter`}
|
||||
filterDropdown={(filterButton: ReactNode) => (
|
||||
<Dropdown
|
||||
dropdownId="settings-objects-filter-dropdown"
|
||||
dropdownPlacement="bottom-end"
|
||||
dropdownOffset={{ x: 0, y: 8 }}
|
||||
clickableComponent={filterButton}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconArchive}
|
||||
onToggleChange={() =>
|
||||
setShowDeactivated(!showDeactivated)
|
||||
}
|
||||
toggled={showDeactivated}
|
||||
text={t`Deactivated`}
|
||||
toggleSize="small"
|
||||
/>
|
||||
{isAdvancedModeEnabled && (
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconSettings}
|
||||
onToggleChange={() =>
|
||||
setShowSystemObjects(!showSystemObjects)
|
||||
}
|
||||
toggled={showSystemObjects}
|
||||
text={t`System objects`}
|
||||
toggleSize="small"
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconArchive}
|
||||
onToggleChange={() => setShowDeactivated(!showDeactivated)}
|
||||
toggled={showDeactivated}
|
||||
text={t`Deactivated`}
|
||||
toggleSize="small"
|
||||
/>
|
||||
{isAdvancedModeEnabled && (
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconSettings}
|
||||
onToggleChange={() =>
|
||||
setShowSystemObjects(!showSystemObjects)
|
||||
}
|
||||
toggled={showSystemObjects}
|
||||
text={t`System objects`}
|
||||
toggleSize="small"
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
)}
|
||||
/>
|
||||
</StyledSearchAndFilterContainer>
|
||||
</StyledSearchInputContainer>
|
||||
)}
|
||||
|
||||
<Table>
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { PageLayoutTabEntity } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { Repository } from 'typeorm';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ import { Repository } from 'typeorm';
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { updateStandardPageLayoutWidgetsWithWorkspaceFieldIds } from 'src/database/commands/upgrade-version-command/1-16/utils/update-standard-page-layout-widgets-with-workspace-field-ids.util';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import {
|
||||
RunOnWorkspaceArgs,
|
||||
WorkspacesMigrationCommandRunner,
|
||||
} from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ import {
|
||||
RunOnWorkspaceArgs,
|
||||
WorkspacesMigrationCommandRunner,
|
||||
} from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { ALL_METADATA_ENTITY_BY_METADATA_NAME } from 'src/engine/metadata-modules/flat-entity/constant/all-metadata-entity-by-metadata-name.constant';
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
|
||||
+1
-1
@@ -8,7 +8,7 @@ import { v4 } from 'uuid';
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ALL_ENTITY_VIEW_NAME } from 'src/database/commands/upgrade-version-command/1-16/utils/compute-formatted-view-name.util';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { ActiveOrSuspendedWorkspacesMigrationCommandRunner } from 'src/database/commands/command-runners/active-or-suspended-workspaces-migration.command-runner';
|
||||
import { RunOnWorkspaceArgs } from 'src/database/commands/command-runners/workspaces-migration.command-runner';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { getMetadataFlatEntityMapsKey } from 'src/engine/metadata-modules/flat-entity/utils/get-metadata-flat-entity-maps-key.util';
|
||||
|
||||
+6
-4
@@ -1,22 +1,23 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-sync.service';
|
||||
import { ApplicationModule } from 'src/engine/core-modules/application/application.module';
|
||||
import { ApplicationResolver } from 'src/engine/core-modules/application/application.resolver';
|
||||
import { ApplicationResolver } from 'src/engine/core-modules/application/resolvers/application.resolver';
|
||||
import { MarketplaceResolver } from 'src/engine/core-modules/application/resolvers/marketplace.resolver';
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/services/application-sync.service';
|
||||
import { ApplicationVariableEntityModule } from 'src/engine/core-modules/applicationVariable/application-variable.module';
|
||||
import { FileStorageModule } from 'src/engine/core-modules/file-storage/file-storage.module';
|
||||
import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { FieldMetadataModule } from 'src/engine/metadata-modules/field-metadata/field-metadata.module';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module';
|
||||
import { LogicFunctionLayerModule } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.module';
|
||||
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
|
||||
import { ObjectMetadataModule } from 'src/engine/metadata-modules/object-metadata/object-metadata.module';
|
||||
import { ObjectPermissionModule } from 'src/engine/metadata-modules/object-permission/object-permission.module';
|
||||
import { PermissionFlagModule } from 'src/engine/metadata-modules/permission-flag/permission-flag.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { RoleModule } from 'src/engine/metadata-modules/role/role.module';
|
||||
import { LogicFunctionLayerModule } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.module';
|
||||
import { LogicFunctionModule } from 'src/engine/metadata-modules/logic-function/logic-function.module';
|
||||
import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache.module';
|
||||
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
|
||||
import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/workspace-migration-runner.module';
|
||||
@@ -46,6 +47,7 @@ import { WorkflowCommonModule } from 'src/modules/workflow/common/workflow-commo
|
||||
],
|
||||
providers: [
|
||||
ApplicationResolver,
|
||||
MarketplaceResolver,
|
||||
ApplicationSyncService,
|
||||
WorkspaceMigrationGraphqlApiExceptionInterceptor,
|
||||
],
|
||||
|
||||
@@ -2,7 +2,8 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { MarketplaceService } from 'src/engine/core-modules/application/services/marketplace.service';
|
||||
import { WorkspaceFlatApplicationMapCacheService } from 'src/engine/core-modules/application/services/workspace-flat-application-map-cache.service';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AgentEntity } from 'src/engine/metadata-modules/ai/ai-agent/entities/agent.entity';
|
||||
@@ -15,7 +16,15 @@ import { WorkspaceCacheModule } from 'src/engine/workspace-cache/workspace-cache
|
||||
WorkspaceManyOrAllFlatEntityMapsCacheModule,
|
||||
WorkspaceCacheModule,
|
||||
],
|
||||
exports: [ApplicationService, WorkspaceFlatApplicationMapCacheService],
|
||||
providers: [ApplicationService, WorkspaceFlatApplicationMapCacheService],
|
||||
exports: [
|
||||
ApplicationService,
|
||||
WorkspaceFlatApplicationMapCacheService,
|
||||
MarketplaceService,
|
||||
],
|
||||
providers: [
|
||||
ApplicationService,
|
||||
WorkspaceFlatApplicationMapCacheService,
|
||||
MarketplaceService,
|
||||
],
|
||||
})
|
||||
export class ApplicationModule {}
|
||||
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
IsArray,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
} from 'class-validator';
|
||||
|
||||
@ObjectType('MarketplaceApp')
|
||||
export class MarketplaceAppDTO {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
@MaxLength(160)
|
||||
description: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
icon: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
version: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
author: string;
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
category: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
logo?: string;
|
||||
|
||||
@IsArray()
|
||||
@Field(() => [String])
|
||||
screenshots: string[];
|
||||
|
||||
@IsString()
|
||||
@Field()
|
||||
aboutDescription: string;
|
||||
|
||||
@IsArray()
|
||||
@Field(() => [String])
|
||||
providers: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
websiteUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
termsUrl?: string;
|
||||
}
|
||||
+2
-2
@@ -16,17 +16,17 @@ import type { FileUpload } from 'graphql-upload/processRequest.mjs';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ApplicationExceptionFilter } from 'src/engine/core-modules/application/application-exception-filter';
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/application-sync.service';
|
||||
import {
|
||||
ApplicationException,
|
||||
ApplicationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
|
||||
import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input';
|
||||
import { InstallApplicationInput } from 'src/engine/core-modules/application/dtos/install-application.input';
|
||||
import { UninstallApplicationInput } from 'src/engine/core-modules/application/dtos/uninstallApplicationInput';
|
||||
import { UploadApplicationFileInput } from 'src/engine/core-modules/application/dtos/uploadApplicationFileInput';
|
||||
import { ApplicationSyncService } from 'src/engine/core-modules/application/services/application-sync.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import { FileDTO } from 'src/engine/core-modules/file/dtos/file.dto';
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { MarketplaceAppDTO } from 'src/engine/core-modules/application/dtos/marketplace-app.dto';
|
||||
import { MarketplaceService } from 'src/engine/core-modules/application/services/marketplace.service';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
|
||||
@Resolver()
|
||||
@UseGuards(UserAuthGuard, WorkspaceAuthGuard, NoPermissionGuard)
|
||||
export class MarketplaceResolver {
|
||||
constructor(private readonly marketplaceService: MarketplaceService) {}
|
||||
|
||||
@Query(() => [MarketplaceAppDTO])
|
||||
async findManyMarketplaceApps(): Promise<MarketplaceAppDTO[]> {
|
||||
return this.marketplaceService.findAllMarketplaceApps();
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.MARKETPLACE_APPS))
|
||||
async installMarketplaceApp(): Promise<boolean> {
|
||||
// TODO
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -5,10 +5,10 @@ import { parse } from 'path';
|
||||
import {
|
||||
Manifest,
|
||||
FieldManifest,
|
||||
ObjectManifest,
|
||||
RoleManifest,
|
||||
LogicFunctionManifest,
|
||||
LogicFunctionTriggerManifest,
|
||||
ObjectManifest,
|
||||
RoleManifest,
|
||||
ObjectFieldManifest,
|
||||
RelationFieldManifest,
|
||||
} from 'twenty-shared/application';
|
||||
@@ -20,8 +20,8 @@ import {
|
||||
ApplicationException,
|
||||
ApplicationExceptionCode,
|
||||
} from 'src/engine/core-modules/application/application.exception';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationInput } from 'src/engine/core-modules/application/dtos/application.input';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { ApplicationVariableEntityService } from 'src/engine/core-modules/applicationVariable/application-variable.service';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
import { CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
@@ -35,15 +35,15 @@ import { ObjectPermissionService } from 'src/engine/metadata-modules/object-perm
|
||||
import { PermissionFlagService } from 'src/engine/metadata-modules/permission-flag/permission-flag.service';
|
||||
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
||||
import { LogicFunctionLayerService } from 'src/engine/metadata-modules/logic-function-layer/logic-function-layer.service';
|
||||
import { LogicFunctionV2Service } from 'src/engine/metadata-modules/logic-function/services/logic-function-v2.service';
|
||||
import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
|
||||
import { computeMetadataNameFromLabelOrThrow } from 'src/engine/metadata-modules/utils/compute-metadata-name-from-label-or-throw.util';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
import {
|
||||
CronTriggerSettings,
|
||||
DatabaseEventTriggerSettings,
|
||||
HttpRouteTriggerSettings,
|
||||
} from 'src/engine/metadata-modules/logic-function/logic-function.entity';
|
||||
import { LogicFunctionV2Service } from 'src/engine/metadata-modules/logic-function/services/logic-function-v2.service';
|
||||
import { FlatLogicFunction } from 'src/engine/metadata-modules/logic-function/types/flat-logic-function.type';
|
||||
import { computeMetadataNameFromLabelOrThrow } from 'src/engine/metadata-modules/utils/compute-metadata-name-from-label-or-throw.util';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
@Injectable()
|
||||
export class ApplicationSyncService {
|
||||
+250
@@ -0,0 +1,250 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { lowerCase, upperFirst } from 'lodash';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
|
||||
import { MarketplaceAppDTO } from 'src/engine/core-modules/application/dtos/marketplace-app.dto';
|
||||
|
||||
type GitHubContent = {
|
||||
name: string;
|
||||
path: string;
|
||||
type: 'file' | 'dir';
|
||||
download_url?: string;
|
||||
};
|
||||
|
||||
const GITHUB_RAW_BASE_URL =
|
||||
'https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-apps';
|
||||
const GITHUB_API_BASE_URL =
|
||||
'https://api.github.com/repos/twentyhq/twenty/contents/packages/twenty-apps';
|
||||
|
||||
const CACHE_TTL_MS = 60 * 60 * 1000;
|
||||
|
||||
@Injectable()
|
||||
export class MarketplaceService {
|
||||
private readonly logger = new Logger(MarketplaceService.name);
|
||||
|
||||
private cachedApps: MarketplaceAppDTO[] | null = null;
|
||||
private cacheTimestamp: number | null = null;
|
||||
|
||||
async findAllMarketplaceApps(): Promise<MarketplaceAppDTO[]> {
|
||||
if (this.isCacheValid()) {
|
||||
return this.cachedApps as MarketplaceAppDTO[];
|
||||
}
|
||||
|
||||
const apps = await this.fetchAllMarketplaceApps();
|
||||
|
||||
this.cachedApps = apps;
|
||||
this.cacheTimestamp = Date.now();
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
private isCacheValid(): boolean {
|
||||
if (this.cachedApps === null || this.cacheTimestamp === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Date.now() - this.cacheTimestamp < CACHE_TTL_MS;
|
||||
}
|
||||
|
||||
private async fetchAllMarketplaceApps(): Promise<MarketplaceAppDTO[]> {
|
||||
const apps: MarketplaceAppDTO[] = [];
|
||||
|
||||
apps.push(this.loadMockApp());
|
||||
|
||||
try {
|
||||
const appDirs = await this.getAppDirectoriesFromGitHub();
|
||||
|
||||
for (const appDir of appDirs) {
|
||||
try {
|
||||
const manifest = await this.loadAppManifestFromGithub(appDir);
|
||||
|
||||
if (manifest) {
|
||||
apps.push(manifest);
|
||||
} else {
|
||||
// to remove after manifest are committed to the repo
|
||||
const appName = appDir.replace(/^community\//, '');
|
||||
const appLabel = upperFirst(lowerCase(appName));
|
||||
|
||||
apps.push({
|
||||
id: appName,
|
||||
name: appLabel,
|
||||
description: '',
|
||||
icon: '',
|
||||
version: '',
|
||||
author: 'Anonymous',
|
||||
category: '',
|
||||
logo: undefined,
|
||||
screenshots: [],
|
||||
aboutDescription: '',
|
||||
providers: [],
|
||||
websiteUrl: '',
|
||||
termsUrl: '',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn(
|
||||
`Failed to load manifest from ${appDir}: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to fetch marketplace apps from GitHub: ${error instanceof Error ? error.message : 'Unknown error'}`,
|
||||
);
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
private loadMockApp(): MarketplaceAppDTO {
|
||||
// SVG logo as data URL
|
||||
const logoSvg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="#1a2744"><ellipse cx="38" cy="20" rx="28" ry="10"/><rect x="10" y="20" width="56" height="50"/><ellipse cx="38" cy="70" rx="28" ry="10"/><ellipse cx="38" cy="35" rx="28" ry="10" fill="none" stroke="#fff" stroke-width="3"/><ellipse cx="38" cy="52" rx="28" ry="10" fill="none" stroke="#fff" stroke-width="3"/><circle cx="72" cy="62" r="22" fill="#1a2744"/><circle cx="72" cy="62" r="18" fill="#fff"/><path d="M72 50 L72 74 M62 58 L72 48 L82 58" stroke="#1a2744" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>`;
|
||||
const logoDataUrl = `data:image/svg+xml,${encodeURIComponent(logoSvg)}`;
|
||||
|
||||
return {
|
||||
id: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'Data Enrichment',
|
||||
description: 'Enrich your data easily. Choose your provider.',
|
||||
icon: 'IconSparkles',
|
||||
version: '1.0.0',
|
||||
author: 'Cosmos Labs',
|
||||
category: 'Data',
|
||||
logo: logoDataUrl,
|
||||
screenshots: [
|
||||
'https://placehold.co/800x400/f5f5f5/666?text=Screenshot+1',
|
||||
'https://placehold.co/800x400/f5f5f5/666?text=Screenshot+2',
|
||||
'https://placehold.co/800x400/f5f5f5/666?text=Screenshot+3',
|
||||
],
|
||||
aboutDescription:
|
||||
'Enhance your workspace with automated data intelligence. This app monitors your new records and automatically populates missing details such as job titles, company size, social profiles, and industry insights.',
|
||||
providers: ['Clearbit', 'Apollo', 'Hunter.io'],
|
||||
websiteUrl: 'https://google.com',
|
||||
termsUrl: 'https://google.com',
|
||||
};
|
||||
}
|
||||
|
||||
private async getAppDirectoriesFromGitHub(): Promise<string[]> {
|
||||
const directories: string[] = [];
|
||||
|
||||
const rootContents = await this.fetchGitHubDirectory('');
|
||||
|
||||
for (const entry of rootContents) {
|
||||
// if (entry.type !== 'dir') continue;
|
||||
// if (entry.name.startsWith('.')) continue;
|
||||
// if (entry.name === 'node_modules') continue;
|
||||
// if (entry.name === 'internal') continue;
|
||||
|
||||
if (entry.name === 'community') {
|
||||
const communityContents = await this.fetchGitHubDirectory('community');
|
||||
|
||||
for (const communityEntry of communityContents) {
|
||||
if (communityEntry.type !== 'dir') continue;
|
||||
if (communityEntry.name.startsWith('.')) continue;
|
||||
|
||||
directories.push(`community/${communityEntry.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return directories;
|
||||
}
|
||||
|
||||
private async fetchGitHubDirectory(path: string): Promise<GitHubContent[]> {
|
||||
const url = path ? `${GITHUB_API_BASE_URL}/${path}` : GITHUB_API_BASE_URL;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
Accept: 'application/vnd.github.v3+json',
|
||||
'User-Agent': 'Twenty-Marketplace',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`GitHub API error: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
private async fetchGitHubFile(path: string): Promise<string | null> {
|
||||
const url = `${GITHUB_RAW_BASE_URL}/${path}`;
|
||||
|
||||
const response = await fetch(url, {
|
||||
headers: {
|
||||
'User-Agent': 'Twenty-Marketplace',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 404) {
|
||||
return null;
|
||||
}
|
||||
throw new Error(`GitHub raw file error: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.text();
|
||||
}
|
||||
|
||||
private async loadAppManifestFromGithub(
|
||||
appPath: string,
|
||||
): Promise<MarketplaceAppDTO | null> {
|
||||
const manifestContent = await this.fetchGitHubFile(
|
||||
`${appPath}/.twenty/output/manifest.json`,
|
||||
);
|
||||
|
||||
if (!manifestContent) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const manifest = JSON.parse(manifestContent) as Manifest;
|
||||
|
||||
const { application, packageJson } = manifest;
|
||||
const marketplaceData = application.marketplaceData;
|
||||
|
||||
if (!marketplaceData?.author || !marketplaceData?.category) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: application.universalIdentifier,
|
||||
name: application.displayName ?? packageJson.name,
|
||||
description: application.description ?? '',
|
||||
icon: application.icon ?? 'IconApps',
|
||||
version: packageJson.version,
|
||||
author: marketplaceData.author,
|
||||
category: marketplaceData.category,
|
||||
logo: this.resolveAssetUrl(appPath, marketplaceData.logo),
|
||||
screenshots: this.resolveAssetUrls(appPath, marketplaceData.screenshots),
|
||||
aboutDescription: marketplaceData.aboutDescription ?? '',
|
||||
providers: marketplaceData.providers ?? [],
|
||||
websiteUrl: marketplaceData.websiteUrl,
|
||||
termsUrl: marketplaceData.termsUrl,
|
||||
};
|
||||
}
|
||||
|
||||
private resolveAssetUrl(
|
||||
appPath: string,
|
||||
relativePath: string | undefined,
|
||||
): string | undefined {
|
||||
if (!relativePath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `${GITHUB_RAW_BASE_URL}/${appPath}/${relativePath}`;
|
||||
}
|
||||
|
||||
private resolveAssetUrls(
|
||||
appPath: string,
|
||||
relativePaths: string[] | undefined,
|
||||
): string[] {
|
||||
if (!relativePaths) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return relativePaths
|
||||
.map((path) => this.resolveAssetUrl(appPath, path))
|
||||
.filter((url): url is string => url !== undefined);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import { v4 } from 'uuid';
|
||||
|
||||
import { USER_SIGNUP_EVENT_NAME } from 'src/engine/api/graphql/workspace-query-runner/constants/user-signup-event-name.constants';
|
||||
import { type AppTokenEntity } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import {
|
||||
AuthException,
|
||||
AuthExceptionCode,
|
||||
|
||||
+1
@@ -4,6 +4,7 @@ export enum FeatureFlagKey {
|
||||
IS_AI_ENABLED = 'IS_AI_ENABLED',
|
||||
IS_APPLICATION_ENABLED = 'IS_APPLICATION_ENABLED',
|
||||
IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED = 'IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED',
|
||||
IS_MARKETPLACE_ENABLED = 'IS_MARKETPLACE_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_ENABLED = 'IS_RECORD_PAGE_LAYOUT_ENABLED',
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED = 'IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED',
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100" fill="#1a2744">
|
||||
<!-- Database cylinder -->
|
||||
<ellipse cx="38" cy="20" rx="28" ry="10"/>
|
||||
<rect x="10" y="20" width="56" height="50"/>
|
||||
<ellipse cx="38" cy="70" rx="28" ry="10"/>
|
||||
<!-- Database stripes -->
|
||||
<ellipse cx="38" cy="35" rx="28" ry="10" fill="none" stroke="#fff" stroke-width="3"/>
|
||||
<ellipse cx="38" cy="52" rx="28" ry="10" fill="none" stroke="#fff" stroke-width="3"/>
|
||||
<!-- Upload circle -->
|
||||
<circle cx="72" cy="62" r="22" fill="#1a2744"/>
|
||||
<circle cx="72" cy="62" r="18" fill="#fff"/>
|
||||
<!-- Upload arrow -->
|
||||
<path d="M72 50 L72 74 M62 58 L72 48 L82 58" stroke="#1a2744" stroke-width="4" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 743 B |
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"application": {
|
||||
"universalIdentifier": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||
"functionRoleUniversalIdentifier": "00000000-0000-0000-0000-000000000000",
|
||||
"displayName": "Data Enrichment",
|
||||
"description": "Enrich your data easily. Choose your provider.",
|
||||
"icon": "IconSparkles",
|
||||
"marketplaceData": {
|
||||
"author": "Cosmos Labs",
|
||||
"category": "Data",
|
||||
"logo": "assets/logo.png",
|
||||
"screenshots": [
|
||||
"assets/screenshot-1.png",
|
||||
"assets/screenshot-2.png",
|
||||
"assets/screenshot-3.png"
|
||||
],
|
||||
"aboutDescription": "Enhance your workspace with automated data intelligence. This app monitors your new records and automatically populates missing details such as job titles, company size, social profiles, and industry insights.",
|
||||
"providers": ["Clearbit", "Apollo", "Hunter.io"],
|
||||
"websiteUrl": "https://google.com",
|
||||
"termsUrl": "https://google.com"
|
||||
}
|
||||
},
|
||||
"entities": {
|
||||
"objects": [],
|
||||
"fields": [],
|
||||
"logicFunctions": [],
|
||||
"frontComponents": [],
|
||||
"roles": []
|
||||
},
|
||||
"publicAssets": [],
|
||||
"sources": {},
|
||||
"packageJson": {
|
||||
"name": "@twenty-apps/data-enrichment",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"yarnLock": ""
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { WorkspaceActivationStatus } from 'twenty-shared/workspace';
|
||||
import { type Repository, type UpdateResult } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
|
||||
import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service';
|
||||
import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service';
|
||||
|
||||
@@ -24,7 +24,7 @@ import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import type { FileUpload } from 'graphql-upload/processRequest.mjs';
|
||||
|
||||
import { ApiKeyEntity } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { ApplicationDTO } from 'src/engine/core-modules/application/dtos/application.dto';
|
||||
import { fromFlatApplicationToApplicationDto } from 'src/engine/core-modules/application/utils/from-flat-application-to-application-dto.util';
|
||||
import { BillingEntitlementDTO } from 'src/engine/core-modules/billing/dtos/billing-entitlement.dto';
|
||||
|
||||
@@ -4,7 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { ILike, IsNull, Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { type CreateAgentInput } from 'src/engine/metadata-modules/ai/ai-agent/dtos/create-agent.input';
|
||||
import { type UpdateAgentInput } from 'src/engine/metadata-modules/ai/ai-agent/dtos/update-agent.input';
|
||||
import { fromCreateAgentInputToFlatAgent } from 'src/engine/metadata-modules/ai/ai-agent/utils/from-create-agent-input-to-flat-agent.util';
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import {
|
||||
CommandMenuItemException,
|
||||
CommandMenuItemExceptionCode,
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type FindOneOptions, type Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { type CreateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/create-field.input';
|
||||
import { type DeleteOneFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/delete-field.input';
|
||||
import { type UpdateFieldInput } from 'src/engine/metadata-modules/field-metadata/dtos/update-field.input';
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ import { Repository } from 'typeorm';
|
||||
import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception';
|
||||
import { type LogicFunctionExecuteResult } from 'src/engine/core-modules/logic-function-executor/drivers/interfaces/logic-function-executor-driver.interface';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { AuditService } from 'src/engine/core-modules/audit/services/audit.service';
|
||||
import { LOGIC_FUNCTION_EXECUTED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/logic-function/logic-function-executed';
|
||||
import { ApplicationTokenService } from 'src/engine/core-modules/auth/token/services/application-token.service';
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import type { CreateLogicFunctionInput } from 'src/engine/metadata-modules/logic-function/dtos/create-logic-function.input';
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { isApiKeyAuthContext } from 'src/engine/core-modules/auth/guards/is-api-key-auth-context.guard';
|
||||
import { isApplicationAuthContext } from 'src/engine/core-modules/auth/guards/is-application-auth-context.guard';
|
||||
import { isUserAuthContext } from 'src/engine/core-modules/auth/guards/is-user-auth-context.guard';
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||
import { fromArrayToUniqueKeyRecord, isDefined } from 'twenty-shared/utils';
|
||||
import { FindManyOptions, FindOneOptions, Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { createEmptyFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/constant/create-empty-flat-entity-maps.constant';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { AllFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps.type';
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type';
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { FlatPageLayoutWidgetMaps } from 'src/engine/metadata-modules/flat-page-layout-widget/types/flat-page-layout-widget-maps.type';
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type';
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { PageLayoutTabLayoutMode } from 'twenty-shared/types';
|
||||
import { computeDiffBetweenObjects, isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { FLAT_PAGE_LAYOUT_TAB_EDITABLE_PROPERTIES } from 'src/engine/metadata-modules/flat-page-layout-tab/constants/flat-page-layout-tab-editable-properties.constant';
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { type FlatPageLayoutTabMaps } from 'src/engine/metadata-modules/flat-page-layout-tab/types/flat-page-layout-tab-maps.type';
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service';
|
||||
import { RoleEntity } from 'src/engine/metadata-modules/role/role.entity';
|
||||
import { UserRoleService } from 'src/engine/metadata-modules/user-role/user-role.service';
|
||||
|
||||
+2
-1
@@ -7,7 +7,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { TOOL_PERMISSION_FLAGS } from 'src/engine/metadata-modules/permissions/constants/tool-permission-flags';
|
||||
import {
|
||||
PermissionsException,
|
||||
@@ -119,6 +119,7 @@ export class PermissionsService {
|
||||
[PermissionFlagType.IMPERSONATE]: false,
|
||||
[PermissionFlagType.SSO_BYPASS]: false,
|
||||
[PermissionFlagType.PROFILE_INFORMATION]: false,
|
||||
[PermissionFlagType.MARKETPLACE_APPS]: false,
|
||||
},
|
||||
objectsPermissions: {},
|
||||
}) as const satisfies UserWorkspacePermissions;
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { FlatRoleTarget } from 'src/engine/metadata-modules/flat-role-target/types/flat-role-target.type';
|
||||
|
||||
@@ -18,7 +18,7 @@ import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { ApiKeyRoleService } from 'src/engine/core-modules/api-key/services/api-key-role.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import { ConfigService } from '@nestjs/config';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum';
|
||||
import { BillingService } from 'src/engine/core-modules/billing/services/billing.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findFlatEntityByIdInFlatEntityMaps } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps.util';
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { type Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { ViewFilterGroupEntity } from 'src/engine/metadata-modules/view-filter-group/entities/view-filter-group.entity';
|
||||
import { ViewFilterGroupLogicalOperator } from 'src/engine/metadata-modules/view-filter-group/enums/view-filter-group-logical-operator';
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, type Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { fromCreateViewFilterGroupInputToFlatViewFilterGroupToCreate } from 'src/engine/metadata-modules/flat-view-filter-group/utils/from-create-view-filter-group-input-to-flat-view-filter-group-to-create.util';
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { fromCreateViewFilterInputToFlatViewFilterToCreate } from 'src/engine/metadata-modules/flat-view-filter/utils/from-create-view-filter-input-to-flat-view-filter-to-create.util';
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { findManyFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-many-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ import { getRepositoryToken } from '@nestjs/typeorm';
|
||||
|
||||
import { type Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
|
||||
import { ViewSortDirection } from 'src/engine/metadata-modules/view-sort/enums/view-sort-direction';
|
||||
import {
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { ViewSortEntity } from 'src/engine/metadata-modules/view-sort/entities/view-sort.entity';
|
||||
import {
|
||||
ViewSortException,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IsNull, Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { fromCreateWebhookInputToFlatWebhookToCreate } from 'src/engine/metadata-modules/flat-webhook/utils/from-create-webhook-input-to-flat-webhook-to-create.util';
|
||||
|
||||
+1
@@ -242,6 +242,7 @@ describe('WorkspaceEntityManager', () => {
|
||||
IS_NAVIGATION_MENU_ITEM_ENABLED: false,
|
||||
IS_FILES_FIELD_ENABLED: false,
|
||||
IS_APPLICATION_INSTALLATION_FROM_TARBALL_ENABLED: false,
|
||||
IS_MARKETPLACE_ENABLED: false,
|
||||
IS_RECORD_PAGE_LAYOUT_EDITING_ENABLED: false,
|
||||
},
|
||||
userWorkspaceRoleMap: {},
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { type DataSource } from 'typeorm';
|
||||
|
||||
import { type ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { type ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { seedBillingCustomers } from 'src/engine/workspace-manager/dev-seeder/core/billing/utils/seed-billing-customers.util';
|
||||
import { seedBillingSubscriptions } from 'src/engine/workspace-manager/dev-seeder/core/billing/utils/seed-billing-subscriptions.util';
|
||||
import {
|
||||
|
||||
+5
@@ -91,6 +91,11 @@ export const seedFeatureFlags = async ({
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_MARKETPLACE_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_COMMAND_MENU_ITEM_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { InjectDataSource } from '@nestjs/typeorm';
|
||||
import { ALL_METADATA_NAME } from 'twenty-shared/metadata';
|
||||
import { DataSource } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { MetadataFlatEntity } from 'src/engine/metadata-modules/flat-entity/types/metadata-flat-entity.type';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { FlatApplication } from 'src/engine/core-modules/application/types/flat-application.type';
|
||||
import { UserWorkspaceEntity } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type ToolSet } from 'ai';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
|
||||
import { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout-tab/services/page-layout-tab.service';
|
||||
import { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service';
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
import type { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import type { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import type { RecordPositionService } from 'src/engine/core-modules/record-position/services/record-position.service';
|
||||
import type { PageLayoutTabService } from 'src/engine/metadata-modules/page-layout-tab/services/page-layout-tab.service';
|
||||
import type { PageLayoutWidgetService } from 'src/engine/metadata-modules/page-layout-widget/services/page-layout-widget.service';
|
||||
|
||||
+1
-1
@@ -3,10 +3,10 @@ import { Injectable } from '@nestjs/common';
|
||||
import { FieldActorSource } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/services/application.service';
|
||||
import { type WorkspaceAuthContext } from 'src/engine/core-modules/auth/types/workspace-auth-context.type';
|
||||
import { buildApplicationAuthContext } from 'src/engine/core-modules/auth/utils/build-application-auth-context.util';
|
||||
import { buildUserAuthContext } from 'src/engine/core-modules/auth/utils/build-user-auth-context.util';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
|
||||
import { ADMIN_ROLE } from 'src/engine/metadata-modules/role/constants/admin-role';
|
||||
import { RoleService } from 'src/engine/metadata-modules/role/role.service';
|
||||
|
||||
@@ -1,10 +1,22 @@
|
||||
import { type ApplicationVariables } from '@/sdk';
|
||||
import { type SyncableEntityOptions } from '@/application/syncableEntityOptionsType';
|
||||
|
||||
export type ApplicationMarketplaceData = {
|
||||
author?: string;
|
||||
category?: string;
|
||||
logo?: string;
|
||||
screenshots?: string[];
|
||||
aboutDescription?: string;
|
||||
providers?: string[];
|
||||
websiteUrl?: string;
|
||||
termsUrl?: string;
|
||||
};
|
||||
|
||||
export type ApplicationManifest = SyncableEntityOptions & {
|
||||
defaultRoleUniversalIdentifier: string;
|
||||
displayName?: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
applicationVariables?: ApplicationVariables;
|
||||
marketplaceData?: ApplicationMarketplaceData;
|
||||
};
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
* |___/
|
||||
*/
|
||||
|
||||
export type { ApplicationManifest } from './applicationType';
|
||||
export type {
|
||||
ApplicationMarketplaceData,
|
||||
ApplicationManifest,
|
||||
} from './applicationType';
|
||||
export type { ApplicationVariables } from './applicationVariablesType';
|
||||
export type { AssetManifest } from './assetManifestType';
|
||||
export { ASSETS_DIR } from './constants/AssetDirectory';
|
||||
|
||||
@@ -10,6 +10,7 @@ export enum PermissionFlagType {
|
||||
IMPERSONATE = 'IMPERSONATE',
|
||||
SSO_BYPASS = 'SSO_BYPASS',
|
||||
APPLICATIONS = 'APPLICATIONS',
|
||||
MARKETPLACE_APPS = 'MARKETPLACE_APPS',
|
||||
LAYOUTS = 'LAYOUTS',
|
||||
BILLING = 'BILLING',
|
||||
AI_SETTINGS = 'AI_SETTINGS',
|
||||
|
||||
@@ -36,6 +36,7 @@ export enum SettingsPath {
|
||||
Applications = 'applications',
|
||||
ApplicationDetail = 'applications/:applicationId',
|
||||
ApplicationLogicFunctionDetail = 'applications/:applicationId/logicFunctions/:logicFunctionId',
|
||||
AvailableApplicationDetail = 'applications/available/:availableApplicationId',
|
||||
LogicFunctions = 'functions',
|
||||
NewLogicFunction = 'functions/new',
|
||||
LogicFunctionDetail = 'functions/:logicFunctionId',
|
||||
|
||||
@@ -89,6 +89,8 @@ export {
|
||||
IconCodeCircle,
|
||||
IconCoins,
|
||||
IconColorSwatch,
|
||||
IconColumns,
|
||||
IconCommand,
|
||||
IconMessageCircle as IconComment,
|
||||
IconCopy,
|
||||
IconCopyPlus,
|
||||
@@ -221,6 +223,7 @@ export {
|
||||
IconLayersLinked,
|
||||
IconLayout,
|
||||
IconLayoutDashboard,
|
||||
IconLayoutGrid,
|
||||
IconLayoutKanban,
|
||||
IconLayoutList,
|
||||
IconLayoutNavbar,
|
||||
|
||||
@@ -156,6 +156,8 @@ export {
|
||||
IconCodeCircle,
|
||||
IconCoins,
|
||||
IconColorSwatch,
|
||||
IconColumns,
|
||||
IconCommand,
|
||||
IconComment,
|
||||
IconCopy,
|
||||
IconCopyPlus,
|
||||
@@ -288,6 +290,7 @@ export {
|
||||
IconLayersLinked,
|
||||
IconLayout,
|
||||
IconLayoutDashboard,
|
||||
IconLayoutGrid,
|
||||
IconLayoutKanban,
|
||||
IconLayoutList,
|
||||
IconLayoutNavbar,
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconFilter, IconSearch } from '@ui/display';
|
||||
import { IconButton } from '@ui/input/button/components/IconButton';
|
||||
import { type ChangeEvent, type ReactNode, useState } from 'react';
|
||||
|
||||
export type SearchInputProps = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
filterDropdown?: (filterButton: ReactNode) => ReactNode;
|
||||
autoFocus?: boolean;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const StyledWrapper = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
height: 32px;
|
||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||
|
||||
&:focus-within {
|
||||
border-color: ${({ theme }) => theme.color.blue};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledIconContainer = styled.div<{ isFocused: boolean }>`
|
||||
align-items: center;
|
||||
color: ${({ theme, isFocused }) =>
|
||||
isFocused ? theme.font.color.secondary : theme.font.color.light};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
flex: 1;
|
||||
font-family: ${({ theme }) => theme.font.family};
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
outline: none;
|
||||
width: 100%;
|
||||
|
||||
&::placeholder {
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
}
|
||||
`;
|
||||
|
||||
export const SearchInput = ({
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
filterDropdown,
|
||||
autoFocus,
|
||||
disabled,
|
||||
className,
|
||||
}: SearchInputProps) => {
|
||||
const theme = useTheme();
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
const filterButton = <IconButton Icon={IconFilter} variant="secondary" />;
|
||||
|
||||
return (
|
||||
<StyledWrapper className={className}>
|
||||
<StyledInputContainer>
|
||||
<StyledIconContainer isFocused={isFocused}>
|
||||
<IconSearch size={theme.icon.size.md} />
|
||||
</StyledIconContainer>
|
||||
<StyledInput
|
||||
value={value}
|
||||
onChange={(event: ChangeEvent<HTMLInputElement>) =>
|
||||
onChange(event.target.value)
|
||||
}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
placeholder={placeholder}
|
||||
autoFocus={autoFocus}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
{filterDropdown && filterDropdown(filterButton)}
|
||||
</StyledWrapper>
|
||||
);
|
||||
};
|
||||
@@ -99,6 +99,8 @@ export { IconListViewGrip } from './components/IconListViewGrip';
|
||||
export type { RadioProps } from './components/Radio';
|
||||
export { RadioSize, LabelPosition, Radio } from './components/Radio';
|
||||
export { RadioGroup } from './components/RadioGroup';
|
||||
export type { SearchInputProps } from './components/SearchInput';
|
||||
export { SearchInput } from './components/SearchInput';
|
||||
export type { ToggleSize, ToggleProps } from './components/Toggle';
|
||||
export { Toggle } from './components/Toggle';
|
||||
export type { ColorScheme } from './types/ColorScheme';
|
||||
|
||||
Reference in New Issue
Block a user