Fix app design 1/2 (#19735)
comply with https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=96977-349627&m=dev ## After <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 40 37" src="https://github.com/user-attachments/assets/6d80191a-79a9-4f0f-aa4f-0e447fff4f6d" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 40 22" src="https://github.com/user-attachments/assets/4f763272-027e-4246-b455-7d46babf7d8c" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 39 11" src="https://github.com/user-attachments/assets/b9b35e18-8068-447e-821d-5ec28bb5bd16" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 39 05" src="https://github.com/user-attachments/assets/57d9318a-902f-4fd7-a2a3-5795ebe0b9dc" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 39 02" src="https://github.com/user-attachments/assets/78a33fa8-6bdd-484e-a82d-bd0f7592a623" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 38 58" src="https://github.com/user-attachments/assets/f7987aed-c6e1-4032-a611-86817655137d" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 38 55" src="https://github.com/user-attachments/assets/d1c451ab-1d2d-41e4-a059-cf4303ecabe7" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 38 48" src="https://github.com/user-attachments/assets/593cae36-2320-443f-a955-93b211a6ee3f" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 37 40" src="https://github.com/user-attachments/assets/c9f602b1-8de3-4e82-a3a6-344594a0c153" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 37 34" src="https://github.com/user-attachments/assets/b54ddddf-5dda-46c8-ace3-cffe6015825a" /> ## before <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42 18" src="https://github.com/user-attachments/assets/c0976a0a-0124-48ec-8e7c-78627cea7063" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42 16" src="https://github.com/user-attachments/assets/d2db926c-4040-411d-9091-8b60e7c519e6" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42 13" src="https://github.com/user-attachments/assets/2d69f2ff-f26e-4249-91a3-2cf3d261e840" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42 07" src="https://github.com/user-attachments/assets/1028aabc-77ac-4c51-a8c3-9a194faba87f" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 42 01" src="https://github.com/user-attachments/assets/1caa9f5e-3eaa-433c-9d3b-e0f094f16e8e" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 41 56" src="https://github.com/user-attachments/assets/f42b6976-3a8f-4591-9283-bda79bdb424b" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 41 53" src="https://github.com/user-attachments/assets/93d00df8-0091-4dfa-9ac0-f6f376be5962" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 41 43" src="https://github.com/user-attachments/assets/9deae7e5-39c1-4518-a463-6d79bc5bf132" /> <img width="1512" height="909" alt="Capture d’écran 2026-04-16 à 09 41 37" src="https://github.com/user-attachments/assets/3e21b521-c47d-482c-ad41-66abfe973772" />
This commit is contained in:
@@ -1732,6 +1732,12 @@ type PublicWorkspaceData {
|
||||
workspaceUrls: WorkspaceUrls!
|
||||
}
|
||||
|
||||
type PublicWorkspaceDataSummary {
|
||||
id: UUID!
|
||||
logo: String
|
||||
displayName: String
|
||||
}
|
||||
|
||||
type NativeModelCapabilities {
|
||||
webSearch: Boolean
|
||||
twitterSearch: Boolean
|
||||
@@ -3344,6 +3350,7 @@ type Query {
|
||||
currentUser: User!
|
||||
currentWorkspace: Workspace!
|
||||
getPublicWorkspaceDataByDomain(origin: String): PublicWorkspaceData!
|
||||
getPublicWorkspaceDataById(id: UUID!): PublicWorkspaceDataSummary!
|
||||
getSSOIdentityProviders: [FindAvailableSSOIDP!]!
|
||||
getConnectedImapSmtpCaldavAccount(id: UUID!): ConnectedImapSmtpCaldavAccount!
|
||||
getAutoCompleteAddress(address: String!, token: String!, country: String, isFieldCity: Boolean): [AutocompleteResult!]!
|
||||
@@ -4599,6 +4606,7 @@ input UpdateApplicationRegistrationVariableInput {
|
||||
|
||||
input UpdateApplicationRegistrationVariablePayload {
|
||||
value: String
|
||||
resetValue: Boolean
|
||||
description: String
|
||||
}
|
||||
|
||||
|
||||
@@ -1430,6 +1430,13 @@ export interface PublicWorkspaceData {
|
||||
__typename: 'PublicWorkspaceData'
|
||||
}
|
||||
|
||||
export interface PublicWorkspaceDataSummary {
|
||||
id: Scalars['UUID']
|
||||
logo?: Scalars['String']
|
||||
displayName?: Scalars['String']
|
||||
__typename: 'PublicWorkspaceDataSummary'
|
||||
}
|
||||
|
||||
export interface NativeModelCapabilities {
|
||||
webSearch?: Scalars['Boolean']
|
||||
twitterSearch?: Scalars['Boolean']
|
||||
@@ -2905,6 +2912,7 @@ export interface Query {
|
||||
currentUser: User
|
||||
currentWorkspace: Workspace
|
||||
getPublicWorkspaceDataByDomain: PublicWorkspaceData
|
||||
getPublicWorkspaceDataById: PublicWorkspaceDataSummary
|
||||
getSSOIdentityProviders: FindAvailableSSOIDP[]
|
||||
getConnectedImapSmtpCaldavAccount: ConnectedImapSmtpCaldavAccount
|
||||
getAutoCompleteAddress: AutocompleteResult[]
|
||||
@@ -4680,6 +4688,14 @@ export interface PublicWorkspaceDataGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface PublicWorkspaceDataSummaryGenqlSelection{
|
||||
id?: boolean | number
|
||||
logo?: boolean | number
|
||||
displayName?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface NativeModelCapabilitiesGenqlSelection{
|
||||
webSearch?: boolean | number
|
||||
twitterSearch?: boolean | number
|
||||
@@ -6274,6 +6290,7 @@ export interface QueryGenqlSelection{
|
||||
currentUser?: UserGenqlSelection
|
||||
currentWorkspace?: WorkspaceGenqlSelection
|
||||
getPublicWorkspaceDataByDomain?: (PublicWorkspaceDataGenqlSelection & { __args?: {origin?: (Scalars['String'] | null)} })
|
||||
getPublicWorkspaceDataById?: (PublicWorkspaceDataSummaryGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
getSSOIdentityProviders?: FindAvailableSSOIDPGenqlSelection
|
||||
getConnectedImapSmtpCaldavAccount?: (ConnectedImapSmtpCaldavAccountGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
getAutoCompleteAddress?: (AutocompleteResultGenqlSelection & { __args: {address: Scalars['String'], token: Scalars['String'], country?: (Scalars['String'] | null), isFieldCity?: (Scalars['Boolean'] | null)} })
|
||||
@@ -6859,7 +6876,7 @@ export interface CreateApplicationRegistrationVariableInput {applicationRegistra
|
||||
|
||||
export interface UpdateApplicationRegistrationVariableInput {id: Scalars['String'],update: UpdateApplicationRegistrationVariablePayload}
|
||||
|
||||
export interface UpdateApplicationRegistrationVariablePayload {value?: (Scalars['String'] | null),description?: (Scalars['String'] | null)}
|
||||
export interface UpdateApplicationRegistrationVariablePayload {value?: (Scalars['String'] | null),resetValue?: (Scalars['Boolean'] | null),description?: (Scalars['String'] | null)}
|
||||
|
||||
export interface UpdateWorkspaceMemberSettingsInput {workspaceMemberId: Scalars['UUID'],update: Scalars['JSON']}
|
||||
|
||||
@@ -7850,6 +7867,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const PublicWorkspaceDataSummary_possibleTypes: string[] = ['PublicWorkspaceDataSummary']
|
||||
export const isPublicWorkspaceDataSummary = (obj?: { __typename?: any } | null): obj is PublicWorkspaceDataSummary => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isPublicWorkspaceDataSummary"')
|
||||
return PublicWorkspaceDataSummary_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const NativeModelCapabilities_possibleTypes: string[] = ['NativeModelCapabilities']
|
||||
export const isNativeModelCapabilities = (obj?: { __typename?: any } | null): obj is NativeModelCapabilities => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isNativeModelCapabilities"')
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -204,6 +204,14 @@ const SettingsApplicationRegistrationDetails = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsApplicationRegistrationConfigVariableDetail = lazy(() =>
|
||||
import(
|
||||
'~/pages/settings/applications/components/SettingsApplicationRegistrationConfigVariableDetail'
|
||||
).then((module) => ({
|
||||
default: module.SettingsApplicationRegistrationConfigVariableDetail,
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsAgentForm = lazy(() =>
|
||||
import('~/pages/settings/ai/SettingsAgentForm').then((module) => ({
|
||||
default: module.SettingsAgentForm,
|
||||
@@ -744,6 +752,10 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
|
||||
path={SettingsPath.ApplicationLogicFunctionDetail}
|
||||
element={<SettingsLogicFunctionDetail />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.ApplicationRegistrationConfigVariableDetails}
|
||||
element={<SettingsApplicationRegistrationConfigVariableDetail />}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const FIND_ONE_APPLICATION_SUMMARY = gql`
|
||||
query FindOneApplicationSummary($universalIdentifier: UUID!) {
|
||||
findOneApplication(universalIdentifier: $universalIdentifier) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
+4
-1
@@ -174,7 +174,10 @@ export const useIdentifyActiveNavigationMenuItems = (): {
|
||||
lastClickedNavigationMenuItemId,
|
||||
objectMetadataItems,
|
||||
views,
|
||||
location,
|
||||
currentPathWithSearch,
|
||||
currentPath,
|
||||
currentObjectMetadataItem,
|
||||
isOnRecordShowPage,
|
||||
contextStoreCurrentViewId,
|
||||
]);
|
||||
|
||||
|
||||
+2
-7
@@ -90,12 +90,7 @@ export const EmailThreadComposer = ({
|
||||
disabled: !composerState.canSend,
|
||||
},
|
||||
];
|
||||
}, [
|
||||
isComposerOpen,
|
||||
composerState.handleSend,
|
||||
composerState.canSend,
|
||||
setIsComposerOpen,
|
||||
]);
|
||||
}, [isComposerOpen, composerState, setIsComposerOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInSidePanel) {
|
||||
@@ -111,7 +106,7 @@ export const EmailThreadComposer = ({
|
||||
if (isComposerOpen && composerState.canSend) {
|
||||
composerState.handleSend();
|
||||
}
|
||||
}, [isComposerOpen, composerState.canSend, composerState.handleSend]);
|
||||
}, [isComposerOpen, composerState]);
|
||||
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: ['ctrl+Enter,meta+Enter'],
|
||||
|
||||
+1
-7
@@ -203,19 +203,13 @@ export const useFieldsWidgetEditorGroupsData = ({
|
||||
return {
|
||||
groups: buildDefaultFieldsWidgetGroups({
|
||||
fields: objectMetadataItem.fields,
|
||||
objectNameSingular,
|
||||
labelIdentifierFieldMetadataItemId:
|
||||
labelIdentifierFieldMetadataItem?.id,
|
||||
}),
|
||||
ungroupedFields: [],
|
||||
editorMode: 'grouped' as const,
|
||||
};
|
||||
}, [
|
||||
objectMetadataItem,
|
||||
objectNameSingular,
|
||||
view,
|
||||
labelIdentifierFieldMetadataItem,
|
||||
]);
|
||||
}, [objectMetadataItem, view, labelIdentifierFieldMetadataItem]);
|
||||
|
||||
return {
|
||||
...result,
|
||||
|
||||
+1
-8
@@ -131,20 +131,13 @@ export const useFieldsWidgetGroups = ({
|
||||
groups: filterDraftGroupsForDisplay(
|
||||
buildDefaultFieldsWidgetGroups({
|
||||
fields: objectMetadataItem.fields,
|
||||
objectNameSingular,
|
||||
labelIdentifierFieldMetadataItemId:
|
||||
labelIdentifierFieldMetadataItem?.id,
|
||||
}),
|
||||
),
|
||||
displayMode: 'grouped',
|
||||
};
|
||||
}, [
|
||||
objectMetadataItem,
|
||||
objectNameSingular,
|
||||
labelIdentifierFieldMetadataItem,
|
||||
view,
|
||||
viewId,
|
||||
]);
|
||||
}, [objectMetadataItem, labelIdentifierFieldMetadataItem, view, viewId]);
|
||||
|
||||
return {
|
||||
groups,
|
||||
|
||||
-2
@@ -6,11 +6,9 @@ import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
export const buildDefaultFieldsWidgetGroups = ({
|
||||
fields,
|
||||
objectNameSingular,
|
||||
labelIdentifierFieldMetadataItemId,
|
||||
}: {
|
||||
fields: FieldMetadataItem[];
|
||||
objectNameSingular: string;
|
||||
labelIdentifierFieldMetadataItemId: string | undefined;
|
||||
}): FieldsWidgetGroup[] => {
|
||||
const eligibleFields = fields.filter((field) =>
|
||||
|
||||
+2
@@ -348,7 +348,9 @@ export const SettingsAdminAI = () => {
|
||||
)
|
||||
) : (
|
||||
<SettingsEnterpriseFeatureGateCard
|
||||
title={t`Enterprise feature`}
|
||||
description={t`AI usage analytics across workspaces is available with an Enterprise key.`}
|
||||
buttonTitle={t`Activate`}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
+63
-47
@@ -6,32 +6,34 @@ import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useState } from 'react';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { useContext, useState } from 'react';
|
||||
import { assertUnreachable, getSettingsPath } from 'twenty-shared/utils';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { H2Title, Status } from 'twenty-ui/display';
|
||||
import {
|
||||
H2Title,
|
||||
IconChevronRight,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui/display';
|
||||
import { SearchInput } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { UndecoratedLink } from 'twenty-ui/navigation';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
type ApplicationRegistrationFragmentFragment,
|
||||
ApplicationRegistrationSourceType,
|
||||
FindAllApplicationRegistrationsDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledTableContainer = styled.div`
|
||||
border-bottom: 1px solid ${themeCssVariables.border.color.light};
|
||||
margin-top: ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
const StyledTableHeaderRowContainer = styled.div`
|
||||
margin-bottom: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const TABLE_GRID = '1fr 1fr 100px 80px';
|
||||
const TABLE_GRID_MOBILE = '3fr 3fr 1fr 70px';
|
||||
const TABLE_GRID = '1fr 100px 100px 40px';
|
||||
const TABLE_GRID_MOBILE = '3fr 3fr 1fr 40px';
|
||||
|
||||
export const SettingsAdminApps = () => {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const { theme } = useContext(ThemeContext);
|
||||
|
||||
const { data } = useQuery(FindAllApplicationRegistrationsDocument);
|
||||
|
||||
@@ -51,6 +53,27 @@ export const SettingsAdminApps = () => {
|
||||
);
|
||||
});
|
||||
|
||||
const getFormattedSource = (
|
||||
registration: ApplicationRegistrationFragmentFragment,
|
||||
) => {
|
||||
switch (registration.sourceType) {
|
||||
case ApplicationRegistrationSourceType.TARBALL: {
|
||||
return 'Tarball';
|
||||
}
|
||||
case ApplicationRegistrationSourceType.NPM: {
|
||||
return 'NPM';
|
||||
}
|
||||
case ApplicationRegistrationSourceType.OAUTH_ONLY: {
|
||||
return 'OAuth';
|
||||
}
|
||||
case ApplicationRegistrationSourceType.LOCAL: {
|
||||
return 'Local';
|
||||
}
|
||||
default:
|
||||
return assertUnreachable(registration.sourceType);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
@@ -64,50 +87,43 @@ export const SettingsAdminApps = () => {
|
||||
/>
|
||||
<StyledTableContainer>
|
||||
<Table>
|
||||
<StyledTableHeaderRowContainer>
|
||||
<TableRow
|
||||
gridAutoColumns={TABLE_GRID}
|
||||
mobileGridAutoColumns={TABLE_GRID_MOBILE}
|
||||
>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader>{t`Source`}</TableHeader>
|
||||
<TableHeader>{t`Listed`}</TableHeader>
|
||||
<TableHeader>{t`Featured`}</TableHeader>
|
||||
</TableRow>
|
||||
</StyledTableHeaderRowContainer>
|
||||
<TableRow
|
||||
gridAutoColumns={TABLE_GRID}
|
||||
mobileGridAutoColumns={TABLE_GRID_MOBILE}
|
||||
>
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader align="right">{t`Source`}</TableHeader>
|
||||
<TableHeader align="right">{t`Listed`}</TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
</TableRow>
|
||||
<TableBody>
|
||||
{filtered.map((registration) => (
|
||||
<UndecoratedLink
|
||||
<TableRow
|
||||
key={registration.id}
|
||||
to={getSettingsPath(
|
||||
SettingsPath.AdminPanelApplicationRegistrationDetail,
|
||||
{ applicationRegistrationId: registration.id },
|
||||
)}
|
||||
fullWidth
|
||||
gridAutoColumns={TABLE_GRID}
|
||||
mobileGridAutoColumns={TABLE_GRID_MOBILE}
|
||||
isClickable
|
||||
>
|
||||
<TableRow
|
||||
gridAutoColumns={TABLE_GRID}
|
||||
mobileGridAutoColumns={TABLE_GRID_MOBILE}
|
||||
isClickable
|
||||
>
|
||||
<TableCell>{registration.name}</TableCell>
|
||||
<TableCell overflow="hidden">
|
||||
{registration.sourcePackage ?? registration.sourceType}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Status
|
||||
color={registration.isListed ? 'green' : 'gray'}
|
||||
text={registration.isListed ? t`Yes` : t`No`}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Status
|
||||
color={registration.isFeatured ? 'yellow' : 'gray'}
|
||||
text={registration.isFeatured ? t`Yes` : t`No`}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</UndecoratedLink>
|
||||
<TableCell color={themeCssVariables.font.color.primary}>
|
||||
<OverflowingTextWithTooltip text={registration.name} />
|
||||
</TableCell>
|
||||
<TableCell overflow="hidden" align="right">
|
||||
{getFormattedSource(registration)}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
{registration.isListed ? t`Yes` : t`No`}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
+6
-3
@@ -1,7 +1,6 @@
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { SettingsOptionCardContentButton } from '@/settings/components/SettingsOptions/SettingsOptionCardContentButton';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { IconArrowUp, IconLock } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
@@ -9,9 +8,13 @@ import { Card } from 'twenty-ui/layout';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
|
||||
export const SettingsEnterpriseFeatureGateCard = ({
|
||||
title,
|
||||
description,
|
||||
buttonTitle,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
buttonTitle: string;
|
||||
}) => {
|
||||
const currentUser = useAtomStateValue(currentUserState);
|
||||
const navigateSettings = useNavigateSettings();
|
||||
@@ -22,12 +25,12 @@ export const SettingsEnterpriseFeatureGateCard = ({
|
||||
<Card rounded>
|
||||
<SettingsOptionCardContentButton
|
||||
Icon={IconLock}
|
||||
title={t`Enterprise feature`}
|
||||
title={title}
|
||||
description={description}
|
||||
Button={
|
||||
canAccessAdminPanel ? (
|
||||
<Button
|
||||
title={t`Activate`}
|
||||
title={buttonTitle}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
size="small"
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useContext } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
@@ -11,20 +10,13 @@ import {
|
||||
import { Card } from 'twenty-ui/layout';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
type TableItem = {
|
||||
export type TableItem = {
|
||||
Icon?: IconComponent;
|
||||
label: string;
|
||||
value: string | number | React.ReactNode;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
const StyledTableBody = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
padding: ${themeCssVariables.spacing[2]} 0;
|
||||
`;
|
||||
|
||||
type SettingsTableCardProps = {
|
||||
items: TableItem[];
|
||||
rounded?: boolean;
|
||||
@@ -50,35 +42,27 @@ export const SettingsTableCard = ({
|
||||
backgroundColor={themeCssVariables.background.secondary}
|
||||
>
|
||||
<Table>
|
||||
<StyledTableBody>
|
||||
{items.map((item, index) => (
|
||||
<TableRow
|
||||
key={index + item.label}
|
||||
gridAutoColumns={gridAutoColumns}
|
||||
height={`${themeCssVariables.spacing[7]}px`}
|
||||
{items.map((item, index) => (
|
||||
<TableRow key={index + item.label} gridAutoColumns={gridAutoColumns}>
|
||||
<TableCell
|
||||
align={labelAlign}
|
||||
color={themeCssVariables.font.color.tertiary}
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
overflow="hidden"
|
||||
>
|
||||
<TableCell
|
||||
align={labelAlign}
|
||||
color={themeCssVariables.font.color.tertiary}
|
||||
height="auto"
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
overflow="hidden"
|
||||
>
|
||||
{item.Icon && <item.Icon size={theme.icon.size.md} />}
|
||||
<OverflowingTextWithTooltip text={item.label} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align={valueAlign}
|
||||
color={themeCssVariables.font.color.primary}
|
||||
height="auto"
|
||||
onClick={item.onClick}
|
||||
clickable={isDefined(item.onClick)}
|
||||
>
|
||||
{item.value}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</StyledTableBody>
|
||||
{item.Icon && <item.Icon size={theme.icon.size.md} />}
|
||||
<OverflowingTextWithTooltip text={item.label} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
align={valueAlign}
|
||||
color={themeCssVariables.font.color.primary}
|
||||
onClick={item.onClick}
|
||||
clickable={isDefined(item.onClick)}
|
||||
>
|
||||
{item.value}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</Table>
|
||||
</Card>
|
||||
);
|
||||
|
||||
+2
-18
@@ -14,6 +14,7 @@ import { SettingsPath } from 'twenty-shared/types';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { OrganizationAdornment } from '~/pages/settings/enterprise/components/OrganizationAdornment';
|
||||
|
||||
const StyledContent = styled.div`
|
||||
padding-bottom: ${themeCssVariables.spacing[2]};
|
||||
@@ -24,18 +25,6 @@ const StyledCardContainer = styled.div`
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledPillContainer = styled.span`
|
||||
align-items: center;
|
||||
background: ${themeCssVariables.background.secondary};
|
||||
border: 1px solid ${themeCssVariables.border.color.light};
|
||||
border-radius: 40px;
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
display: inline-flex;
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
padding: ${themeCssVariables.spacing[1]} ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
type SettingsRolePermissionsObjectLevelRecordLevelSectionProps = {
|
||||
objectMetadataItem: EnrichedObjectMetadataItem;
|
||||
roleId: string;
|
||||
@@ -57,12 +46,7 @@ export const SettingsRolePermissionsObjectLevelRecordLevelSection = ({
|
||||
<H2Title
|
||||
title={t`Record-level`}
|
||||
description={t`Ability to filter the records a user can interact with`}
|
||||
adornment={
|
||||
<StyledPillContainer>
|
||||
<IconLock size={12} />
|
||||
{t`Organization`}
|
||||
</StyledPillContainer>
|
||||
}
|
||||
adornment={<OrganizationAdornment />}
|
||||
/>
|
||||
<StyledCardContainer>
|
||||
<Card rounded>
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ export const SidePanelComposeEmailPage = () => {
|
||||
if (composerState.canSend) {
|
||||
composerState.handleSend();
|
||||
}
|
||||
}, [composerState.canSend, composerState.handleSend]);
|
||||
}, [composerState]);
|
||||
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: ['ctrl+Enter,meta+Enter'],
|
||||
|
||||
+6
-1
@@ -17,6 +17,7 @@ type SubMenuTopBarContainerProps = {
|
||||
actionButton?: ReactNode;
|
||||
className?: string;
|
||||
links: BreadcrumbProps['links'];
|
||||
tag?: JSX.Element;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@@ -25,10 +26,12 @@ const StyledContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTitle = styled.h3<{ reserveTitleSpace?: boolean }>`
|
||||
const StyledTitle = styled.span<{ reserveTitleSpace?: boolean }>`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
display: flex;
|
||||
font-size: ${themeCssVariables.font.size.lg};
|
||||
font-weight: ${themeCssVariables.font.weight.semiBold};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
line-height: 1.2;
|
||||
margin: ${themeCssVariables.spacing[8]} ${themeCssVariables.spacing[8]}
|
||||
${themeCssVariables.spacing[2]};
|
||||
@@ -39,6 +42,7 @@ const StyledTitle = styled.h3<{ reserveTitleSpace?: boolean }>`
|
||||
export const SubMenuTopBarContainer = ({
|
||||
children,
|
||||
title,
|
||||
tag,
|
||||
reserveTitleSpace,
|
||||
actionButton,
|
||||
className,
|
||||
@@ -54,6 +58,7 @@ export const SubMenuTopBarContainer = ({
|
||||
{(isDefined(title) || reserveTitleSpace === true) && (
|
||||
<StyledTitle reserveTitleSpace={reserveTitleSpace}>
|
||||
{title}
|
||||
{tag}
|
||||
</StyledTitle>
|
||||
)}
|
||||
{children}
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_PUBLIC_WORKSPACE_DATA_BY_ID = gql`
|
||||
query GetPublicWorkspaceDataById($id: UUID!) {
|
||||
getPublicWorkspaceDataById(id: $id) {
|
||||
id
|
||||
displayName
|
||||
logo
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -1,14 +1,12 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
|
||||
import { type CalendarChannel } from '@/accounts/types/CalendarChannel';
|
||||
import { type MessageChannel } from '@/accounts/types/MessageChannel';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { GET_MY_CALENDAR_CHANNELS } from '@/settings/accounts/graphql/queries/getMyCalendarChannels';
|
||||
import { GET_MY_MESSAGE_CHANNELS } from '@/settings/accounts/graphql/queries/getMyMessageChannels';
|
||||
import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
@@ -32,9 +30,6 @@ export const SettingsAccountsConfiguration = () => {
|
||||
const [startChannelSyncMutation, { loading: isSubmitting }] = useMutation(
|
||||
StartChannelSyncDocument,
|
||||
);
|
||||
const setSettingsAccountsSelectedMessageChannel = useSetAtomState(
|
||||
settingsAccountsSelectedMessageChannelState,
|
||||
);
|
||||
|
||||
const [currentStep, setCurrentStep] =
|
||||
useState<SettingsAccountsConfigurationStep>(
|
||||
|
||||
+1
-5
@@ -298,11 +298,7 @@ export const SettingsAdminAiProviderDetail = () => {
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`Admin Panel`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`AI`,
|
||||
children: t`Admin Panel - AI`,
|
||||
href: AI_ADMIN_PATH,
|
||||
},
|
||||
{
|
||||
|
||||
+57
-16
@@ -1,13 +1,27 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { FindOneAdminApplicationRegistrationDocument } from '~/generated-metadata/graphql';
|
||||
import type { ApplicationRegistrationData } from '~/pages/settings/applications/tabs/types/ApplicationRegistrationData';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import {
|
||||
FindAllApplicationRegistrationsDocument,
|
||||
FindOneAdminApplicationRegistrationDocument,
|
||||
UpdateApplicationRegistrationDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { SettingsApplicationRegistrationContent } from '~/pages/settings/applications/components/SettingsApplicationRegistrationContent';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { APPLICATION_REGISTRATION_ADMIN_PATH } from '@/settings/admin-panel/apps/constants/ApplicationRegistrationAdminPath';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsAdminApplicationRegistrationDetailContent } from '~/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDetailContent';
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
import { IconArrowBarToDown } from 'twenty-ui/display';
|
||||
import { Card, Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
const StyledToggleContainer = styled.div`
|
||||
display: flex;
|
||||
margin-top: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
export const SettingsAdminApplicationRegistrationDetail = () => {
|
||||
const { t } = useLingui();
|
||||
@@ -24,9 +38,17 @@ export const SettingsAdminApplicationRegistrationDetail = () => {
|
||||
},
|
||||
);
|
||||
|
||||
const registration = data?.findOneAdminApplicationRegistration as
|
||||
| ApplicationRegistrationData
|
||||
| undefined;
|
||||
const [updateRegistration] = useMutation(
|
||||
UpdateApplicationRegistrationDocument,
|
||||
{
|
||||
refetchQueries: [
|
||||
FindOneAdminApplicationRegistrationDocument,
|
||||
FindAllApplicationRegistrationsDocument,
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const registration = data?.findOneAdminApplicationRegistration;
|
||||
|
||||
if (loading || !isDefined(registration)) {
|
||||
return null;
|
||||
@@ -41,20 +63,39 @@ export const SettingsAdminApplicationRegistrationDetail = () => {
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`Admin Panel`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`App registrations`,
|
||||
children: t`Admin Panel - Apps`,
|
||||
href: APPLICATION_REGISTRATION_ADMIN_PATH,
|
||||
},
|
||||
{ children: registration.name },
|
||||
]}
|
||||
>
|
||||
<SettingsApplicationRegistrationContent
|
||||
registration={registration}
|
||||
hasActiveInstalls={false}
|
||||
/>
|
||||
<SettingsPageContainer>
|
||||
<SettingsAdminApplicationRegistrationDetailContent
|
||||
registration={registration}
|
||||
/>
|
||||
<Section>
|
||||
<StyledToggleContainer>
|
||||
<Card rounded fullWidth>
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconArrowBarToDown}
|
||||
title={t`Allow installation`}
|
||||
description={t`Display this app in the NPM packages list`}
|
||||
checked={registration.isListed}
|
||||
onChange={(checked) =>
|
||||
updateRegistration({
|
||||
variables: {
|
||||
input: {
|
||||
id: registration.id,
|
||||
update: { isListed: checked },
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</StyledToggleContainer>
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+274
@@ -0,0 +1,274 @@
|
||||
import {
|
||||
H2Title,
|
||||
IconBox,
|
||||
IconBrandDocker,
|
||||
IconChartBar,
|
||||
IconDownload,
|
||||
IconStatusChange,
|
||||
IconTag,
|
||||
IconWorld,
|
||||
IconGitBranch,
|
||||
} from 'twenty-ui/display';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
SettingsTableCard,
|
||||
type TableItem,
|
||||
} from '@/settings/components/SettingsTableCard';
|
||||
import {
|
||||
type ApplicationRegistration,
|
||||
ApplicationRegistrationSourceType,
|
||||
ApplicationRegistrationTarballUrlDocument,
|
||||
FindApplicationRegistrationStatsDocument,
|
||||
FindOneApplicationSummaryDocument,
|
||||
GetPublicWorkspaceDataByIdDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import {
|
||||
AvatarOrIcon,
|
||||
Chip,
|
||||
ChipSize,
|
||||
ChipVariant,
|
||||
Tag,
|
||||
} from 'twenty-ui/components';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { SettingsApplicationRegistrationShareLinkButtons } from '~/pages/settings/applications/components/SettingsApplicationRegistrationShareLinkButtons';
|
||||
|
||||
const StyledSourceRow = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
`;
|
||||
|
||||
const StyledDownloadLink = styled.a`
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
&:hover {
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledGeneralContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
export const SettingsAdminApplicationRegistrationDetailContent = ({
|
||||
registration,
|
||||
}: {
|
||||
registration: ApplicationRegistration;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const applicationRegistrationId = registration.id;
|
||||
|
||||
const { data: tarballUrlData } = useQuery(
|
||||
ApplicationRegistrationTarballUrlDocument,
|
||||
{
|
||||
variables: { id: applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: applicationSummaryData } = useQuery(
|
||||
FindOneApplicationSummaryDocument,
|
||||
{
|
||||
variables: { universalIdentifier: registration.universalIdentifier },
|
||||
skip: !registration.universalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
const isApplicationInstalled = isDefined(
|
||||
applicationSummaryData?.findOneApplication,
|
||||
);
|
||||
|
||||
const { data: ownerWorkspaceData } = useQuery(
|
||||
GetPublicWorkspaceDataByIdDocument,
|
||||
{
|
||||
variables: { id: registration.ownerWorkspaceId ?? '' },
|
||||
skip: !registration.ownerWorkspaceId,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: statsData } = useQuery(
|
||||
FindApplicationRegistrationStatsDocument,
|
||||
{
|
||||
variables: { id: applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
},
|
||||
);
|
||||
|
||||
const shareLink = getSettingsPath(SettingsPath.AvailableApplicationDetail, {
|
||||
availableApplicationId: registration.universalIdentifier,
|
||||
});
|
||||
|
||||
const ownerWorkspace = ownerWorkspaceData?.getPublicWorkspaceDataById;
|
||||
|
||||
const generateItems = () => {
|
||||
const items: TableItem[] = [
|
||||
{
|
||||
Icon: IconWorld,
|
||||
label: t`Universal ID`,
|
||||
value: registration.universalIdentifier,
|
||||
},
|
||||
];
|
||||
|
||||
if (isDefined(ownerWorkspace?.displayName)) {
|
||||
items.push({
|
||||
Icon: IconTag,
|
||||
label: t`Owner`,
|
||||
value: (
|
||||
<Chip
|
||||
size={ChipSize.Large}
|
||||
variant={ChipVariant.Highlighted}
|
||||
clickable={false}
|
||||
leftComponent={
|
||||
<AvatarOrIcon
|
||||
avatarType="rounded"
|
||||
avatarUrl={ownerWorkspace?.logo ?? undefined}
|
||||
/>
|
||||
}
|
||||
label={ownerWorkspace.displayName}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
switch (registration.sourceType) {
|
||||
case ApplicationRegistrationSourceType.NPM:
|
||||
items.push({
|
||||
Icon: IconBox,
|
||||
label: t`Package`,
|
||||
value: 'NPM',
|
||||
});
|
||||
break;
|
||||
case ApplicationRegistrationSourceType.TARBALL:
|
||||
items.push({
|
||||
Icon: IconBox,
|
||||
label: t`Source`,
|
||||
value: isNonEmptyString(
|
||||
tarballUrlData?.applicationRegistrationTarballUrl,
|
||||
) ? (
|
||||
<StyledSourceRow>
|
||||
<span>Tarball</span>
|
||||
<StyledDownloadLink
|
||||
href={tarballUrlData.applicationRegistrationTarballUrl}
|
||||
download
|
||||
>
|
||||
<Trans>Download</Trans>
|
||||
</StyledDownloadLink>
|
||||
</StyledSourceRow>
|
||||
) : (
|
||||
'Tarball'
|
||||
),
|
||||
});
|
||||
break;
|
||||
case ApplicationRegistrationSourceType.LOCAL:
|
||||
items.push({
|
||||
Icon: IconBox,
|
||||
label: t`Source`,
|
||||
value: 'Local',
|
||||
});
|
||||
break;
|
||||
case ApplicationRegistrationSourceType.OAUTH_ONLY:
|
||||
items.push({
|
||||
Icon: IconBox,
|
||||
label: t`Source`,
|
||||
value: 'OAuth',
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
if (isNonEmptyString(registration.latestAvailableVersion)) {
|
||||
items.push({
|
||||
Icon: IconGitBranch,
|
||||
label: t`Latest version`,
|
||||
value: registration.latestAvailableVersion,
|
||||
});
|
||||
}
|
||||
|
||||
items.push({
|
||||
Icon: IconDownload,
|
||||
label: t`Installed`,
|
||||
value: isApplicationInstalled ? (
|
||||
<Tag color="green" text={t`Yes`} />
|
||||
) : (
|
||||
<Tag color="orange" text={t`No`} />
|
||||
),
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
const stats = statsData?.findApplicationRegistrationStats;
|
||||
|
||||
const hasStats = (stats?.activeInstalls ?? 0) > 0;
|
||||
|
||||
const versionDistributionLabel =
|
||||
stats?.versionDistribution
|
||||
?.map(
|
||||
(entry: { version: string; count: number }) =>
|
||||
`${entry.version} (${entry.count})`,
|
||||
)
|
||||
.join(', ') || '—';
|
||||
|
||||
const statsItems = [
|
||||
{
|
||||
Icon: IconBrandDocker,
|
||||
label: t`Active installs`,
|
||||
value: stats?.activeInstalls ?? '—',
|
||||
},
|
||||
{
|
||||
Icon: IconStatusChange,
|
||||
label: t`Most installed version`,
|
||||
value: stats?.mostInstalledVersion ?? '—',
|
||||
},
|
||||
{
|
||||
Icon: IconChartBar,
|
||||
label: t`Distribution`,
|
||||
value: versionDistributionLabel,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title title={t`General`} description={t`About your app`} />
|
||||
<StyledGeneralContainer>
|
||||
<SettingsTableCard
|
||||
rounded
|
||||
items={generateItems()}
|
||||
gridAutoColumns="3fr 8fr"
|
||||
/>
|
||||
<SettingsApplicationRegistrationShareLinkButtons
|
||||
shareLink={shareLink}
|
||||
isInstalled={isApplicationInstalled}
|
||||
universalIdentifier={registration.universalIdentifier}
|
||||
isNpmSource={
|
||||
registration.sourceType === ApplicationRegistrationSourceType.NPM
|
||||
}
|
||||
/>
|
||||
</StyledGeneralContainer>
|
||||
</Section>
|
||||
{hasStats && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Install Stats`}
|
||||
description={t`Usage across all workspaces on this server`}
|
||||
/>
|
||||
<SettingsTableCard
|
||||
rounded
|
||||
items={statsItems}
|
||||
gridAutoColumns="200px 1fr"
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+1
-5
@@ -128,11 +128,7 @@ export const SettingsAdminConfigVariableDetails = () => {
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`Admin Panel`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`Config Variables`,
|
||||
children: t`Admin Panel - Config`,
|
||||
href: getSettingsPath(
|
||||
SettingsPath.AdminPanel,
|
||||
undefined,
|
||||
|
||||
+1
-5
@@ -51,11 +51,7 @@ export const SettingsAdminIndicatorHealthStatus = () => {
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`Admin Panel`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`Health Status`,
|
||||
children: t`Admin Panel - Health`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanelHealthStatus),
|
||||
},
|
||||
{ children: `${data?.getIndicatorHealthStatus?.label}` },
|
||||
|
||||
@@ -56,11 +56,11 @@ export const SettingsAdminQueueDetail = () => {
|
||||
title={t`Queue: ${queueName}`}
|
||||
links={[
|
||||
{
|
||||
children: t`Admin Panel`,
|
||||
children: t`Other`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`Health Status`,
|
||||
children: t`Admin Panel - Health`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanelHealthStatus),
|
||||
},
|
||||
{
|
||||
|
||||
+1
-5
@@ -44,11 +44,7 @@ export const SettingsAdminWorkspaceChatThread = () => {
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`Admin Panel`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`AI`,
|
||||
children: t`Admin Panel - AI`,
|
||||
href: AI_ADMIN_PATH,
|
||||
},
|
||||
{
|
||||
|
||||
+1
-5
@@ -169,11 +169,7 @@ export const SettingsAdminWorkspaceDetail = () => {
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`Admin Panel`,
|
||||
href: getSettingsPath(SettingsPath.AdminPanel),
|
||||
},
|
||||
{
|
||||
children: t`AI`,
|
||||
children: t`Admin Panel - AI`,
|
||||
href: AI_ADMIN_PATH,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -50,7 +50,9 @@ export const SettingsAIUsageTab = () => {
|
||||
}
|
||||
/>
|
||||
<SettingsEnterpriseFeatureGateCard
|
||||
title={t`Enterprise feature`}
|
||||
description={t`AI usage analytics is available with an Enterprise key.`}
|
||||
buttonTitle={t`Activate`}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
|
||||
+12
-26
@@ -3,13 +3,10 @@ import { useQuery } from '@apollo/client/react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
FindApplicationRegistrationStatsDocument,
|
||||
FindOneApplicationRegistrationDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { type ApplicationRegistrationData } from '~/pages/settings/applications/tabs/types/ApplicationRegistrationData';
|
||||
import { FindOneApplicationRegistrationDocument } from '~/generated-metadata/graphql';
|
||||
import { SettingsApplicationRegistrationContent } from '~/pages/settings/applications/components/SettingsApplicationRegistrationContent';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Tag } from 'twenty-ui/components';
|
||||
|
||||
export const SettingsApplicationRegistrationDetails = () => {
|
||||
const { t } = useLingui();
|
||||
@@ -23,21 +20,7 @@ export const SettingsApplicationRegistrationDetails = () => {
|
||||
skip: !applicationRegistrationId,
|
||||
});
|
||||
|
||||
const { data: statsData } = useQuery(
|
||||
FindApplicationRegistrationStatsDocument,
|
||||
{
|
||||
variables: { id: applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
},
|
||||
);
|
||||
|
||||
const registration = data?.findOneApplicationRegistration as
|
||||
| ApplicationRegistrationData
|
||||
| undefined;
|
||||
|
||||
const stats = statsData?.findApplicationRegistrationStats;
|
||||
|
||||
const hasActiveInstalls = (stats?.activeInstalls ?? 0) > 0;
|
||||
const registration = data?.findOneApplicationRegistration;
|
||||
|
||||
if (loading || !isDefined(registration)) {
|
||||
return null;
|
||||
@@ -46,22 +29,25 @@ export const SettingsApplicationRegistrationDetails = () => {
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={registration.name}
|
||||
tag={<Tag text={t`Owner`} color={'gray'} />}
|
||||
links={[
|
||||
{
|
||||
children: t`Workspace`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: t`Applications`,
|
||||
href: getSettingsPath(SettingsPath.Applications),
|
||||
children: t`Applications - Developer`,
|
||||
href: getSettingsPath(
|
||||
SettingsPath.Applications,
|
||||
undefined,
|
||||
undefined,
|
||||
'developer',
|
||||
),
|
||||
},
|
||||
{ children: registration.name },
|
||||
]}
|
||||
>
|
||||
<SettingsApplicationRegistrationContent
|
||||
registration={registration}
|
||||
hasActiveInstalls={hasActiveInstalls}
|
||||
/>
|
||||
<SettingsApplicationRegistrationContent registration={registration} />
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,13 +9,10 @@ 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 { useQuery } from '@apollo/client/react';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
PermissionFlagType,
|
||||
FindManyApplicationsDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { SettingsApplicationsTable } from '~/pages/settings/applications/components/SettingsApplicationsTable';
|
||||
import { SettingsApplicationsAvailableTab } from '~/pages/settings/applications/tabs/SettingsApplicationsAvailableTab';
|
||||
import { SettingsApplicationsDeveloperTab } from '~/pages/settings/applications/tabs/SettingsApplicationsDeveloperTab';
|
||||
import { SettingsApplicationsInstalledTab } from '~/pages/settings/applications/tabs/SettingsApplicationsInstalledTab';
|
||||
|
||||
-1
@@ -222,7 +222,6 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
const sourceType = detail?.sourceType;
|
||||
const isNpmApp = sourceType === ApplicationRegistrationSourceType.NPM;
|
||||
const registrationId = detail?.id;
|
||||
const sourcePackage = detail?.sourcePackage;
|
||||
const sourcePackageUrl =
|
||||
isNpmApp && detail?.sourcePackage
|
||||
? `https://www.npmjs.com/package/${detail.sourcePackage}`
|
||||
|
||||
+5
-3
@@ -88,9 +88,11 @@ const StyledUnlistedBanner = styled.div`
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
display: flex;
|
||||
font-size: ${themeCssVariables.font.size.md};
|
||||
font-weight: ${themeCssVariables.font.weight.regular};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
margin-bottom: ${themeCssVariables.spacing[4]};
|
||||
padding: ${themeCssVariables.spacing[3]} ${themeCssVariables.spacing[4]};
|
||||
justify-content: center;
|
||||
margin-bottom: ${themeCssVariables.spacing[8]};
|
||||
padding: ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
export const SettingsApplicationDetailTitle = ({
|
||||
@@ -104,7 +106,7 @@ export const SettingsApplicationDetailTitle = ({
|
||||
{isUnlisted && (
|
||||
<StyledUnlistedBanner>
|
||||
<IconEyeOff size={16} />
|
||||
{t`This application is not listed on the marketplace. It was shared via a direct link.`}
|
||||
{t`Application not listed on the marketplace. It was shared via a direct link`}
|
||||
</StyledUnlistedBanner>
|
||||
)}
|
||||
<StyledHeader>
|
||||
|
||||
+236
@@ -0,0 +1,236 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
FindApplicationRegistrationVariablesDocument,
|
||||
FindOneApplicationRegistrationDocument,
|
||||
UpdateApplicationRegistrationVariableDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { Tag } from 'twenty-ui/components';
|
||||
import { styled } from '@linaria/react';
|
||||
import { H3Title, IconCheck, IconX } from 'twenty-ui/display';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { SettingsApplicationRegistrationConfigVariableStatus } from '~/pages/settings/applications/components/SettingsApplicationRegistrationConfigVariableStatus';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
|
||||
const RESET_VARIABLE_MODAL_ID =
|
||||
'reset-application-registration-config-variable-modal';
|
||||
|
||||
const StyledRow = styled.div`
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
min-width: 200px;
|
||||
`;
|
||||
|
||||
export const SettingsApplicationRegistrationConfigVariableDetail = () => {
|
||||
const { t } = useLingui();
|
||||
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
|
||||
const { variableKey } = useParams();
|
||||
const [value, setValue] = useState<string>('');
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const { openModal } = useModal();
|
||||
|
||||
const { applicationRegistrationId = '' } = useParams<{
|
||||
applicationRegistrationId: string;
|
||||
}>();
|
||||
|
||||
const { data: applicationRegistrationData } = useQuery(
|
||||
FindOneApplicationRegistrationDocument,
|
||||
{
|
||||
variables: { id: applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
},
|
||||
);
|
||||
|
||||
const registration =
|
||||
applicationRegistrationData?.findOneApplicationRegistration;
|
||||
|
||||
const { data: variablesData } = useQuery(
|
||||
FindApplicationRegistrationVariablesDocument,
|
||||
{
|
||||
variables: { applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
},
|
||||
);
|
||||
|
||||
const variable = (
|
||||
variablesData?.findApplicationRegistrationVariables ?? []
|
||||
).find((variable) => variable.key === variableKey);
|
||||
|
||||
const [updateVariable] = useMutation(
|
||||
UpdateApplicationRegistrationVariableDocument,
|
||||
{
|
||||
refetchQueries: [FindApplicationRegistrationVariablesDocument],
|
||||
},
|
||||
);
|
||||
|
||||
if (!variable || !registration) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleXButtonClick = () => {
|
||||
if (!isEditing) {
|
||||
openModal(RESET_VARIABLE_MODAL_ID);
|
||||
return;
|
||||
}
|
||||
|
||||
setValue('');
|
||||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const handleSaveVariableValue = async ({
|
||||
resetValue,
|
||||
}: {
|
||||
resetValue?: boolean;
|
||||
}) => {
|
||||
const variableKey = variable.key;
|
||||
|
||||
if (!isNonEmptyString(value) && !resetValue) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsSubmitting(true);
|
||||
await updateVariable({
|
||||
variables: {
|
||||
input: {
|
||||
id: variable.id,
|
||||
update: {
|
||||
value,
|
||||
resetValue,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Variable ${variableKey} updated`,
|
||||
});
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Error updating variable`,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
setValue('');
|
||||
setIsEditing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmReset = async () => {
|
||||
await handleSaveVariableValue({ resetValue: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={registration.name}
|
||||
tag={<Tag text={t`Owner`} color={'gray'} />}
|
||||
links={[
|
||||
{
|
||||
children: t`Workspace`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: t`Applications - Developer`,
|
||||
href: getSettingsPath(
|
||||
SettingsPath.Applications,
|
||||
undefined,
|
||||
undefined,
|
||||
'developer',
|
||||
),
|
||||
},
|
||||
{
|
||||
children: t`${registration.name} - Config`,
|
||||
href: getSettingsPath(
|
||||
SettingsPath.ApplicationRegistrationDetail,
|
||||
{ applicationRegistrationId },
|
||||
undefined,
|
||||
'config',
|
||||
),
|
||||
},
|
||||
{
|
||||
children: variableKey,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H3Title
|
||||
title={
|
||||
<>
|
||||
{variable.key}
|
||||
{variable.isRequired && (
|
||||
<span style={{ color: 'red' }}> *</span>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
description={variable.description}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<StyledRow>
|
||||
<TextInput
|
||||
value={value}
|
||||
placeholder={
|
||||
variable.isFilled
|
||||
? '••••••••••••••••••••••••'
|
||||
: t`set-config-value`
|
||||
}
|
||||
onChange={(value) => {
|
||||
setValue(value);
|
||||
setIsEditing(true);
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
Icon={IconCheck}
|
||||
variant="secondary"
|
||||
onClick={() => handleSaveVariableValue({ resetValue: false })}
|
||||
disabled={isSubmitting || !isEditing}
|
||||
/>
|
||||
<Button
|
||||
Icon={IconX}
|
||||
variant="secondary"
|
||||
onClick={handleXButtonClick}
|
||||
type="button"
|
||||
disabled={isSubmitting || (!isEditing && !variable.isFilled)}
|
||||
/>
|
||||
<SettingsApplicationRegistrationConfigVariableStatus
|
||||
variable={variable}
|
||||
/>
|
||||
</StyledButtonContainer>
|
||||
</StyledRow>
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
|
||||
<ConfirmationModal
|
||||
modalInstanceId={RESET_VARIABLE_MODAL_ID}
|
||||
title={t`Reset variable`}
|
||||
subtitle={t`Are you sure you want to reset this variable?`}
|
||||
onConfirmClick={handleConfirmReset}
|
||||
confirmButtonText={t`Reset`}
|
||||
confirmButtonAccent="danger"
|
||||
/>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
import { Status } from 'twenty-ui/display';
|
||||
import { type ApplicationRegistrationVariable } from '~/generated-metadata/graphql';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
export const SettingsApplicationRegistrationConfigVariableStatus = ({
|
||||
variable,
|
||||
}: {
|
||||
variable: ApplicationRegistrationVariable;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
return variable.isFilled ? (
|
||||
<Status color="green" text={t`Configured`} />
|
||||
) : variable.isRequired ? (
|
||||
<Status color="red" text={t`Required`} />
|
||||
) : (
|
||||
<Status color="gray" text={t`Not set`} />
|
||||
);
|
||||
};
|
||||
+16
-6
@@ -3,23 +3,27 @@ import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconInfoCircle, IconKey, IconWorld } from 'twenty-ui/display';
|
||||
import { type ApplicationRegistrationData } from '~/pages/settings/applications/tabs/types/ApplicationRegistrationData';
|
||||
import {
|
||||
IconInfoCircle,
|
||||
IconKey,
|
||||
IconSettings,
|
||||
IconWorld,
|
||||
} from 'twenty-ui/display';
|
||||
import { SettingsApplicationRegistrationGeneralTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationGeneralTab';
|
||||
import { SettingsApplicationRegistrationOAuthTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationOAuthTab';
|
||||
import { SettingsApplicationRegistrationDistributionTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationDistributionTab';
|
||||
import { type ApplicationRegistration } from '~/generated-metadata/graphql';
|
||||
import { SettingsApplicationRegistrationConfigTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationConfigTab';
|
||||
|
||||
const REGISTRATION_DETAIL_TAB_LIST_ID =
|
||||
'application-registration-detail-tab-list';
|
||||
|
||||
type SettingsApplicationRegistrationContentProps = {
|
||||
registration: ApplicationRegistrationData;
|
||||
hasActiveInstalls: boolean;
|
||||
registration: ApplicationRegistration;
|
||||
};
|
||||
|
||||
export const SettingsApplicationRegistrationContent = ({
|
||||
registration,
|
||||
hasActiveInstalls,
|
||||
}: SettingsApplicationRegistrationContentProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
@@ -32,10 +36,17 @@ export const SettingsApplicationRegistrationContent = ({
|
||||
{ id: 'general', title: t`General`, Icon: IconInfoCircle },
|
||||
{ id: 'oauth', title: t`OAuth`, Icon: IconKey },
|
||||
{ id: 'distribution', title: t`Distribution`, Icon: IconWorld },
|
||||
{ id: 'config', title: t`Config`, Icon: IconSettings },
|
||||
];
|
||||
|
||||
const renderActiveTabContent = () => {
|
||||
switch (activeTabId) {
|
||||
case 'config':
|
||||
return (
|
||||
<SettingsApplicationRegistrationConfigTab
|
||||
registration={registration}
|
||||
/>
|
||||
);
|
||||
case 'oauth':
|
||||
return (
|
||||
<SettingsApplicationRegistrationOAuthTab
|
||||
@@ -53,7 +64,6 @@ export const SettingsApplicationRegistrationContent = ({
|
||||
return (
|
||||
<SettingsApplicationRegistrationGeneralTab
|
||||
registration={registration}
|
||||
hasActiveInstalls={hasActiveInstalls}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useEffect } from 'react';
|
||||
import { Controller, useForm } from 'react-hook-form';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`;
|
||||
|
||||
const StyledLinkContainer = styled.div`
|
||||
flex: 1;
|
||||
margin-right: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
type SettingsApplicationRegistrationRedirectURIsInputProps = {
|
||||
updateRedirectUris: (newRedirectUris: string[]) => void;
|
||||
redirectUris: string[];
|
||||
};
|
||||
|
||||
type FormInput = {
|
||||
redirectUri: string;
|
||||
};
|
||||
|
||||
export const SettingsApplicationRegistrationRedirectURIsInput = ({
|
||||
updateRedirectUris,
|
||||
redirectUris,
|
||||
}: SettingsApplicationRegistrationRedirectURIsInputProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const validationSchema = (redirectUris: string[]) =>
|
||||
z
|
||||
.object({
|
||||
redirectUri: z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, 'URI is required')
|
||||
.url(t`Please enter a valid URL`)
|
||||
.refine(
|
||||
(value) => !redirectUris.includes(value),
|
||||
t`URI is already in redirect URIs list`,
|
||||
),
|
||||
})
|
||||
.required();
|
||||
|
||||
const { reset, handleSubmit, control, formState } = useForm<FormInput>({
|
||||
mode: 'onSubmit',
|
||||
resolver: zodResolver(validationSchema(redirectUris)),
|
||||
defaultValues: {
|
||||
redirectUri: '',
|
||||
},
|
||||
});
|
||||
|
||||
const submit = handleSubmit((data) => {
|
||||
updateRedirectUris([...redirectUris, data.redirectUri]);
|
||||
});
|
||||
|
||||
const { isSubmitSuccessful } = formState;
|
||||
|
||||
useEffect(() => {
|
||||
if (isSubmitSuccessful) {
|
||||
reset();
|
||||
}
|
||||
}, [isSubmitSuccessful, reset]);
|
||||
|
||||
return (
|
||||
<form onSubmit={submit}>
|
||||
<StyledContainer>
|
||||
<StyledLinkContainer>
|
||||
<Controller
|
||||
name="redirectUri"
|
||||
control={control}
|
||||
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||
<SettingsTextInput
|
||||
instanceId="settings-application-registration-redirect-uris-input"
|
||||
placeholder={t`https://example.com/callback`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
error={error?.message}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</StyledLinkContainer>
|
||||
<Button title={t`Add URI`} type="submit" />
|
||||
</StyledContainer>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { styled } from '@linaria/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { IconX, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
|
||||
type SettingsApplicationRegistrationRedirectURIsTableProps = {
|
||||
redirectUris: string[];
|
||||
updateRedirectUris: (redirectUris: string[]) => void;
|
||||
};
|
||||
|
||||
const StyledTableContainer = styled.div`
|
||||
margin-top: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
const StyledTableBodyContainer = styled.div`
|
||||
border-bottom: 1px solid ${themeCssVariables.border.color.light};
|
||||
`;
|
||||
|
||||
export const SettingsApplicationRegistrationRedirectURIsTable = ({
|
||||
redirectUris,
|
||||
updateRedirectUris,
|
||||
}: SettingsApplicationRegistrationRedirectURIsTableProps) => {
|
||||
return (
|
||||
<>
|
||||
{redirectUris.length > 0 && (
|
||||
<StyledTableContainer>
|
||||
<Table>
|
||||
<TableRow
|
||||
gridAutoColumns="1fr 20px"
|
||||
mobileGridAutoColumns="1fr 20px"
|
||||
>
|
||||
<TableHeader>{t`URI`}</TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
</TableRow>
|
||||
<StyledTableBodyContainer>
|
||||
<TableBody>
|
||||
{redirectUris.map((redirectUri) => (
|
||||
<TableRow
|
||||
key={redirectUri}
|
||||
gridAutoColumns="1fr 20px"
|
||||
mobileGridAutoColumns="1fr 20px"
|
||||
>
|
||||
<TableCell color={themeCssVariables.font.color.primary}>
|
||||
<OverflowingTextWithTooltip text={redirectUri} />
|
||||
</TableCell>
|
||||
|
||||
<TableCell align="right">
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
updateRedirectUris(
|
||||
redirectUris.filter((uri) => uri !== redirectUri),
|
||||
);
|
||||
}}
|
||||
variant="tertiary"
|
||||
size="small"
|
||||
Icon={IconX}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</StyledTableBodyContainer>
|
||||
</Table>
|
||||
</StyledTableContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import {
|
||||
IconArrowUpRight,
|
||||
IconCopy,
|
||||
IconDownload,
|
||||
IconInfoCircle,
|
||||
} from 'twenty-ui/display';
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useInstallMarketplaceApp } from '@/marketplace/hooks/useInstallMarketplaceApp';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const StyledButtonGroup = styled.div`
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
export const SettingsApplicationRegistrationShareLinkButtons = ({
|
||||
shareLink,
|
||||
isInstalled,
|
||||
universalIdentifier,
|
||||
isNpmSource = false,
|
||||
withCopyButton = false,
|
||||
}: {
|
||||
shareLink: string;
|
||||
isInstalled?: boolean;
|
||||
universalIdentifier?: string;
|
||||
isNpmSource?: boolean;
|
||||
withCopyButton?: boolean;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const { copyToClipboard } = useCopyToClipboard();
|
||||
|
||||
const { install, isInstalling } = useInstallMarketplaceApp();
|
||||
|
||||
const installable =
|
||||
isDefined(isInstalled) && isDefined(universalIdentifier) && !isInstalled;
|
||||
|
||||
const handleInstall = async () => {
|
||||
if (installable) {
|
||||
await install({
|
||||
universalIdentifier,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledButtonGroup>
|
||||
{installable && (
|
||||
<Button
|
||||
Icon={IconDownload}
|
||||
title={isInstalling ? t`Installing...` : t`Install`}
|
||||
variant={'secondary'}
|
||||
onClick={handleInstall}
|
||||
disabled={isInstalling}
|
||||
/>
|
||||
)}
|
||||
{withCopyButton && (
|
||||
<Button
|
||||
Icon={IconCopy}
|
||||
title={t`Copy sharing link`}
|
||||
variant="secondary"
|
||||
disabled={!shareLink}
|
||||
onClick={async () => {
|
||||
if (shareLink) {
|
||||
await copyToClipboard(
|
||||
`${window.location.origin}${shareLink}`,
|
||||
t`Sharing link copied to clipboard`,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
Icon={isNpmSource ? IconArrowUpRight : IconInfoCircle}
|
||||
title={isNpmSource ? t`See on marketplace` : t`See app page`}
|
||||
variant="secondary"
|
||||
disabled={!shareLink}
|
||||
to={shareLink}
|
||||
/>
|
||||
</StyledButtonGroup>
|
||||
);
|
||||
};
|
||||
+1
-1
@@ -31,7 +31,7 @@ export const SettingsApplicationTableRow = ({
|
||||
to={link}
|
||||
>
|
||||
<TableCell
|
||||
color={themeCssVariables.font.color.secondary}
|
||||
color={themeCssVariables.font.color.primary}
|
||||
gap={themeCssVariables.spacing[2]}
|
||||
minWidth="0"
|
||||
overflow="hidden"
|
||||
|
||||
+40
-46
@@ -18,10 +18,6 @@ import { SearchInput } from 'twenty-ui/input';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { ApplicationRegistrationSourceType } from '~/generated-metadata/graphql';
|
||||
|
||||
const StyledTableContainer = styled.div`
|
||||
margin-top: ${themeCssVariables.spacing[3]};
|
||||
`;
|
||||
|
||||
const StyledTableRowsContainer = styled.div`
|
||||
border-bottom: 1px solid ${themeCssVariables.border.color.light};
|
||||
padding: ${themeCssVariables.spacing[2]} 0;
|
||||
@@ -63,51 +59,49 @@ export const SettingsApplicationsTable = ({
|
||||
onChange={setSearchTerm}
|
||||
/>
|
||||
</StyledSearchInputContainer>
|
||||
<StyledTableContainer>
|
||||
<Table>
|
||||
<TableRow
|
||||
gridTemplateColumns={APPLICATION_TABLE_ROW_GRID_TEMPLATE_COLUMNS}
|
||||
>
|
||||
<TableHeader> {t`Name`}</TableHeader>
|
||||
<TableHeader> {t`Description`}</TableHeader>
|
||||
<TableHeader> {''}</TableHeader>
|
||||
<TableHeader />
|
||||
</TableRow>
|
||||
<StyledTableRowsContainer>
|
||||
{filteredApplications.map((application) => {
|
||||
const isNpmApp =
|
||||
application.applicationRegistration?.sourceType ===
|
||||
ApplicationRegistrationSourceType.NPM;
|
||||
<Table>
|
||||
<TableRow
|
||||
gridTemplateColumns={APPLICATION_TABLE_ROW_GRID_TEMPLATE_COLUMNS}
|
||||
>
|
||||
<TableHeader> {t`Name`}</TableHeader>
|
||||
<TableHeader> {t`Description`}</TableHeader>
|
||||
<TableHeader> {''}</TableHeader>
|
||||
<TableHeader />
|
||||
</TableRow>
|
||||
<StyledTableRowsContainer>
|
||||
{filteredApplications.map((application) => {
|
||||
const isNpmApp =
|
||||
application.applicationRegistration?.sourceType ===
|
||||
ApplicationRegistrationSourceType.NPM;
|
||||
|
||||
const latestVersion =
|
||||
application.applicationRegistration?.latestAvailableVersion;
|
||||
const latestVersion =
|
||||
application.applicationRegistration?.latestAvailableVersion;
|
||||
|
||||
const hasUpdate =
|
||||
isNpmApp &&
|
||||
isDefined(latestVersion) &&
|
||||
isDefined(application.version) &&
|
||||
isNewerSemver(latestVersion, application.version);
|
||||
const hasUpdate =
|
||||
isNpmApp &&
|
||||
isDefined(latestVersion) &&
|
||||
isDefined(application.version) &&
|
||||
isNewerSemver(latestVersion, application.version);
|
||||
|
||||
return (
|
||||
<SettingsApplicationTableRow
|
||||
key={application.id}
|
||||
application={application}
|
||||
hasUpdate={hasUpdate}
|
||||
action={
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
}
|
||||
link={getSettingsPath(SettingsPath.ApplicationDetail, {
|
||||
applicationId: application.id,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</StyledTableRowsContainer>
|
||||
</Table>
|
||||
</StyledTableContainer>
|
||||
return (
|
||||
<SettingsApplicationTableRow
|
||||
key={application.id}
|
||||
application={application}
|
||||
hasUpdate={hasUpdate}
|
||||
action={
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
}
|
||||
link={getSettingsPath(SettingsPath.ApplicationDetail, {
|
||||
applicationId: application.id,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</StyledTableRowsContainer>
|
||||
</Table>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -109,7 +109,7 @@ export const SettingsApplicationDetailContentTab = ({
|
||||
return null;
|
||||
}
|
||||
|
||||
const { logicFunctions, agents } = application;
|
||||
const { logicFunctions } = application;
|
||||
|
||||
const shouldDisplayLogicFunctions =
|
||||
isDefined(logicFunctions) && logicFunctions?.length > 0;
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import type { ApplicationRegistrationData } from '~/pages/settings/applications/tabs/types/ApplicationRegistrationData';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import {
|
||||
type ApplicationRegistrationVariable,
|
||||
FindApplicationRegistrationVariablesDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import {
|
||||
H2Title,
|
||||
IconChevronRight,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui/display';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext } from 'react';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { SettingsApplicationRegistrationConfigVariableStatus } from '~/pages/settings/applications/components/SettingsApplicationRegistrationConfigVariableStatus';
|
||||
|
||||
const StyledTableBodyContainer = styled.div`
|
||||
border-bottom: 1px solid ${themeCssVariables.border.color.light};
|
||||
`;
|
||||
|
||||
export const SettingsApplicationRegistrationConfigTab = ({
|
||||
registration,
|
||||
}: {
|
||||
registration: ApplicationRegistrationData;
|
||||
}) => {
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { t } = useLingui();
|
||||
|
||||
const applicationRegistrationId = registration.id;
|
||||
|
||||
const { data: variablesData } = useQuery(
|
||||
FindApplicationRegistrationVariablesDocument,
|
||||
{
|
||||
variables: { applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
},
|
||||
);
|
||||
|
||||
const variables: ApplicationRegistrationVariable[] =
|
||||
variablesData?.findApplicationRegistrationVariables ?? [];
|
||||
|
||||
return (
|
||||
variables.length > 0 && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Server Variables`}
|
||||
description={t`Server variables are applied to all workspace installations.`}
|
||||
/>
|
||||
<Table>
|
||||
<TableRow gridAutoColumns="4fr 3fr 3fr 1fr">
|
||||
<TableHeader>{t`Name`}</TableHeader>
|
||||
<TableHeader>{t`Description`}</TableHeader>
|
||||
<TableHeader align="right">{t`Status`}</TableHeader>
|
||||
<TableHeader align="right"></TableHeader>
|
||||
</TableRow>
|
||||
<StyledTableBodyContainer>
|
||||
<TableBody>
|
||||
{variables.map((variable) => (
|
||||
<TableRow
|
||||
key={variable.key}
|
||||
gridAutoColumns="4fr 3fr 3fr 1fr"
|
||||
to={getSettingsPath(
|
||||
SettingsPath.ApplicationRegistrationConfigVariableDetails,
|
||||
{
|
||||
applicationRegistrationId,
|
||||
variableKey: variable.key,
|
||||
},
|
||||
)}
|
||||
>
|
||||
<TableCell
|
||||
color={theme.font.color.primary}
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
clickable
|
||||
>
|
||||
<OverflowingTextWithTooltip text={variable.key} />
|
||||
{variable.isRequired && (
|
||||
<span style={{ color: 'red' }}> *</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
color={theme.font.color.secondary}
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
clickable
|
||||
>
|
||||
<OverflowingTextWithTooltip text={variable.description} />
|
||||
</TableCell>
|
||||
<TableCell
|
||||
color={theme.font.color.secondary}
|
||||
align="right"
|
||||
whiteSpace="nowrap"
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
clickable
|
||||
>
|
||||
<SettingsApplicationRegistrationConfigVariableStatus
|
||||
variable={variable}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell align="right" color={theme.font.color.secondary}>
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</StyledTableBodyContainer>
|
||||
</Table>
|
||||
</Section>
|
||||
)
|
||||
);
|
||||
};
|
||||
+37
-134
@@ -1,36 +1,13 @@
|
||||
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
H2Title,
|
||||
IconChartBar,
|
||||
IconCopy,
|
||||
IconDownload,
|
||||
IconExternalLink,
|
||||
IconTag,
|
||||
} from 'twenty-ui/display';
|
||||
import { CommandBlock, H2Title, IconCopy } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Card, Section } from 'twenty-ui/layout';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import {
|
||||
ApplicationRegistrationSourceType,
|
||||
FindApplicationRegistrationStatsDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { ApplicationRegistrationSourceType } from '~/generated-metadata/graphql';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
import { type ApplicationRegistrationData } from '~/pages/settings/applications/tabs/types/ApplicationRegistrationData';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { SettingsEnterpriseFeatureGateCard } from '@/settings/components/SettingsEnterpriseFeatureGateCard';
|
||||
|
||||
const StyledButtonGroup = styled.div`
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
padding-top: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
import { SettingsApplicationRegistrationShareLinkButtons } from '~/pages/settings/applications/components/SettingsApplicationRegistrationShareLinkButtons';
|
||||
|
||||
export const SettingsApplicationRegistrationDistributionTab = ({
|
||||
registration,
|
||||
@@ -39,135 +16,61 @@ export const SettingsApplicationRegistrationDistributionTab = ({
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const applicationRegistrationId = registration.id;
|
||||
|
||||
const { copyToClipboard } = useCopyToClipboard();
|
||||
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
|
||||
const hasEnterpriseAccess = currentWorkspace?.hasValidEnterpriseKey === true;
|
||||
|
||||
const isNpmSource =
|
||||
registration.sourceType === ApplicationRegistrationSourceType.NPM;
|
||||
|
||||
const isTarballSource =
|
||||
registration.sourceType === ApplicationRegistrationSourceType.TARBALL;
|
||||
|
||||
const { data: statsData } = useQuery(
|
||||
FindApplicationRegistrationStatsDocument,
|
||||
{
|
||||
variables: { id: applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
},
|
||||
);
|
||||
|
||||
const shareLink = getSettingsPath(SettingsPath.AvailableApplicationDetail, {
|
||||
availableApplicationId: registration.universalIdentifier,
|
||||
});
|
||||
|
||||
const stats = statsData?.findApplicationRegistrationStats;
|
||||
const hasStats = (stats?.activeInstalls ?? 0) > 0;
|
||||
|
||||
const versionDistributionLabel =
|
||||
stats?.versionDistribution
|
||||
?.map(
|
||||
(entry: { version: string; count: number }) =>
|
||||
`${entry.version} (${entry.count})`,
|
||||
)
|
||||
.join(', ') || '—';
|
||||
|
||||
const statsItems = [
|
||||
{
|
||||
Icon: IconDownload,
|
||||
label: t`Active installs`,
|
||||
value: stats?.activeInstalls ?? '—',
|
||||
},
|
||||
{
|
||||
Icon: IconTag,
|
||||
label: t`Most installed version`,
|
||||
value: stats?.mostInstalledVersion ?? '—',
|
||||
},
|
||||
{
|
||||
Icon: IconChartBar,
|
||||
label: t`Distribution`,
|
||||
value: versionDistributionLabel,
|
||||
},
|
||||
];
|
||||
const publishCommands = ['yarn twenty publish'];
|
||||
|
||||
return (
|
||||
<>
|
||||
{isNpmSource && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Marketplace Listing`}
|
||||
description={t`This app is listed on the marketplace because it is published to npm.`}
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Public`}
|
||||
description={t`Publish your app to the marketplace so others can install it`}
|
||||
/>
|
||||
{isNpmSource && (
|
||||
<SettingsApplicationRegistrationShareLinkButtons
|
||||
shareLink={shareLink}
|
||||
isNpmSource
|
||||
withCopyButton
|
||||
/>
|
||||
<Card rounded>
|
||||
<SettingsOptionCardContentToggle
|
||||
title={t`Featured`}
|
||||
description={t`Featured apps are curated. Open a PR to request featured status.`}
|
||||
checked={registration.isFeatured}
|
||||
onChange={() => {}}
|
||||
disabled
|
||||
/>
|
||||
</Card>
|
||||
<StyledButtonGroup>
|
||||
<Button
|
||||
Icon={IconExternalLink}
|
||||
title={t`View marketplace page`}
|
||||
variant="secondary"
|
||||
to={shareLink}
|
||||
/>
|
||||
</StyledButtonGroup>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{isTarballSource &&
|
||||
(!hasEnterpriseAccess ? (
|
||||
<SettingsEnterpriseFeatureGateCard
|
||||
description={t`Upgrade to Enterprise to share private applications.`}
|
||||
/>
|
||||
) : (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Share`}
|
||||
description={t`Share this link with other workspaces on this server to let them install this application.`}
|
||||
/>
|
||||
<StyledButtonGroup>
|
||||
)}
|
||||
{isTarballSource && (
|
||||
<CommandBlock
|
||||
commands={publishCommands}
|
||||
button={
|
||||
<Button
|
||||
Icon={IconCopy}
|
||||
title={t`Copy share link`}
|
||||
variant="secondary"
|
||||
disabled={!shareLink}
|
||||
onClick={async () => {
|
||||
if (shareLink) {
|
||||
await copyToClipboard(
|
||||
shareLink,
|
||||
t`Share link copied to clipboard`,
|
||||
);
|
||||
}
|
||||
onClick={() => {
|
||||
copyToClipboard(
|
||||
publishCommands.join('\n'),
|
||||
t`Command copied to clipboard`,
|
||||
);
|
||||
}}
|
||||
ariaLabel={t`Copy command`}
|
||||
Icon={IconCopy}
|
||||
/>
|
||||
<Button
|
||||
Icon={IconExternalLink}
|
||||
title={t`View private application page`}
|
||||
variant="secondary"
|
||||
to={shareLink}
|
||||
/>
|
||||
</StyledButtonGroup>
|
||||
</Section>
|
||||
))}
|
||||
|
||||
{hasStats && (
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
{isTarballSource && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Install Stats`}
|
||||
description={t`Usage across all workspaces on this server`}
|
||||
title={t`Private`}
|
||||
description={t`Share your app to other workspaces without pushing it on the marketplace`}
|
||||
/>
|
||||
<SettingsTableCard
|
||||
rounded
|
||||
items={statsItems}
|
||||
gridAutoColumns="3fr 8fr"
|
||||
<SettingsApplicationRegistrationShareLinkButtons
|
||||
shareLink={shareLink}
|
||||
withCopyButton
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
+33
-390
@@ -1,42 +1,30 @@
|
||||
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { useInstallMarketplaceApp } from '~/modules/marketplace/hooks/useInstallMarketplaceApp';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
H1Title,
|
||||
H1TitleFontColor,
|
||||
H2Title,
|
||||
IconArrowRight,
|
||||
IconBox,
|
||||
IconCheck,
|
||||
IconDownload,
|
||||
IconTag,
|
||||
IconTextCaption,
|
||||
IconShare,
|
||||
IconTrash,
|
||||
IconWorld,
|
||||
Status,
|
||||
AppTooltip,
|
||||
TooltipDelay,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section, SectionAlignment, SectionFontColor } from 'twenty-ui/layout';
|
||||
import {
|
||||
ApplicationRegistrationSourceType,
|
||||
ApplicationRegistrationTarballUrlDocument,
|
||||
type ApplicationRegistration,
|
||||
DeleteApplicationRegistrationDocument,
|
||||
FindApplicationRegistrationVariablesDocument,
|
||||
FindApplicationRegistrationStatsDocument,
|
||||
FindManyApplicationRegistrationsDocument,
|
||||
FindManyApplicationsDocument,
|
||||
TransferApplicationRegistrationOwnershipDocument,
|
||||
UninstallApplicationDocument,
|
||||
UpdateApplicationRegistrationVariableDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
@@ -46,114 +34,50 @@ import {
|
||||
StyledAppModalSection,
|
||||
StyledAppModalTitle,
|
||||
} from '~/pages/settings/applications/components/SettingsAppModalLayout';
|
||||
import { type ApplicationRegistrationData } from '~/pages/settings/applications/tabs/types/ApplicationRegistrationData';
|
||||
import { SettingsAdminApplicationRegistrationDetailContent } from '~/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDetailContent';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const DELETE_REGISTRATION_MODAL_ID = 'delete-application-registration-modal';
|
||||
|
||||
const TRANSFER_OWNERSHIP_MODAL_ID =
|
||||
'transfer-application-registration-ownership-modal';
|
||||
const UNINSTALL_APPLICATION_MODAL_ID =
|
||||
'uninstall-application-from-registration-modal';
|
||||
|
||||
const StyledVariableRow = styled.div`
|
||||
align-items: center;
|
||||
border-bottom: 1px solid ${themeCssVariables.border.color.light};
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
padding: ${themeCssVariables.spacing[2]} 0;
|
||||
`;
|
||||
|
||||
const StyledVariableInfo = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing['0.5']};
|
||||
`;
|
||||
|
||||
const StyledVariableKey = styled.span`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-family: monospace;
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
`;
|
||||
|
||||
const StyledVariableDescription = styled.span`
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
`;
|
||||
const DELETE_REGISTRATION_BUTTON_ID = 'delete-registration-button';
|
||||
|
||||
const StyledDangerButtonGroup = styled.div`
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledSourceRow = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledDownloadLink = styled.a`
|
||||
color: ${themeCssVariables.font.color.secondary};
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
&:hover {
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButtonGroup = styled.div`
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
padding-top: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
type ServerVariable = {
|
||||
id: string;
|
||||
key: string;
|
||||
description: string;
|
||||
isSecret: boolean;
|
||||
isRequired: boolean;
|
||||
isFilled: boolean;
|
||||
};
|
||||
|
||||
export const SettingsApplicationRegistrationGeneralTab = ({
|
||||
registration,
|
||||
hasActiveInstalls,
|
||||
}: {
|
||||
registration: ApplicationRegistrationData;
|
||||
hasActiveInstalls: boolean;
|
||||
registration: ApplicationRegistration;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
const navigate = useNavigateSettings();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
const { openModal, closeModal } = useModal();
|
||||
|
||||
const [isInstalling, setIsInstalling] = useState(false);
|
||||
const [isUninstalling, setIsUninstalling] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isTransferring, setIsTransferring] = useState(false);
|
||||
const [transferSubdomain, setTransferSubdomain] = useState('');
|
||||
const [variableValues, setVariableValues] = useState<Record<string, string>>(
|
||||
{},
|
||||
);
|
||||
|
||||
const applicationRegistrationId = registration.id;
|
||||
|
||||
const { data: variablesData } = useQuery(
|
||||
FindApplicationRegistrationVariablesDocument,
|
||||
{
|
||||
variables: { applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: tarballUrlData } = useQuery(
|
||||
ApplicationRegistrationTarballUrlDocument,
|
||||
const { data: statsData } = useQuery(
|
||||
FindApplicationRegistrationStatsDocument,
|
||||
{
|
||||
variables: { id: applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
},
|
||||
);
|
||||
|
||||
const stats = statsData?.findApplicationRegistrationStats;
|
||||
|
||||
const hasActiveInstalls =
|
||||
!isDefined(stats) || (stats.activeInstalls ?? 0) > 0;
|
||||
|
||||
const [deleteRegistration] = useMutation(
|
||||
DeleteApplicationRegistrationDocument,
|
||||
{
|
||||
@@ -161,13 +85,6 @@ export const SettingsApplicationRegistrationGeneralTab = ({
|
||||
},
|
||||
);
|
||||
|
||||
const [updateVariable] = useMutation(
|
||||
UpdateApplicationRegistrationVariableDocument,
|
||||
{
|
||||
refetchQueries: [FindApplicationRegistrationVariablesDocument],
|
||||
},
|
||||
);
|
||||
|
||||
const [transferOwnership] = useMutation(
|
||||
TransferApplicationRegistrationOwnershipDocument,
|
||||
{
|
||||
@@ -175,74 +92,6 @@ export const SettingsApplicationRegistrationGeneralTab = ({
|
||||
},
|
||||
);
|
||||
|
||||
const { install } = useInstallMarketplaceApp();
|
||||
const [uninstallApplication] = useMutation(UninstallApplicationDocument);
|
||||
const { data: applicationsData, refetch: refetchApplications } = useQuery(
|
||||
FindManyApplicationsDocument,
|
||||
);
|
||||
|
||||
const variables: ServerVariable[] =
|
||||
variablesData?.findApplicationRegistrationVariables ?? [];
|
||||
|
||||
const isNpmSource =
|
||||
registration.sourceType === ApplicationRegistrationSourceType.NPM;
|
||||
|
||||
const installedApp = (applicationsData?.findManyApplications ?? []).find(
|
||||
(application) =>
|
||||
application.universalIdentifier === registration.universalIdentifier,
|
||||
);
|
||||
|
||||
const isInstalledOnWorkspace = isDefined(installedApp);
|
||||
|
||||
const installedAppUrl = isInstalledOnWorkspace
|
||||
? getSettingsPath(SettingsPath.ApplicationDetail, {
|
||||
applicationId: installedApp.id,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
const handleInstallOnWorkspace = async () => {
|
||||
setIsInstalling(true);
|
||||
try {
|
||||
const success = await install({
|
||||
universalIdentifier: registration.universalIdentifier,
|
||||
});
|
||||
|
||||
if (success) {
|
||||
await refetchApplications();
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`App installed on this workspace`,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Error installing app`,
|
||||
});
|
||||
} finally {
|
||||
setIsInstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleUninstallFromWorkspace = async () => {
|
||||
setIsUninstalling(true);
|
||||
try {
|
||||
await uninstallApplication({
|
||||
variables: {
|
||||
universalIdentifier: registration.universalIdentifier,
|
||||
},
|
||||
});
|
||||
await refetchApplications();
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Application uninstalled from this workspace`,
|
||||
});
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Error uninstalling application`,
|
||||
});
|
||||
} finally {
|
||||
setIsUninstalling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@@ -288,249 +137,43 @@ export const SettingsApplicationRegistrationGeneralTab = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveVariableValue = async (variable: ServerVariable) => {
|
||||
const value = variableValues[variable.id];
|
||||
const variableKey = variable.key;
|
||||
|
||||
if (!isNonEmptyString(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await updateVariable({
|
||||
variables: {
|
||||
input: {
|
||||
id: variable.id,
|
||||
update: {
|
||||
value,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
setVariableValues((previous) => {
|
||||
const next = { ...previous };
|
||||
|
||||
delete next[variable.id];
|
||||
|
||||
return next;
|
||||
});
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Variable ${variableKey} updated`,
|
||||
});
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Error updating variable`,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const confirmationValue = t`yes`;
|
||||
|
||||
const generalItems = [
|
||||
{
|
||||
Icon: IconTag,
|
||||
label: t`Name`,
|
||||
value: registration.name,
|
||||
},
|
||||
{
|
||||
Icon: IconWorld,
|
||||
label: t`Universal ID`,
|
||||
value: registration.universalIdentifier,
|
||||
},
|
||||
...(isNpmSource && isNonEmptyString(registration.sourcePackage)
|
||||
? [
|
||||
{
|
||||
Icon: IconBox,
|
||||
label: t`Package`,
|
||||
value: registration.sourcePackage,
|
||||
},
|
||||
]
|
||||
: registration.sourceType === ApplicationRegistrationSourceType.TARBALL
|
||||
? [
|
||||
{
|
||||
Icon: IconBox,
|
||||
label: t`Source`,
|
||||
value: isNonEmptyString(
|
||||
tarballUrlData?.applicationRegistrationTarballUrl,
|
||||
) ? (
|
||||
<StyledSourceRow>
|
||||
<span>
|
||||
<Trans>Tarball upload</Trans>
|
||||
</span>
|
||||
<StyledDownloadLink
|
||||
href={tarballUrlData.applicationRegistrationTarballUrl}
|
||||
download
|
||||
>
|
||||
<Trans>Download</Trans>
|
||||
</StyledDownloadLink>
|
||||
</StyledSourceRow>
|
||||
) : (
|
||||
t`Tarball upload`
|
||||
),
|
||||
},
|
||||
]
|
||||
: registration.sourceType === ApplicationRegistrationSourceType.LOCAL
|
||||
? [
|
||||
{
|
||||
Icon: IconBox,
|
||||
label: t`Source`,
|
||||
value: t`Local development`,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`General`}
|
||||
description={t`Name and description are managed via your app manifest (CLI)`}
|
||||
/>
|
||||
<SettingsTableCard
|
||||
rounded
|
||||
items={generalItems}
|
||||
gridAutoColumns="3fr 8fr"
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Installation`}
|
||||
description={
|
||||
isInstalledOnWorkspace
|
||||
? t`This app is installed on the current workspace`
|
||||
: t`Install this app on the current workspace`
|
||||
}
|
||||
/>
|
||||
{isInstalledOnWorkspace ? (
|
||||
<StyledButtonGroup>
|
||||
<Button
|
||||
Icon={IconArrowRight}
|
||||
title={t`View installed app`}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
to={installedAppUrl}
|
||||
/>
|
||||
<Button
|
||||
Icon={IconTrash}
|
||||
title={t`Uninstall`}
|
||||
variant="secondary"
|
||||
accent="danger"
|
||||
onClick={() => openModal(UNINSTALL_APPLICATION_MODAL_ID)}
|
||||
/>
|
||||
</StyledButtonGroup>
|
||||
) : (
|
||||
<Button
|
||||
Icon={IconDownload}
|
||||
title={
|
||||
isInstalling ? t`Installing...` : t`Install on this workspace`
|
||||
}
|
||||
variant="secondary"
|
||||
accent="blue"
|
||||
disabled={isInstalling}
|
||||
onClick={handleInstallOnWorkspace}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
<ConfirmationModal
|
||||
confirmationPlaceholder={confirmationValue}
|
||||
confirmationValue={confirmationValue}
|
||||
modalInstanceId={UNINSTALL_APPLICATION_MODAL_ID}
|
||||
title={t`Uninstall Application?`}
|
||||
subtitle={
|
||||
<Trans>
|
||||
This will remove the application and all its data from this
|
||||
workspace. Please type {`"${confirmationValue}"`} to confirm.
|
||||
</Trans>
|
||||
}
|
||||
onConfirmClick={handleUninstallFromWorkspace}
|
||||
confirmButtonText={t`Uninstall`}
|
||||
loading={isUninstalling}
|
||||
<SettingsAdminApplicationRegistrationDetailContent
|
||||
registration={registration}
|
||||
/>
|
||||
|
||||
{variables.length > 0 && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Server Variables`}
|
||||
description={t`Variables declared by the app manifest. Fill in values here — they apply to all workspace installations.`}
|
||||
/>
|
||||
{variables.map((variable) => (
|
||||
<StyledVariableRow key={variable.id}>
|
||||
<StyledVariableInfo>
|
||||
<StyledVariableKey>
|
||||
{variable.key}
|
||||
{variable.isRequired && (
|
||||
<span style={{ color: 'red' }}> *</span>
|
||||
)}
|
||||
</StyledVariableKey>
|
||||
{isNonEmptyString(variable.description) && (
|
||||
<StyledVariableDescription>
|
||||
{variable.description}
|
||||
</StyledVariableDescription>
|
||||
)}
|
||||
</StyledVariableInfo>
|
||||
{variable.isFilled &&
|
||||
!isNonEmptyString(variableValues[variable.id]) && (
|
||||
<Status color="green" text={t`Configured`} />
|
||||
)}
|
||||
{!variable.isFilled &&
|
||||
!isNonEmptyString(variableValues[variable.id]) && (
|
||||
<Status
|
||||
color={variable.isRequired ? 'red' : 'gray'}
|
||||
text={variable.isRequired ? t`Required` : t`Not set`}
|
||||
/>
|
||||
)}
|
||||
<SettingsTextInput
|
||||
instanceId={`var-${variable.id}`}
|
||||
value={variableValues[variable.id] ?? ''}
|
||||
onChange={(value) =>
|
||||
setVariableValues((previous) => ({
|
||||
...previous,
|
||||
[variable.id]: value,
|
||||
}))
|
||||
}
|
||||
placeholder={
|
||||
variable.isSecret ? t`Enter secret value` : t`Enter value`
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
<Button
|
||||
Icon={IconCheck}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
disabled={!isNonEmptyString(variableValues[variable.id])}
|
||||
onClick={() => handleSaveVariableValue(variable)}
|
||||
/>
|
||||
</StyledVariableRow>
|
||||
))}
|
||||
</Section>
|
||||
)}
|
||||
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Danger zone`}
|
||||
description={
|
||||
hasActiveInstalls
|
||||
? t`Uninstall this app from all workspaces before deleting it`
|
||||
: t`Delete or transfer this app registration`
|
||||
}
|
||||
description={t`Delete or transfer this app registration`}
|
||||
/>
|
||||
<StyledDangerButtonGroup>
|
||||
<Button
|
||||
id={DELETE_REGISTRATION_BUTTON_ID}
|
||||
accent="danger"
|
||||
variant="secondary"
|
||||
title={t`Delete`}
|
||||
title={t`Delete app`}
|
||||
Icon={IconTrash}
|
||||
disabled={hasActiveInstalls}
|
||||
onClick={() => openModal(DELETE_REGISTRATION_MODAL_ID)}
|
||||
/>
|
||||
{hasActiveInstalls && (
|
||||
<AppTooltip
|
||||
anchorSelect={`#${DELETE_REGISTRATION_BUTTON_ID}`}
|
||||
content={t`Uninstall this app from all workspaces before deleting it`}
|
||||
noArrow
|
||||
place="bottom"
|
||||
positionStrategy="fixed"
|
||||
delay={TooltipDelay.shortDelay}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
accent="default"
|
||||
variant="secondary"
|
||||
title={t`Transfer ownership`}
|
||||
Icon={IconArrowRight}
|
||||
Icon={IconShare}
|
||||
onClick={() => openModal(TRANSFER_OWNERSHIP_MODAL_ID)}
|
||||
/>
|
||||
</StyledDangerButtonGroup>
|
||||
|
||||
+17
-130
@@ -1,7 +1,6 @@
|
||||
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
|
||||
import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { useAtomFamilyStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomFamilyStateValue';
|
||||
@@ -10,59 +9,25 @@ import { styled } from '@linaria/react';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
H2Title,
|
||||
IconKey,
|
||||
IconRefresh,
|
||||
IconShield,
|
||||
IconTrash,
|
||||
} from 'twenty-ui/display';
|
||||
import { H2Title, IconKey, IconRefresh, IconShield } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
FindManyApplicationRegistrationsDocument,
|
||||
FindOneApplicationRegistrationDocument,
|
||||
RotateApplicationRegistrationClientSecretDocument,
|
||||
UpdateApplicationRegistrationDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { applicationRegistrationClientSecretFamilyState } from '~/pages/settings/applications/states/applicationRegistrationClientSecretFamilyState';
|
||||
import { type ApplicationRegistrationData } from '~/pages/settings/applications/tabs/types/ApplicationRegistrationData';
|
||||
import { isValidUrl } from 'twenty-shared/utils';
|
||||
import { SettingsApplicationRegistrationRedirectURIsInput } from '~/pages/settings/applications/components/SettingsApplicationRegistrationRedirectURIsInput';
|
||||
import { SettingsApplicationRegistrationRedirectURIsTable } from '~/pages/settings/applications/components/SettingsApplicationRegistrationRedirectURIsTable';
|
||||
|
||||
const ROTATE_SECRET_MODAL_ID = 'rotate-application-registration-secret-modal';
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledRedirectUriRow = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
padding: ${themeCssVariables.spacing[1]} 0;
|
||||
`;
|
||||
|
||||
const StyledRedirectUriValue = styled.span`
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
font-family: monospace;
|
||||
word-break: break-all;
|
||||
`;
|
||||
|
||||
const StyledRotateContainer = styled.div`
|
||||
padding-top: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledSaveContainer = styled.div`
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
padding-top: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
export const SettingsApplicationRegistrationOAuthTab = ({
|
||||
registration,
|
||||
}: {
|
||||
@@ -84,40 +49,31 @@ export const SettingsApplicationRegistrationOAuthTab = ({
|
||||
const [formRedirectUris, setFormRedirectUris] = useState<string[]>(
|
||||
registration.oAuthRedirectUris ?? [],
|
||||
);
|
||||
const [newRedirectUri, setNewRedirectUri] = useState('');
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
|
||||
const [updateRegistration] = useMutation(
|
||||
UpdateApplicationRegistrationDocument,
|
||||
{
|
||||
refetchQueries: [
|
||||
FindOneApplicationRegistrationDocument,
|
||||
FindManyApplicationRegistrationsDocument,
|
||||
],
|
||||
},
|
||||
);
|
||||
|
||||
const [rotateSecret] = useMutation(
|
||||
RotateApplicationRegistrationClientSecretDocument,
|
||||
);
|
||||
|
||||
const markDirty = () => setHasChanges(true);
|
||||
|
||||
const handleSave = async () => {
|
||||
const handleSave = async (newFormRedirectUris: string[]) => {
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
await updateRegistration({
|
||||
variables: {
|
||||
input: {
|
||||
id: applicationRegistrationId,
|
||||
update: {
|
||||
oAuthRedirectUris: formRedirectUris,
|
||||
oAuthRedirectUris: newFormRedirectUris,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
setHasChanges(false);
|
||||
enqueueSuccessSnackBar({ message: t`Redirect URIs updated` });
|
||||
setFormRedirectUris(newFormRedirectUris);
|
||||
} catch {
|
||||
enqueueErrorSnackBar({ message: t`Error updating redirect URIs` });
|
||||
} finally {
|
||||
@@ -125,11 +81,6 @@ export const SettingsApplicationRegistrationOAuthTab = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setFormRedirectUris(registration.oAuthRedirectUris ?? []);
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
const handleRotateSecret = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
@@ -154,38 +105,8 @@ export const SettingsApplicationRegistrationOAuthTab = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddRedirectUri = () => {
|
||||
const trimmed = newRedirectUri.trim();
|
||||
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isValidUrl(trimmed)) {
|
||||
enqueueErrorSnackBar({ message: t`Please enter a valid URL` });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (formRedirectUris.includes(trimmed)) {
|
||||
enqueueErrorSnackBar({ message: t`This redirect URI is already added` });
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setFormRedirectUris([...formRedirectUris, trimmed]);
|
||||
setNewRedirectUri('');
|
||||
markDirty();
|
||||
};
|
||||
|
||||
const handleRemoveRedirectUri = (index: number) => {
|
||||
setFormRedirectUris(
|
||||
formRedirectUris.filter((_, uriIndex) => uriIndex !== index),
|
||||
);
|
||||
markDirty();
|
||||
};
|
||||
|
||||
const displayedSecret = rotatedSecret ?? applicationRegistrationClientSecret;
|
||||
|
||||
const confirmationValue = t`yes`;
|
||||
|
||||
const credentialItems = [
|
||||
@@ -205,7 +126,7 @@ export const SettingsApplicationRegistrationOAuthTab = ({
|
||||
<>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`OAuth Credentials`}
|
||||
title={t`OAuth`}
|
||||
description={t`Credentials and scopes for OAuth authorization flows`}
|
||||
/>
|
||||
<SettingsTableCard
|
||||
@@ -238,48 +159,14 @@ export const SettingsApplicationRegistrationOAuthTab = ({
|
||||
title={t`Redirect URIs`}
|
||||
description={t`Allowed redirect URIs for OAuth flows`}
|
||||
/>
|
||||
{formRedirectUris.map((uri, index) => (
|
||||
<StyledRedirectUriRow key={`${uri}-${index}`}>
|
||||
<StyledRedirectUriValue>{uri}</StyledRedirectUriValue>
|
||||
<Button
|
||||
Icon={IconTrash}
|
||||
variant="tertiary"
|
||||
size="medium"
|
||||
onClick={() => handleRemoveRedirectUri(index)}
|
||||
/>
|
||||
</StyledRedirectUriRow>
|
||||
))}
|
||||
<StyledInputContainer>
|
||||
<SettingsTextInput
|
||||
instanceId="application-registration-new-redirect-uri"
|
||||
value={newRedirectUri}
|
||||
onChange={setNewRedirectUri}
|
||||
placeholder={t`https://example.com/callback`}
|
||||
fullWidth
|
||||
/>
|
||||
<Button
|
||||
title={t`Add`}
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
onClick={handleAddRedirectUri}
|
||||
/>
|
||||
</StyledInputContainer>
|
||||
{hasChanges && (
|
||||
<StyledSaveContainer>
|
||||
<Button
|
||||
title={t`Save`}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
onClick={handleSave}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
<Button
|
||||
title={t`Cancel`}
|
||||
variant="secondary"
|
||||
onClick={handleCancel}
|
||||
/>
|
||||
</StyledSaveContainer>
|
||||
)}
|
||||
<SettingsApplicationRegistrationRedirectURIsInput
|
||||
redirectUris={formRedirectUris}
|
||||
updateRedirectUris={handleSave}
|
||||
/>
|
||||
<SettingsApplicationRegistrationRedirectURIsTable
|
||||
redirectUris={formRedirectUris}
|
||||
updateRedirectUris={handleSave}
|
||||
/>
|
||||
</Section>
|
||||
|
||||
<ConfirmationModal
|
||||
|
||||
+60
-85
@@ -1,6 +1,5 @@
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { SettingsEmptyPlaceholder } from '@/settings/components/SettingsEmptyPlaceholder';
|
||||
import { SettingsListCard } from '@/settings/components/SettingsListCard';
|
||||
import {
|
||||
StyledActionTableCell,
|
||||
StyledNameTableCell,
|
||||
@@ -17,15 +16,13 @@ import { useLingui } from '@lingui/react/macro';
|
||||
import { useContext, useMemo, useState } from 'react';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { Tag } from 'twenty-ui/components';
|
||||
import {
|
||||
Avatar,
|
||||
CommandBlock,
|
||||
H2Title,
|
||||
IconApps,
|
||||
IconChevronRight,
|
||||
IconCopy,
|
||||
IconFileInfo,
|
||||
IconArrowUpRight,
|
||||
OverflowingTextWithTooltip,
|
||||
InlineBanner,
|
||||
} from 'twenty-ui/display';
|
||||
@@ -35,17 +32,20 @@ import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import {
|
||||
type ApplicationRegistrationFragmentFragment,
|
||||
ApplicationRegistrationSourceType,
|
||||
FeatureFlagKey,
|
||||
FindManyApplicationRegistrationsDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
import { useMarketplaceApps } from '~/modules/marketplace/hooks/useMarketplaceApps';
|
||||
import {
|
||||
APPLICATION_TABLE_ROW_GRID_TEMPLATE_COLUMNS,
|
||||
SettingsApplicationTableRow,
|
||||
} from '~/pages/settings/applications/components/SettingsApplicationTableRow';
|
||||
|
||||
const StyledButtonContainer = styled.div`
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin: ${themeCssVariables.spacing[2]} 0;
|
||||
margin-top: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledSearchInputContainer = styled.div`
|
||||
@@ -59,28 +59,6 @@ const StyledTableRowsContainer = styled.div`
|
||||
|
||||
const NPM_PACKAGES_GRID_COLUMNS = '200px 1fr 36px';
|
||||
|
||||
const SOURCE_TYPE_BADGE_CONFIG: Record<
|
||||
ApplicationRegistrationSourceType,
|
||||
{ label: string; color: 'gray' | 'blue' | 'green' }
|
||||
> = {
|
||||
[ApplicationRegistrationSourceType.LOCAL]: {
|
||||
label: 'Dev',
|
||||
color: 'gray',
|
||||
},
|
||||
[ApplicationRegistrationSourceType.NPM]: {
|
||||
label: 'Npm',
|
||||
color: 'blue',
|
||||
},
|
||||
[ApplicationRegistrationSourceType.TARBALL]: {
|
||||
label: 'Internal',
|
||||
color: 'green',
|
||||
},
|
||||
[ApplicationRegistrationSourceType.OAUTH_ONLY]: {
|
||||
label: 'OAuth',
|
||||
color: 'blue',
|
||||
},
|
||||
};
|
||||
|
||||
export const SettingsApplicationsDeveloperTab = () => {
|
||||
const { t } = useLingui();
|
||||
const { theme } = useContext(ThemeContext);
|
||||
@@ -90,28 +68,31 @@ export const SettingsApplicationsDeveloperTab = () => {
|
||||
|
||||
const { copyToClipboard } = useCopyToClipboard();
|
||||
|
||||
const { data, loading } = useQuery(FindManyApplicationRegistrationsDocument);
|
||||
const { data } = useQuery(FindManyApplicationRegistrationsDocument);
|
||||
|
||||
const isMarketplaceSettingTabVisible = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_MARKETPLACE_SETTING_TAB_VISIBLE,
|
||||
);
|
||||
|
||||
const [npmSearchTerm, setNpmSearchTerm] = useState('');
|
||||
const [marketplaceAppSearchTerm, setMarketplaceAppSearchTerm] = useState('');
|
||||
|
||||
const [myAppsSearchTerm, setMyAppsSearchTerm] = useState('');
|
||||
|
||||
const { data: marketplaceApps } = useMarketplaceApps();
|
||||
|
||||
const filteredMarketplaceApps = useMemo(() => {
|
||||
if (!npmSearchTerm) {
|
||||
if (!marketplaceAppSearchTerm) {
|
||||
return marketplaceApps;
|
||||
}
|
||||
|
||||
const lowerSearch = npmSearchTerm.toLowerCase();
|
||||
const lowerSearch = marketplaceAppSearchTerm.toLowerCase();
|
||||
|
||||
return marketplaceApps.filter(
|
||||
(application) =>
|
||||
application.name.toLowerCase().includes(lowerSearch) ||
|
||||
application.description.toLowerCase().includes(lowerSearch),
|
||||
);
|
||||
}, [marketplaceApps, npmSearchTerm]);
|
||||
}, [marketplaceApps, marketplaceAppSearchTerm]);
|
||||
|
||||
const registrations: ApplicationRegistrationFragmentFragment[] =
|
||||
data?.findManyApplicationRegistrations ?? [];
|
||||
@@ -143,36 +124,6 @@ export const SettingsApplicationsDeveloperTab = () => {
|
||||
applicationRegistrationId: registration.id,
|
||||
});
|
||||
|
||||
const syncCommands = [
|
||||
// oxlint-disable-next-line lingui/no-unlocalized-strings
|
||||
'yarn twenty dev',
|
||||
];
|
||||
|
||||
const syncCopyButton = (
|
||||
<Button
|
||||
onClick={() => {
|
||||
copyToClipboard(
|
||||
syncCommands.join('\n'),
|
||||
t`Command copied to clipboard`,
|
||||
);
|
||||
}}
|
||||
ariaLabel={t`Copy command`}
|
||||
Icon={IconCopy}
|
||||
/>
|
||||
);
|
||||
|
||||
const RowRightWithBadge = ({
|
||||
item,
|
||||
}: {
|
||||
item: ApplicationRegistrationFragmentFragment;
|
||||
}) => {
|
||||
const badgeConfig = SOURCE_TYPE_BADGE_CONFIG[item.sourceType];
|
||||
|
||||
return (
|
||||
<Tag text={badgeConfig.label} color={badgeConfig.color} preventShrink />
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
@@ -183,7 +134,9 @@ export const SettingsApplicationsDeveloperTab = () => {
|
||||
<CommandBlock commands={createCommands} button={createCopyButton} />
|
||||
<StyledButtonContainer>
|
||||
<Button
|
||||
Icon={IconFileInfo}
|
||||
Icon={IconArrowUpRight}
|
||||
variant={'secondary'}
|
||||
size={'small'}
|
||||
title={t`Read documentation`}
|
||||
onClick={() =>
|
||||
window.open(
|
||||
@@ -198,26 +151,48 @@ export const SettingsApplicationsDeveloperTab = () => {
|
||||
</StyledButtonContainer>
|
||||
</Section>
|
||||
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Your apps`}
|
||||
description={t`All applications registered on this workspace`}
|
||||
/>
|
||||
{registrations.length > 0 ? (
|
||||
<SettingsListCard
|
||||
items={registrations}
|
||||
getItemLabel={(registration) => registration.name}
|
||||
isLoading={loading}
|
||||
RowIcon={IconApps}
|
||||
to={getRegistrationLink}
|
||||
RowRightComponent={RowRightWithBadge}
|
||||
{registrations.length > 0 && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`My apps`}
|
||||
description={t`Apps you're the developer of`}
|
||||
/>
|
||||
) : (
|
||||
!loading && (
|
||||
<CommandBlock commands={syncCommands} button={syncCopyButton} />
|
||||
)
|
||||
)}
|
||||
</Section>
|
||||
<StyledSearchInputContainer>
|
||||
<SearchInput
|
||||
placeholder={t`Search an application`}
|
||||
value={myAppsSearchTerm}
|
||||
onChange={setMyAppsSearchTerm}
|
||||
/>
|
||||
</StyledSearchInputContainer>
|
||||
<Table>
|
||||
<TableRow
|
||||
gridTemplateColumns={APPLICATION_TABLE_ROW_GRID_TEMPLATE_COLUMNS}
|
||||
>
|
||||
<TableHeader> {t`Name`}</TableHeader>
|
||||
<TableHeader>{''}</TableHeader>
|
||||
<TableHeader>{''}</TableHeader>
|
||||
<TableHeader />
|
||||
</TableRow>
|
||||
<StyledTableRowsContainer>
|
||||
{registrations.map((registration) => {
|
||||
return (
|
||||
<SettingsApplicationTableRow
|
||||
key={registration.id}
|
||||
application={registration}
|
||||
action={
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
}
|
||||
link={getRegistrationLink(registration)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</StyledTableRowsContainer>
|
||||
</Table>
|
||||
</Section>
|
||||
)}
|
||||
|
||||
{!isMarketplaceSettingTabVisible && (
|
||||
<Section>
|
||||
@@ -239,8 +214,8 @@ export const SettingsApplicationsDeveloperTab = () => {
|
||||
<StyledSearchInputContainer>
|
||||
<SearchInput
|
||||
placeholder={t`Search an application`}
|
||||
value={npmSearchTerm}
|
||||
onChange={setNpmSearchTerm}
|
||||
value={marketplaceAppSearchTerm}
|
||||
onChange={setMarketplaceAppSearchTerm}
|
||||
/>
|
||||
</StyledSearchInputContainer>
|
||||
{filteredMarketplaceApps.length === 0 ? (
|
||||
|
||||
+2
-2
@@ -21,8 +21,8 @@ export const SettingsAvailableApplicationDetailContentTab = ({
|
||||
}) => {
|
||||
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
|
||||
|
||||
const objects = content?.objects ?? [];
|
||||
const fields = content?.fields ?? [];
|
||||
const objects = useMemo(() => content?.objects ?? [], [content?.objects]);
|
||||
const fields = useMemo(() => content?.fields ?? [], [content?.fields]);
|
||||
const logicFunctions = content?.logicFunctions ?? [];
|
||||
const frontComponents = content?.frontComponents ?? [];
|
||||
|
||||
|
||||
+1
-5
@@ -29,11 +29,7 @@ export const WithStandardSelected: Story = {
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await canvas.findByRole(
|
||||
'heading',
|
||||
{ name: 'New Object', level: 3 },
|
||||
{ timeout: 5000 },
|
||||
);
|
||||
await canvas.findByText('New Object');
|
||||
|
||||
const listingInput = await canvas.findByPlaceholderText('Listing');
|
||||
const pluralInput = await canvas.findByPlaceholderText('Listings');
|
||||
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { IconLock } from 'twenty-ui/display';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledPillContainer = styled.span`
|
||||
align-items: center;
|
||||
background: ${themeCssVariables.background.secondary};
|
||||
border: 1px solid ${themeCssVariables.border.color.light};
|
||||
border-radius: 40px;
|
||||
color: ${themeCssVariables.font.color.tertiary};
|
||||
display: inline-flex;
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
gap: ${themeCssVariables.spacing[1]};
|
||||
padding: ${themeCssVariables.spacing[1]} ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
export const OrganizationAdornment = () => (
|
||||
<StyledPillContainer>
|
||||
<IconLock size={12} />
|
||||
{t`Organization`}
|
||||
</StyledPillContainer>
|
||||
);
|
||||
@@ -295,7 +295,9 @@ export const SettingsSecurity = () => {
|
||||
</Card>
|
||||
) : (
|
||||
<SettingsEnterpriseFeatureGateCard
|
||||
title={t`Enterprise feature`}
|
||||
description={t`Upgrade to Enterprise to access audit logs.`}
|
||||
buttonTitle={t`Activate`}
|
||||
/>
|
||||
)}
|
||||
</Section>
|
||||
|
||||
+4
@@ -90,6 +90,10 @@ export class ApplicationRegistrationVariableService {
|
||||
updateData.encryptedValue = this.encryptionService.encrypt(update.value);
|
||||
}
|
||||
|
||||
if (isDefined(update.resetValue) && update.resetValue) {
|
||||
updateData.encryptedValue = '';
|
||||
}
|
||||
|
||||
if (isDefined(update.description)) {
|
||||
updateData.description = update.description;
|
||||
}
|
||||
|
||||
+4
@@ -18,6 +18,10 @@ export class UpdateApplicationRegistrationVariablePayload {
|
||||
@IsOptional()
|
||||
value?: string;
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsOptional()
|
||||
resetValue?: boolean;
|
||||
|
||||
@Field({ nullable: true })
|
||||
@IsString()
|
||||
@MaxLength(2000)
|
||||
|
||||
-1
@@ -60,7 +60,6 @@ export class ApplicationRegistrationResolver {
|
||||
private readonly applicationRegistrationVariableService: ApplicationRegistrationVariableService,
|
||||
private readonly applicationTarballService: ApplicationTarballService,
|
||||
private readonly fileUrlService: FileUrlService,
|
||||
private readonly domainServerConfigService: DomainServerConfigService,
|
||||
) {}
|
||||
|
||||
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
|
||||
|
||||
+36
-16
@@ -24,6 +24,7 @@ import { ApplicationRegistrationSourceType } from 'src/engine/core-modules/appli
|
||||
import { ApplicationEntity } from 'src/engine/core-modules/application/application.entity';
|
||||
import { validateRedirectUri } from 'src/engine/core-modules/auth/utils/validate-redirect-uri.util';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { ApplicationRegistrationVariableService } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.service';
|
||||
const BCRYPT_SALT_ROUNDS = 10;
|
||||
|
||||
@Injectable()
|
||||
@@ -35,6 +36,7 @@ export class ApplicationRegistrationService {
|
||||
private readonly applicationRepository: Repository<ApplicationEntity>,
|
||||
@InjectRepository(WorkspaceEntity)
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
private readonly applicationRegistrationVariableService: ApplicationRegistrationVariableService,
|
||||
) {}
|
||||
|
||||
async findMany(
|
||||
@@ -288,26 +290,41 @@ export class ApplicationRegistrationService {
|
||||
isListed: params.isListed,
|
||||
isFeatured: params.isFeatured,
|
||||
});
|
||||
} else {
|
||||
const registration = this.applicationRegistrationRepository.create({
|
||||
universalIdentifier: params.universalIdentifier,
|
||||
name: params.name,
|
||||
sourceType: params.sourceType,
|
||||
sourcePackage: params.sourcePackage,
|
||||
latestAvailableVersion: params.latestAvailableVersion,
|
||||
isListed: params.isListed,
|
||||
isFeatured: params.isFeatured,
|
||||
manifest: params.manifest,
|
||||
oAuthClientId: v4(),
|
||||
oAuthRedirectUris: [],
|
||||
oAuthScopes: [],
|
||||
ownerWorkspaceId: params.ownerWorkspaceId,
|
||||
});
|
||||
|
||||
await this.applicationRegistrationRepository.save(registration);
|
||||
}
|
||||
|
||||
if (!isDefined(params.manifest?.application?.serverVariables)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const registration = this.applicationRegistrationRepository.create({
|
||||
universalIdentifier: params.universalIdentifier,
|
||||
name: params.name,
|
||||
sourceType: params.sourceType,
|
||||
sourcePackage: params.sourcePackage,
|
||||
latestAvailableVersion: params.latestAvailableVersion,
|
||||
isListed: params.isListed,
|
||||
isFeatured: params.isFeatured,
|
||||
manifest: params.manifest,
|
||||
oAuthClientId: v4(),
|
||||
oAuthRedirectUris: [],
|
||||
oAuthScopes: [],
|
||||
ownerWorkspaceId: params.ownerWorkspaceId,
|
||||
});
|
||||
const registration = await this.findOneByUniversalIdentifier(
|
||||
params.universalIdentifier,
|
||||
);
|
||||
|
||||
await this.applicationRegistrationRepository.save(registration);
|
||||
if (!isDefined(registration)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.applicationRegistrationVariableService.syncVariableSchemas(
|
||||
registration.id,
|
||||
params.manifest.application.serverVariables,
|
||||
);
|
||||
}
|
||||
|
||||
async createCliRegistrationIfNotExists(): Promise<ApplicationRegistrationEntity | null> {
|
||||
@@ -337,7 +354,10 @@ export class ApplicationRegistrationService {
|
||||
|
||||
async findManyListed(): Promise<ApplicationRegistrationEntity[]> {
|
||||
return this.applicationRegistrationRepository.find({
|
||||
where: { isListed: true },
|
||||
where: {
|
||||
isListed: true,
|
||||
sourceType: ApplicationRegistrationSourceType.NPM,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+11
-4
@@ -22,6 +22,8 @@ import { readJsonFile } from 'src/engine/core-modules/application/application-pa
|
||||
import { resolvePackageContentDir } from 'src/engine/core-modules/application/application-package/utils/tarball-utils';
|
||||
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
|
||||
import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service';
|
||||
import type { ApplicationManifest } from 'twenty-shared/application';
|
||||
import { ApplicationRegistrationVariableService } from 'src/engine/core-modules/application/application-registration-variable/application-registration-variable.service';
|
||||
|
||||
export const MAX_TARBALL_UPLOAD_SIZE_BYTES = 50 * 1024 * 1024;
|
||||
|
||||
@@ -34,6 +36,7 @@ export class ApplicationTarballService {
|
||||
private readonly appRegistrationRepository: Repository<ApplicationRegistrationEntity>,
|
||||
private readonly fileStorageService: FileStorageService,
|
||||
private readonly applicationService: ApplicationService,
|
||||
private readonly applicationRegistrationVariableService: ApplicationRegistrationVariableService,
|
||||
) {}
|
||||
|
||||
async uploadTarball(params: {
|
||||
@@ -58,10 +61,7 @@ export class ApplicationTarballService {
|
||||
const contentDir = await resolvePackageContentDir(extractDir);
|
||||
|
||||
const manifest = await readJsonFile<{
|
||||
application?: {
|
||||
universalIdentifier?: string;
|
||||
displayName?: string;
|
||||
};
|
||||
application?: ApplicationManifest;
|
||||
}>(contentDir, 'manifest.json');
|
||||
|
||||
const packageJson = await readJsonFile<{
|
||||
@@ -181,6 +181,13 @@ export class ApplicationTarballService {
|
||||
ownerWorkspaceId: params.ownerWorkspaceId,
|
||||
});
|
||||
|
||||
if (manifest.application?.serverVariables) {
|
||||
await this.applicationRegistrationVariableService.syncVariableSchemas(
|
||||
appRegistration.id,
|
||||
manifest.application.serverVariables,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.log(
|
||||
`Tarball uploaded for app ${universalIdentifier} (registration ${appRegistration.id})`,
|
||||
);
|
||||
|
||||
-17
@@ -168,23 +168,6 @@ export class LocalDriver implements LogicFunctionDriver {
|
||||
|
||||
async delete() {}
|
||||
|
||||
async build({
|
||||
flatApplication,
|
||||
applicationUniversalIdentifier,
|
||||
}: {
|
||||
flatApplication: FlatApplication;
|
||||
applicationUniversalIdentifier: string;
|
||||
}) {
|
||||
await this.createLayerIfNotExist({
|
||||
flatApplication,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
await this.ensureSdkLayer({
|
||||
flatApplication,
|
||||
applicationUniversalIdentifier,
|
||||
});
|
||||
}
|
||||
|
||||
// Symlinks everything from the deps layer except twenty-client-sdk,
|
||||
// which comes from the SDK layer (workspace-specific generated client).
|
||||
private async assembleNodeModules({
|
||||
|
||||
+12
@@ -76,3 +76,15 @@ export class PublicWorkspaceDataDTO {
|
||||
@Field(() => WorkspaceUrlsDTO)
|
||||
workspaceUrls: WorkspaceUrlsDTO;
|
||||
}
|
||||
|
||||
@ObjectType('PublicWorkspaceDataSummary')
|
||||
export class PublicWorkspaceDataSummaryDTO {
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
logo: WorkspaceEntity['logo'];
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
displayName: WorkspaceEntity['displayName'];
|
||||
}
|
||||
|
||||
@@ -820,4 +820,10 @@ export class WorkspaceService extends TypeOrmQueryService<WorkspaceEntity> {
|
||||
this.exceptionHandlerService.captureExceptions([error as Error]);
|
||||
}
|
||||
}
|
||||
|
||||
async findOneWorkspaceById(id: string) {
|
||||
return await this.workspaceRepository.findOneBy({
|
||||
id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ import { ActivateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/a
|
||||
import {
|
||||
type AuthProvidersDTO,
|
||||
PublicWorkspaceDataDTO,
|
||||
PublicWorkspaceDataSummaryDTO,
|
||||
} from 'src/engine/core-modules/workspace/dtos/public-workspace-data.dto';
|
||||
import { UpdateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/update-workspace-input';
|
||||
import { WorkspaceUrlsDTO } from 'src/engine/core-modules/workspace/dtos/workspace-urls.dto';
|
||||
@@ -69,6 +70,7 @@ import { fromRoleEntityToRoleDto } from 'src/engine/metadata-modules/role/utils/
|
||||
import { ViewDTO } from 'src/engine/metadata-modules/view/dtos/view.dto';
|
||||
import { ViewService } from 'src/engine/metadata-modules/view/services/view.service';
|
||||
import { getRequest } from 'src/utils/extract-request';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
const OriginHeader = createParamDecorator(
|
||||
(_: unknown, ctx: ExecutionContext) => {
|
||||
const request = getRequest(ctx);
|
||||
@@ -418,6 +420,39 @@ export class WorkspaceResolver {
|
||||
}
|
||||
}
|
||||
|
||||
@Query(() => PublicWorkspaceDataSummaryDTO)
|
||||
@UseGuards(PublicEndpointGuard, NoPermissionGuard)
|
||||
async getPublicWorkspaceDataById(
|
||||
@Args({
|
||||
name: 'id',
|
||||
type: () => UUIDScalarType,
|
||||
nullable: false,
|
||||
})
|
||||
id: string,
|
||||
): Promise<PublicWorkspaceDataSummaryDTO | undefined> {
|
||||
try {
|
||||
const workspace = await this.workspaceService.findOneWorkspaceById(id);
|
||||
|
||||
assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError);
|
||||
|
||||
const logo = isDefined(workspace.logoFileId)
|
||||
? this.fileUrlService.signFileByIdUrl({
|
||||
fileId: workspace.logoFileId,
|
||||
workspaceId: workspace.id,
|
||||
fileFolder: FileFolder.CorePicture,
|
||||
})
|
||||
: (workspace.logo ?? '');
|
||||
|
||||
return {
|
||||
id: workspace.id,
|
||||
logo,
|
||||
displayName: workspace.displayName,
|
||||
};
|
||||
} catch (err) {
|
||||
workspaceGraphqlApiExceptionHandler(err);
|
||||
}
|
||||
}
|
||||
|
||||
@Mutation(() => DomainValidRecords, { nullable: true })
|
||||
@UseGuards(
|
||||
WorkspaceAuthGuard,
|
||||
|
||||
@@ -45,6 +45,7 @@ export enum SettingsPath {
|
||||
ApplicationLogicFunctionDetail = 'applications/:applicationId/logicFunctions/:logicFunctionId',
|
||||
AvailableApplicationDetail = 'applications/available/:availableApplicationId',
|
||||
ApplicationRegistrationDetail = 'applications/registrations/:applicationRegistrationId',
|
||||
ApplicationRegistrationConfigVariableDetails = 'applications/registrations/:applicationRegistrationId/config-variables/:variableKey',
|
||||
LogicFunctions = 'functions',
|
||||
NewLogicFunction = 'functions/new',
|
||||
LogicFunctionDetail = 'functions/:logicFunctionId',
|
||||
|
||||
@@ -7,7 +7,7 @@ import { type ThemeColor } from '@ui/theme';
|
||||
import { ThemeContext, themeCssVariables } from '@ui/theme-constants';
|
||||
import { useContext } from 'react';
|
||||
|
||||
const StyledTag = styled.h3<{
|
||||
const StyledTag = styled.span<{
|
||||
color: TagColor;
|
||||
weight: TagWeight;
|
||||
variant: TagVariant;
|
||||
|
||||
@@ -31,7 +31,7 @@ export const Default: Story = {
|
||||
play: async ({ canvasElement, args }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const tag = canvas.getByRole('heading', { level: 3 });
|
||||
const tag = canvas.getByText('Urgent');
|
||||
|
||||
await userEvent.click(tag);
|
||||
await expect(args.onClick).toHaveBeenCalled();
|
||||
|
||||
@@ -7,6 +7,7 @@ const StyledContainer = styled.div`
|
||||
border: 1px solid ${themeCssVariables.border.color.medium};
|
||||
background: ${themeCssVariables.background.secondary};
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: ${themeCssVariables.spacing[3]};
|
||||
gap: ${themeCssVariables.spacing[3]};
|
||||
|
||||
@@ -14,6 +14,7 @@ export {
|
||||
IconArchive,
|
||||
IconArchiveOff,
|
||||
IconArrowBackUp,
|
||||
IconArrowBarToDown,
|
||||
IconArrowDown,
|
||||
IconArrowLeft,
|
||||
IconArrowMerge,
|
||||
@@ -44,6 +45,7 @@ export {
|
||||
IconBracketsContain,
|
||||
IconBrain,
|
||||
IconBrandDaysCounter,
|
||||
IconBrandDocker,
|
||||
IconBrandGithub,
|
||||
IconBrandGoogle,
|
||||
IconBrandGraphql,
|
||||
@@ -199,6 +201,7 @@ export {
|
||||
IconFrame,
|
||||
IconFunction,
|
||||
IconGauge,
|
||||
IconGitBranch,
|
||||
IconGitBranchDeleted,
|
||||
IconGitCommit,
|
||||
IconGizmo,
|
||||
|
||||
@@ -93,6 +93,7 @@ export {
|
||||
IconArchive,
|
||||
IconArchiveOff,
|
||||
IconArrowBackUp,
|
||||
IconArrowBarToDown,
|
||||
IconArrowDown,
|
||||
IconArrowLeft,
|
||||
IconArrowMerge,
|
||||
@@ -123,6 +124,7 @@ export {
|
||||
IconBracketsContain,
|
||||
IconBrain,
|
||||
IconBrandDaysCounter,
|
||||
IconBrandDocker,
|
||||
IconBrandGithub,
|
||||
IconBrandGoogle,
|
||||
IconBrandGraphql,
|
||||
@@ -278,6 +280,7 @@ export {
|
||||
IconFrame,
|
||||
IconFunction,
|
||||
IconGauge,
|
||||
IconGitBranch,
|
||||
IconGitBranchDeleted,
|
||||
IconGitCommit,
|
||||
IconGizmo,
|
||||
|
||||
Reference in New Issue
Block a user