Compare commits
3 Commits
main
...
fix-app-design-2
| Author | SHA1 | Date | |
|---|---|---|---|
| 6a32032b78 | |||
| ea5091f8c0 | |||
| 5f5f654602 |
@@ -2524,6 +2524,7 @@ type File {
|
||||
|
||||
type MarketplaceApp {
|
||||
id: String!
|
||||
universalIdentifier: String!
|
||||
name: String!
|
||||
description: String!
|
||||
icon: String!
|
||||
|
||||
@@ -2235,6 +2235,7 @@ export interface File {
|
||||
|
||||
export interface MarketplaceApp {
|
||||
id: Scalars['String']
|
||||
universalIdentifier: Scalars['String']
|
||||
name: Scalars['String']
|
||||
description: Scalars['String']
|
||||
icon: Scalars['String']
|
||||
@@ -5564,6 +5565,7 @@ export interface FileGenqlSelection{
|
||||
|
||||
export interface MarketplaceAppGenqlSelection{
|
||||
id?: boolean | number
|
||||
universalIdentifier?: boolean | number
|
||||
name?: boolean | number
|
||||
description?: boolean | number
|
||||
icon?: boolean | number
|
||||
|
||||
@@ -5036,6 +5036,9 @@ export default {
|
||||
"id": [
|
||||
1
|
||||
],
|
||||
"universalIdentifier": [
|
||||
1
|
||||
],
|
||||
"name": [
|
||||
1
|
||||
],
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -172,14 +172,6 @@ const SettingsApplications = lazy(() =>
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsApplicationDetails = lazy(() =>
|
||||
import('~/pages/settings/applications/SettingsApplicationDetails').then(
|
||||
(module) => ({
|
||||
default: module.SettingsApplicationDetails,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsAdminApplicationRegistrationDetail = lazy(() =>
|
||||
import(
|
||||
'~/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDetail'
|
||||
@@ -188,12 +180,12 @@ const SettingsAdminApplicationRegistrationDetail = lazy(() =>
|
||||
})),
|
||||
);
|
||||
|
||||
const SettingsAvailableApplicationDetails = lazy(() =>
|
||||
import(
|
||||
'~/pages/settings/applications/SettingsAvailableApplicationDetails'
|
||||
).then((module) => ({
|
||||
default: module.SettingsAvailableApplicationDetails,
|
||||
})),
|
||||
const SettingsApplicationPage = lazy(() =>
|
||||
import('~/pages/settings/applications/SettingsApplicationPage').then(
|
||||
(module) => ({
|
||||
default: module.SettingsApplicationPage,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsApplicationRegistrationDetails = lazy(() =>
|
||||
@@ -738,11 +730,7 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.ApplicationDetail}
|
||||
element={<SettingsApplicationDetails />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.AvailableApplicationDetail}
|
||||
element={<SettingsAvailableApplicationDetails />}
|
||||
element={<SettingsApplicationPage />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.ApplicationRegistrationDetail}
|
||||
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { APPLICATION_REGISTRATION_FRAGMENT } from '@/settings/application-registrations/graphql/fragments/applicationRegistrationFragment';
|
||||
|
||||
export const FIND_APPLICATION_REGISTRATION_BY_UNIVERSAL_IDENTIFIER = gql`
|
||||
query findApplicationRegistrationByUniversalIdentifier(
|
||||
$universalIdentifier: String!
|
||||
) {
|
||||
findApplicationRegistrationByUniversalIdentifier(
|
||||
universalIdentifier: $universalIdentifier
|
||||
) {
|
||||
...ApplicationRegistrationFragment
|
||||
}
|
||||
${APPLICATION_REGISTRATION_FRAGMENT}
|
||||
}
|
||||
`;
|
||||
+1
@@ -3,6 +3,7 @@ import gql from 'graphql-tag';
|
||||
export const MARKETPLACE_APP_FRAGMENT = gql`
|
||||
fragment MarketplaceAppFields on MarketplaceApp {
|
||||
id
|
||||
universalIdentifier
|
||||
name
|
||||
description
|
||||
icon
|
||||
|
||||
+4
-1
@@ -102,7 +102,10 @@ export const SettingsAdminApps = () => {
|
||||
key={registration.id}
|
||||
to={getSettingsPath(
|
||||
SettingsPath.AdminPanelApplicationRegistrationDetail,
|
||||
{ applicationRegistrationId: registration.id },
|
||||
{
|
||||
applicationUniversalIdentifier:
|
||||
registration.universalIdentifier,
|
||||
},
|
||||
)}
|
||||
gridAutoColumns={TABLE_GRID}
|
||||
mobileGridAutoColumns={TABLE_GRID_MOBILE}
|
||||
|
||||
-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,
|
||||
};
|
||||
};
|
||||
+171
@@ -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>
|
||||
);
|
||||
};
|
||||
+95
@@ -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>
|
||||
);
|
||||
};
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { LinkChip } from 'twenty-ui/components';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
|
||||
export const SettingsLogicFunctionTabEnvironmentVariablesSection = () => {
|
||||
const { applicationId = '' } = useParams<{ applicationId: string }>();
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Environment Variables`}
|
||||
description={t`Accessible in your function via process.env.KEY`}
|
||||
/>
|
||||
<Trans>
|
||||
Environment variables are defined at application level for all
|
||||
functions. Please check{' '}
|
||||
<LinkChip
|
||||
label={t`application detail page`}
|
||||
to={getSettingsPath(
|
||||
SettingsPath.ApplicationDetail,
|
||||
{
|
||||
applicationId,
|
||||
},
|
||||
undefined,
|
||||
'settings',
|
||||
)}
|
||||
/>
|
||||
.
|
||||
</Trans>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+1
-8
@@ -1,5 +1,4 @@
|
||||
import { SettingsLogicFunctionNewForm } from '@/settings/logic-functions/components/SettingsLogicFunctionNewForm';
|
||||
import { SettingsLogicFunctionTabEnvironmentVariablesSection } from '@/settings/logic-functions/components/SettingsLogicFunctionTabEnvironmentVariablesSection';
|
||||
import { type LogicFunctionFormValues } from '@/logic-functions/hooks/useLogicFunctionUpdateFormState';
|
||||
|
||||
export const SettingsLogicFunctionSettingsTab = ({
|
||||
@@ -12,12 +11,6 @@ export const SettingsLogicFunctionSettingsTab = ({
|
||||
) => (value: LogicFunctionFormValues[TKey]) => void;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<SettingsLogicFunctionNewForm
|
||||
formValues={formValues}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<SettingsLogicFunctionTabEnvironmentVariablesSection />
|
||||
</>
|
||||
<SettingsLogicFunctionNewForm formValues={formValues} onChange={onChange} />
|
||||
);
|
||||
};
|
||||
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import {
|
||||
H1Title,
|
||||
H1TitleFontColor,
|
||||
H2Title,
|
||||
IconShare,
|
||||
IconTrash,
|
||||
AppTooltip,
|
||||
TooltipDelay,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section, SectionAlignment, SectionFontColor } from 'twenty-ui/layout';
|
||||
import {
|
||||
type ApplicationRegistration,
|
||||
DeleteApplicationRegistrationDocument,
|
||||
FindApplicationRegistrationStatsDocument,
|
||||
FindManyApplicationRegistrationsDocument,
|
||||
TransferApplicationRegistrationOwnershipDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
StyledAppModal,
|
||||
StyledAppModalButton,
|
||||
StyledAppModalSection,
|
||||
StyledAppModalTitle,
|
||||
} from '~/pages/settings/applications/components/SettingsAppModalLayout';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const DELETE_REGISTRATION_MODAL_ID = 'delete-application-registration-modal';
|
||||
|
||||
const TRANSFER_OWNERSHIP_MODAL_ID =
|
||||
'transfer-application-registration-ownership-modal';
|
||||
|
||||
const DELETE_REGISTRATION_BUTTON_ID = 'delete-registration-button';
|
||||
|
||||
const StyledDangerButtonGroup = styled.div`
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
export const SettingsAdminApplicationRegistrationDangerZone = ({
|
||||
registration,
|
||||
}: {
|
||||
registration: ApplicationRegistration;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
const navigate = useNavigateSettings();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
const { openModal, closeModal } = useModal();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isTransferring, setIsTransferring] = useState(false);
|
||||
const [transferSubdomain, setTransferSubdomain] = useState('');
|
||||
|
||||
const applicationRegistrationId = registration.id;
|
||||
|
||||
const { data: statsData } = useQuery(
|
||||
FindApplicationRegistrationStatsDocument,
|
||||
{
|
||||
variables: { id: applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
},
|
||||
);
|
||||
|
||||
const stats = statsData?.findApplicationRegistrationStats;
|
||||
|
||||
const hasActiveInstalls =
|
||||
!isDefined(stats) || (stats.activeInstalls ?? 0) > 0;
|
||||
|
||||
const [deleteRegistration] = useMutation(
|
||||
DeleteApplicationRegistrationDocument,
|
||||
{
|
||||
refetchQueries: [FindManyApplicationRegistrationsDocument],
|
||||
},
|
||||
);
|
||||
|
||||
const [transferOwnership] = useMutation(
|
||||
TransferApplicationRegistrationOwnershipDocument,
|
||||
{
|
||||
refetchQueries: [FindManyApplicationRegistrationsDocument],
|
||||
},
|
||||
);
|
||||
|
||||
const handleDelete = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await deleteRegistration({
|
||||
variables: { id: applicationRegistrationId },
|
||||
});
|
||||
navigate(SettingsPath.Applications);
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Error deleting app`,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTransferOwnership = async () => {
|
||||
const trimmed = transferSubdomain.trim();
|
||||
|
||||
if (!isNonEmptyString(trimmed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsTransferring(true);
|
||||
try {
|
||||
await transferOwnership({
|
||||
variables: {
|
||||
applicationRegistrationId,
|
||||
targetWorkspaceSubdomain: trimmed,
|
||||
},
|
||||
});
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Ownership transferred successfully`,
|
||||
});
|
||||
setTransferSubdomain('');
|
||||
navigate(SettingsPath.Applications);
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Failed to transfer ownership. Check that the subdomain is correct.`,
|
||||
});
|
||||
} finally {
|
||||
setIsTransferring(false);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmationValue = t`yes`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Danger zone`}
|
||||
description={t`Delete or transfer this app registration`}
|
||||
/>
|
||||
<StyledDangerButtonGroup>
|
||||
<Button
|
||||
id={DELETE_REGISTRATION_BUTTON_ID}
|
||||
accent="danger"
|
||||
variant="secondary"
|
||||
title={t`Delete app`}
|
||||
Icon={IconTrash}
|
||||
disabled={hasActiveInstalls}
|
||||
onClick={() => openModal(DELETE_REGISTRATION_MODAL_ID)}
|
||||
/>
|
||||
{hasActiveInstalls && (
|
||||
<AppTooltip
|
||||
anchorSelect={`#${DELETE_REGISTRATION_BUTTON_ID}`}
|
||||
content={t`Uninstall this app from all workspaces before deleting it`}
|
||||
noArrow
|
||||
place="bottom"
|
||||
positionStrategy="fixed"
|
||||
delay={TooltipDelay.shortDelay}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
accent="default"
|
||||
variant="secondary"
|
||||
title={t`Transfer ownership`}
|
||||
Icon={IconShare}
|
||||
onClick={() => openModal(TRANSFER_OWNERSHIP_MODAL_ID)}
|
||||
/>
|
||||
</StyledDangerButtonGroup>
|
||||
</Section>
|
||||
|
||||
<ConfirmationModal
|
||||
confirmationPlaceholder={confirmationValue}
|
||||
confirmationValue={confirmationValue}
|
||||
modalInstanceId={DELETE_REGISTRATION_MODAL_ID}
|
||||
title={t`Delete app`}
|
||||
subtitle={
|
||||
<Trans>
|
||||
Please type {`"${confirmationValue}"`} to confirm you want to delete
|
||||
this app. All workspace installations linked to it will lose their
|
||||
OAuth credentials.
|
||||
</Trans>
|
||||
}
|
||||
onConfirmClick={handleDelete}
|
||||
confirmButtonText={t`Delete`}
|
||||
loading={isLoading}
|
||||
/>
|
||||
|
||||
<StyledAppModal
|
||||
modalId={TRANSFER_OWNERSHIP_MODAL_ID}
|
||||
isClosable
|
||||
onClose={() => setTransferSubdomain('')}
|
||||
padding="large"
|
||||
dataGloballyPreventClickOutside
|
||||
>
|
||||
<StyledAppModalTitle>
|
||||
<H1Title
|
||||
title={t`Transfer ownership`}
|
||||
fontColor={H1TitleFontColor.Primary}
|
||||
/>
|
||||
</StyledAppModalTitle>
|
||||
<StyledAppModalSection
|
||||
alignment={SectionAlignment.Center}
|
||||
fontColor={SectionFontColor.Primary}
|
||||
>
|
||||
{t`Enter the workspace subdomain to transfer this app to. You will lose access to manage it.`}
|
||||
</StyledAppModalSection>
|
||||
<Section>
|
||||
<SettingsTextInput
|
||||
instanceId="transfer-ownership-subdomain"
|
||||
value={transferSubdomain}
|
||||
onChange={setTransferSubdomain}
|
||||
placeholder={t`e.g. my-workspace`}
|
||||
fullWidth
|
||||
disableHotkeys
|
||||
label={t`Target workspace subdomain`}
|
||||
autoFocusOnMount
|
||||
/>
|
||||
</Section>
|
||||
<StyledAppModalButton
|
||||
onClick={() => {
|
||||
closeModal(TRANSFER_OWNERSHIP_MODAL_ID);
|
||||
setTransferSubdomain('');
|
||||
}}
|
||||
variant="secondary"
|
||||
title={t`Cancel`}
|
||||
fullWidth
|
||||
/>
|
||||
<StyledAppModalButton
|
||||
onClick={handleTransferOwnership}
|
||||
variant="secondary"
|
||||
accent="danger"
|
||||
title={t`Transfer`}
|
||||
disabled={
|
||||
!isNonEmptyString(transferSubdomain.trim()) || isTransferring
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledAppModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
+68
-54
@@ -1,50 +1,46 @@
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import {
|
||||
FindAllApplicationRegistrationsDocument,
|
||||
FindOneAdminApplicationRegistrationDocument,
|
||||
UpdateApplicationRegistrationDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { FindOneAdminApplicationRegistrationDocument } from '~/generated-metadata/graphql';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { APPLICATION_REGISTRATION_ADMIN_PATH } from '@/settings/admin-panel/apps/constants/ApplicationRegistrationAdminPath';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsAdminApplicationRegistrationDetailContent } from '~/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDetailContent';
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
import { IconArrowBarToDown } from 'twenty-ui/display';
|
||||
import { Card, Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { styled } from '@linaria/react';
|
||||
import {
|
||||
IconInfoCircle,
|
||||
IconKey,
|
||||
IconSettings,
|
||||
IconWorld,
|
||||
} from 'twenty-ui/display';
|
||||
import { SettingsApplicationRegistrationConfigTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationConfigTab';
|
||||
import { SettingsApplicationRegistrationOAuthTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationOAuthTab';
|
||||
import { SettingsApplicationRegistrationDistributionTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationDistributionTab';
|
||||
import { SettingsApplicationRegistrationGeneralTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationGeneralTab';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
|
||||
const StyledToggleContainer = styled.div`
|
||||
display: flex;
|
||||
margin-top: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
const REGISTRATION_DETAIL_TAB_LIST_ID =
|
||||
'admin-application-registration-detail-tab-list';
|
||||
|
||||
export const SettingsAdminApplicationRegistrationDetail = () => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const { applicationRegistrationId = '' } = useParams<{
|
||||
applicationRegistrationId: string;
|
||||
const activeTabId = useAtomComponentStateValue(
|
||||
activeTabIdComponentState,
|
||||
REGISTRATION_DETAIL_TAB_LIST_ID,
|
||||
);
|
||||
|
||||
const { applicationUniversalIdentifier = '' } = useParams<{
|
||||
applicationUniversalIdentifier: string;
|
||||
}>();
|
||||
|
||||
const { data, loading } = useQuery(
|
||||
FindOneAdminApplicationRegistrationDocument,
|
||||
{
|
||||
variables: { id: applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
},
|
||||
);
|
||||
|
||||
const [updateRegistration] = useMutation(
|
||||
UpdateApplicationRegistrationDocument,
|
||||
{
|
||||
refetchQueries: [
|
||||
FindOneAdminApplicationRegistrationDocument,
|
||||
FindAllApplicationRegistrationsDocument,
|
||||
],
|
||||
variables: { id: applicationUniversalIdentifier },
|
||||
skip: !applicationUniversalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -54,6 +50,44 @@ export const SettingsAdminApplicationRegistrationDetail = () => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ id: 'general', title: t`General`, Icon: IconInfoCircle },
|
||||
{ id: 'oauth', title: t`OAuth`, Icon: IconKey },
|
||||
{ id: 'distribution', title: t`Distribution`, Icon: IconWorld },
|
||||
{ id: 'config', title: t`Config`, Icon: IconSettings },
|
||||
];
|
||||
|
||||
const renderActiveTabContent = () => {
|
||||
switch (activeTabId) {
|
||||
case 'config':
|
||||
return (
|
||||
<SettingsApplicationRegistrationConfigTab
|
||||
registration={registration}
|
||||
/>
|
||||
);
|
||||
case 'oauth':
|
||||
return (
|
||||
<SettingsApplicationRegistrationOAuthTab
|
||||
registration={registration}
|
||||
/>
|
||||
);
|
||||
case 'distribution':
|
||||
return (
|
||||
<SettingsApplicationRegistrationDistributionTab
|
||||
registration={registration}
|
||||
/>
|
||||
);
|
||||
case 'general':
|
||||
default:
|
||||
return (
|
||||
<SettingsApplicationRegistrationGeneralTab
|
||||
registration={registration}
|
||||
displayAdminToggles
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={registration.name}
|
||||
@@ -70,31 +104,11 @@ export const SettingsAdminApplicationRegistrationDetail = () => {
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<SettingsAdminApplicationRegistrationDetailContent
|
||||
registration={registration}
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
componentInstanceId={REGISTRATION_DETAIL_TAB_LIST_ID}
|
||||
/>
|
||||
<Section>
|
||||
<StyledToggleContainer>
|
||||
<Card rounded fullWidth>
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconArrowBarToDown}
|
||||
title={t`Allow installation`}
|
||||
description={t`Display this app in the NPM packages list`}
|
||||
checked={registration.isListed}
|
||||
onChange={(checked) =>
|
||||
updateRegistration({
|
||||
variables: {
|
||||
input: {
|
||||
id: registration.id,
|
||||
update: { isListed: checked },
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</StyledToggleContainer>
|
||||
</Section>
|
||||
{renderActiveTabContent()}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { Card, Section } from 'twenty-ui/layout';
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
import { IconArrowBarToDown } from 'twenty-ui/display';
|
||||
import {
|
||||
ApplicationRegistration,
|
||||
UpdateApplicationRegistrationDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { styled } from '@linaria/react';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
const StyledToggleContainer = styled.div`
|
||||
display: flex;
|
||||
margin-top: ${themeCssVariables.spacing[4]};
|
||||
`;
|
||||
|
||||
export const SettingsAdminApplicationRegistrationGeneralToggles = ({
|
||||
registration,
|
||||
}: {
|
||||
registration: ApplicationRegistration;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const [updateRegistration] = useMutation(
|
||||
UpdateApplicationRegistrationDocument,
|
||||
);
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<StyledToggleContainer>
|
||||
<Card rounded fullWidth>
|
||||
<SettingsOptionCardContentToggle
|
||||
Icon={IconArrowBarToDown}
|
||||
title={t`Allow installation`}
|
||||
description={t`Display this app in the NPM packages list`}
|
||||
checked={registration.isListed}
|
||||
onChange={(checked) =>
|
||||
updateRegistration({
|
||||
variables: {
|
||||
input: {
|
||||
id: registration.id,
|
||||
update: { isListed: checked },
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</StyledToggleContainer>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+81
-148
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+1
-1
@@ -27,7 +27,7 @@ import type { SingleTabProps } from '@/ui/layout/tab-list/types/SingleTabProps';
|
||||
|
||||
const APPLICATION_DETAIL_ID = 'application-detail-id';
|
||||
|
||||
export const SettingsApplicationDetails = () => {
|
||||
export const SettingsApplicationDetailsToRemove = () => {
|
||||
const { applicationId = '' } = useParams<{ applicationId: string }>();
|
||||
|
||||
const activeTabId = useAtomComponentStateValue(
|
||||
+10
-10
@@ -38,10 +38,10 @@ import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import {
|
||||
PermissionFlagType,
|
||||
FindOneApplicationByUniversalIdentifierDocument,
|
||||
FindMarketplaceAppDetailDocument,
|
||||
ApplicationRegistrationSourceType,
|
||||
FindMarketplaceAppDetailDocument,
|
||||
FindOneApplicationByUniversalIdentifierDocument,
|
||||
PermissionFlagType,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { SettingsApplicationPermissionsTab } from '~/pages/settings/applications/tabs/SettingsApplicationPermissionsTab';
|
||||
import { SettingsAvailableApplicationDetailContentTab } from '~/pages/settings/applications/tabs/SettingsAvailableApplicationDetailContentTab';
|
||||
@@ -179,9 +179,9 @@ const StyledSectionTitle = styled.h2`
|
||||
|
||||
const StyledAboutContainer = styled.div``;
|
||||
|
||||
export const SettingsAvailableApplicationDetails = () => {
|
||||
const { availableApplicationId = '' } = useParams<{
|
||||
availableApplicationId: string;
|
||||
export const SettingsApplicationPage = () => {
|
||||
const { applicationUniversalIdentifier = '' } = useParams<{
|
||||
applicationUniversalIdentifier: string;
|
||||
}>();
|
||||
|
||||
const [selectedScreenshotIndex, setSelectedScreenshotIndex] = useState(0);
|
||||
@@ -195,14 +195,14 @@ export const SettingsAvailableApplicationDetails = () => {
|
||||
const { data: applicationData } = useQuery(
|
||||
FindOneApplicationByUniversalIdentifierDocument,
|
||||
{
|
||||
variables: { universalIdentifier: availableApplicationId },
|
||||
skip: !availableApplicationId,
|
||||
variables: { universalIdentifier: applicationUniversalIdentifier },
|
||||
skip: !applicationUniversalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: detailData } = useQuery(FindMarketplaceAppDetailDocument, {
|
||||
variables: { universalIdentifier: availableApplicationId },
|
||||
skip: !availableApplicationId,
|
||||
variables: { universalIdentifier: applicationUniversalIdentifier },
|
||||
skip: !applicationUniversalIdentifier,
|
||||
});
|
||||
|
||||
const application = applicationData?.findOneApplication;
|
||||
+77
-10
@@ -3,29 +3,90 @@ import { useQuery } from '@apollo/client/react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { FindOneApplicationRegistrationDocument } from '~/generated-metadata/graphql';
|
||||
import { SettingsApplicationRegistrationContent } from '~/pages/settings/applications/components/SettingsApplicationRegistrationContent';
|
||||
import { FindApplicationRegistrationByUniversalIdentifierDocument } from '~/generated-metadata/graphql';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { Tag } from 'twenty-ui/components';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import {
|
||||
IconInfoCircle,
|
||||
IconKey,
|
||||
IconSettings,
|
||||
IconWorld,
|
||||
} from 'twenty-ui/display';
|
||||
import { SettingsApplicationRegistrationConfigTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationConfigTab';
|
||||
import { SettingsApplicationRegistrationOAuthTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationOAuthTab';
|
||||
import { SettingsApplicationRegistrationDistributionTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationDistributionTab';
|
||||
import { SettingsApplicationRegistrationGeneralTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationGeneralTab';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
|
||||
const REGISTRATION_DETAIL_TAB_LIST_ID =
|
||||
'application-registration-detail-tab-list';
|
||||
|
||||
export const SettingsApplicationRegistrationDetails = () => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const { applicationRegistrationId = '' } = useParams<{
|
||||
applicationRegistrationId: string;
|
||||
const activeTabId = useAtomComponentStateValue(
|
||||
activeTabIdComponentState,
|
||||
REGISTRATION_DETAIL_TAB_LIST_ID,
|
||||
);
|
||||
|
||||
const { applicationUniversalIdentifier = '' } = useParams<{
|
||||
applicationUniversalIdentifier: string;
|
||||
}>();
|
||||
|
||||
const { data, loading } = useQuery(FindOneApplicationRegistrationDocument, {
|
||||
variables: { id: applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
});
|
||||
const { data, loading } = useQuery(
|
||||
FindApplicationRegistrationByUniversalIdentifierDocument,
|
||||
{
|
||||
variables: { universalIdentifier: applicationUniversalIdentifier },
|
||||
skip: !applicationUniversalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
const registration = data?.findOneApplicationRegistration;
|
||||
const registration = data?.findApplicationRegistrationByUniversalIdentifier;
|
||||
|
||||
if (loading || !isDefined(registration)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ id: 'general', title: t`General`, Icon: IconInfoCircle },
|
||||
{ id: 'oauth', title: t`OAuth`, Icon: IconKey },
|
||||
{ id: 'distribution', title: t`Distribution`, Icon: IconWorld },
|
||||
{ id: 'config', title: t`Config`, Icon: IconSettings },
|
||||
];
|
||||
|
||||
const renderActiveTabContent = () => {
|
||||
switch (activeTabId) {
|
||||
case 'config':
|
||||
return (
|
||||
<SettingsApplicationRegistrationConfigTab
|
||||
registration={registration}
|
||||
/>
|
||||
);
|
||||
case 'oauth':
|
||||
return (
|
||||
<SettingsApplicationRegistrationOAuthTab
|
||||
registration={registration}
|
||||
/>
|
||||
);
|
||||
case 'distribution':
|
||||
return (
|
||||
<SettingsApplicationRegistrationDistributionTab
|
||||
registration={registration}
|
||||
/>
|
||||
);
|
||||
case 'general':
|
||||
default:
|
||||
return (
|
||||
<SettingsApplicationRegistrationGeneralTab
|
||||
registration={registration}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={registration.name}
|
||||
@@ -47,7 +108,13 @@ export const SettingsApplicationRegistrationDetails = () => {
|
||||
{ children: registration.name },
|
||||
]}
|
||||
>
|
||||
<SettingsApplicationRegistrationContent registration={registration} />
|
||||
<SettingsPageContainer>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
componentInstanceId={REGISTRATION_DETAIL_TAB_LIST_ID}
|
||||
/>
|
||||
{renderActiveTabContent()}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
|
||||
+60
-130
@@ -2,72 +2,48 @@ import { useLingui } from '@lingui/react/macro';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
FindApplicationRegistrationByUniversalIdentifierDocument,
|
||||
FindApplicationRegistrationVariablesDocument,
|
||||
FindOneApplicationRegistrationDocument,
|
||||
UpdateApplicationRegistrationVariableDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
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;
|
||||
const { variableKey } = useParams();
|
||||
|
||||
const [value, setValue] = useState<string>('');
|
||||
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const { applicationUniversalIdentifier = '' } = useParams<{
|
||||
applicationUniversalIdentifier: string;
|
||||
}>();
|
||||
|
||||
const { data: applicationRegistrationData } = useQuery(
|
||||
FindOneApplicationRegistrationDocument,
|
||||
FindApplicationRegistrationByUniversalIdentifierDocument,
|
||||
{
|
||||
variables: { id: applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
variables: { universalIdentifier: applicationUniversalIdentifier },
|
||||
skip: !applicationUniversalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
const registration =
|
||||
applicationRegistrationData?.findOneApplicationRegistration;
|
||||
applicationRegistrationData?.findApplicationRegistrationByUniversalIdentifier;
|
||||
|
||||
const { data: variablesData } = useQuery(
|
||||
FindApplicationRegistrationVariablesDocument,
|
||||
{
|
||||
variables: { applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
variables: { applicationRegistrationId: registration?.id ?? '' },
|
||||
skip: !registration?.id,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -83,65 +59,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`,
|
||||
@@ -160,7 +127,7 @@ export const SettingsApplicationRegistrationConfigVariableDetail = () => {
|
||||
children: t`${registration.name} - Config`,
|
||||
href: getSettingsPath(
|
||||
SettingsPath.ApplicationRegistrationDetail,
|
||||
{ applicationRegistrationId },
|
||||
{ applicationUniversalIdentifier },
|
||||
undefined,
|
||||
'config',
|
||||
),
|
||||
@@ -170,66 +137,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>
|
||||
);
|
||||
|
||||
-19
@@ -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`} />
|
||||
);
|
||||
};
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
IconInfoCircle,
|
||||
IconKey,
|
||||
IconSettings,
|
||||
IconWorld,
|
||||
} from 'twenty-ui/display';
|
||||
import { SettingsApplicationRegistrationGeneralTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationGeneralTab';
|
||||
import { SettingsApplicationRegistrationOAuthTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationOAuthTab';
|
||||
import { SettingsApplicationRegistrationDistributionTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationDistributionTab';
|
||||
import { type ApplicationRegistration } from '~/generated-metadata/graphql';
|
||||
import { SettingsApplicationRegistrationConfigTab } from '~/pages/settings/applications/tabs/SettingsApplicationRegistrationConfigTab';
|
||||
|
||||
const REGISTRATION_DETAIL_TAB_LIST_ID =
|
||||
'application-registration-detail-tab-list';
|
||||
|
||||
type SettingsApplicationRegistrationContentProps = {
|
||||
registration: ApplicationRegistration;
|
||||
};
|
||||
|
||||
export const SettingsApplicationRegistrationContent = ({
|
||||
registration,
|
||||
}: SettingsApplicationRegistrationContentProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const activeTabId = useAtomComponentStateValue(
|
||||
activeTabIdComponentState,
|
||||
REGISTRATION_DETAIL_TAB_LIST_ID,
|
||||
);
|
||||
|
||||
const tabs = [
|
||||
{ id: 'general', title: t`General`, Icon: IconInfoCircle },
|
||||
{ id: 'oauth', title: t`OAuth`, Icon: IconKey },
|
||||
{ id: 'distribution', title: t`Distribution`, Icon: IconWorld },
|
||||
{ id: 'config', title: t`Config`, Icon: IconSettings },
|
||||
];
|
||||
|
||||
const renderActiveTabContent = () => {
|
||||
switch (activeTabId) {
|
||||
case 'config':
|
||||
return (
|
||||
<SettingsApplicationRegistrationConfigTab
|
||||
registration={registration}
|
||||
/>
|
||||
);
|
||||
case 'oauth':
|
||||
return (
|
||||
<SettingsApplicationRegistrationOAuthTab
|
||||
registration={registration}
|
||||
/>
|
||||
);
|
||||
case 'distribution':
|
||||
return (
|
||||
<SettingsApplicationRegistrationDistributionTab
|
||||
registration={registration}
|
||||
/>
|
||||
);
|
||||
case 'general':
|
||||
default:
|
||||
return (
|
||||
<SettingsApplicationRegistrationGeneralTab
|
||||
registration={registration}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsPageContainer>
|
||||
<TabList
|
||||
tabs={tabs}
|
||||
componentInstanceId={REGISTRATION_DETAIL_TAB_LIST_ID}
|
||||
/>
|
||||
{renderActiveTabContent()}
|
||||
</SettingsPageContainer>
|
||||
);
|
||||
};
|
||||
+22
-79
@@ -1,13 +1,10 @@
|
||||
import {
|
||||
H2Title,
|
||||
IconBox,
|
||||
IconBrandDocker,
|
||||
IconChartBar,
|
||||
IconDownload,
|
||||
IconStatusChange,
|
||||
IconGitBranch,
|
||||
IconTag,
|
||||
IconWorld,
|
||||
IconGitBranch,
|
||||
} from 'twenty-ui/display';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import {
|
||||
@@ -18,7 +15,6 @@ import {
|
||||
type ApplicationRegistration,
|
||||
ApplicationRegistrationSourceType,
|
||||
ApplicationRegistrationTarballUrlDocument,
|
||||
FindApplicationRegistrationStatsDocument,
|
||||
FindOneApplicationSummaryDocument,
|
||||
GetPublicWorkspaceDataByIdDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
@@ -59,7 +55,7 @@ const StyledGeneralContainer = styled.div`
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
export const SettingsAdminApplicationRegistrationDetailContent = ({
|
||||
export const SettingsApplicationRegistrationGeneralInfo = ({
|
||||
registration,
|
||||
}: {
|
||||
registration: ApplicationRegistration;
|
||||
@@ -96,16 +92,8 @@ export const SettingsAdminApplicationRegistrationDetailContent = ({
|
||||
},
|
||||
);
|
||||
|
||||
const { data: statsData } = useQuery(
|
||||
FindApplicationRegistrationStatsDocument,
|
||||
{
|
||||
variables: { id: applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
},
|
||||
);
|
||||
|
||||
const shareLink = getSettingsPath(SettingsPath.AvailableApplicationDetail, {
|
||||
availableApplicationId: registration.universalIdentifier,
|
||||
const shareLink = getSettingsPath(SettingsPath.ApplicationDetail, {
|
||||
applicationUniversalIdentifier: registration.universalIdentifier,
|
||||
});
|
||||
|
||||
const ownerWorkspace = ownerWorkspaceData?.getPublicWorkspaceDataById;
|
||||
@@ -206,69 +194,24 @@ export const SettingsAdminApplicationRegistrationDetailContent = ({
|
||||
return items;
|
||||
};
|
||||
|
||||
const stats = statsData?.findApplicationRegistrationStats;
|
||||
|
||||
const hasStats = (stats?.activeInstalls ?? 0) > 0;
|
||||
|
||||
const versionDistributionLabel =
|
||||
stats?.versionDistribution
|
||||
?.map(
|
||||
(entry: { version: string; count: number }) =>
|
||||
`${entry.version} (${entry.count})`,
|
||||
)
|
||||
.join(', ') || '—';
|
||||
|
||||
const statsItems = [
|
||||
{
|
||||
Icon: IconBrandDocker,
|
||||
label: t`Active installs`,
|
||||
value: stats?.activeInstalls ?? '—',
|
||||
},
|
||||
{
|
||||
Icon: IconStatusChange,
|
||||
label: t`Most installed version`,
|
||||
value: stats?.mostInstalledVersion ?? '—',
|
||||
},
|
||||
{
|
||||
Icon: IconChartBar,
|
||||
label: t`Distribution`,
|
||||
value: versionDistributionLabel,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Section>
|
||||
<H2Title title={t`General`} description={t`About your app`} />
|
||||
<StyledGeneralContainer>
|
||||
<SettingsTableCard
|
||||
rounded
|
||||
items={generateItems()}
|
||||
gridAutoColumns="3fr 8fr"
|
||||
/>
|
||||
<SettingsApplicationRegistrationShareLinkButtons
|
||||
shareLink={shareLink}
|
||||
isInstalled={isApplicationInstalled}
|
||||
universalIdentifier={registration.universalIdentifier}
|
||||
isNpmSource={
|
||||
registration.sourceType === ApplicationRegistrationSourceType.NPM
|
||||
}
|
||||
/>
|
||||
</StyledGeneralContainer>
|
||||
</Section>
|
||||
{hasStats && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Install Stats`}
|
||||
description={t`Usage across all workspaces on this server`}
|
||||
/>
|
||||
<SettingsTableCard
|
||||
rounded
|
||||
items={statsItems}
|
||||
gridAutoColumns="200px 1fr"
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
</>
|
||||
<Section>
|
||||
<H2Title title={t`General`} description={t`About your app`} />
|
||||
<StyledGeneralContainer>
|
||||
<SettingsTableCard
|
||||
rounded
|
||||
items={generateItems()}
|
||||
gridAutoColumns="3fr 8fr"
|
||||
/>
|
||||
<SettingsApplicationRegistrationShareLinkButtons
|
||||
shareLink={shareLink}
|
||||
isInstalled={isApplicationInstalled}
|
||||
universalIdentifier={registration.universalIdentifier}
|
||||
isNpmSource={
|
||||
registration.sourceType === ApplicationRegistrationSourceType.NPM
|
||||
}
|
||||
/>
|
||||
</StyledGeneralContainer>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
import {
|
||||
H2Title,
|
||||
IconBrandDocker,
|
||||
IconChartBar,
|
||||
IconStatusChange,
|
||||
} from 'twenty-ui/display';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { SettingsTableCard } from '@/settings/components/SettingsTableCard';
|
||||
import {
|
||||
type ApplicationRegistration,
|
||||
FindApplicationRegistrationStatsDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
|
||||
export const SettingsApplicationRegistrationGeneralStats = ({
|
||||
registration,
|
||||
}: {
|
||||
registration: ApplicationRegistration;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const applicationRegistrationId = registration.id;
|
||||
|
||||
const { data: statsData } = useQuery(
|
||||
FindApplicationRegistrationStatsDocument,
|
||||
{
|
||||
variables: { id: applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
},
|
||||
);
|
||||
|
||||
const stats = statsData?.findApplicationRegistrationStats;
|
||||
|
||||
const hasStats = (stats?.activeInstalls ?? 0) > 0;
|
||||
|
||||
if (!hasStats) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const versionDistributionLabel =
|
||||
stats?.versionDistribution
|
||||
?.map(
|
||||
(entry: { version: string; count: number }) =>
|
||||
`${entry.version} (${entry.count})`,
|
||||
)
|
||||
.join(', ') || '—';
|
||||
|
||||
const statsItems = [
|
||||
{
|
||||
Icon: IconBrandDocker,
|
||||
label: t`Active installs`,
|
||||
value: stats?.activeInstalls ?? '—',
|
||||
},
|
||||
{
|
||||
Icon: IconStatusChange,
|
||||
label: t`Most installed version`,
|
||||
value: stats?.mostInstalledVersion ?? '—',
|
||||
},
|
||||
{
|
||||
Icon: IconChartBar,
|
||||
label: t`Distribution`,
|
||||
value: versionDistributionLabel,
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Install Stats`}
|
||||
description={t`Usage across all workspaces on this server`}
|
||||
/>
|
||||
<SettingsTableCard
|
||||
rounded
|
||||
items={statsItems}
|
||||
gridAutoColumns="200px 1fr"
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+2
-1
@@ -95,7 +95,8 @@ export const SettingsApplicationsTable = ({
|
||||
/>
|
||||
}
|
||||
link={getSettingsPath(SettingsPath.ApplicationDetail, {
|
||||
applicationId: application.id,
|
||||
applicationUniversalIdentifier:
|
||||
application.universalIdentifier,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
||||
+2
-2
@@ -48,8 +48,8 @@ export const SettingsAvailableApplicationCard = ({
|
||||
return (
|
||||
<StyledLinkContainer>
|
||||
<Link
|
||||
to={getSettingsPath(SettingsPath.AvailableApplicationDetail, {
|
||||
availableApplicationId: application.id,
|
||||
to={getSettingsPath(SettingsPath.ApplicationDetail, {
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
})}
|
||||
>
|
||||
<Card rounded fullWidth>
|
||||
|
||||
+20
-84
@@ -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,
|
||||
{
|
||||
applicationUniversalIdentifier: registration.universalIdentifier,
|
||||
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>
|
||||
)
|
||||
);
|
||||
|
||||
+2
-2
@@ -24,8 +24,8 @@ export const SettingsApplicationRegistrationDistributionTab = ({
|
||||
const isTarballSource =
|
||||
registration.sourceType === ApplicationRegistrationSourceType.TARBALL;
|
||||
|
||||
const shareLink = getSettingsPath(SettingsPath.AvailableApplicationDetail, {
|
||||
availableApplicationId: registration.universalIdentifier,
|
||||
const shareLink = getSettingsPath(SettingsPath.ApplicationDetail, {
|
||||
applicationUniversalIdentifier: registration.universalIdentifier,
|
||||
});
|
||||
|
||||
const publishCommands = ['yarn twenty publish'];
|
||||
|
||||
+16
-236
@@ -1,252 +1,32 @@
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import { styled } from '@linaria/react';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { useState } from 'react';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import {
|
||||
H1Title,
|
||||
H1TitleFontColor,
|
||||
H2Title,
|
||||
IconShare,
|
||||
IconTrash,
|
||||
AppTooltip,
|
||||
TooltipDelay,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section, SectionAlignment, SectionFontColor } from 'twenty-ui/layout';
|
||||
import {
|
||||
type ApplicationRegistration,
|
||||
DeleteApplicationRegistrationDocument,
|
||||
FindApplicationRegistrationStatsDocument,
|
||||
FindManyApplicationRegistrationsDocument,
|
||||
TransferApplicationRegistrationOwnershipDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import {
|
||||
StyledAppModal,
|
||||
StyledAppModalButton,
|
||||
StyledAppModalSection,
|
||||
StyledAppModalTitle,
|
||||
} from '~/pages/settings/applications/components/SettingsAppModalLayout';
|
||||
import { SettingsAdminApplicationRegistrationDetailContent } from '~/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDetailContent';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type ApplicationRegistration } from '~/generated-metadata/graphql';
|
||||
|
||||
const DELETE_REGISTRATION_MODAL_ID = 'delete-application-registration-modal';
|
||||
import { SettingsApplicationRegistrationGeneralInfo } from '~/pages/settings/applications/components/SettingsApplicationRegistrationGeneralInfo';
|
||||
|
||||
const TRANSFER_OWNERSHIP_MODAL_ID =
|
||||
'transfer-application-registration-ownership-modal';
|
||||
|
||||
const DELETE_REGISTRATION_BUTTON_ID = 'delete-registration-button';
|
||||
|
||||
const StyledDangerButtonGroup = styled.div`
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
import { SettingsAdminApplicationRegistrationDangerZone } from '~/pages/settings/admin-panel/SettingsAdminApplicationRegistrationDangerZone';
|
||||
import { SettingsApplicationRegistrationGeneralStats } from '~/pages/settings/applications/components/SettingsApplicationRegistrationGeneralStats';
|
||||
import { SettingsAdminApplicationRegistrationGeneralToggles } from '~/pages/settings/admin-panel/SettingsAdminApplicationRegistrationGeneralToggles';
|
||||
|
||||
export const SettingsApplicationRegistrationGeneralTab = ({
|
||||
registration,
|
||||
displayAdminToggles,
|
||||
}: {
|
||||
registration: ApplicationRegistration;
|
||||
displayAdminToggles?: boolean;
|
||||
}) => {
|
||||
const { t } = useLingui();
|
||||
const navigate = useNavigateSettings();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
const { openModal, closeModal } = useModal();
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isTransferring, setIsTransferring] = useState(false);
|
||||
const [transferSubdomain, setTransferSubdomain] = useState('');
|
||||
|
||||
const applicationRegistrationId = registration.id;
|
||||
|
||||
const { data: statsData } = useQuery(
|
||||
FindApplicationRegistrationStatsDocument,
|
||||
{
|
||||
variables: { id: applicationRegistrationId },
|
||||
skip: !applicationRegistrationId,
|
||||
},
|
||||
);
|
||||
|
||||
const stats = statsData?.findApplicationRegistrationStats;
|
||||
|
||||
const hasActiveInstalls =
|
||||
!isDefined(stats) || (stats.activeInstalls ?? 0) > 0;
|
||||
|
||||
const [deleteRegistration] = useMutation(
|
||||
DeleteApplicationRegistrationDocument,
|
||||
{
|
||||
refetchQueries: [FindManyApplicationRegistrationsDocument],
|
||||
},
|
||||
);
|
||||
|
||||
const [transferOwnership] = useMutation(
|
||||
TransferApplicationRegistrationOwnershipDocument,
|
||||
{
|
||||
refetchQueries: [FindManyApplicationRegistrationsDocument],
|
||||
},
|
||||
);
|
||||
|
||||
const handleDelete = async () => {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
await deleteRegistration({
|
||||
variables: { id: applicationRegistrationId },
|
||||
});
|
||||
navigate(SettingsPath.Applications);
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Error deleting app`,
|
||||
});
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTransferOwnership = async () => {
|
||||
const trimmed = transferSubdomain.trim();
|
||||
|
||||
if (!isNonEmptyString(trimmed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsTransferring(true);
|
||||
try {
|
||||
await transferOwnership({
|
||||
variables: {
|
||||
applicationRegistrationId,
|
||||
targetWorkspaceSubdomain: trimmed,
|
||||
},
|
||||
});
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Ownership transferred successfully`,
|
||||
});
|
||||
setTransferSubdomain('');
|
||||
navigate(SettingsPath.Applications);
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Failed to transfer ownership. Check that the subdomain is correct.`,
|
||||
});
|
||||
} finally {
|
||||
setIsTransferring(false);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmationValue = t`yes`;
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsAdminApplicationRegistrationDetailContent
|
||||
<SettingsApplicationRegistrationGeneralInfo registration={registration} />
|
||||
{displayAdminToggles && (
|
||||
<SettingsAdminApplicationRegistrationGeneralToggles
|
||||
registration={registration}
|
||||
/>
|
||||
)}
|
||||
<SettingsApplicationRegistrationGeneralStats
|
||||
registration={registration}
|
||||
/>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Danger zone`}
|
||||
description={t`Delete or transfer this app registration`}
|
||||
/>
|
||||
<StyledDangerButtonGroup>
|
||||
<Button
|
||||
id={DELETE_REGISTRATION_BUTTON_ID}
|
||||
accent="danger"
|
||||
variant="secondary"
|
||||
title={t`Delete app`}
|
||||
Icon={IconTrash}
|
||||
disabled={hasActiveInstalls}
|
||||
onClick={() => openModal(DELETE_REGISTRATION_MODAL_ID)}
|
||||
/>
|
||||
{hasActiveInstalls && (
|
||||
<AppTooltip
|
||||
anchorSelect={`#${DELETE_REGISTRATION_BUTTON_ID}`}
|
||||
content={t`Uninstall this app from all workspaces before deleting it`}
|
||||
noArrow
|
||||
place="bottom"
|
||||
positionStrategy="fixed"
|
||||
delay={TooltipDelay.shortDelay}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
accent="default"
|
||||
variant="secondary"
|
||||
title={t`Transfer ownership`}
|
||||
Icon={IconShare}
|
||||
onClick={() => openModal(TRANSFER_OWNERSHIP_MODAL_ID)}
|
||||
/>
|
||||
</StyledDangerButtonGroup>
|
||||
</Section>
|
||||
|
||||
<ConfirmationModal
|
||||
confirmationPlaceholder={confirmationValue}
|
||||
confirmationValue={confirmationValue}
|
||||
modalInstanceId={DELETE_REGISTRATION_MODAL_ID}
|
||||
title={t`Delete app`}
|
||||
subtitle={
|
||||
<Trans>
|
||||
Please type {`"${confirmationValue}"`} to confirm you want to delete
|
||||
this app. All workspace installations linked to it will lose their
|
||||
OAuth credentials.
|
||||
</Trans>
|
||||
}
|
||||
onConfirmClick={handleDelete}
|
||||
confirmButtonText={t`Delete`}
|
||||
loading={isLoading}
|
||||
<SettingsAdminApplicationRegistrationDangerZone
|
||||
registration={registration}
|
||||
/>
|
||||
|
||||
<StyledAppModal
|
||||
modalId={TRANSFER_OWNERSHIP_MODAL_ID}
|
||||
isClosable
|
||||
onClose={() => setTransferSubdomain('')}
|
||||
padding="large"
|
||||
dataGloballyPreventClickOutside
|
||||
>
|
||||
<StyledAppModalTitle>
|
||||
<H1Title
|
||||
title={t`Transfer ownership`}
|
||||
fontColor={H1TitleFontColor.Primary}
|
||||
/>
|
||||
</StyledAppModalTitle>
|
||||
<StyledAppModalSection
|
||||
alignment={SectionAlignment.Center}
|
||||
fontColor={SectionFontColor.Primary}
|
||||
>
|
||||
{t`Enter the workspace subdomain to transfer this app to. You will lose access to manage it.`}
|
||||
</StyledAppModalSection>
|
||||
<Section>
|
||||
<SettingsTextInput
|
||||
instanceId="transfer-ownership-subdomain"
|
||||
value={transferSubdomain}
|
||||
onChange={setTransferSubdomain}
|
||||
placeholder={t`e.g. my-workspace`}
|
||||
fullWidth
|
||||
disableHotkeys
|
||||
label={t`Target workspace subdomain`}
|
||||
autoFocusOnMount
|
||||
/>
|
||||
</Section>
|
||||
<StyledAppModalButton
|
||||
onClick={() => {
|
||||
closeModal(TRANSFER_OWNERSHIP_MODAL_ID);
|
||||
setTransferSubdomain('');
|
||||
}}
|
||||
variant="secondary"
|
||||
title={t`Cancel`}
|
||||
fullWidth
|
||||
/>
|
||||
<StyledAppModalButton
|
||||
onClick={handleTransferOwnership}
|
||||
variant="secondary"
|
||||
accent="danger"
|
||||
title={t`Transfer`}
|
||||
disabled={
|
||||
!isNonEmptyString(transferSubdomain.trim()) || isTransferring
|
||||
}
|
||||
fullWidth
|
||||
/>
|
||||
</StyledAppModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
+7
-9
@@ -20,11 +20,11 @@ import {
|
||||
Avatar,
|
||||
CommandBlock,
|
||||
H2Title,
|
||||
IconArrowUpRight,
|
||||
IconChevronRight,
|
||||
IconCopy,
|
||||
IconArrowUpRight,
|
||||
OverflowingTextWithTooltip,
|
||||
InlineBanner,
|
||||
OverflowingTextWithTooltip,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button, SearchInput } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
@@ -121,7 +121,7 @@ export const SettingsApplicationsDeveloperTab = () => {
|
||||
registration: ApplicationRegistrationFragmentFragment,
|
||||
) =>
|
||||
getSettingsPath(SettingsPath.ApplicationRegistrationDetail, {
|
||||
applicationRegistrationId: registration.id,
|
||||
applicationUniversalIdentifier: registration.universalIdentifier,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -232,12 +232,10 @@ export const SettingsApplicationsDeveloperTab = () => {
|
||||
<TableRow
|
||||
key={application.id}
|
||||
gridAutoColumns={NPM_PACKAGES_GRID_COLUMNS}
|
||||
to={getSettingsPath(
|
||||
SettingsPath.AvailableApplicationDetail,
|
||||
{
|
||||
availableApplicationId: application.id,
|
||||
},
|
||||
)}
|
||||
to={getSettingsPath(SettingsPath.ApplicationDetail, {
|
||||
applicationUniversalIdentifier:
|
||||
application.universalIdentifier,
|
||||
})}
|
||||
>
|
||||
<StyledNameTableCell>
|
||||
<Avatar
|
||||
|
||||
+1
@@ -3,6 +3,7 @@ import { type Application } from '~/generated-metadata/graphql';
|
||||
export type ApplicationWithoutRelation = Pick<
|
||||
Application,
|
||||
| 'id'
|
||||
| 'universalIdentifier'
|
||||
| 'name'
|
||||
| 'description'
|
||||
| 'version'
|
||||
|
||||
+16
-11
@@ -20,7 +20,7 @@ import {
|
||||
IconSettings,
|
||||
} from 'twenty-ui/display';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
import { FindOneApplicationDocument } from '~/generated-metadata/graphql';
|
||||
import { FindOneApplicationByUniversalIdentifierDocument } from '~/generated-metadata/graphql';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { SettingsLogicFunctionCodeEditorTab } from '@/settings/logic-functions/components/tabs/SettingsLogicFunctionCodeEditorTab';
|
||||
@@ -29,25 +29,30 @@ import { useExecuteLogicFunction } from '@/logic-functions/hooks/useExecuteLogic
|
||||
const LOGIC_FUNCTION_DETAIL_ID = 'logic-function-detail';
|
||||
|
||||
export const SettingsLogicFunctionDetail = () => {
|
||||
const { logicFunctionId = '', applicationId = '' } = useParams();
|
||||
const { logicFunctionId = '', applicationUniversalIdentifier = '' } =
|
||||
useParams();
|
||||
|
||||
const navigate = useNavigate();
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
|
||||
const { data, loading: applicationLoading } = useQuery(
|
||||
FindOneApplicationDocument,
|
||||
const { data: applicationData, loading: applicationLoading } = useQuery(
|
||||
FindOneApplicationByUniversalIdentifierDocument,
|
||||
{
|
||||
variables: { id: applicationId },
|
||||
skip: !applicationId,
|
||||
variables: { universalIdentifier: applicationUniversalIdentifier },
|
||||
skip: !applicationUniversalIdentifier,
|
||||
},
|
||||
);
|
||||
|
||||
const applicationName = data?.findOneApplication?.name;
|
||||
const application = applicationData?.findOneApplication;
|
||||
|
||||
if (!application) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workspaceCustomApplicationId =
|
||||
currentWorkspace?.workspaceCustomApplication?.id;
|
||||
|
||||
const isManaged = applicationId !== workspaceCustomApplicationId;
|
||||
const isManaged = application.id !== workspaceCustomApplicationId;
|
||||
|
||||
const instanceId = `${LOGIC_FUNCTION_DETAIL_ID}-${logicFunctionId}`;
|
||||
|
||||
@@ -88,7 +93,7 @@ export const SettingsLogicFunctionDetail = () => {
|
||||
const isTestTab = activeTabId === 'test';
|
||||
|
||||
const breadcrumbLinks =
|
||||
isDefined(applicationId) && applicationId !== ''
|
||||
isDefined(application.id) && application.id !== ''
|
||||
? [
|
||||
{
|
||||
children: t`Workspace`,
|
||||
@@ -99,11 +104,11 @@ export const SettingsLogicFunctionDetail = () => {
|
||||
href: getSettingsPath(SettingsPath.Applications),
|
||||
},
|
||||
{
|
||||
children: `${applicationName}`,
|
||||
children: `${application.name} - ${t`Content`}`,
|
||||
href: getSettingsPath(
|
||||
SettingsPath.ApplicationDetail,
|
||||
{
|
||||
applicationId,
|
||||
applicationUniversalIdentifier: application.universalIdentifier,
|
||||
},
|
||||
undefined,
|
||||
'content',
|
||||
|
||||
+5
@@ -15,6 +15,11 @@ export class MarketplaceAppDTO {
|
||||
@Field()
|
||||
id: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
universalIdentifier: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Field()
|
||||
|
||||
+2
-1
@@ -84,7 +84,8 @@ export class MarketplaceQueryService {
|
||||
const app = registration.manifest?.application;
|
||||
|
||||
return {
|
||||
id: registration.universalIdentifier,
|
||||
id: registration.id,
|
||||
universalIdentifier: registration.universalIdentifier,
|
||||
name: app?.displayName ?? registration.name,
|
||||
description: app?.description ?? '',
|
||||
icon: app?.icon ?? 'IconApps',
|
||||
|
||||
+10
-1
@@ -26,9 +26,18 @@ export class ApplicationRegistrationVariableService {
|
||||
) {}
|
||||
|
||||
async findVariables(
|
||||
applicationRegistrationId: string,
|
||||
applicationUniversalIdentifier: string,
|
||||
workspaceId: string,
|
||||
): Promise<ApplicationRegistrationVariableEntity[]> {
|
||||
const applicationRegistration =
|
||||
await this.applicationRegistrationRepository.findOneOrFail({
|
||||
where: {
|
||||
universalIdentifier: applicationUniversalIdentifier,
|
||||
},
|
||||
});
|
||||
|
||||
const applicationRegistrationId = applicationRegistration.id;
|
||||
|
||||
await this.assertRegistrationOwnedByWorkspace(
|
||||
applicationRegistrationId,
|
||||
workspaceId,
|
||||
|
||||
@@ -30,24 +30,6 @@ export class ApplicationService {
|
||||
private readonly workspaceRepository: Repository<WorkspaceEntity>,
|
||||
) {}
|
||||
|
||||
async findApplicationRoleId(
|
||||
applicationId: string,
|
||||
workspaceId: string,
|
||||
): Promise<string> {
|
||||
const application = await this.applicationRepository.findOne({
|
||||
where: { id: applicationId, workspaceId },
|
||||
});
|
||||
|
||||
if (!isDefined(application) || !isDefined(application.defaultRoleId)) {
|
||||
throw new ApplicationException(
|
||||
`Could not find application ${applicationId}`,
|
||||
ApplicationExceptionCode.APPLICATION_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
return application.defaultRoleId;
|
||||
}
|
||||
|
||||
async findWorkspaceTwentyStandardAndCustomApplicationOrThrow({
|
||||
workspace: workspaceInput,
|
||||
workspaceId,
|
||||
|
||||
@@ -41,11 +41,10 @@ export enum SettingsPath {
|
||||
AISkillDetail = 'ai/skills/:skillId',
|
||||
AIToolDetail = 'ai/tools/:toolIdentifier',
|
||||
Applications = 'applications',
|
||||
ApplicationDetail = 'applications/:applicationId',
|
||||
ApplicationLogicFunctionDetail = 'applications/:applicationId/logicFunctions/:logicFunctionId',
|
||||
AvailableApplicationDetail = 'applications/available/:availableApplicationId',
|
||||
ApplicationRegistrationDetail = 'applications/registrations/:applicationRegistrationId',
|
||||
ApplicationRegistrationConfigVariableDetails = 'applications/registrations/:applicationRegistrationId/config-variables/:variableKey',
|
||||
ApplicationLogicFunctionDetail = 'applications/:applicationUniversalIdentifier/logicFunctions/:logicFunctionId',
|
||||
ApplicationDetail = 'applications/:applicationUniversalIdentifier',
|
||||
ApplicationRegistrationDetail = 'applications/registrations/:applicationUniversalIdentifier',
|
||||
ApplicationRegistrationConfigVariableDetails = 'applications/registrations/:applicationUniversalIdentifier/config-variables/:variableKey',
|
||||
LogicFunctions = 'functions',
|
||||
NewLogicFunction = 'functions/new',
|
||||
LogicFunctionDetail = 'functions/:logicFunctionId',
|
||||
@@ -72,7 +71,7 @@ export enum SettingsPath {
|
||||
AdminPanelNewAiModel = 'admin-panel/ai/providers/:providerName/new-model',
|
||||
AdminPanelUserDetail = 'admin-panel/users/:userId',
|
||||
AdminPanelWorkspaceDetail = 'admin-panel/workspaces/:workspaceId',
|
||||
AdminPanelApplicationRegistrationDetail = 'admin-panel/applications/registrations/:applicationRegistrationId',
|
||||
AdminPanelApplicationRegistrationDetail = 'admin-panel/applications/registrations/:applicationUniversalIdentifier',
|
||||
AdminPanelWorkspaceChatThread = 'admin-panel/workspaces/:workspaceId/threads/:threadId',
|
||||
|
||||
Roles = 'roles',
|
||||
|
||||
Reference in New Issue
Block a user