diff --git a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/ConfigVariableActionButtons.tsx b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/ConfigVariableActionButtons.tsx
deleted file mode 100644
index f6706f33df..0000000000
--- a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/ConfigVariableActionButtons.tsx
+++ /dev/null
@@ -1,65 +0,0 @@
-import { useLingui } from '@lingui/react/macro';
-import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
-
-import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState';
-import {
- IconDeviceFloppy,
- IconPencil,
- IconRefreshAlert,
-} from 'twenty-ui/display';
-import { Button } from 'twenty-ui/input';
-import {
- ConfigSource,
- type ConfigVariable,
-} from '~/generated-metadata/graphql';
-
-type ConfigVariableActionButtonsProps = {
- variable: ConfigVariable;
- isValueValid: boolean;
- isSubmitting: boolean;
- onSave: () => void;
- onReset: () => void;
-};
-
-export const ConfigVariableActionButtons = ({
- variable,
- isValueValid,
- isSubmitting,
- onSave,
- onReset,
-}: ConfigVariableActionButtonsProps) => {
- const { t } = useLingui();
- const isConfigVariablesInDbEnabled = useAtomStateValue(
- isConfigVariablesInDbEnabledState,
- );
- const isFromDatabase = variable.source === ConfigSource.DATABASE;
-
- return (
- <>
- {isConfigVariablesInDbEnabled &&
- variable.source === ConfigSource.DATABASE && (
-
- )}
- {isConfigVariablesInDbEnabled && !variable.isEnvOnly && (
-
- )}
- >
- );
-};
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigVariablesRow.tsx b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigVariablesRow.tsx
deleted file mode 100644
index 35239a6df2..0000000000
--- a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigVariablesRow.tsx
+++ /dev/null
@@ -1,82 +0,0 @@
-import { TableCell } from '@/ui/layout/table/components/TableCell';
-import { TableRow } from '@/ui/layout/table/components/TableRow';
-import { styled } from '@linaria/react';
-import { SettingsPath } from 'twenty-shared/types';
-import { getSettingsPath } from 'twenty-shared/utils';
-import { IconChevronRight } from 'twenty-ui/display';
-import { useContext } from 'react';
-import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
-import { type ConfigVariable } from '~/generated-metadata/graphql';
-
-type SettingsAdminConfigVariablesRowProps = {
- variable: ConfigVariable;
-};
-
-const StyledTableRowContainer = styled.div`
- > * {
- &:hover {
- background-color: ${themeCssVariables.background.transparent.light};
- }
- }
-`;
-
-const StyledEllipsisLabel = styled.div`
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
-`;
-
-export const SettingsAdminConfigVariablesRow = ({
- variable,
-}: SettingsAdminConfigVariablesRowProps) => {
- const { theme } = useContext(ThemeContext);
-
- const displayValue =
- variable.value === ''
- ? 'null'
- : variable.isSensitive
- ? '••••••'
- : typeof variable.value === 'boolean'
- ? variable.value
- ? 'true'
- : 'false'
- : typeof variable.value === 'object' && variable.value !== null
- ? JSON.stringify(variable.value)
- : variable.value;
-
- return (
-
-
-
- {variable.name}
-
-
- {displayValue}
-
-
-
-
-
-
- );
-};
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigVariablesTable.tsx b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigVariablesTable.tsx
index bff3e09e00..e42d505eb7 100644
--- a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigVariablesTable.tsx
+++ b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/components/SettingsAdminConfigVariablesTable.tsx
@@ -1,16 +1,7 @@
-import { t } from '@lingui/core/macro';
-import { SettingsAdminConfigVariablesRow } from '@/settings/admin-panel/config-variables/components/SettingsAdminConfigVariablesRow';
-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 { themeCssVariables } from 'twenty-ui/theme-constants';
import { type ConfigVariable } from '~/generated-metadata/graphql';
-
-const StyledTableBodyContainer = styled.div`
- border-bottom: 1px solid ${themeCssVariables.border.color.light};
-`;
+import { ConfigVariableTable } from '@/settings/config-variables/components/ConfigVariableTable';
+import { getSettingsPath } from 'twenty-shared/utils';
+import { SettingsPath } from 'twenty-shared/types';
type SettingsAdminConfigVariablesTableProps = {
variables: ConfigVariable[];
@@ -19,23 +10,25 @@ type SettingsAdminConfigVariablesTableProps = {
export const SettingsAdminConfigVariablesTable = ({
variables,
}: SettingsAdminConfigVariablesTableProps) => {
- return (
-
-
- {t`Name`}
- {t`Value`}
-
-
-
-
- {variables.map((variable) => (
-
- ))}
-
-
-
- );
+ const configVariables = variables.map((variable) => ({
+ name: variable.name,
+ description: variable.description,
+ value:
+ variable.value === ''
+ ? 'null'
+ : variable.isSensitive
+ ? '••••••'
+ : typeof variable.value === 'boolean'
+ ? variable.value
+ ? 'true'
+ : 'false'
+ : typeof variable.value === 'object' && variable.value !== null
+ ? JSON.stringify(variable.value)
+ : variable.value,
+ to: getSettingsPath(SettingsPath.AdminPanelConfigVariableDetails, {
+ variableName: variable.name,
+ }),
+ }));
+
+ return ;
};
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/hooks/useConfigVariableActions.ts b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/hooks/useConfigVariableActions.ts
index 43fb4e0215..9f5b54a47f 100644
--- a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/hooks/useConfigVariableActions.ts
+++ b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/hooks/useConfigVariableActions.ts
@@ -1,8 +1,5 @@
-import { useLingui } from '@lingui/react/macro';
-
import { useClientConfig } from '@/client-config/hooks/useClientConfig';
import { GET_DATABASE_CONFIG_VARIABLE } from '@/settings/admin-panel/config-variables/graphql/queries/getDatabaseConfigVariable';
-import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { type ConfigVariableValue } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { useMutation } from '@apollo/client/react';
@@ -13,8 +10,6 @@ import {
} from '~/generated-metadata/graphql';
export const useConfigVariableActions = (variableName: string) => {
- const { t } = useLingui();
- const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
const { refetch: refetchClientConfig } = useClientConfig();
const [updateDatabaseConfigVariable] = useMutation(
@@ -31,65 +26,20 @@ export const useConfigVariableActions = (variableName: string) => {
value: ConfigVariableValue,
isFromDatabase: boolean,
) => {
- try {
- if (
- value === null ||
- (typeof value === 'string' && value === '') ||
- (Array.isArray(value) && value.length === 0)
- ) {
- await handleDeleteVariable();
- return;
- }
-
- if (isFromDatabase) {
- await updateDatabaseConfigVariable({
- variables: {
- key: variableName,
- value,
- },
- refetchQueries: [
- {
- query: GET_DATABASE_CONFIG_VARIABLE,
- variables: { key: variableName },
- },
- ],
- });
- } else {
- await createDatabaseConfigVariable({
- variables: {
- key: variableName,
- value,
- },
- refetchQueries: [
- {
- query: GET_DATABASE_CONFIG_VARIABLE,
- variables: { key: variableName },
- },
- ],
- });
- }
-
- await refetchClientConfig();
-
- enqueueSuccessSnackBar({
- message: t`Variable updated successfully.`,
- });
- } catch {
- enqueueErrorSnackBar({
- message: t`Failed to update variable`,
- });
- }
- };
-
- const handleDeleteVariable = async (e?: React.MouseEvent) => {
- if (isDefined(e)) {
- e.preventDefault();
+ if (
+ value === null ||
+ (typeof value === 'string' && value === '') ||
+ (Array.isArray(value) && value.length === 0)
+ ) {
+ await handleDeleteVariable();
+ return;
}
- try {
- await deleteDatabaseConfigVariable({
+ if (isFromDatabase) {
+ await updateDatabaseConfigVariable({
variables: {
key: variableName,
+ value,
},
refetchQueries: [
{
@@ -98,17 +48,42 @@ export const useConfigVariableActions = (variableName: string) => {
},
],
});
-
- await refetchClientConfig();
-
- enqueueSuccessSnackBar({
- message: t`Variable deleted successfully.`,
- });
- } catch {
- enqueueErrorSnackBar({
- message: t`Failed to remove override`,
+ } else {
+ await createDatabaseConfigVariable({
+ variables: {
+ key: variableName,
+ value,
+ },
+ refetchQueries: [
+ {
+ query: GET_DATABASE_CONFIG_VARIABLE,
+ variables: { key: variableName },
+ },
+ ],
});
}
+
+ await refetchClientConfig();
+ };
+
+ const handleDeleteVariable = async (e?: React.MouseEvent) => {
+ if (isDefined(e)) {
+ e.preventDefault();
+ }
+
+ await deleteDatabaseConfigVariable({
+ variables: {
+ key: variableName,
+ },
+ refetchQueries: [
+ {
+ query: GET_DATABASE_CONFIG_VARIABLE,
+ variables: { key: variableName },
+ },
+ ],
+ });
+
+ await refetchClientConfig();
};
return {
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/hooks/useConfigVariableForm.ts b/packages/twenty-front/src/modules/settings/admin-panel/config-variables/hooks/useConfigVariableForm.ts
deleted file mode 100644
index 7935d65382..0000000000
--- a/packages/twenty-front/src/modules/settings/admin-panel/config-variables/hooks/useConfigVariableForm.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-import { zodResolver } from '@hookform/resolvers/zod';
-import { useForm } from 'react-hook-form';
-import { type ConfigVariableValue } from 'twenty-shared/types';
-import { z } from 'zod';
-
-import { type ConfigVariable } from '~/generated-metadata/graphql';
-
-type FormValues = {
- value: ConfigVariableValue;
-};
-
-const hasMeaningfulValue = (value: ConfigVariableValue): boolean => {
- if (value === null || value === undefined) {
- return false;
- }
- if (typeof value === 'string') {
- return value.trim() !== '';
- }
- if (Array.isArray(value)) {
- return value.length > 0;
- }
- return true;
-};
-
-export const useConfigVariableForm = (variable?: ConfigVariable) => {
- const validationSchema = z.object({
- value: z.union([
- z.string(),
- z.number(),
- z.boolean(),
- z.array(z.string()),
- z.record(z.string(), z.unknown()),
- z.null(),
- ]),
- });
-
- const {
- control,
- handleSubmit,
- reset,
- formState: { isSubmitting, isDirty },
- watch,
- } = useForm({
- resolver: zodResolver(validationSchema),
- values: { value: variable?.value ?? null },
- });
-
- const currentValue = watch('value');
- const isValueValid =
- variable !== undefined &&
- !variable.isEnvOnly &&
- isDirty &&
- hasMeaningfulValue(currentValue);
-
- return {
- control,
- handleSubmit,
- reset,
- isSubmitting,
- currentValue,
- hasValueChanged: isDirty,
- isValueValid,
- };
-};
diff --git a/packages/twenty-front/src/modules/settings/config-variables/components/ConfigVariableEdit.tsx b/packages/twenty-front/src/modules/settings/config-variables/components/ConfigVariableEdit.tsx
new file mode 100644
index 0000000000..d5ffebfd96
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/config-variables/components/ConfigVariableEdit.tsx
@@ -0,0 +1,169 @@
+import { styled } from '@linaria/react';
+import { H3Title, IconCheck, IconPencil, IconX } from 'twenty-ui/display';
+import { Button } from 'twenty-ui/input';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
+import { useLingui } from '@lingui/react/macro';
+import { Section } from 'twenty-ui/layout';
+import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
+import { type Dispatch, type SetStateAction, useState } from 'react';
+import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+import { useModal } from '@/ui/layout/modal/hooks/useModal';
+import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
+
+const RESET_VARIABLE_MODAL_ID =
+ 'reset-application-registration-config-variable-modal';
+
+const StyledRow = styled.div`
+ align-items: flex-end;
+ display: flex;
+ gap: ${themeCssVariables.spacing[2]};
+`;
+
+const StyledButtonContainer = styled.div`
+ display: flex;
+ & > :not(:first-of-type) > button {
+ border-left: none;
+ }
+`;
+
+type ConfigVariableEditProps = {
+ title: string;
+ description?: string;
+ input: React.ReactNode;
+ isEditing: boolean;
+ setIsEditing: Dispatch>;
+ isSaveDisabled?: boolean;
+ canOpenCancelModal?: boolean;
+ onSave?: () => Promise;
+ onCancel: () => void;
+ onEdit?: () => void;
+ onConfirmReset?: () => Promise;
+ editDisabled?: boolean;
+ helpContent?: React.ReactNode;
+};
+
+export const ConfigVariableEdit = ({
+ title,
+ description,
+ input,
+ isEditing,
+ setIsEditing,
+ canOpenCancelModal,
+ isSaveDisabled = false,
+ onSave,
+ onCancel,
+ onEdit,
+ onConfirmReset,
+ editDisabled = false,
+ helpContent,
+}: ConfigVariableEditProps) => {
+ const { t } = useLingui();
+
+ const { openModal } = useModal();
+
+ const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
+
+ const [isSubmitting, setIsSubmitting] = useState(false);
+
+ const handleSave = async () => {
+ try {
+ setIsSubmitting(true);
+ await onSave?.();
+ enqueueSuccessSnackBar({
+ message: t`Variable ${title} updated`,
+ });
+ } catch {
+ enqueueErrorSnackBar({
+ message: t`Error updating variable`,
+ });
+ } finally {
+ setIsSubmitting(false);
+ setIsEditing(false);
+ }
+ };
+
+ const handleConfirmReset = async () => {
+ try {
+ setIsSubmitting(true);
+ await onConfirmReset?.();
+ enqueueSuccessSnackBar({
+ message: t`Variable ${title} reset`,
+ });
+ } catch {
+ enqueueErrorSnackBar({
+ message: t`Error resetting variable`,
+ });
+ } finally {
+ setIsSubmitting(false);
+ setIsEditing(false);
+ }
+ };
+
+ const handleCancel = () => {
+ if (canOpenCancelModal) {
+ openModal(RESET_VARIABLE_MODAL_ID);
+ return;
+ }
+
+ onCancel?.();
+
+ setIsEditing(false);
+ };
+
+ const handleEdit = () => {
+ onEdit?.();
+ setIsEditing(true);
+ };
+
+ return (
+
+
+
+
+
+ {input}
+ {!isEditing ? (
+
+ ) : (
+
+
+
+
+ )}
+
+
+ {helpContent}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/settings/config-variables/components/ConfigVariableTable.tsx b/packages/twenty-front/src/modules/settings/config-variables/components/ConfigVariableTable.tsx
new file mode 100644
index 0000000000..161c2134c8
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/config-variables/components/ConfigVariableTable.tsx
@@ -0,0 +1,95 @@
+import { TableRow } from '@/ui/layout/table/components/TableRow';
+import { TableBody } from '@/ui/layout/table/components/TableBody';
+import { Table } from '@/ui/layout/table/components/Table';
+import { styled } from '@linaria/react';
+import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
+import { TableHeader } from '@/ui/layout/table/components/TableHeader';
+import { t } from '@lingui/core/macro';
+import { TableCell } from '@/ui/layout/table/components/TableCell';
+import {
+ IconChevronRight,
+ OverflowingTextWithTooltip,
+} from 'twenty-ui/display';
+import { useContext } from 'react';
+
+const StyledTableBodyContainer = styled.div`
+ border-bottom: 1px solid ${themeCssVariables.border.color.light};
+`;
+
+const GRID_AUTO_COLUMNS = '5fr 3fr 3fr 1fr';
+
+type ConfigVariable = {
+ name: string;
+ description?: string;
+ value?: string | React.ReactNode;
+ to: string;
+};
+
+type ConfigVariableTableProps = { configVariables: ConfigVariable[] };
+
+export const ConfigVariableTable = ({
+ configVariables,
+}: ConfigVariableTableProps) => {
+ const { theme } = useContext(ThemeContext);
+
+ return (
+
+
+ {t`Name`}
+ {t`Description`}
+ {t`Value`}
+
+
+
+
+ {configVariables.map((variable, index) => (
+
+
+
+
+
+
+
+
+ {typeof variable.value === 'string' ? (
+
+ ) : (
+ variable.value
+ )}
+
+
+
+
+
+ ))}
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
new file mode 100644
index 0000000000..bd4fd7d1d3
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone.tsx
@@ -0,0 +1,248 @@
+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 { 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 {
+ H1Title,
+ H1TitleFontColor,
+ H2Title,
+ IconShare,
+ IconTrash,
+ AppTooltip,
+ TooltipDelay,
+} from 'twenty-ui/display';
+import { Button } from 'twenty-ui/input';
+import { Section, SectionAlignment, SectionFontColor } from 'twenty-ui/layout';
+import {
+ type ApplicationRegistration,
+ DeleteApplicationRegistrationDocument,
+ FindApplicationRegistrationStatsDocument,
+ FindManyApplicationRegistrationsDocument,
+ TransferApplicationRegistrationOwnershipDocument,
+} from '~/generated-metadata/graphql';
+import { useNavigateSettings } from '~/hooks/useNavigateSettings';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+import {
+ StyledAppModal,
+ StyledAppModalButton,
+ StyledAppModalSection,
+ StyledAppModalTitle,
+} from '~/pages/settings/applications/components/SettingsAppModalLayout';
+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 DELETE_REGISTRATION_BUTTON_ID = 'delete-registration-button';
+
+const StyledDangerButtonGroup = styled.div`
+ display: flex;
+ gap: ${themeCssVariables.spacing[2]};
+`;
+
+export const SettingsAdminApplicationRegistrationDangerZone = ({
+ registration,
+}: {
+ registration: ApplicationRegistration;
+}) => {
+ const { t } = useLingui();
+ const navigate = useNavigateSettings();
+ const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
+ const { openModal, closeModal } = useModal();
+
+ const [isLoading, setIsLoading] = useState(false);
+ const [isTransferring, setIsTransferring] = useState(false);
+ const [transferSubdomain, setTransferSubdomain] = useState('');
+
+ const applicationRegistrationId = registration.id;
+
+ 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,
+ {
+ refetchQueries: [FindManyApplicationRegistrationsDocument],
+ },
+ );
+
+ const [transferOwnership] = useMutation(
+ TransferApplicationRegistrationOwnershipDocument,
+ {
+ refetchQueries: [FindManyApplicationRegistrationsDocument],
+ },
+ );
+
+ const handleDelete = async () => {
+ setIsLoading(true);
+ try {
+ await deleteRegistration({
+ variables: { id: applicationRegistrationId },
+ });
+ navigate(SettingsPath.Applications);
+ } catch {
+ enqueueErrorSnackBar({
+ message: t`Error deleting app`,
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleTransferOwnership = async () => {
+ const trimmed = transferSubdomain.trim();
+
+ if (!isNonEmptyString(trimmed)) {
+ return;
+ }
+
+ setIsTransferring(true);
+ try {
+ await transferOwnership({
+ variables: {
+ applicationRegistrationId,
+ targetWorkspaceSubdomain: trimmed,
+ },
+ });
+ enqueueSuccessSnackBar({
+ message: t`Ownership transferred successfully`,
+ });
+ setTransferSubdomain('');
+ navigate(SettingsPath.Applications);
+ } catch {
+ enqueueErrorSnackBar({
+ message: t`Failed to transfer ownership. Check that the subdomain is correct.`,
+ });
+ } finally {
+ setIsTransferring(false);
+ }
+ };
+
+ const confirmationValue = t`yes`;
+
+ return (
+ <>
+
+
+
+
+
+
+
+ Please type {`"${confirmationValue}"`} to confirm you want to delete
+ this app. All workspace installations linked to it will lose their
+ OAuth credentials.
+
+ }
+ onConfirmClick={handleDelete}
+ confirmButtonText={t`Delete`}
+ loading={isLoading}
+ />
+
+ setTransferSubdomain('')}
+ padding="large"
+ dataGloballyPreventClickOutside
+ >
+
+
+
+
+ {t`Enter the workspace subdomain to transfer this app to. You will lose access to manage it.`}
+
+
+ {
+ closeModal(TRANSFER_OWNERSHIP_MODAL_ID);
+ setTransferSubdomain('');
+ }}
+ variant="secondary"
+ title={t`Cancel`}
+ fullWidth
+ />
+
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDetail.tsx b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDetail.tsx
index 9cfa511e98..d6dc485ffb 100644
--- a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDetail.tsx
+++ b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDetail.tsx
@@ -1,31 +1,37 @@
import { useParams } from 'react-router-dom';
-import { useMutation, useQuery } from '@apollo/client/react';
-import {
- FindAllApplicationRegistrationsDocument,
- FindOneAdminApplicationRegistrationDocument,
- UpdateApplicationRegistrationDocument,
-} from '~/generated-metadata/graphql';
+import { useQuery } from '@apollo/client/react';
+import { FindOneAdminApplicationRegistrationDocument } from '~/generated-metadata/graphql';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
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';
+import {
+ IconInfoCircle,
+ IconKey,
+ IconSettings,
+ IconWorld,
+} from 'twenty-ui/display';
+import { SettingsApplicationRegistrationConfigTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationConfigTab';
+import { SettingsApplicationRegistrationOAuthTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationOAuthTab';
+import { SettingsApplicationRegistrationDistributionTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationDistributionTab';
+import { SettingsApplicationRegistrationGeneralTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationGeneralTab';
+import { TabList } from '@/ui/layout/tab-list/components/TabList';
+import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
-const StyledToggleContainer = styled.div`
- display: flex;
- margin-top: ${themeCssVariables.spacing[4]};
-`;
+const REGISTRATION_DETAIL_TAB_LIST_ID =
+ 'admin-application-registration-detail-tab-list';
export const SettingsAdminApplicationRegistrationDetail = () => {
const { t } = useLingui();
+ const activeTabId = useAtomComponentStateValue(
+ activeTabIdComponentState,
+ REGISTRATION_DETAIL_TAB_LIST_ID,
+ );
+
const { applicationRegistrationId = '' } = useParams<{
applicationRegistrationId: string;
}>();
@@ -38,22 +44,50 @@ export const SettingsAdminApplicationRegistrationDetail = () => {
},
);
- const [updateRegistration] = useMutation(
- UpdateApplicationRegistrationDocument,
- {
- refetchQueries: [
- FindOneAdminApplicationRegistrationDocument,
- FindAllApplicationRegistrationsDocument,
- ],
- },
- );
-
const registration = data?.findOneAdminApplicationRegistration;
if (loading || !isDefined(registration)) {
return null;
}
+ const tabs = [
+ { 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 (
+
+ );
+ case 'oauth':
+ return (
+
+ );
+ case 'distribution':
+ return (
+
+ );
+ case 'general':
+ default:
+ return (
+
+ );
+ }
+ };
+
return (
{
]}
>
-
-
-
-
-
- updateRegistration({
- variables: {
- input: {
- id: registration.id,
- update: { isListed: checked },
- },
- },
- })
- }
- />
-
-
-
+ {renderActiveTabContent()}
);
diff --git a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationGeneralToggles.tsx b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationGeneralToggles.tsx
new file mode 100644
index 0000000000..8fa635349e
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationGeneralToggles.tsx
@@ -0,0 +1,53 @@
+import { Card, Section } from 'twenty-ui/layout';
+import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
+import { IconArrowBarToDown } from 'twenty-ui/display';
+import {
+ type ApplicationRegistration,
+ UpdateApplicationRegistrationDocument,
+} from '~/generated-metadata/graphql';
+import { styled } from '@linaria/react';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+import { useMutation } from '@apollo/client/react';
+import { useLingui } from '@lingui/react/macro';
+
+const StyledToggleContainer = styled.div`
+ display: flex;
+ margin-top: ${themeCssVariables.spacing[4]};
+`;
+
+export const SettingsAdminApplicationRegistrationGeneralToggles = ({
+ registration,
+}: {
+ registration: ApplicationRegistration;
+}) => {
+ const { t } = useLingui();
+
+ const [updateRegistration] = useMutation(
+ UpdateApplicationRegistrationDocument,
+ );
+
+ return (
+
+
+
+
+ updateRegistration({
+ variables: {
+ input: {
+ id: registration.id,
+ update: { isListed: checked },
+ },
+ },
+ })
+ }
+ />
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminConfigVariableDetails.tsx b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminConfigVariableDetails.tsx
index d8d14b6cd2..9a268bf798 100644
--- a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminConfigVariableDetails.tsx
+++ b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminConfigVariableDetails.tsx
@@ -1,64 +1,43 @@
-import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useState } from 'react';
-import { Controller } from 'react-hook-form';
-import { Form, useParams } from 'react-router-dom';
+import { useParams } from 'react-router-dom';
import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState';
import { ConfigVariableHelpText } from '@/settings/admin-panel/config-variables/components/ConfigVariableHelpText';
import { ConfigVariableValueInput } from '@/settings/admin-panel/config-variables/components/ConfigVariableValueInput';
import { useConfigVariableActions } from '@/settings/admin-panel/config-variables/hooks/useConfigVariableActions';
-import { useConfigVariableForm } from '@/settings/admin-panel/config-variables/hooks/useConfigVariableForm';
-import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
+import { ConfigVariableEdit } from '@/settings/config-variables/components/ConfigVariableEdit';
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
-import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
-import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { SettingsPath, type ConfigVariableValue } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
-import { H3Title, IconCheck, IconPencil, IconX } from 'twenty-ui/display';
-import { Button } from 'twenty-ui/input';
-import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useQuery } from '@apollo/client/react';
import {
ConfigSource,
GetDatabaseConfigVariableDocument,
} from '~/generated-metadata/graphql';
-const StyledFormContainer = styled.div`
- > form {
- display: flex;
- flex-direction: column;
- gap: ${themeCssVariables.spacing[4]};
- width: 100%;
+const hasMeaningfulValue = (value: ConfigVariableValue): boolean => {
+ if (value === null || value === undefined) {
+ return false;
}
-`;
-
-const StyledH3TitleContainer = styled.div`
- margin-top: ${themeCssVariables.spacing[2]};
-`;
-
-const StyledRow = styled.div`
- align-items: flex-end;
- display: flex;
- gap: ${themeCssVariables.spacing[2]};
-`;
-
-const StyledButtonContainer = styled.div`
- display: flex;
- & > :not(:first-of-type) > button {
- border-left: none;
+ if (typeof value === 'string') {
+ return value.trim() !== '';
}
-`;
-
-const RESET_VARIABLE_MODAL_ID = 'reset-variable-modal';
+ if (Array.isArray(value)) {
+ return value.length > 0;
+ }
+ return true;
+};
export const SettingsAdminConfigVariableDetails = () => {
const { variableName } = useParams();
+
const { t } = useLingui();
+
const [isEditing, setIsEditing] = useState(false);
- const { openModal } = useModal();
+
const isConfigVariablesInDbEnabled = useAtomStateValue(
isConfigVariablesInDbEnabledState,
);
@@ -76,138 +55,92 @@ export const SettingsAdminConfigVariableDetails = () => {
const { handleUpdateVariable, handleDeleteVariable } =
useConfigVariableActions(variable?.name ?? '');
- const {
- control,
- handleSubmit,
- reset,
- isSubmitting,
- hasValueChanged,
- isValueValid,
- } = useConfigVariableForm(variable);
+ const [value, setValue] = useState(
+ variable?.value ?? null,
+ );
if (loading === true || isDefined(variable) === false) {
return ;
}
const isEnvOnly = variable.isEnvOnly;
+
const isFromDatabase = variable.source === ConfigSource.DATABASE;
- const onSubmit = async (formData: { value: ConfigVariableValue }) => {
- await handleUpdateVariable(formData.value, isFromDatabase);
- setIsEditing(false);
+ const hasValueChanged =
+ JSON.stringify(value) !== JSON.stringify(variable.value);
+
+ const isValueValid =
+ !isEnvOnly && hasValueChanged && hasMeaningfulValue(value);
+
+ const onSave = async () => {
+ await handleUpdateVariable(value, isFromDatabase);
};
- const handleEditClick = () => {
+ const onEdit = () => {
if (variable.isSensitive) {
- reset({ value: '' });
+ setValue('');
}
- setIsEditing(true);
};
- const handleXButtonClick = () => {
- if (isFromDatabase && !hasValueChanged) {
- openModal(RESET_VARIABLE_MODAL_ID);
- return;
- }
+ const canOpenCancelModal = isFromDatabase && !hasValueChanged;
- reset({ value: variable.value });
- setIsEditing(false);
+ const onCancel = () => {
+ setValue(variable.value);
};
- const handleConfirmReset = () => {
- handleDeleteVariable();
- setIsEditing(false);
+ const onConfirmReset = async () => {
+ await handleDeleteVariable();
};
return (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+ }
+ isEditing={isEditing}
+ setIsEditing={setIsEditing}
+ isSaveDisabled={!isValueValid}
+ onSave={onSave}
+ onCancel={onCancel}
+ canOpenCancelModal={canOpenCancelModal}
+ onEdit={onEdit}
+ onConfirmReset={onConfirmReset}
+ editDisabled={isEnvOnly || !isConfigVariablesInDbEnabled}
+ helpContent={
+
+ }
/>
- >
+
);
};
diff --git a/packages/twenty-front/src/pages/settings/applications/SettingsApplicationRegistrationDetails.tsx b/packages/twenty-front/src/pages/settings/applications/SettingsApplicationRegistrationDetails.tsx
index 5b95f155ac..57fcdb41aa 100644
--- a/packages/twenty-front/src/pages/settings/applications/SettingsApplicationRegistrationDetails.tsx
+++ b/packages/twenty-front/src/pages/settings/applications/SettingsApplicationRegistrationDetails.tsx
@@ -4,13 +4,34 @@ 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 { SettingsApplicationRegistrationContent } from '~/pages/settings/applications/components/SettingsApplicationRegistrationContent';
import { useLingui } from '@lingui/react/macro';
import { Tag } from 'twenty-ui/components';
+import { TabList } from '@/ui/layout/tab-list/components/TabList';
+import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
+import {
+ IconInfoCircle,
+ IconKey,
+ IconSettings,
+ IconWorld,
+} from 'twenty-ui/display';
+import { SettingsApplicationRegistrationConfigTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationConfigTab';
+import { SettingsApplicationRegistrationOAuthTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationOAuthTab';
+import { SettingsApplicationRegistrationDistributionTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationDistributionTab';
+import { SettingsApplicationRegistrationGeneralTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationGeneralTab';
+import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
+
+const REGISTRATION_DETAIL_TAB_LIST_ID =
+ 'application-registration-detail-tab-list';
export const SettingsApplicationRegistrationDetails = () => {
const { t } = useLingui();
+ const activeTabId = useAtomComponentStateValue(
+ activeTabIdComponentState,
+ REGISTRATION_DETAIL_TAB_LIST_ID,
+ );
+
const { applicationRegistrationId = '' } = useParams<{
applicationRegistrationId: string;
}>();
@@ -26,6 +47,43 @@ export const SettingsApplicationRegistrationDetails = () => {
return null;
}
+ const tabs = [
+ { 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 (
+
+ );
+ case 'oauth':
+ return (
+
+ );
+ case 'distribution':
+ return (
+
+ );
+ case 'general':
+ default:
+ return (
+
+ );
+ }
+ };
+
return (
{
{ children: registration.name },
]}
>
-
+
+
+ {renderActiveTabContent()}
+
);
};
diff --git a/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationConfigVariableDetail.tsx b/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationConfigVariableDetail.tsx
index 4bae095713..9f0539e9b4 100644
--- a/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationConfigVariableDetail.tsx
+++ b/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationConfigVariableDetail.tsx
@@ -12,41 +12,18 @@ 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 { ConfigVariableEdit } from '@/settings/config-variables/components/ConfigVariableEdit';
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;
-`;
+import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
export const SettingsApplicationRegistrationConfigVariableDetail = () => {
const { t } = useLingui();
- const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
+
const { variableKey } = useParams();
+
const [value, setValue] = useState('');
+
const [isEditing, setIsEditing] = useState(false);
- const [isSubmitting, setIsSubmitting] = useState(false);
- const { openModal } = useModal();
const { applicationRegistrationId = '' } = useParams<{
applicationRegistrationId: string;
@@ -83,65 +60,56 @@ export const SettingsApplicationRegistrationConfigVariableDetail = () => {
);
if (!variable || !registration) {
- return null;
+ return ;
}
- const handleXButtonClick = () => {
- if (!isEditing) {
- openModal(RESET_VARIABLE_MODAL_ID);
- return;
- }
+ const canOpenCancelModal = variable.isFilled && !isNonEmptyString(value);
+ const onCancel = () => {
setValue('');
- setIsEditing(false);
};
- const handleSaveVariableValue = async ({
- resetValue,
- }: {
- resetValue?: boolean;
- }) => {
- const variableKey = variable.key;
-
- if (!isNonEmptyString(value) && !resetValue) {
+ const onSave = async () => {
+ if (!isNonEmptyString(value)) {
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 });
+ const onConfirmReset = async () => {
+ try {
+ await updateVariable({
+ variables: {
+ input: {
+ id: variable.id,
+ update: {
+ value: '',
+ resetValue: true,
+ },
+ },
+ },
+ });
+ } finally {
+ setValue('');
+ }
};
return (
}
links={[
{
children: t`Workspace`,
@@ -170,66 +138,29 @@ export const SettingsApplicationRegistrationConfigVariableDetail = () => {
},
]}
>
-
-
-
- {variable.key}
- {variable.isRequired && (
- *
- )}
- >
+
-
-
-
-
- {
- setValue(value);
- setIsEditing(true);
- }}
- fullWidth
- />
-
-
- handleSaveVariableValue({ resetValue: false })}
- disabled={isSubmitting || !isEditing}
- />
-
-
-
-
-
-
-
-
);
diff --git a/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationConfigVariableStatus.tsx b/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationConfigVariableStatus.tsx
deleted file mode 100644
index 9512a2fbe5..0000000000
--- a/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationConfigVariableStatus.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-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 ? (
-
- ) : variable.isRequired ? (
-
- ) : (
-
- );
-};
diff --git a/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationContent.tsx b/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationContent.tsx
deleted file mode 100644
index 3c9488af14..0000000000
--- a/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationContent.tsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
-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,
- 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: ApplicationRegistration;
-};
-
-export const SettingsApplicationRegistrationContent = ({
- registration,
-}: SettingsApplicationRegistrationContentProps) => {
- const { t } = useLingui();
-
- const activeTabId = useAtomComponentStateValue(
- activeTabIdComponentState,
- REGISTRATION_DETAIL_TAB_LIST_ID,
- );
-
- const tabs = [
- { 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 (
-
- );
- case 'oauth':
- return (
-
- );
- case 'distribution':
- return (
-
- );
- case 'general':
- default:
- return (
-
- );
- }
- };
-
- return (
-
-
- {renderActiveTabContent()}
-
- );
-};
diff --git a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDetailContent.tsx b/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationGeneralInfo.tsx
similarity index 71%
rename from packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDetailContent.tsx
rename to packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationGeneralInfo.tsx
index fbdb31bb71..d2454894fd 100644
--- a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDetailContent.tsx
+++ b/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationGeneralInfo.tsx
@@ -1,13 +1,10 @@
import {
H2Title,
IconBox,
- IconBrandDocker,
- IconChartBar,
IconDownload,
- IconStatusChange,
+ IconGitBranch,
IconTag,
IconWorld,
- IconGitBranch,
} from 'twenty-ui/display';
import { Trans, useLingui } from '@lingui/react/macro';
import {
@@ -18,7 +15,6 @@ import {
type ApplicationRegistration,
ApplicationRegistrationSourceType,
ApplicationRegistrationTarballUrlDocument,
- FindApplicationRegistrationStatsDocument,
FindOneApplicationSummaryDocument,
GetPublicWorkspaceDataByIdDocument,
} from '~/generated-metadata/graphql';
@@ -59,7 +55,7 @@ const StyledGeneralContainer = styled.div`
gap: ${themeCssVariables.spacing[2]};
`;
-export const SettingsAdminApplicationRegistrationDetailContent = ({
+export const SettingsApplicationRegistrationGeneralInfo = ({
registration,
}: {
registration: ApplicationRegistration;
@@ -96,14 +92,6 @@ export const SettingsAdminApplicationRegistrationDetailContent = ({
},
);
- const { data: statsData } = useQuery(
- FindApplicationRegistrationStatsDocument,
- {
- variables: { id: applicationRegistrationId },
- skip: !applicationRegistrationId,
- },
- );
-
const shareLink = getSettingsPath(SettingsPath.AvailableApplicationDetail, {
availableApplicationId: registration.universalIdentifier,
});
@@ -206,69 +194,24 @@ export const SettingsAdminApplicationRegistrationDetailContent = ({
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 (
- <>
-
- {hasStats && (
-
- )}
- >
+
);
};
diff --git a/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationGeneralStats.tsx b/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationGeneralStats.tsx
new file mode 100644
index 0000000000..49f18aa268
--- /dev/null
+++ b/packages/twenty-front/src/pages/settings/applications/components/SettingsApplicationRegistrationGeneralStats.tsx
@@ -0,0 +1,80 @@
+import {
+ H2Title,
+ IconBrandDocker,
+ IconChartBar,
+ IconStatusChange,
+} from 'twenty-ui/display';
+import { useLingui } from '@lingui/react/macro';
+import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
+import {
+ type ApplicationRegistration,
+ FindApplicationRegistrationStatsDocument,
+} from '~/generated-metadata/graphql';
+import { useQuery } from '@apollo/client/react';
+import { Section } from 'twenty-ui/layout';
+
+export const SettingsApplicationRegistrationGeneralStats = ({
+ registration,
+}: {
+ registration: ApplicationRegistration;
+}) => {
+ const { t } = useLingui();
+
+ const applicationRegistrationId = registration.id;
+
+ const { data: statsData } = useQuery(
+ FindApplicationRegistrationStatsDocument,
+ {
+ variables: { id: applicationRegistrationId },
+ skip: !applicationRegistrationId,
+ },
+ );
+
+ const stats = statsData?.findApplicationRegistrationStats;
+
+ const hasStats = (stats?.activeInstalls ?? 0) > 0;
+
+ if (!hasStats) {
+ return null;
+ }
+
+ 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 (
+
+ );
+};
diff --git a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationRegistrationConfigTab.tsx b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationRegistrationConfigTab.tsx
index 757fd7e508..b84bb6f54d 100644
--- a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationRegistrationConfigTab.tsx
+++ b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationRegistrationConfigTab.tsx
@@ -5,34 +5,17 @@ import {
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 { H2Title, Status } from 'twenty-ui/display';
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};
-`;
+import { ConfigVariableTable } from '@/settings/config-variables/components/ConfigVariableTable';
export const SettingsApplicationRegistrationConfigTab = ({
registration,
}: {
registration: ApplicationRegistrationData;
}) => {
- const { theme } = useContext(ThemeContext);
const { t } = useLingui();
const applicationRegistrationId = registration.id;
@@ -48,6 +31,23 @@ export const SettingsApplicationRegistrationConfigTab = ({
const variables: ApplicationRegistrationVariable[] =
variablesData?.findApplicationRegistrationVariables ?? [];
+ const configVariables = variables.map((variable) => ({
+ name: variable.key,
+ description: variable.description,
+ value: variable.isFilled ? (
+ '••••••••••'
+ ) : (
+
+ ),
+ to: getSettingsPath(
+ SettingsPath.ApplicationRegistrationConfigVariableDetails,
+ {
+ applicationRegistrationId,
+ variableKey: variable.key,
+ },
+ ),
+ }));
+
return (
variables.length > 0 && (
@@ -55,71 +55,7 @@ export const SettingsApplicationRegistrationConfigTab = ({
title={t`Server Variables`}
description={t`Server variables are applied to all workspace installations.`}
/>
-
-
- {t`Name`}
- {t`Description`}
- {t`Status`}
-
-
-
-
- {variables.map((variable) => (
-
-
-
- {variable.isRequired && (
- *
- )}
-
-
-
-
-
-
-
-
-
-
-
- ))}
-
-
-
+
)
);
diff --git a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationRegistrationGeneralTab.tsx b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationRegistrationGeneralTab.tsx
index 3f3f94ce0b..0b0ac05d7f 100644
--- a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationRegistrationGeneralTab.tsx
+++ b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationRegistrationGeneralTab.tsx
@@ -1,252 +1,32 @@
-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 { 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 {
- H1Title,
- H1TitleFontColor,
- H2Title,
- IconShare,
- IconTrash,
- AppTooltip,
- TooltipDelay,
-} from 'twenty-ui/display';
-import { Button } from 'twenty-ui/input';
-import { Section, SectionAlignment, SectionFontColor } from 'twenty-ui/layout';
-import {
- type ApplicationRegistration,
- DeleteApplicationRegistrationDocument,
- FindApplicationRegistrationStatsDocument,
- FindManyApplicationRegistrationsDocument,
- TransferApplicationRegistrationOwnershipDocument,
-} from '~/generated-metadata/graphql';
-import { useNavigateSettings } from '~/hooks/useNavigateSettings';
-import { themeCssVariables } from 'twenty-ui/theme-constants';
-import {
- StyledAppModal,
- StyledAppModalButton,
- StyledAppModalSection,
- StyledAppModalTitle,
-} from '~/pages/settings/applications/components/SettingsAppModalLayout';
-import { SettingsAdminApplicationRegistrationDetailContent } from '~/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDetailContent';
-import { isDefined } from 'twenty-shared/utils';
+import { type ApplicationRegistration } from '~/generated-metadata/graphql';
-const DELETE_REGISTRATION_MODAL_ID = 'delete-application-registration-modal';
+import { SettingsApplicationRegistrationGeneralInfo } from '~/pages/settings/applications/components/SettingsApplicationRegistrationGeneralInfo';
-const TRANSFER_OWNERSHIP_MODAL_ID =
- 'transfer-application-registration-ownership-modal';
-
-const DELETE_REGISTRATION_BUTTON_ID = 'delete-registration-button';
-
-const StyledDangerButtonGroup = styled.div`
- display: flex;
- gap: ${themeCssVariables.spacing[2]};
-`;
+import { SettingsAdminApplicationRegistrationDangerZone } from '~/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone';
+import { SettingsApplicationRegistrationGeneralStats } from '~/pages/settings/applications/components/SettingsApplicationRegistrationGeneralStats';
+import { SettingsAdminApplicationRegistrationGeneralToggles } from '~/pages/settings/admin-panel/SettingsAdminApplicationRegistrationGeneralToggles';
export const SettingsApplicationRegistrationGeneralTab = ({
registration,
+ displayAdminToggles,
}: {
registration: ApplicationRegistration;
+ displayAdminToggles?: boolean;
}) => {
- const { t } = useLingui();
- const navigate = useNavigateSettings();
- const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
- const { openModal, closeModal } = useModal();
-
- const [isLoading, setIsLoading] = useState(false);
- const [isTransferring, setIsTransferring] = useState(false);
- const [transferSubdomain, setTransferSubdomain] = useState('');
-
- const applicationRegistrationId = registration.id;
-
- 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,
- {
- refetchQueries: [FindManyApplicationRegistrationsDocument],
- },
- );
-
- const [transferOwnership] = useMutation(
- TransferApplicationRegistrationOwnershipDocument,
- {
- refetchQueries: [FindManyApplicationRegistrationsDocument],
- },
- );
-
- const handleDelete = async () => {
- setIsLoading(true);
- try {
- await deleteRegistration({
- variables: { id: applicationRegistrationId },
- });
- navigate(SettingsPath.Applications);
- } catch {
- enqueueErrorSnackBar({
- message: t`Error deleting app`,
- });
- } finally {
- setIsLoading(false);
- }
- };
-
- const handleTransferOwnership = async () => {
- const trimmed = transferSubdomain.trim();
-
- if (!isNonEmptyString(trimmed)) {
- return;
- }
-
- setIsTransferring(true);
- try {
- await transferOwnership({
- variables: {
- applicationRegistrationId,
- targetWorkspaceSubdomain: trimmed,
- },
- });
- enqueueSuccessSnackBar({
- message: t`Ownership transferred successfully`,
- });
- setTransferSubdomain('');
- navigate(SettingsPath.Applications);
- } catch {
- enqueueErrorSnackBar({
- message: t`Failed to transfer ownership. Check that the subdomain is correct.`,
- });
- } finally {
- setIsTransferring(false);
- }
- };
-
- const confirmationValue = t`yes`;
-
return (
<>
-
+ {displayAdminToggles && (
+
+ )}
+
-
-
-
- openModal(DELETE_REGISTRATION_MODAL_ID)}
- />
- {hasActiveInstalls && (
-
- )}
- openModal(TRANSFER_OWNERSHIP_MODAL_ID)}
- />
-
-
-
-
- Please type {`"${confirmationValue}"`} to confirm you want to delete
- this app. All workspace installations linked to it will lose their
- OAuth credentials.
-
- }
- onConfirmClick={handleDelete}
- confirmButtonText={t`Delete`}
- loading={isLoading}
+
-
- setTransferSubdomain('')}
- padding="large"
- dataGloballyPreventClickOutside
- >
-
-
-
-
- {t`Enter the workspace subdomain to transfer this app to. You will lose access to manage it.`}
-
-
- {
- closeModal(TRANSFER_OWNERSHIP_MODAL_ID);
- setTransferSubdomain('');
- }}
- variant="secondary"
- title={t`Cancel`}
- fullWidth
- />
-
-
>
);
};