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:
-65
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
-82
@@ -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>
|
||||
);
|
||||
};
|
||||
+24
-31
@@ -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} />;
|
||||
};
|
||||
|
||||
+44
-69
@@ -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 {
|
||||
|
||||
-64
@@ -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,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user