Use universal identifiers in all setting paths

This commit is contained in:
martmull 2026-04-16 19:53:52 +02:00
parent ea5091f8c0
commit 6a32032b78
28 changed files with 138 additions and 156 deletions

View file

@ -2524,6 +2524,7 @@ type File {
type MarketplaceApp {
id: String!
universalIdentifier: String!
name: String!
description: String!
icon: String!

View file

@ -2235,6 +2235,7 @@ export interface File {
export interface MarketplaceApp {
id: Scalars['String']
universalIdentifier: Scalars['String']
name: Scalars['String']
description: Scalars['String']
icon: Scalars['String']
@ -5564,6 +5565,7 @@ export interface FileGenqlSelection{
export interface MarketplaceAppGenqlSelection{
id?: boolean | number
universalIdentifier?: boolean | number
name?: boolean | number
description?: boolean | number
icon?: boolean | number

View file

@ -5036,6 +5036,9 @@ export default {
"id": [
1
],
"universalIdentifier": [
1
],
"name": [
1
],

File diff suppressed because one or more lines are too long

View file

@ -172,14 +172,6 @@ const SettingsApplications = lazy(() =>
),
);
const SettingsApplicationDetails = lazy(() =>
import('~/pages/settings/applications/SettingsApplicationDetails').then(
(module) => ({
default: module.SettingsApplicationDetails,
}),
),
);
const SettingsAdminApplicationRegistrationDetail = lazy(() =>
import(
'~/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDetail'
@ -188,12 +180,12 @@ const SettingsAdminApplicationRegistrationDetail = lazy(() =>
})),
);
const SettingsAvailableApplicationDetails = lazy(() =>
import(
'~/pages/settings/applications/SettingsAvailableApplicationDetails'
).then((module) => ({
default: module.SettingsAvailableApplicationDetails,
})),
const SettingsApplicationPage = lazy(() =>
import('~/pages/settings/applications/SettingsApplicationPage').then(
(module) => ({
default: module.SettingsApplicationPage,
}),
),
);
const SettingsApplicationRegistrationDetails = lazy(() =>
@ -738,11 +730,7 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
/>
<Route
path={SettingsPath.ApplicationDetail}
element={<SettingsApplicationDetails />}
/>
<Route
path={SettingsPath.AvailableApplicationDetail}
element={<SettingsAvailableApplicationDetails />}
element={<SettingsApplicationPage />}
/>
<Route
path={SettingsPath.ApplicationRegistrationDetail}

View file

@ -0,0 +1,15 @@
import { gql } from '@apollo/client';
import { APPLICATION_REGISTRATION_FRAGMENT } from '@/settings/application-registrations/graphql/fragments/applicationRegistrationFragment';
export const FIND_APPLICATION_REGISTRATION_BY_UNIVERSAL_IDENTIFIER = gql`
query findApplicationRegistrationByUniversalIdentifier(
$universalIdentifier: String!
) {
findApplicationRegistrationByUniversalIdentifier(
universalIdentifier: $universalIdentifier
) {
...ApplicationRegistrationFragment
}
${APPLICATION_REGISTRATION_FRAGMENT}
}
`;

View file

@ -3,6 +3,7 @@ import gql from 'graphql-tag';
export const MARKETPLACE_APP_FRAGMENT = gql`
fragment MarketplaceAppFields on MarketplaceApp {
id
universalIdentifier
name
description
icon

View file

@ -102,7 +102,10 @@ export const SettingsAdminApps = () => {
key={registration.id}
to={getSettingsPath(
SettingsPath.AdminPanelApplicationRegistrationDetail,
{ applicationRegistrationId: registration.id },
{
applicationUniversalIdentifier:
registration.universalIdentifier,
},
)}
gridAutoColumns={TABLE_GRID}
mobileGridAutoColumns={TABLE_GRID_MOBILE}

View file

@ -1,36 +0,0 @@
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
import { SettingsPath } from 'twenty-shared/types';
import { LinkChip } from 'twenty-ui/components';
import { getSettingsPath } from 'twenty-shared/utils';
import { useParams } from 'react-router-dom';
import { t } from '@lingui/core/macro';
import { Trans } from '@lingui/react/macro';
export const SettingsLogicFunctionTabEnvironmentVariablesSection = () => {
const { applicationId = '' } = useParams<{ applicationId: string }>();
return (
<Section>
<H2Title
title={t`Environment Variables`}
description={t`Accessible in your function via process.env.KEY`}
/>
<Trans>
Environment variables are defined at application level for all
functions. Please check{' '}
<LinkChip
label={t`application detail page`}
to={getSettingsPath(
SettingsPath.ApplicationDetail,
{
applicationId,
},
undefined,
'settings',
)}
/>
.
</Trans>
</Section>
);
};

View file

@ -1,5 +1,4 @@
import { SettingsLogicFunctionNewForm } from '@/settings/logic-functions/components/SettingsLogicFunctionNewForm';
import { SettingsLogicFunctionTabEnvironmentVariablesSection } from '@/settings/logic-functions/components/SettingsLogicFunctionTabEnvironmentVariablesSection';
import { type LogicFunctionFormValues } from '@/logic-functions/hooks/useLogicFunctionUpdateFormState';
export const SettingsLogicFunctionSettingsTab = ({
@ -12,12 +11,6 @@ export const SettingsLogicFunctionSettingsTab = ({
) => (value: LogicFunctionFormValues[TKey]) => void;
}) => {
return (
<>
<SettingsLogicFunctionNewForm
formValues={formValues}
onChange={onChange}
/>
<SettingsLogicFunctionTabEnvironmentVariablesSection />
</>
<SettingsLogicFunctionNewForm formValues={formValues} onChange={onChange} />
);
};

View file

@ -32,15 +32,15 @@ export const SettingsAdminApplicationRegistrationDetail = () => {
REGISTRATION_DETAIL_TAB_LIST_ID,
);
const { applicationRegistrationId = '' } = useParams<{
applicationRegistrationId: string;
const { applicationUniversalIdentifier = '' } = useParams<{
applicationUniversalIdentifier: string;
}>();
const { data, loading } = useQuery(
FindOneAdminApplicationRegistrationDocument,
{
variables: { id: applicationRegistrationId },
skip: !applicationRegistrationId,
variables: { id: applicationUniversalIdentifier },
skip: !applicationUniversalIdentifier,
},
);

View file

@ -27,7 +27,7 @@ import type { SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
const APPLICATION_DETAIL_ID = 'application-detail-id';
export const SettingsApplicationDetails = () => {
export const SettingsApplicationDetailsToRemove = () => {
const { applicationId = '' } = useParams<{ applicationId: string }>();
const activeTabId = useAtomComponentStateValue(

View file

@ -38,10 +38,10 @@ import { Section } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useQuery } from '@apollo/client/react';
import {
PermissionFlagType,
FindOneApplicationByUniversalIdentifierDocument,
FindMarketplaceAppDetailDocument,
ApplicationRegistrationSourceType,
FindMarketplaceAppDetailDocument,
FindOneApplicationByUniversalIdentifierDocument,
PermissionFlagType,
} from '~/generated-metadata/graphql';
import { SettingsApplicationPermissionsTab } from '~/pages/settings/applications/tabs/SettingsApplicationPermissionsTab';
import { SettingsAvailableApplicationDetailContentTab } from '~/pages/settings/applications/tabs/SettingsAvailableApplicationDetailContentTab';
@ -179,9 +179,9 @@ const StyledSectionTitle = styled.h2`
const StyledAboutContainer = styled.div``;
export const SettingsAvailableApplicationDetails = () => {
const { availableApplicationId = '' } = useParams<{
availableApplicationId: string;
export const SettingsApplicationPage = () => {
const { applicationUniversalIdentifier = '' } = useParams<{
applicationUniversalIdentifier: string;
}>();
const [selectedScreenshotIndex, setSelectedScreenshotIndex] = useState(0);
@ -195,14 +195,14 @@ export const SettingsAvailableApplicationDetails = () => {
const { data: applicationData } = useQuery(
FindOneApplicationByUniversalIdentifierDocument,
{
variables: { universalIdentifier: availableApplicationId },
skip: !availableApplicationId,
variables: { universalIdentifier: applicationUniversalIdentifier },
skip: !applicationUniversalIdentifier,
},
);
const { data: detailData } = useQuery(FindMarketplaceAppDetailDocument, {
variables: { universalIdentifier: availableApplicationId },
skip: !availableApplicationId,
variables: { universalIdentifier: applicationUniversalIdentifier },
skip: !applicationUniversalIdentifier,
});
const application = applicationData?.findOneApplication;

View file

@ -3,7 +3,7 @@ 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 { FindOneApplicationRegistrationDocument } from '~/generated-metadata/graphql';
import { FindApplicationRegistrationByUniversalIdentifierDocument } from '~/generated-metadata/graphql';
import { useLingui } from '@lingui/react/macro';
import { Tag } from 'twenty-ui/components';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
@ -32,16 +32,19 @@ export const SettingsApplicationRegistrationDetails = () => {
REGISTRATION_DETAIL_TAB_LIST_ID,
);
const { applicationRegistrationId = '' } = useParams<{
applicationRegistrationId: string;
const { applicationUniversalIdentifier = '' } = useParams<{
applicationUniversalIdentifier: string;
}>();
const { data, loading } = useQuery(FindOneApplicationRegistrationDocument, {
variables: { id: applicationRegistrationId },
skip: !applicationRegistrationId,
});
const { data, loading } = useQuery(
FindApplicationRegistrationByUniversalIdentifierDocument,
{
variables: { universalIdentifier: applicationUniversalIdentifier },
skip: !applicationUniversalIdentifier,
},
);
const registration = data?.findOneApplicationRegistration;
const registration = data?.findApplicationRegistrationByUniversalIdentifier;
if (loading || !isDefined(registration)) {
return null;

View file

@ -2,12 +2,11 @@ import { useLingui } from '@lingui/react/macro';
import { useParams } from 'react-router-dom';
import { useState } from 'react';
import {
FindApplicationRegistrationByUniversalIdentifierDocument,
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';
@ -25,26 +24,26 @@ export const SettingsApplicationRegistrationConfigVariableDetail = () => {
const [isEditing, setIsEditing] = useState(false);
const { applicationRegistrationId = '' } = useParams<{
applicationRegistrationId: string;
const { applicationUniversalIdentifier = '' } = useParams<{
applicationUniversalIdentifier: string;
}>();
const { data: applicationRegistrationData } = useQuery(
FindOneApplicationRegistrationDocument,
FindApplicationRegistrationByUniversalIdentifierDocument,
{
variables: { id: applicationRegistrationId },
skip: !applicationRegistrationId,
variables: { universalIdentifier: applicationUniversalIdentifier },
skip: !applicationUniversalIdentifier,
},
);
const registration =
applicationRegistrationData?.findOneApplicationRegistration;
applicationRegistrationData?.findApplicationRegistrationByUniversalIdentifier;
const { data: variablesData } = useQuery(
FindApplicationRegistrationVariablesDocument,
{
variables: { applicationRegistrationId },
skip: !applicationRegistrationId,
variables: { applicationRegistrationId: registration?.id ?? '' },
skip: !registration?.id,
},
);
@ -128,7 +127,7 @@ export const SettingsApplicationRegistrationConfigVariableDetail = () => {
children: t`${registration.name} - Config`,
href: getSettingsPath(
SettingsPath.ApplicationRegistrationDetail,
{ applicationRegistrationId },
{ applicationUniversalIdentifier },
undefined,
'config',
),

View file

@ -92,8 +92,8 @@ export const SettingsApplicationRegistrationGeneralInfo = ({
},
);
const shareLink = getSettingsPath(SettingsPath.AvailableApplicationDetail, {
availableApplicationId: registration.universalIdentifier,
const shareLink = getSettingsPath(SettingsPath.ApplicationDetail, {
applicationUniversalIdentifier: registration.universalIdentifier,
});
const ownerWorkspace = ownerWorkspaceData?.getPublicWorkspaceDataById;

View file

@ -95,7 +95,8 @@ export const SettingsApplicationsTable = ({
/>
}
link={getSettingsPath(SettingsPath.ApplicationDetail, {
applicationId: application.id,
applicationUniversalIdentifier:
application.universalIdentifier,
})}
/>
);

View file

@ -48,8 +48,8 @@ export const SettingsAvailableApplicationCard = ({
return (
<StyledLinkContainer>
<Link
to={getSettingsPath(SettingsPath.AvailableApplicationDetail, {
availableApplicationId: application.id,
to={getSettingsPath(SettingsPath.ApplicationDetail, {
applicationUniversalIdentifier: application.universalIdentifier,
})}
>
<Card rounded fullWidth>

View file

@ -42,7 +42,7 @@ export const SettingsApplicationRegistrationConfigTab = ({
to: getSettingsPath(
SettingsPath.ApplicationRegistrationConfigVariableDetails,
{
applicationRegistrationId,
applicationUniversalIdentifier: registration.universalIdentifier,
variableKey: variable.key,
},
),

View file

@ -24,8 +24,8 @@ export const SettingsApplicationRegistrationDistributionTab = ({
const isTarballSource =
registration.sourceType === ApplicationRegistrationSourceType.TARBALL;
const shareLink = getSettingsPath(SettingsPath.AvailableApplicationDetail, {
availableApplicationId: registration.universalIdentifier,
const shareLink = getSettingsPath(SettingsPath.ApplicationDetail, {
applicationUniversalIdentifier: registration.universalIdentifier,
});
const publishCommands = ['yarn twenty publish'];

View file

@ -20,11 +20,11 @@ import {
Avatar,
CommandBlock,
H2Title,
IconArrowUpRight,
IconChevronRight,
IconCopy,
IconArrowUpRight,
OverflowingTextWithTooltip,
InlineBanner,
OverflowingTextWithTooltip,
} from 'twenty-ui/display';
import { Button, SearchInput } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
@ -121,7 +121,7 @@ export const SettingsApplicationsDeveloperTab = () => {
registration: ApplicationRegistrationFragmentFragment,
) =>
getSettingsPath(SettingsPath.ApplicationRegistrationDetail, {
applicationRegistrationId: registration.id,
applicationUniversalIdentifier: registration.universalIdentifier,
});
return (
@ -232,12 +232,10 @@ export const SettingsApplicationsDeveloperTab = () => {
<TableRow
key={application.id}
gridAutoColumns={NPM_PACKAGES_GRID_COLUMNS}
to={getSettingsPath(
SettingsPath.AvailableApplicationDetail,
{
availableApplicationId: application.id,
},
)}
to={getSettingsPath(SettingsPath.ApplicationDetail, {
applicationUniversalIdentifier:
application.universalIdentifier,
})}
>
<StyledNameTableCell>
<Avatar

View file

@ -3,6 +3,7 @@ import { type Application } from '~/generated-metadata/graphql';
export type ApplicationWithoutRelation = Pick<
Application,
| 'id'
| 'universalIdentifier'
| 'name'
| 'description'
| 'version'

View file

@ -20,7 +20,7 @@ import {
IconSettings,
} from 'twenty-ui/display';
import { useQuery } from '@apollo/client/react';
import { FindOneApplicationDocument } from '~/generated-metadata/graphql';
import { FindOneApplicationByUniversalIdentifierDocument } from '~/generated-metadata/graphql';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { SettingsLogicFunctionCodeEditorTab } from '@/settings/logic-functions/components/tabs/SettingsLogicFunctionCodeEditorTab';
@ -29,25 +29,30 @@ import { useExecuteLogicFunction } from '@/logic-functions/hooks/useExecuteLogic
const LOGIC_FUNCTION_DETAIL_ID = 'logic-function-detail';
export const SettingsLogicFunctionDetail = () => {
const { logicFunctionId = '', applicationId = '' } = useParams();
const { logicFunctionId = '', applicationUniversalIdentifier = '' } =
useParams();
const navigate = useNavigate();
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const { data, loading: applicationLoading } = useQuery(
FindOneApplicationDocument,
const { data: applicationData, loading: applicationLoading } = useQuery(
FindOneApplicationByUniversalIdentifierDocument,
{
variables: { id: applicationId },
skip: !applicationId,
variables: { universalIdentifier: applicationUniversalIdentifier },
skip: !applicationUniversalIdentifier,
},
);
const applicationName = data?.findOneApplication?.name;
const application = applicationData?.findOneApplication;
if (!application) {
return null;
}
const workspaceCustomApplicationId =
currentWorkspace?.workspaceCustomApplication?.id;
const isManaged = applicationId !== workspaceCustomApplicationId;
const isManaged = application.id !== workspaceCustomApplicationId;
const instanceId = `${LOGIC_FUNCTION_DETAIL_ID}-${logicFunctionId}`;
@ -88,7 +93,7 @@ export const SettingsLogicFunctionDetail = () => {
const isTestTab = activeTabId === 'test';
const breadcrumbLinks =
isDefined(applicationId) && applicationId !== ''
isDefined(application.id) && application.id !== ''
? [
{
children: t`Workspace`,
@ -99,11 +104,11 @@ export const SettingsLogicFunctionDetail = () => {
href: getSettingsPath(SettingsPath.Applications),
},
{
children: `${applicationName}`,
children: `${application.name} - ${t`Content`}`,
href: getSettingsPath(
SettingsPath.ApplicationDetail,
{
applicationId,
applicationUniversalIdentifier: application.universalIdentifier,
},
undefined,
'content',

View file

@ -15,6 +15,11 @@ export class MarketplaceAppDTO {
@Field()
id: string;
@IsString()
@IsNotEmpty()
@Field()
universalIdentifier: string;
@IsString()
@IsNotEmpty()
@Field()

View file

@ -84,7 +84,8 @@ export class MarketplaceQueryService {
const app = registration.manifest?.application;
return {
id: registration.universalIdentifier,
id: registration.id,
universalIdentifier: registration.universalIdentifier,
name: app?.displayName ?? registration.name,
description: app?.description ?? '',
icon: app?.icon ?? 'IconApps',

View file

@ -26,9 +26,18 @@ export class ApplicationRegistrationVariableService {
) {}
async findVariables(
applicationRegistrationId: string,
applicationUniversalIdentifier: string,
workspaceId: string,
): Promise<ApplicationRegistrationVariableEntity[]> {
const applicationRegistration =
await this.applicationRegistrationRepository.findOneOrFail({
where: {
universalIdentifier: applicationUniversalIdentifier,
},
});
const applicationRegistrationId = applicationRegistration.id;
await this.assertRegistrationOwnedByWorkspace(
applicationRegistrationId,
workspaceId,

View file

@ -30,24 +30,6 @@ export class ApplicationService {
private readonly workspaceRepository: Repository<WorkspaceEntity>,
) {}
async findApplicationRoleId(
applicationId: string,
workspaceId: string,
): Promise<string> {
const application = await this.applicationRepository.findOne({
where: { id: applicationId, workspaceId },
});
if (!isDefined(application) || !isDefined(application.defaultRoleId)) {
throw new ApplicationException(
`Could not find application ${applicationId}`,
ApplicationExceptionCode.APPLICATION_NOT_FOUND,
);
}
return application.defaultRoleId;
}
async findWorkspaceTwentyStandardAndCustomApplicationOrThrow({
workspace: workspaceInput,
workspaceId,

View file

@ -41,11 +41,10 @@ export enum SettingsPath {
AISkillDetail = 'ai/skills/:skillId',
AIToolDetail = 'ai/tools/:toolIdentifier',
Applications = 'applications',
ApplicationDetail = 'applications/:applicationId',
ApplicationLogicFunctionDetail = 'applications/:applicationId/logicFunctions/:logicFunctionId',
AvailableApplicationDetail = 'applications/available/:availableApplicationId',
ApplicationRegistrationDetail = 'applications/registrations/:applicationRegistrationId',
ApplicationRegistrationConfigVariableDetails = 'applications/registrations/:applicationRegistrationId/config-variables/:variableKey',
ApplicationLogicFunctionDetail = 'applications/:applicationUniversalIdentifier/logicFunctions/:logicFunctionId',
ApplicationDetail = 'applications/:applicationUniversalIdentifier',
ApplicationRegistrationDetail = 'applications/registrations/:applicationUniversalIdentifier',
ApplicationRegistrationConfigVariableDetails = 'applications/registrations/:applicationUniversalIdentifier/config-variables/:variableKey',
LogicFunctions = 'functions',
NewLogicFunction = 'functions/new',
LogicFunctionDetail = 'functions/:logicFunctionId',
@ -72,7 +71,7 @@ export enum SettingsPath {
AdminPanelNewAiModel = 'admin-panel/ai/providers/:providerName/new-model',
AdminPanelUserDetail = 'admin-panel/users/:userId',
AdminPanelWorkspaceDetail = 'admin-panel/workspaces/:workspaceId',
AdminPanelApplicationRegistrationDetail = 'admin-panel/applications/registrations/:applicationRegistrationId',
AdminPanelApplicationRegistrationDetail = 'admin-panel/applications/registrations/:applicationUniversalIdentifier',
AdminPanelWorkspaceChatThread = 'admin-panel/workspaces/:workspaceId/threads/:threadId',
Roles = 'roles',