Fix app design 4 (#19803)

## Before

<img width="1512" height="584" alt="image"
src="https://github.com/user-attachments/assets/2a05d0c7-4bba-438f-9b05-4abd159530ba"
/>
<img width="1512" height="908" alt="image"
src="https://github.com/user-attachments/assets/a36da096-505d-4f25-84bc-a0feca436d53"
/>


## After

<img width="1503" height="574" alt="image"
src="https://github.com/user-attachments/assets/e039b92f-057a-4ed7-869a-a248f446eb2b"
/>
<img width="1512" height="904" alt="image"
src="https://github.com/user-attachments/assets/065767b5-8a70-4ea7-a520-5b2ccbdcffa3"
/>

---------

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
This commit is contained in:
martmull
2026-04-17 16:52:41 +02:00
committed by GitHub
parent 3268a86f4b
commit 120ced44a9
19 changed files with 1027 additions and 1124 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,
};
};