Fix config settings

This commit is contained in:
martmull
2026-04-16 15:00:39 +02:00
parent 381f3ba7d9
commit 5f5f654602
11 changed files with 482 additions and 678 deletions
@@ -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 && (
<Button
title={t`Reset to Default`}
variant="secondary"
size="small"
accent="danger"
disabled={isSubmitting}
onClick={onReset}
Icon={IconRefreshAlert}
/>
)}
{isConfigVariablesInDbEnabled && !variable.isEnvOnly && (
<Button
title={isFromDatabase ? t`Save` : t`Edit`}
variant="primary"
size="small"
accent="blue"
disabled={isSubmitting || !isValueValid}
onClick={onSave}
type="submit"
Icon={isFromDatabase ? IconDeviceFloppy : IconPencil}
/>
)}
</>
);
};
@@ -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 (
<StyledTableRowContainer>
<TableRow
gridAutoColumns="5fr 3fr 1fr"
to={getSettingsPath(SettingsPath.AdminPanelConfigVariableDetails, {
variableName: variable.name,
})}
>
<TableCell
color={theme.font.color.primary}
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
clickable
>
<StyledEllipsisLabel>{variable.name}</StyledEllipsisLabel>
</TableCell>
<TableCell
align="right"
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
clickable
>
<StyledEllipsisLabel>{displayValue}</StyledEllipsisLabel>
</TableCell>
<TableCell align="right">
<IconChevronRight
size={theme.icon.size.md}
color={theme.font.color.tertiary}
/>
</TableCell>
</TableRow>
</StyledTableRowContainer>
);
};
@@ -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 (
<Table>
<TableRow gridAutoColumns="5fr 3fr 1fr">
<TableHeader>{t`Name`}</TableHeader>
<TableHeader align="right">{t`Value`}</TableHeader>
<TableHeader align="right"></TableHeader>
</TableRow>
<StyledTableBodyContainer>
<TableBody>
{variables.map((variable) => (
<SettingsAdminConfigVariablesRow
key={variable.name}
variable={variable}
/>
))}
</TableBody>
</StyledTableBodyContainer>
</Table>
);
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 <ConfigVariableTable configVariables={configVariables} />;
};
@@ -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<HTMLElement>) => {
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<HTMLElement>) => {
if (isDefined(e)) {
e.preventDefault();
}
await deleteDatabaseConfigVariable({
variables: {
key: variableName,
},
refetchQueries: [
{
query: GET_DATABASE_CONFIG_VARIABLE,
variables: { key: variableName },
},
],
});
await refetchClientConfig();
};
return {
@@ -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<FormValues>({
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,
};
};
@@ -0,0 +1,171 @@
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 { ConfigVariableHelpText } from '@/settings/admin-panel/config-variables/components/ConfigVariableHelpText';
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 { isDefined } from 'twenty-shared/utils';
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<SetStateAction<boolean>>;
isSaveDisabled?: boolean;
canOpenCancelModal?: boolean;
onSave?: () => Promise<void>;
onCancel: () => void;
onEdit?: () => void;
onConfirmReset?: () => Promise<void>;
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 (
<SettingsPageContainer>
<Section>
<H3Title title={title} description={description} />
</Section>
<Section>
<StyledRow>
{input}
{!isEditing ? (
<Button
Icon={IconPencil}
variant="primary"
onClick={handleEdit}
type="button"
disabled={editDisabled}
/>
) : (
<StyledButtonContainer>
<Button
Icon={IconCheck}
variant="secondary"
position="left"
type={'button'}
onClick={handleSave}
disabled={isSaveDisabled || isSubmitting}
/>
<Button
Icon={IconX}
variant="secondary"
position="right"
onClick={handleCancel}
type="button"
disabled={isSubmitting}
/>
</StyledButtonContainer>
)}
<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"
/>
</StyledRow>
{helpContent}
</Section>
</SettingsPageContainer>
);
};
@@ -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 (
<Table>
<TableRow gridAutoColumns={GRID_AUTO_COLUMNS}>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader align="right">{t`Description`}</TableHeader>
<TableHeader align="right">{t`Value`}</TableHeader>
<TableHeader align="right"></TableHeader>
</TableRow>
<StyledTableBodyContainer>
<TableBody>
{configVariables.map((variable, index) => (
<TableRow
key={index}
gridAutoColumns={GRID_AUTO_COLUMNS}
to={variable.to}
>
<TableCell
color={theme.font.color.primary}
whiteSpace="nowrap"
overflow="hidden"
textOverflow="ellipsis"
clickable
>
<OverflowingTextWithTooltip text={variable.name} />
</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
>
{typeof variable.value === 'string' ? (
<OverflowingTextWithTooltip text={variable.value} />
) : (
variable.value
)}
</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>
);
};
@@ -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<ConfigVariableValue>(
variable?.value ?? null,
);
if (loading === true || isDefined(variable) === false) {
return <SettingsSkeletonLoader />;
}
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 (
<>
<SubMenuTopBarContainer
links={[
{
children: t`Other`,
href: getSettingsPath(SettingsPath.AdminPanel),
},
{
children: t`Admin Panel - Config`,
href: getSettingsPath(
SettingsPath.AdminPanel,
undefined,
undefined,
'config-variables',
),
},
{
children: variable.name,
},
]}
>
<SettingsPageContainer>
<StyledH3TitleContainer>
<H3Title title={variable.name} description={variable.description} />
</StyledH3TitleContainer>
<StyledFormContainer>
<Form onSubmit={handleSubmit(onSubmit)}>
<StyledRow>
<Controller
control={control}
name="value"
render={({ field }) => (
<ConfigVariableValueInput
variable={variable}
value={field.value}
onChange={field.onChange}
disabled={isEnvOnly || !isEditing}
/>
)}
/>
{!isEditing ? (
<Button
Icon={IconPencil}
variant="primary"
onClick={handleEditClick}
type="button"
disabled={isEnvOnly || !isConfigVariablesInDbEnabled}
/>
) : (
<StyledButtonContainer>
<Button
Icon={IconCheck}
variant="secondary"
position="left"
type="submit"
disabled={isSubmitting || !isValueValid}
/>
<Button
Icon={IconX}
variant="secondary"
position="right"
onClick={handleXButtonClick}
type="button"
disabled={isSubmitting}
/>
</StyledButtonContainer>
)}
</StyledRow>
<ConfigVariableHelpText
variable={variable}
hasValueChanged={hasValueChanged}
/>
</Form>
</StyledFormContainer>
</SettingsPageContainer>
</SubMenuTopBarContainer>
<ConfirmationModal
modalInstanceId={RESET_VARIABLE_MODAL_ID}
title={t`Reset variable`}
subtitle={t`This will revert the database value to environment/default value. The database override will be removed and the system will use the environment settings.`}
onConfirmClick={handleConfirmReset}
confirmButtonText={t`Reset`}
confirmButtonAccent="danger"
<SubMenuTopBarContainer
links={[
{
children: t`Other`,
href: getSettingsPath(SettingsPath.AdminPanel),
},
{
children: t`Admin Panel - Config`,
href: getSettingsPath(
SettingsPath.AdminPanel,
undefined,
undefined,
'config-variables',
),
},
{
children: variable.name,
},
]}
>
<ConfigVariableEdit
title={variable.name}
description={variable.description}
input={
<ConfigVariableValueInput
variable={variable}
value={value}
onChange={setValue}
disabled={isEnvOnly || !isEditing}
/>
}
isEditing={isEditing}
setIsEditing={setIsEditing}
isSaveDisabled={!isValueValid}
onSave={onSave}
onCancel={onCancel}
canOpenCancelModal={canOpenCancelModal}
onEdit={onEdit}
onConfirmReset={onConfirmReset}
editDisabled={isEnvOnly || !isConfigVariablesInDbEnabled}
helpContent={
<ConfigVariableHelpText
variable={variable}
hasValueChanged={hasValueChanged}
/>
}
/>
</>
</SubMenuTopBarContainer>
);
};
@@ -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<string>('');
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 <SettingsSkeletonLoader />;
}
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 (
<SubMenuTopBarContainer
title={registration.name}
tag={<Tag text={t`Owner`} color={'gray'} />}
links={[
{
children: t`Workspace`,
@@ -170,66 +138,29 @@ export const SettingsApplicationRegistrationConfigVariableDetail = () => {
},
]}
>
<SettingsPageContainer>
<Section>
<H3Title
title={
<>
{variable.key}
{variable.isRequired && (
<span style={{ color: 'red' }}> *</span>
)}
</>
<ConfigVariableEdit
title={variable.key}
description={variable.description}
input={
<TextInput
value={value}
placeholder={
variable.isFilled
? '••••••••••••••••••••••••'
: t`set-config-value`
}
description={variable.description}
onChange={setValue}
disabled={!isEditing}
fullWidth
/>
</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"
}
isEditing={isEditing}
setIsEditing={setIsEditing}
isSaveDisabled={!isNonEmptyString(value)}
onSave={onSave}
onCancel={onCancel}
canOpenCancelModal={canOpenCancelModal}
onConfirmReset={onConfirmReset}
/>
</SubMenuTopBarContainer>
);
@@ -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 ? (
<Status color="green" text={t`Configured`} />
) : variable.isRequired ? (
<Status color="red" text={t`Required`} />
) : (
<Status color="gray" text={t`Not set`} />
);
};
@@ -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 ? (
'••••••••••'
) : (
<Status color="gray" text={t`Not set`} />
),
to: getSettingsPath(
SettingsPath.ApplicationRegistrationConfigVariableDetails,
{
applicationRegistrationId,
variableKey: variable.key,
},
),
}));
return (
variables.length > 0 && (
<Section>
@@ -55,71 +55,7 @@ export const SettingsApplicationRegistrationConfigTab = ({
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>
<ConfigVariableTable configVariables={configVariables} />
</Section>
)
);