Twenty config admin panel integration (#11755)
closes https://github.com/twentyhq/core-team-issues/issues/761 closes https://github.com/twentyhq/core-team-issues/issues/762 --------- Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
@ -281,11 +281,11 @@ const SettingsAdminIndicatorHealthStatus = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsAdminSecondaryEnvVariables = lazy(() =>
|
||||
const SettingsAdminConfigVariableDetails = lazy(() =>
|
||||
import(
|
||||
'~/pages/settings/admin-panel/SettingsAdminSecondaryEnvVariables'
|
||||
'~/pages/settings/admin-panel/SettingsAdminConfigVariableDetails'
|
||||
).then((module) => ({
|
||||
default: module.SettingsAdminSecondaryEnvVariables,
|
||||
default: module.SettingsAdminConfigVariableDetails,
|
||||
})),
|
||||
);
|
||||
|
||||
@ -505,9 +505,10 @@ export const SettingsRoutes = ({
|
||||
path={SettingsPath.AdminPanelIndicatorHealthStatus}
|
||||
element={<SettingsAdminIndicatorHealthStatus />}
|
||||
/>
|
||||
|
||||
<Route
|
||||
path={SettingsPath.AdminPanelOtherEnvVariables}
|
||||
element={<SettingsAdminSecondaryEnvVariables />}
|
||||
path={SettingsPath.AdminPanelConfigVariableDetails}
|
||||
element={<SettingsAdminConfigVariableDetails />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -7,6 +7,7 @@ import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionId
|
||||
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||
import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState';
|
||||
import { isAttachmentPreviewEnabledState } from '@/client-config/states/isAttachmentPreviewEnabledState';
|
||||
import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState';
|
||||
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
|
||||
import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState';
|
||||
@ -21,8 +22,8 @@ import { supportChatState } from '@/client-config/states/supportChatState';
|
||||
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||
import { useGetClientConfigQuery } from '~/generated/graphql';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { useGetClientConfigQuery } from '~/generated/graphql';
|
||||
|
||||
export const ClientConfigProviderEffect = () => {
|
||||
const setIsDebugMode = useSetRecoilState(isDebugModeState);
|
||||
@ -82,6 +83,10 @@ export const ClientConfigProviderEffect = () => {
|
||||
isAttachmentPreviewEnabledState,
|
||||
);
|
||||
|
||||
const setIsConfigVariablesInDbEnabled = useSetRecoilState(
|
||||
isConfigVariablesInDbEnabledState,
|
||||
);
|
||||
|
||||
const { data, loading, error } = useGetClientConfigQuery({
|
||||
skip: clientConfigApiStatus.isLoaded,
|
||||
});
|
||||
@ -157,6 +162,9 @@ export const ClientConfigProviderEffect = () => {
|
||||
setIsAttachmentPreviewEnabled(
|
||||
data?.clientConfig?.isAttachmentPreviewEnabled,
|
||||
);
|
||||
setIsConfigVariablesInDbEnabled(
|
||||
data?.clientConfig?.isConfigVariablesInDbEnabled,
|
||||
);
|
||||
}, [
|
||||
data,
|
||||
setIsDebugMode,
|
||||
@ -182,6 +190,7 @@ export const ClientConfigProviderEffect = () => {
|
||||
setGoogleMessagingEnabled,
|
||||
setGoogleCalendarEnabled,
|
||||
setIsAttachmentPreviewEnabled,
|
||||
setIsConfigVariablesInDbEnabled,
|
||||
]);
|
||||
|
||||
return <></>;
|
||||
|
||||
@ -61,6 +61,7 @@ export const GET_CLIENT_CONFIG = gql`
|
||||
isMicrosoftCalendarEnabled
|
||||
isGoogleMessagingEnabled
|
||||
isGoogleCalendarEnabled
|
||||
isConfigVariablesInDbEnabled
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
export const isConfigVariablesInDbEnabledState = createState<boolean>({
|
||||
key: 'isConfigVariablesInDbEnabled',
|
||||
defaultValue: false,
|
||||
});
|
||||
@ -20,8 +20,8 @@ export const SettingsAdminContent = () => {
|
||||
disabled: !canAccessFullAdminPanel && !canImpersonate,
|
||||
},
|
||||
{
|
||||
id: SETTINGS_ADMIN_TABS.ENV_VARIABLES,
|
||||
title: t`Env Variables`,
|
||||
id: SETTINGS_ADMIN_TABS.CONFIG_VARIABLES,
|
||||
title: t`Config Variables`,
|
||||
Icon: IconVariable,
|
||||
disabled: !canAccessFullAdminPanel,
|
||||
},
|
||||
|
||||
@ -1,68 +0,0 @@
|
||||
import { SettingsAdminEnvVariablesTable } from '@/settings/admin-panel/components/SettingsAdminEnvVariablesTable';
|
||||
import { SettingsAdminTabSkeletonLoader } from '@/settings/admin-panel/components/SettingsAdminTabSkeletonLoader';
|
||||
import { SettingsListItemCardContent } from '@/settings/components/SettingsListItemCardContent';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { H2Title, IconHeartRateMonitor } from 'twenty-ui/display';
|
||||
import { Card, Section } from 'twenty-ui/layout';
|
||||
import { useGetConfigVariablesGroupedQuery } from '~/generated/graphql';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
const StyledGroupContainer = styled.div``;
|
||||
|
||||
const StyledInfoText = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
`;
|
||||
|
||||
const StyledCard = styled(Card)`
|
||||
margin-bottom: ${({ theme }) => theme.spacing(8)};
|
||||
`;
|
||||
|
||||
export const SettingsAdminEnvVariables = () => {
|
||||
const theme = useTheme();
|
||||
const { data: configVariables, loading: configVariablesLoading } =
|
||||
useGetConfigVariablesGroupedQuery({
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
const visibleGroups =
|
||||
configVariables?.getConfigVariablesGrouped.groups.filter(
|
||||
(group) => !group.isHiddenOnLoad,
|
||||
) ?? [];
|
||||
|
||||
if (configVariablesLoading) {
|
||||
return <SettingsAdminTabSkeletonLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<StyledInfoText>
|
||||
{t`These are only the server values. Ensure your worker environment has the same variables and values, this is required for asynchronous tasks like email sync.`}
|
||||
</StyledInfoText>
|
||||
</Section>
|
||||
{visibleGroups.map((group) => (
|
||||
<StyledGroupContainer key={group.name}>
|
||||
<H2Title title={group.name} description={group.description} />
|
||||
{group.variables.length > 0 && (
|
||||
<SettingsAdminEnvVariablesTable variables={group.variables} />
|
||||
)}
|
||||
</StyledGroupContainer>
|
||||
))}
|
||||
|
||||
<Section>
|
||||
<StyledCard rounded>
|
||||
<SettingsListItemCardContent
|
||||
label={t`Other Variables`}
|
||||
to={getSettingsPath(SettingsPath.AdminPanelOtherEnvVariables)}
|
||||
rightComponent={null}
|
||||
LeftIcon={IconHeartRateMonitor}
|
||||
LeftIconColor={theme.font.color.tertiary}
|
||||
/>
|
||||
</StyledCard>
|
||||
</Section>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,170 +0,0 @@
|
||||
import { SettingsAdminEnvCopyableText } from '@/settings/admin-panel/components/SettingsAdminEnvCopyableText';
|
||||
import { SettingsAdminTableCard } from '@/settings/admin-panel/components/SettingsAdminTableCard';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { motion } from 'framer-motion';
|
||||
import { useState } from 'react';
|
||||
import { IconChevronRight, IconEye, IconEyeOff } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { AnimatedExpandableContainer } from 'twenty-ui/layout';
|
||||
|
||||
type SettingsAdminEnvVariablesRowProps = {
|
||||
variable: {
|
||||
name: string;
|
||||
description: string;
|
||||
value: string;
|
||||
isSensitive: boolean;
|
||||
};
|
||||
isExpanded: boolean;
|
||||
onExpandToggle: (name: string) => void;
|
||||
};
|
||||
|
||||
const StyledTruncatedCell = styled(TableCell)`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const StyledButton = styled(motion.button)`
|
||||
align-items: center;
|
||||
border: none;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-inline: ${({ theme }) => theme.spacing(1)};
|
||||
background-color: transparent;
|
||||
height: 24px;
|
||||
width: 24px;
|
||||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const MotionIconChevronDown = motion(IconChevronRight);
|
||||
|
||||
const StyledEllipsisLabel = styled.div`
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledValueContainer = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(1)};
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledTableRow = styled(TableRow)<{ isExpanded: boolean }>`
|
||||
background-color: ${({ isExpanded, theme }) =>
|
||||
isExpanded ? theme.background.transparent.light : 'transparent'};
|
||||
`;
|
||||
|
||||
const StyledExpandableContainer = styled.div`
|
||||
width: 100%;
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const SettingsAdminEnvVariablesRow = ({
|
||||
variable,
|
||||
isExpanded,
|
||||
onExpandToggle,
|
||||
}: SettingsAdminEnvVariablesRowProps) => {
|
||||
const [showSensitiveValue, setShowSensitiveValue] = useState(false);
|
||||
const theme = useTheme();
|
||||
|
||||
const displayValue =
|
||||
variable.value === ''
|
||||
? 'null'
|
||||
: variable.isSensitive && !showSensitiveValue
|
||||
? '••••••'
|
||||
: variable.value;
|
||||
|
||||
const handleToggleVisibility = (event: React.MouseEvent) => {
|
||||
event.stopPropagation();
|
||||
setShowSensitiveValue(!showSensitiveValue);
|
||||
};
|
||||
|
||||
const environmentVariablesDetails = [
|
||||
{
|
||||
label: 'Name',
|
||||
value: <SettingsAdminEnvCopyableText text={variable.name} />,
|
||||
},
|
||||
{
|
||||
label: 'Description',
|
||||
value: (
|
||||
<SettingsAdminEnvCopyableText
|
||||
text={variable.description}
|
||||
maxRows={1}
|
||||
multiline={true}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
label: 'Value',
|
||||
value: (
|
||||
<StyledValueContainer>
|
||||
<SettingsAdminEnvCopyableText
|
||||
text={variable.value}
|
||||
displayText={displayValue}
|
||||
multiline={true}
|
||||
/>
|
||||
{variable.isSensitive && variable.value !== '' && (
|
||||
<LightIconButton
|
||||
Icon={showSensitiveValue ? IconEyeOff : IconEye}
|
||||
size="small"
|
||||
accent="secondary"
|
||||
onClick={handleToggleVisibility}
|
||||
/>
|
||||
)}
|
||||
</StyledValueContainer>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledTableRow
|
||||
onClick={() => onExpandToggle(variable.name)}
|
||||
gridAutoColumns="5fr 4fr 3fr 1fr"
|
||||
isExpanded={isExpanded}
|
||||
>
|
||||
<StyledTruncatedCell color={theme.font.color.primary}>
|
||||
<StyledEllipsisLabel>{variable.name}</StyledEllipsisLabel>
|
||||
</StyledTruncatedCell>
|
||||
<StyledTruncatedCell>
|
||||
<StyledEllipsisLabel>{variable.description}</StyledEllipsisLabel>
|
||||
</StyledTruncatedCell>
|
||||
<StyledTruncatedCell align="right">
|
||||
<StyledEllipsisLabel>{displayValue}</StyledEllipsisLabel>
|
||||
</StyledTruncatedCell>
|
||||
<TableCell align="right">
|
||||
<StyledButton
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onExpandToggle(variable.name);
|
||||
}}
|
||||
>
|
||||
<MotionIconChevronDown
|
||||
size={theme.icon.size.md}
|
||||
color={theme.font.color.tertiary}
|
||||
initial={false}
|
||||
animate={{ rotate: isExpanded ? 90 : 0 }}
|
||||
/>
|
||||
</StyledButton>
|
||||
</TableCell>
|
||||
</StyledTableRow>
|
||||
<AnimatedExpandableContainer isExpanded={isExpanded} mode="fit-content">
|
||||
<StyledExpandableContainer>
|
||||
<SettingsAdminTableCard
|
||||
items={environmentVariablesDetails}
|
||||
gridAutoColumns="1fr 4fr"
|
||||
/>
|
||||
</StyledExpandableContainer>
|
||||
</AnimatedExpandableContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -1,51 +0,0 @@
|
||||
import { SettingsAdminEnvVariablesRow } from '@/settings/admin-panel/components/SettingsAdminEnvVariablesRow';
|
||||
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 '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
|
||||
const StyledTableBody = styled(TableBody)`
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
`;
|
||||
|
||||
type SettingsAdminEnvVariablesTableProps = {
|
||||
variables: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
value: string;
|
||||
isSensitive: boolean;
|
||||
}>;
|
||||
};
|
||||
|
||||
export const SettingsAdminEnvVariablesTable = ({
|
||||
variables,
|
||||
}: SettingsAdminEnvVariablesTableProps) => {
|
||||
const [expandedRowName, setExpandedRowName] = useState<string | null>(null);
|
||||
|
||||
const handleExpandToggle = (name: string) => {
|
||||
setExpandedRowName(expandedRowName === name ? null : name);
|
||||
};
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableRow gridAutoColumns="5fr 4fr 3fr 1fr">
|
||||
<TableHeader>Name</TableHeader>
|
||||
<TableHeader>Description</TableHeader>
|
||||
<TableHeader align="right">Value</TableHeader>
|
||||
<TableHeader align="right"></TableHeader>
|
||||
</TableRow>
|
||||
<StyledTableBody>
|
||||
{variables.map((variable) => (
|
||||
<SettingsAdminEnvVariablesRow
|
||||
key={variable.name}
|
||||
variable={variable}
|
||||
isExpanded={expandedRowName === variable.name}
|
||||
onExpandToggle={handleExpandToggle}
|
||||
/>
|
||||
))}
|
||||
</StyledTableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
import { SettingsAdminEnvVariables } from '@/settings/admin-panel/components/SettingsAdminEnvVariables';
|
||||
import { SettingsAdminGeneral } from '@/settings/admin-panel/components/SettingsAdminGeneral';
|
||||
import { SettingsAdminConfigVariables } from '@/settings/admin-panel/config-variables/components/SettingsAdminConfigVariables';
|
||||
import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs';
|
||||
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
|
||||
import { SettingsAdminHealthStatus } from '@/settings/admin-panel/health-status/components/SettingsAdminHealthStatus';
|
||||
@ -15,8 +15,8 @@ export const SettingsAdminTabContent = () => {
|
||||
switch (activeTabId) {
|
||||
case SETTINGS_ADMIN_TABS.GENERAL:
|
||||
return <SettingsAdminGeneral />;
|
||||
case SETTINGS_ADMIN_TABS.ENV_VARIABLES:
|
||||
return <SettingsAdminEnvVariables />;
|
||||
case SETTINGS_ADMIN_TABS.CONFIG_VARIABLES:
|
||||
return <SettingsAdminConfigVariables />;
|
||||
case SETTINGS_ADMIN_TABS.HEALTH_STATUS:
|
||||
return <SettingsAdminHealthStatus />;
|
||||
default:
|
||||
|
||||
@ -0,0 +1,62 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState';
|
||||
import {
|
||||
IconDeviceFloppy,
|
||||
IconPencil,
|
||||
IconRefreshAlert,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { ConfigSource, ConfigVariable } from '~/generated/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 = useRecoilValue(
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,190 @@
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { SelectControl } from '@/ui/input/components/SelectControl';
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||
import { SelectHotkeyScope } from '@/ui/input/types/SelectHotkeyScope';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { ConfigVariableValue } from 'twenty-shared/types';
|
||||
import { MenuItemMultiSelect } from 'twenty-ui/navigation';
|
||||
import { ConfigVariableType } from '~/generated/graphql';
|
||||
import { ConfigVariableOptions } from '../types/ConfigVariableOptions';
|
||||
|
||||
type ConfigVariableDatabaseInputProps = {
|
||||
label: string;
|
||||
value: ConfigVariableValue;
|
||||
onChange: (value: string | number | boolean | string[] | null) => void;
|
||||
type: ConfigVariableType;
|
||||
options?: ConfigVariableOptions;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
export const ConfigVariableDatabaseInput = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
type,
|
||||
options,
|
||||
disabled,
|
||||
placeholder,
|
||||
}: ConfigVariableDatabaseInputProps) => {
|
||||
const selectOptions =
|
||||
options && Array.isArray(options)
|
||||
? options.map((option) => ({
|
||||
value: String(option),
|
||||
label: String(option),
|
||||
}))
|
||||
: [];
|
||||
|
||||
const booleanOptions = [
|
||||
{ value: 'true', label: 'true' },
|
||||
{ value: 'false', label: 'false' },
|
||||
];
|
||||
|
||||
const isValueSelected = (optionValue: string) => {
|
||||
if (!Array.isArray(value)) return false;
|
||||
return value.includes(optionValue);
|
||||
};
|
||||
|
||||
const handleMultiSelectChange = (optionValue: string) => {
|
||||
if (!Array.isArray(value)) return;
|
||||
|
||||
let newValues = [...value];
|
||||
if (isValueSelected(optionValue)) {
|
||||
newValues = newValues.filter((val) => val !== optionValue);
|
||||
} else {
|
||||
newValues.push(optionValue);
|
||||
}
|
||||
onChange(newValues);
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case ConfigVariableType.BOOLEAN:
|
||||
return (
|
||||
<Select
|
||||
label={label}
|
||||
value={String(value ?? '')}
|
||||
onChange={(newValue: string) => onChange(newValue === 'true')}
|
||||
disabled={disabled}
|
||||
options={booleanOptions}
|
||||
dropdownId="config-variable-boolean-select"
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
|
||||
case ConfigVariableType.NUMBER:
|
||||
return (
|
||||
<TextInputV2
|
||||
label={label}
|
||||
value={value !== null && value !== undefined ? String(value) : ''}
|
||||
onChange={(text) => {
|
||||
const num = Number(text);
|
||||
onChange(isNaN(num) ? text : num);
|
||||
}}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder}
|
||||
type="number"
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
|
||||
case ConfigVariableType.ARRAY:
|
||||
return (
|
||||
<>
|
||||
{options && Array.isArray(options) ? (
|
||||
<Dropdown
|
||||
dropdownId="config-variable-array-dropdown"
|
||||
dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }}
|
||||
dropdownPlacement="bottom-start"
|
||||
dropdownOffset={{
|
||||
y: 8,
|
||||
}}
|
||||
clickableComponent={
|
||||
<SelectControl
|
||||
selectedOption={{
|
||||
value: '',
|
||||
label:
|
||||
Array.isArray(value) && value.length > 0
|
||||
? value.join(', ')
|
||||
: 'Select options',
|
||||
}}
|
||||
isDisabled={disabled}
|
||||
hasRightElement={false}
|
||||
selectSizeVariant="default"
|
||||
/>
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownMenuItemsContainer>
|
||||
{selectOptions.map((option) => (
|
||||
<MenuItemMultiSelect
|
||||
key={option.value}
|
||||
text={option.label}
|
||||
selected={isValueSelected(option.value)}
|
||||
className="config-variable-array-menu-item-multi-select"
|
||||
onSelectChange={() =>
|
||||
handleMultiSelectChange(option.value)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<TextArea
|
||||
label={label}
|
||||
value={
|
||||
Array.isArray(value)
|
||||
? JSON.stringify(value)
|
||||
: String(value ?? '')
|
||||
}
|
||||
onChange={(text) => {
|
||||
try {
|
||||
const arr = JSON.parse(text);
|
||||
onChange(Array.isArray(arr) ? arr : value);
|
||||
} catch {
|
||||
onChange(text);
|
||||
}
|
||||
}}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder || 'Enter JSON array'}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
case ConfigVariableType.ENUM:
|
||||
return (
|
||||
<Select
|
||||
label={label}
|
||||
value={String(value ?? '')}
|
||||
onChange={(newValue: string) => onChange(newValue)}
|
||||
disabled={disabled}
|
||||
options={selectOptions}
|
||||
dropdownId="config-variable-enum-select"
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
|
||||
case ConfigVariableType.STRING:
|
||||
return (
|
||||
<TextInputV2
|
||||
label={label}
|
||||
value={
|
||||
typeof value === 'string'
|
||||
? value
|
||||
: value !== null && value !== undefined
|
||||
? JSON.stringify(value)
|
||||
: ''
|
||||
}
|
||||
onChange={(text) => onChange(text)}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder || 'Enter value'}
|
||||
fullWidth
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
throw new Error(`Unsupported type: ${type}`);
|
||||
}
|
||||
};
|
||||
@ -0,0 +1,44 @@
|
||||
import { ConfigVariableSourceFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableSourceFilter';
|
||||
import { SortOrFilterChip } from '@/views/components/SortOrFilterChip';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledChipContainer = styled.div`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
`;
|
||||
|
||||
type ConfigVariableFilterContainerProps = {
|
||||
children: React.ReactNode;
|
||||
activeChips: {
|
||||
label: string;
|
||||
onRemove: () => void;
|
||||
source?: ConfigVariableSourceFilter;
|
||||
variant?: 'default' | 'danger';
|
||||
}[];
|
||||
};
|
||||
|
||||
export const ConfigVariableFilterContainer = ({
|
||||
children,
|
||||
activeChips,
|
||||
}: ConfigVariableFilterContainerProps) => {
|
||||
return (
|
||||
<div>
|
||||
{children}
|
||||
{activeChips.length > 0 && (
|
||||
<StyledChipContainer>
|
||||
{activeChips.map((chip) => (
|
||||
<SortOrFilterChip
|
||||
key={chip.label + chip.source}
|
||||
labelKey={chip.label}
|
||||
onRemove={chip.onRemove}
|
||||
labelValue={chip.source ?? ''}
|
||||
variant={chip.variant}
|
||||
/>
|
||||
))}
|
||||
</StyledChipContainer>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,66 @@
|
||||
import { ConfigVariableFilterCategory } from '@/settings/admin-panel/config-variables/types/ConfigVariableFilterCategory';
|
||||
import { ConfigVariableGroupFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableGroupFilter';
|
||||
import { ConfigVariableSourceFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableSourceFilter';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { useState } from 'react';
|
||||
import { IconSettings } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { ConfigVariableOptionsDropdownContent } from './ConfigVariableOptionsDropdownContent';
|
||||
|
||||
type ConfigVariableFilterDropdownProps = {
|
||||
sourceFilter: ConfigVariableSourceFilter;
|
||||
groupFilter: ConfigVariableGroupFilter;
|
||||
groupOptions: { value: string; label: string }[];
|
||||
showHiddenGroupVariables: boolean;
|
||||
onSourceFilterChange: (source: ConfigVariableSourceFilter) => void;
|
||||
onGroupFilterChange: (group: ConfigVariableGroupFilter) => void;
|
||||
onShowHiddenChange: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const ConfigVariableFilterDropdown = ({
|
||||
sourceFilter,
|
||||
groupFilter,
|
||||
groupOptions,
|
||||
showHiddenGroupVariables,
|
||||
onSourceFilterChange,
|
||||
onGroupFilterChange,
|
||||
onShowHiddenChange,
|
||||
}: ConfigVariableFilterDropdownProps) => {
|
||||
const [selectedCategory, setSelectedCategory] =
|
||||
useState<ConfigVariableFilterCategory | null>(null);
|
||||
|
||||
const handleSelectCategory = (
|
||||
category: ConfigVariableFilterCategory | null,
|
||||
) => {
|
||||
setSelectedCategory(category);
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
clickableComponent={
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
title="Options"
|
||||
Icon={IconSettings}
|
||||
/>
|
||||
}
|
||||
dropdownId="env-var-options-dropdown"
|
||||
dropdownHotkeyScope={{ scope: 'env-var-options' }}
|
||||
dropdownOffset={{ x: 0, y: 10 }}
|
||||
dropdownComponents={
|
||||
<ConfigVariableOptionsDropdownContent
|
||||
selectedCategory={selectedCategory}
|
||||
onSelectCategory={handleSelectCategory}
|
||||
sourceFilter={sourceFilter}
|
||||
groupFilter={groupFilter}
|
||||
groupOptions={groupOptions}
|
||||
showHiddenGroupVariables={showHiddenGroupVariables}
|
||||
onSourceFilterChange={onSourceFilterChange}
|
||||
onGroupFilterChange={onGroupFilterChange}
|
||||
onShowHiddenChange={onShowHiddenChange}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,85 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { ConfigSource, ConfigVariable } from '~/generated/graphql';
|
||||
|
||||
const StyledHelpText = styled.div<{ color?: string }>`
|
||||
color: ${({ theme, color }) => color || theme.font.color.tertiary};
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
line-height: 1.5;
|
||||
`;
|
||||
|
||||
type ConfigVariableHelpTextProps = {
|
||||
variable: ConfigVariable;
|
||||
hasValueChanged: boolean;
|
||||
color?: string;
|
||||
};
|
||||
|
||||
export const ConfigVariableHelpText = ({
|
||||
variable,
|
||||
hasValueChanged,
|
||||
}: ConfigVariableHelpTextProps) => {
|
||||
const isConfigVariablesInDbEnabled = useRecoilValue(
|
||||
isConfigVariablesInDbEnabledState,
|
||||
);
|
||||
const { t } = useLingui();
|
||||
const isFromDatabase = variable.source === ConfigSource.DATABASE;
|
||||
const isFromEnvironment = variable.source === ConfigSource.ENVIRONMENT;
|
||||
const isReadOnly = !isConfigVariablesInDbEnabled;
|
||||
|
||||
if (isReadOnly) {
|
||||
return (
|
||||
<StyledHelpText>
|
||||
{t`Database configuration is currently disabled.`}{' '}
|
||||
{isFromEnvironment
|
||||
? t`Value is set in the server environment, it may be a different value on the worker.`
|
||||
: t`Using default application value. Configure via environment variables.`}
|
||||
</StyledHelpText>
|
||||
);
|
||||
}
|
||||
|
||||
if (isConfigVariablesInDbEnabled && variable.isEnvOnly) {
|
||||
return (
|
||||
<StyledHelpText>
|
||||
{t`This setting can only be configured through environment variables.`}
|
||||
</StyledHelpText>
|
||||
);
|
||||
}
|
||||
|
||||
if (isConfigVariablesInDbEnabled && !variable.isEnvOnly && hasValueChanged) {
|
||||
return (
|
||||
<StyledHelpText>
|
||||
{isFromDatabase
|
||||
? t`Click on the checkmark to apply your changes.`
|
||||
: t`This value will be saved to the database.`}
|
||||
</StyledHelpText>
|
||||
);
|
||||
}
|
||||
|
||||
if (isConfigVariablesInDbEnabled && !variable.isEnvOnly && !hasValueChanged) {
|
||||
if (isFromDatabase) {
|
||||
return (
|
||||
<>
|
||||
<StyledHelpText>
|
||||
{t`This database value overrides environment settings. `}
|
||||
</StyledHelpText>
|
||||
<StyledHelpText>
|
||||
{t`Clear the field or "X" to revert to environment/default value.`}
|
||||
</StyledHelpText>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<StyledHelpText>
|
||||
{isFromEnvironment
|
||||
? t`Current value from server environment. Set a custom value to override.`
|
||||
: t`Using default value. Set a custom value to override.`}
|
||||
</StyledHelpText>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return <StyledHelpText>{t`This should never happen`}</StyledHelpText>;
|
||||
};
|
||||
@ -0,0 +1,141 @@
|
||||
import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState';
|
||||
import { ConfigVariableSourceOptions } from '@/settings/admin-panel/config-variables/constants/ConfigVariableSourceOptions';
|
||||
import { ConfigVariableFilterCategory } from '@/settings/admin-panel/config-variables/types/ConfigVariableFilterCategory';
|
||||
import { ConfigVariableGroupFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableGroupFilter';
|
||||
import { ConfigVariableSourceFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableSourceFilter';
|
||||
import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader/DropdownMenuHeader';
|
||||
import { DropdownMenuHeaderLeftComponent } from '@/ui/layout/dropdown/components/DropdownMenuHeader/internal/DropdownMenuHeaderLeftComponent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconChevronLeft, IconEye, IconEyeOff } from 'twenty-ui/display';
|
||||
import { MenuItem, MenuItemSelectTag } from 'twenty-ui/navigation';
|
||||
|
||||
type ConfigVariableOptionsDropdownContentProps = {
|
||||
selectedCategory: ConfigVariableFilterCategory | null;
|
||||
onSelectCategory: (category: ConfigVariableFilterCategory | null) => void;
|
||||
sourceFilter: ConfigVariableSourceFilter;
|
||||
groupFilter: ConfigVariableGroupFilter;
|
||||
groupOptions: { value: ConfigVariableGroupFilter; label: string }[];
|
||||
showHiddenGroupVariables: boolean;
|
||||
onSourceFilterChange: (source: ConfigVariableSourceFilter) => void;
|
||||
onGroupFilterChange: (group: ConfigVariableGroupFilter) => void;
|
||||
onShowHiddenChange: (value: boolean) => void;
|
||||
};
|
||||
|
||||
export const ConfigVariableOptionsDropdownContent = ({
|
||||
selectedCategory,
|
||||
onSelectCategory,
|
||||
sourceFilter,
|
||||
groupFilter,
|
||||
groupOptions,
|
||||
showHiddenGroupVariables,
|
||||
onSourceFilterChange,
|
||||
onGroupFilterChange,
|
||||
onShowHiddenChange,
|
||||
}: ConfigVariableOptionsDropdownContentProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const isConfigVariablesInDbEnabled = useRecoilValue(
|
||||
isConfigVariablesInDbEnabledState,
|
||||
);
|
||||
|
||||
const availableSourceOptions = ConfigVariableSourceOptions.filter(
|
||||
(option) => isConfigVariablesInDbEnabled || option.value !== 'database',
|
||||
);
|
||||
|
||||
if (!selectedCategory) {
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItemSelectTag
|
||||
text={t`Source`}
|
||||
color="transparent"
|
||||
onClick={() => onSelectCategory('source')}
|
||||
/>
|
||||
<MenuItemSelectTag
|
||||
text={t`Group`}
|
||||
color="transparent"
|
||||
onClick={() => onSelectCategory('group')}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItemsContainer scrollable={false}>
|
||||
<MenuItem
|
||||
text={
|
||||
showHiddenGroupVariables
|
||||
? t`Hide hidden groups`
|
||||
: t`Show hidden groups`
|
||||
}
|
||||
LeftIcon={() =>
|
||||
showHiddenGroupVariables ? (
|
||||
<IconEyeOff
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
) : (
|
||||
<IconEye
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
)
|
||||
}
|
||||
onClick={() => onShowHiddenChange(!showHiddenGroupVariables)}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuHeader
|
||||
StartComponent={
|
||||
<DropdownMenuHeaderLeftComponent
|
||||
onClick={() => onSelectCategory(null)}
|
||||
Icon={IconChevronLeft}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{selectedCategory === 'source' && t`Select Source`}
|
||||
{selectedCategory === 'group' && t`Select Group`}
|
||||
</DropdownMenuHeader>
|
||||
<DropdownMenuItemsContainer>
|
||||
{selectedCategory === 'source' && (
|
||||
<>
|
||||
{availableSourceOptions.map((option) => (
|
||||
<MenuItemSelectTag
|
||||
key={option.value}
|
||||
text={option.label}
|
||||
color={option.color}
|
||||
selected={option.value === sourceFilter}
|
||||
onClick={() => {
|
||||
onSourceFilterChange(option.value);
|
||||
onSelectCategory(null);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
{selectedCategory === 'group' && (
|
||||
<>
|
||||
{groupOptions.map((option) => (
|
||||
<MenuItemSelectTag
|
||||
key={option.value}
|
||||
text={option.label}
|
||||
color="transparent"
|
||||
selected={option.value === groupFilter}
|
||||
onClick={() => {
|
||||
onGroupFilterChange(option.value);
|
||||
onSelectCategory(null);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuItemsContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,28 @@
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconSearch } from 'twenty-ui/display';
|
||||
|
||||
const StyledSearchInput = styled(TextInput)`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type ConfigVariableSearchInputProps = {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
};
|
||||
|
||||
export const ConfigVariableSearchInput = ({
|
||||
value,
|
||||
onChange,
|
||||
}: ConfigVariableSearchInputProps) => {
|
||||
return (
|
||||
<StyledSearchInput
|
||||
placeholder={t`Search config variables`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
autoFocus={false}
|
||||
LeftIcon={IconSearch}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,57 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState';
|
||||
import { TextInputV2 } from '@/ui/input/components/TextInputV2';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { ConfigVariableValue } from 'twenty-shared/types';
|
||||
import { ConfigVariable } from '~/generated/graphql';
|
||||
import { ConfigVariableDatabaseInput } from './ConfigVariableDatabaseInput';
|
||||
|
||||
type ConfigVariableValueInputProps = {
|
||||
variable: ConfigVariable;
|
||||
value: ConfigVariableValue;
|
||||
onChange: (value: string | number | boolean | string[] | null) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const StyledValueContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const ConfigVariableValueInput = ({
|
||||
variable,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
}: ConfigVariableValueInputProps) => {
|
||||
const { t } = useLingui();
|
||||
const isConfigVariablesInDbEnabled = useRecoilValue(
|
||||
isConfigVariablesInDbEnabledState,
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledValueContainer>
|
||||
{isConfigVariablesInDbEnabled && !variable.isEnvOnly ? (
|
||||
<ConfigVariableDatabaseInput
|
||||
label={t`Value`}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
type={variable.type}
|
||||
options={variable.options}
|
||||
disabled={disabled}
|
||||
placeholder={
|
||||
disabled ? 'Undefined' : t`Enter a value to store in database`
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<TextInputV2
|
||||
value={String(value)}
|
||||
disabled
|
||||
label={t`Value`}
|
||||
fullWidth
|
||||
/>
|
||||
)}
|
||||
</StyledValueContainer>
|
||||
);
|
||||
};
|
||||
@ -3,8 +3,15 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { IconCopy, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
type SettingsAdminConfigCopyableTextProps = {
|
||||
text: string;
|
||||
displayText?: React.ReactNode;
|
||||
multiline?: boolean;
|
||||
maxRows?: number;
|
||||
};
|
||||
|
||||
const StyledEllipsisLabel = styled.div`
|
||||
white-space: nowrap;
|
||||
@ -20,17 +27,12 @@ const StyledExpandedEllipsisLabel = styled.div`
|
||||
const StyledCopyContainer = styled.span`
|
||||
cursor: pointer;
|
||||
`;
|
||||
export const SettingsAdminEnvCopyableText = ({
|
||||
export const SettingsAdminConfigCopyableText = ({
|
||||
text,
|
||||
displayText,
|
||||
multiline = false,
|
||||
maxRows,
|
||||
}: {
|
||||
text: string;
|
||||
displayText?: React.ReactNode;
|
||||
multiline?: boolean;
|
||||
maxRows?: number;
|
||||
}) => {
|
||||
}: SettingsAdminConfigCopyableTextProps) => {
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
const theme = useTheme();
|
||||
const { t } = useLingui();
|
||||
@ -0,0 +1,207 @@
|
||||
import { SettingsAdminTabSkeletonLoader } from '@/settings/admin-panel/components/SettingsAdminTabSkeletonLoader';
|
||||
import { ConfigVariableFilterContainer } from '@/settings/admin-panel/config-variables/components/ConfigVariableFilterContainer';
|
||||
import { ConfigVariableFilterDropdown } from '@/settings/admin-panel/config-variables/components/ConfigVariableFilterDropdown';
|
||||
import { SettingsAdminConfigVariablesTable } from '@/settings/admin-panel/config-variables/components/SettingsAdminConfigVariablesTable';
|
||||
import { ConfigVariableSourceOptions } from '@/settings/admin-panel/config-variables/constants/ConfigVariableSourceOptions';
|
||||
import { ConfigVariableGroupFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableGroupFilter';
|
||||
import { ConfigVariableSourceFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableSourceFilter';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import {
|
||||
ConfigSource,
|
||||
useGetConfigVariablesGroupedQuery,
|
||||
} from '~/generated/graphql';
|
||||
import { ConfigVariableSearchInput } from './ConfigVariableSearchInput';
|
||||
|
||||
const StyledControlsContainer = styled.div`
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const StyledTableContainer = styled.div`
|
||||
margin-bottom: 24px;
|
||||
`;
|
||||
|
||||
export const SettingsAdminConfigVariables = () => {
|
||||
const { data: configVariables, loading: configVariablesLoading } =
|
||||
useGetConfigVariablesGroupedQuery({
|
||||
fetchPolicy: 'network-only',
|
||||
});
|
||||
|
||||
const [search, setSearch] = useState('');
|
||||
const [showHiddenGroupVariables, setShowHiddenGroupVariables] =
|
||||
useState(false);
|
||||
const [sourceFilter, setSourceFilter] =
|
||||
useState<ConfigVariableSourceFilter>('all');
|
||||
const [groupFilter, setGroupFilter] =
|
||||
useState<ConfigVariableGroupFilter>('all');
|
||||
|
||||
// Get all groups, not filtered by visibility
|
||||
const allGroups = useMemo(
|
||||
() => configVariables?.getConfigVariablesGrouped.groups ?? [],
|
||||
[configVariables],
|
||||
);
|
||||
|
||||
// Compute group options from all groups, not just visible ones
|
||||
const groupOptions = useMemo(
|
||||
() => [
|
||||
{ value: 'all', label: 'All Groups' },
|
||||
...allGroups.map((group) => ({
|
||||
value: group.name,
|
||||
label: group.name,
|
||||
})),
|
||||
],
|
||||
[allGroups],
|
||||
);
|
||||
|
||||
// Flatten all variables for filtering, attaching isHiddenOnLoad and groupName from group
|
||||
const allVariables = useMemo(
|
||||
() =>
|
||||
configVariables?.getConfigVariablesGrouped.groups.flatMap((group) =>
|
||||
group.variables.map((variable) => ({
|
||||
...variable,
|
||||
isHiddenOnLoad: group.isHiddenOnLoad,
|
||||
groupName: group.name,
|
||||
})),
|
||||
) ?? [],
|
||||
[configVariables],
|
||||
);
|
||||
|
||||
// Filtering logic
|
||||
const filteredVariables = useMemo(() => {
|
||||
const isSearching = search.trim().length > 0;
|
||||
const hasSelectedSpecificGroup = groupFilter !== 'all';
|
||||
|
||||
return allVariables.filter((v) => {
|
||||
// Search filter
|
||||
const matchesSearch =
|
||||
v.name.toLowerCase().includes(search.toLowerCase()) ||
|
||||
(v.description?.toLowerCase() || '').includes(search.toLowerCase());
|
||||
|
||||
if (isSearching && !matchesSearch) return false;
|
||||
|
||||
// Group filter
|
||||
const matchesGroup = hasSelectedSpecificGroup
|
||||
? v.groupName === groupFilter
|
||||
: true;
|
||||
|
||||
if (hasSelectedSpecificGroup && !matchesGroup) return false;
|
||||
|
||||
// Hidden filter - Only apply if:
|
||||
// 1. User is not searching
|
||||
// 2. Show hidden is off
|
||||
// 3. Item is from a hidden group
|
||||
// 4. No specific group is selected (if a specific group is selected, show all its variables)
|
||||
if (
|
||||
!isSearching &&
|
||||
!showHiddenGroupVariables &&
|
||||
v.isHiddenOnLoad &&
|
||||
!hasSelectedSpecificGroup
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Source filter
|
||||
let matchesSource = true;
|
||||
if (sourceFilter === 'database')
|
||||
matchesSource = v.source === ConfigSource.DATABASE;
|
||||
if (sourceFilter === 'environment')
|
||||
matchesSource = v.source === ConfigSource.ENVIRONMENT;
|
||||
if (sourceFilter === 'default')
|
||||
matchesSource = v.source === ConfigSource.DEFAULT;
|
||||
|
||||
return matchesSource;
|
||||
});
|
||||
}, [
|
||||
allVariables,
|
||||
search,
|
||||
showHiddenGroupVariables,
|
||||
sourceFilter,
|
||||
groupFilter,
|
||||
]);
|
||||
|
||||
// Build activeChips for current filters
|
||||
const activeChips = [];
|
||||
if (sourceFilter !== 'all') {
|
||||
activeChips.push({
|
||||
label:
|
||||
ConfigVariableSourceOptions.find((o) => o.value === sourceFilter)
|
||||
?.label || '',
|
||||
onRemove: () => setSourceFilter('all'),
|
||||
variant: 'default' as const,
|
||||
});
|
||||
}
|
||||
if (groupFilter !== 'all') {
|
||||
activeChips.push({
|
||||
label: groupOptions.find((o) => o.value === groupFilter)?.label || '',
|
||||
onRemove: () => setGroupFilter('all'),
|
||||
variant: 'danger' as const,
|
||||
});
|
||||
}
|
||||
|
||||
// Group variables by groupName for rendering
|
||||
const groupedVariables = useMemo(() => {
|
||||
const groupMap = new Map();
|
||||
filteredVariables.forEach((v) => {
|
||||
if (!groupMap.has(v.groupName)) {
|
||||
const group = allGroups.find((g) => g.name === v.groupName);
|
||||
groupMap.set(v.groupName, {
|
||||
variables: [],
|
||||
description: group?.description || '',
|
||||
});
|
||||
}
|
||||
groupMap.get(v.groupName).variables.push(v);
|
||||
});
|
||||
return groupMap;
|
||||
}, [filteredVariables, allGroups]);
|
||||
|
||||
if (configVariablesLoading) {
|
||||
return <SettingsAdminTabSkeletonLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title title={t`Config Variables`} />
|
||||
|
||||
<ConfigVariableFilterContainer activeChips={activeChips}>
|
||||
<StyledControlsContainer>
|
||||
<ConfigVariableSearchInput value={search} onChange={setSearch} />
|
||||
<ConfigVariableFilterDropdown
|
||||
sourceFilter={sourceFilter}
|
||||
groupFilter={groupFilter}
|
||||
groupOptions={groupOptions}
|
||||
showHiddenGroupVariables={showHiddenGroupVariables}
|
||||
onSourceFilterChange={setSourceFilter}
|
||||
onGroupFilterChange={setGroupFilter}
|
||||
onShowHiddenChange={setShowHiddenGroupVariables}
|
||||
/>
|
||||
</StyledControlsContainer>
|
||||
</ConfigVariableFilterContainer>
|
||||
</Section>
|
||||
|
||||
{groupedVariables.size === 0 && (
|
||||
<StyledTableContainer>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`No variables found`}
|
||||
description={t`No config variables match your current filters. Try adjusting your filters or search criteria.`}
|
||||
/>
|
||||
</Section>
|
||||
</StyledTableContainer>
|
||||
)}
|
||||
|
||||
{[...groupedVariables.entries()].map(([groupName, groupData]) => (
|
||||
<StyledTableContainer key={groupName}>
|
||||
<H2Title title={groupName} description={groupData.description} />
|
||||
|
||||
<SettingsAdminConfigVariablesTable variables={groupData.variables} />
|
||||
</StyledTableContainer>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,70 @@
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { IconChevronRight } from 'twenty-ui/display';
|
||||
import { ConfigVariable } from '~/generated/graphql';
|
||||
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
|
||||
|
||||
type SettingsAdminConfigVariablesRowProps = {
|
||||
variable: ConfigVariable;
|
||||
};
|
||||
|
||||
const StyledTruncatedCell = styled(TableCell)`
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
const StyledTableRow = styled(TableRow)`
|
||||
&:hover {
|
||||
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledEllipsisLabel = styled.div`
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
export const SettingsAdminConfigVariablesRow = ({
|
||||
variable,
|
||||
}: SettingsAdminConfigVariablesRowProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
const displayValue =
|
||||
variable.value === ''
|
||||
? 'null'
|
||||
: variable.isSensitive
|
||||
? '••••••'
|
||||
: typeof variable.value === 'boolean'
|
||||
? variable.value
|
||||
? 'true'
|
||||
: 'false'
|
||||
: variable.value;
|
||||
|
||||
return (
|
||||
<StyledTableRow
|
||||
gridAutoColumns="5fr 3fr 1fr"
|
||||
to={getSettingsPath(SettingsPath.AdminPanelConfigVariableDetails, {
|
||||
variableName: variable.name,
|
||||
})}
|
||||
>
|
||||
<StyledTruncatedCell color={theme.font.color.primary}>
|
||||
<StyledEllipsisLabel>{variable.name}</StyledEllipsisLabel>
|
||||
</StyledTruncatedCell>
|
||||
<StyledTruncatedCell align="right">
|
||||
<StyledEllipsisLabel>{displayValue}</StyledEllipsisLabel>
|
||||
</StyledTruncatedCell>
|
||||
<TableCell align="right">
|
||||
<IconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
color={theme.font.color.tertiary}
|
||||
/>
|
||||
</TableCell>
|
||||
</StyledTableRow>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,37 @@
|
||||
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 '@emotion/styled';
|
||||
import { ConfigVariable } from '~/generated/graphql';
|
||||
|
||||
const StyledTableBody = styled(TableBody)`
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
`;
|
||||
|
||||
type SettingsAdminConfigVariablesTableProps = {
|
||||
variables: ConfigVariable[];
|
||||
};
|
||||
|
||||
export const SettingsAdminConfigVariablesTable = ({
|
||||
variables,
|
||||
}: SettingsAdminConfigVariablesTableProps) => {
|
||||
return (
|
||||
<Table>
|
||||
<TableRow gridAutoColumns="5fr 3fr 1fr">
|
||||
<TableHeader>Name</TableHeader>
|
||||
<TableHeader align="right">Value</TableHeader>
|
||||
<TableHeader align="right"></TableHeader>
|
||||
</TableRow>
|
||||
<StyledTableBody>
|
||||
{variables.map((variable) => (
|
||||
<SettingsAdminConfigVariablesRow
|
||||
key={variable.name}
|
||||
variable={variable}
|
||||
/>
|
||||
))}
|
||||
</StyledTableBody>
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,15 @@
|
||||
import { ConfigVariableSourceFilter } from '@/settings/admin-panel/config-variables/types/ConfigVariableSourceFilter';
|
||||
import { ThemeColor } from 'twenty-ui/theme';
|
||||
|
||||
type ConfigVariableSourceOption = {
|
||||
value: ConfigVariableSourceFilter;
|
||||
label: string;
|
||||
color: ThemeColor | 'transparent';
|
||||
};
|
||||
|
||||
export const ConfigVariableSourceOptions: ConfigVariableSourceOption[] = [
|
||||
{ value: 'all', label: 'All Sources', color: 'transparent' },
|
||||
{ value: 'database', label: 'Database', color: 'blue' },
|
||||
{ value: 'environment', label: 'Environment', color: 'green' },
|
||||
{ value: 'default', label: 'Default', color: 'gray' },
|
||||
];
|
||||
@ -0,0 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const CREATE_DATABASE_CONFIG_VARIABLE = gql`
|
||||
mutation CreateDatabaseConfigVariable($key: String!, $value: JSON!) {
|
||||
createDatabaseConfigVariable(key: $key, value: $value)
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_DATABASE_CONFIG_VARIABLE = gql`
|
||||
mutation DeleteDatabaseConfigVariable($key: String!) {
|
||||
deleteDatabaseConfigVariable(key: $key)
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,7 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPDATE_DATABASE_CONFIG_VARIABLE = gql`
|
||||
mutation UpdateDatabaseConfigVariable($key: String!, $value: JSON!) {
|
||||
updateDatabaseConfigVariable(key: $key, value: $value)
|
||||
}
|
||||
`;
|
||||
@ -12,6 +12,10 @@ export const GET_CONFIG_VARIABLES_GROUPED = gql`
|
||||
description
|
||||
value
|
||||
isSensitive
|
||||
isEnvOnly
|
||||
type
|
||||
options
|
||||
source
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,16 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_DATABASE_CONFIG_VARIABLE = gql`
|
||||
query GetDatabaseConfigVariable($key: String!) {
|
||||
getDatabaseConfigVariable(key: $key) {
|
||||
name
|
||||
description
|
||||
value
|
||||
isSensitive
|
||||
isEnvOnly
|
||||
type
|
||||
options
|
||||
source
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,115 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
import { GET_CLIENT_CONFIG } from '@/client-config/graphql/queries/getClientConfig';
|
||||
import { GET_DATABASE_CONFIG_VARIABLE } from '@/settings/admin-panel/config-variables/graphql/queries/getDatabaseConfigVariable';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { ConfigVariableValue } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
useCreateDatabaseConfigVariableMutation,
|
||||
useDeleteDatabaseConfigVariableMutation,
|
||||
useUpdateDatabaseConfigVariableMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export const useConfigVariableActions = (variableName: string) => {
|
||||
const { t } = useLingui();
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const [updateDatabaseConfigVariable] =
|
||||
useUpdateDatabaseConfigVariableMutation();
|
||||
const [createDatabaseConfigVariable] =
|
||||
useCreateDatabaseConfigVariableMutation();
|
||||
const [deleteDatabaseConfigVariable] =
|
||||
useDeleteDatabaseConfigVariableMutation();
|
||||
|
||||
const handleUpdateVariable = async (
|
||||
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 },
|
||||
},
|
||||
{
|
||||
query: GET_CLIENT_CONFIG,
|
||||
},
|
||||
],
|
||||
});
|
||||
} else {
|
||||
await createDatabaseConfigVariable({
|
||||
variables: {
|
||||
key: variableName,
|
||||
value,
|
||||
},
|
||||
refetchQueries: [
|
||||
{
|
||||
query: GET_DATABASE_CONFIG_VARIABLE,
|
||||
variables: { key: variableName },
|
||||
},
|
||||
{
|
||||
query: GET_CLIENT_CONFIG,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
enqueueSnackBar(t`Variable updated successfully`, {
|
||||
variant: SnackBarVariant.Success,
|
||||
});
|
||||
} catch (error) {
|
||||
enqueueSnackBar(t`Failed to update variable`, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteVariable = async (e?: React.MouseEvent<HTMLElement>) => {
|
||||
if (isDefined(e)) {
|
||||
e.preventDefault();
|
||||
}
|
||||
|
||||
try {
|
||||
await deleteDatabaseConfigVariable({
|
||||
variables: {
|
||||
key: variableName,
|
||||
},
|
||||
refetchQueries: [
|
||||
{
|
||||
query: GET_DATABASE_CONFIG_VARIABLE,
|
||||
variables: { key: variableName },
|
||||
},
|
||||
{
|
||||
query: GET_CLIENT_CONFIG,
|
||||
},
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
enqueueSnackBar(t`Failed to remove override`, {
|
||||
variant: SnackBarVariant.Error,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
handleUpdateVariable,
|
||||
handleDeleteVariable,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,54 @@
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { ConfigVariableValue } from 'twenty-shared/types';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { ConfigVariable } from '~/generated/graphql';
|
||||
|
||||
type FormValues = {
|
||||
value: ConfigVariableValue;
|
||||
};
|
||||
|
||||
export const useConfigVariableForm = (variable?: ConfigVariable) => {
|
||||
const validationSchema = z.object({
|
||||
value: z.union([
|
||||
z.string(),
|
||||
z.number(),
|
||||
z.boolean(),
|
||||
z.array(z.string()),
|
||||
z.null(),
|
||||
]),
|
||||
});
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
setValue,
|
||||
formState: { isSubmitting },
|
||||
watch,
|
||||
} = useForm<FormValues>({
|
||||
resolver: zodResolver(validationSchema),
|
||||
values: { value: variable?.value ?? null },
|
||||
});
|
||||
|
||||
const currentValue = watch('value');
|
||||
const hasValueChanged = currentValue !== variable?.value;
|
||||
const isValueValid = !!(
|
||||
variable &&
|
||||
!variable.isEnvOnly &&
|
||||
hasValueChanged &&
|
||||
((typeof currentValue === 'string' && currentValue.trim() !== '') ||
|
||||
typeof currentValue === 'boolean' ||
|
||||
typeof currentValue === 'number' ||
|
||||
(Array.isArray(currentValue) && currentValue.length > 0))
|
||||
);
|
||||
|
||||
return {
|
||||
handleSubmit,
|
||||
setValue,
|
||||
isSubmitting,
|
||||
watch,
|
||||
currentValue,
|
||||
hasValueChanged,
|
||||
isValueValid,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1 @@
|
||||
export type ConfigVariableFilterCategory = 'source' | 'group';
|
||||
@ -0,0 +1 @@
|
||||
export type ConfigVariableGroupFilter = 'all' | string;
|
||||
@ -0,0 +1,3 @@
|
||||
export type ConfigVariableOptions =
|
||||
| readonly (string | number | boolean)[]
|
||||
| Record<string, string>;
|
||||
@ -0,0 +1,5 @@
|
||||
export type ConfigVariableSourceFilter =
|
||||
| 'all'
|
||||
| 'database'
|
||||
| 'environment'
|
||||
| 'default';
|
||||
@ -0,0 +1,29 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
import { ConfigSource } from '~/generated/graphql';
|
||||
|
||||
export const useSourceContent = (source: ConfigSource) => {
|
||||
const { t } = useLingui();
|
||||
const theme = useTheme();
|
||||
|
||||
switch (source) {
|
||||
case ConfigSource.DATABASE:
|
||||
return {
|
||||
text: t`Stored in database`,
|
||||
color: theme.color.blue50,
|
||||
};
|
||||
case ConfigSource.ENVIRONMENT:
|
||||
return {
|
||||
text: t`Environment variable`,
|
||||
color: theme.color.green50,
|
||||
};
|
||||
case ConfigSource.DEFAULT:
|
||||
return {
|
||||
text: t`Default value`,
|
||||
color: theme.font.color.tertiary,
|
||||
};
|
||||
default:
|
||||
throw new Error(`Unknown source: ${source}`);
|
||||
}
|
||||
};
|
||||
@ -1,5 +1,5 @@
|
||||
export const SETTINGS_ADMIN_TABS = {
|
||||
GENERAL: 'general',
|
||||
ENV_VARIABLES: 'env-variables',
|
||||
CONFIG_VARIABLES: 'config-variables',
|
||||
HEALTH_STATUS: 'health-status',
|
||||
};
|
||||
|
||||
@ -3,11 +3,11 @@ import { z } from 'zod';
|
||||
|
||||
import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem';
|
||||
import { SettingsOptionCardContentSelect } from '@/settings/components/SettingsOptions/SettingsOptionCardContentSelect';
|
||||
import { BOOLEAN_DATA_MODEL_SELECT_OPTIONS } from '@/settings/data-model/fields/forms/boolean/constants/BooleanDataModelSelectOptions';
|
||||
import { useBooleanSettingsFormInitialValues } from '@/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues';
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { IconCheck } from 'twenty-ui/display';
|
||||
import { BOOLEAN_DATA_MODEL_SELECT_OPTIONS } from '@/settings/data-model/fields/forms/boolean/constants/BooleanDataModelSelectOptions';
|
||||
|
||||
export const settingsDataModelFieldBooleanFormSchema = z.object({
|
||||
defaultValue: z.boolean(),
|
||||
|
||||
@ -39,7 +39,7 @@ export enum SettingsPath {
|
||||
AdminPanel = 'admin-panel',
|
||||
AdminPanelHealthStatus = 'admin-panel#health-status',
|
||||
AdminPanelIndicatorHealthStatus = 'admin-panel/health-status/:indicatorId',
|
||||
AdminPanelOtherEnvVariables = 'admin-panel/other-env-variables',
|
||||
AdminPanelConfigVariableDetails = 'admin-panel/config-variables/:variableName',
|
||||
Lab = 'lab',
|
||||
Roles = 'roles',
|
||||
RoleCreate = 'roles/create',
|
||||
|
||||
Reference in New Issue
Block a user