Serverless function UI (#6388)
https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?node-id=36235-120877 Did not do the file manager part. A Function is defined using one unique file at the moment Feature protected by featureFlag `IS_FUNCTION_SETTINGS_ENABLED` ## Demo https://github.com/user-attachments/assets/0acb8291-47b4-4521-a6fa-a88b9198609b
This commit is contained in:
@ -23,13 +23,13 @@ import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDraw
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { isNonTextWritingKey } from '@/ui/utilities/hotkey/utils/isNonTextWritingKey';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { FileFolder, useUploadFileMutation } from '~/generated/graphql';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
|
||||
|
||||
import { getFileType } from '../files/utils/getFileType';
|
||||
|
||||
import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI';
|
||||
import '@blocknote/core/fonts/inter.css';
|
||||
import '@blocknote/mantine/style.css';
|
||||
|
||||
@ -127,9 +127,7 @@ export const ActivityBodyEditor = ({
|
||||
if (!result?.data?.uploadFile) {
|
||||
throw new Error("Couldn't upload Image");
|
||||
}
|
||||
const imageUrl =
|
||||
REACT_APP_SERVER_BASE_URL + '/files/' + result?.data?.uploadFile;
|
||||
return imageUrl;
|
||||
return getFileAbsoluteURI(result.data.uploadFile);
|
||||
};
|
||||
|
||||
const handlePersistBody = useCallback(
|
||||
|
||||
@ -13,8 +13,8 @@ import {
|
||||
FieldContext,
|
||||
GenericFieldContextType,
|
||||
} from '@/object-record/record-field/contexts/FieldContext';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { formatToHumanReadableDate } from '~/utils/date-utils';
|
||||
import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI';
|
||||
|
||||
const StyledRow = styled.div`
|
||||
align-items: center;
|
||||
@ -76,7 +76,7 @@ export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => {
|
||||
<StyledLeftContent>
|
||||
<AttachmentIcon attachmentType={attachment.type} />
|
||||
<StyledLink
|
||||
href={REACT_APP_SERVER_BASE_URL + '/files/' + attachment.fullPath}
|
||||
href={getFileAbsoluteURI(attachment.fullPath)}
|
||||
target="__blank"
|
||||
>
|
||||
{attachment.name}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI';
|
||||
|
||||
export const downloadFile = (fullPath: string, fileName: string) => {
|
||||
fetch(REACT_APP_SERVER_BASE_URL + '/files/' + fullPath)
|
||||
fetch(getFileAbsoluteURI(fullPath))
|
||||
.then((resp) =>
|
||||
resp.status === 200
|
||||
? resp.blob()
|
||||
|
||||
@ -13,6 +13,7 @@ import {
|
||||
IconSettings,
|
||||
IconUserCircle,
|
||||
IconUsers,
|
||||
IconFunction,
|
||||
} from 'twenty-ui';
|
||||
|
||||
import { useAuth } from '@/auth/hooks/useAuth';
|
||||
@ -29,6 +30,9 @@ export const SettingsNavigationDrawerItems = () => {
|
||||
const { signOut } = useAuth();
|
||||
|
||||
const billing = useRecoilValue(billingState);
|
||||
const isFunctionSettingsEnabled = useIsFeatureEnabled(
|
||||
'IS_FUNCTION_SETTINGS_ENABLED',
|
||||
);
|
||||
const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED');
|
||||
|
||||
return (
|
||||
@ -99,6 +103,13 @@ export const SettingsNavigationDrawerItems = () => {
|
||||
path={SettingsPath.Developers}
|
||||
Icon={IconCode}
|
||||
/>
|
||||
{isFunctionSettingsEnabled && (
|
||||
<SettingsNavigationDrawerItem
|
||||
label="Functions"
|
||||
path={SettingsPath.ServerlessFunctions}
|
||||
Icon={IconFunction}
|
||||
/>
|
||||
)}
|
||||
<SettingsNavigationDrawerItem
|
||||
label="Integrations"
|
||||
path={SettingsPath.Integrations}
|
||||
|
||||
@ -0,0 +1,41 @@
|
||||
import { H2Title } from 'twenty-ui';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { TextArea } from '@/ui/input/components/TextArea';
|
||||
import styled from '@emotion/styled';
|
||||
import { ServerlessFunctionNewFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
|
||||
const StyledInputsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const SettingsServerlessFunctionNewForm = ({
|
||||
formValues,
|
||||
onChange,
|
||||
}: {
|
||||
formValues: ServerlessFunctionNewFormValues;
|
||||
onChange: (key: string) => (value: string) => void;
|
||||
}) => {
|
||||
return (
|
||||
<Section>
|
||||
<H2Title title="About" description="Name and set your function" />
|
||||
<StyledInputsContainer>
|
||||
<TextInput
|
||||
placeholder="Name"
|
||||
fullWidth
|
||||
focused
|
||||
value={formValues.name}
|
||||
onChange={onChange('name')}
|
||||
/>
|
||||
<TextArea
|
||||
placeholder="Description"
|
||||
minRows={4}
|
||||
value={formValues.description}
|
||||
onChange={onChange('description')}
|
||||
/>
|
||||
</StyledInputsContainer>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,46 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { ServerlessFunction } from '~/generated-metadata/graphql';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { IconChevronRight } from 'twenty-ui';
|
||||
import { useTheme } from '@emotion/react';
|
||||
|
||||
export const StyledApisFieldTableRow = styled(TableRow)`
|
||||
grid-template-columns: 312px 132px 68px;
|
||||
`;
|
||||
|
||||
const StyledNameTableCell = styled(TableCell)`
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledIconTableCell = styled(TableCell)`
|
||||
justify-content: center;
|
||||
padding-right: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledIconChevronRight = styled(IconChevronRight)`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
`;
|
||||
|
||||
export const SettingsServerlessFunctionsFieldItemTableRow = ({
|
||||
serverlessFunction,
|
||||
to,
|
||||
}: {
|
||||
serverlessFunction: ServerlessFunction;
|
||||
to: string;
|
||||
}) => {
|
||||
const theme = useTheme();
|
||||
return (
|
||||
<StyledApisFieldTableRow to={to}>
|
||||
<StyledNameTableCell>{serverlessFunction.name}</StyledNameTableCell>
|
||||
<StyledNameTableCell>{serverlessFunction.runtime}</StyledNameTableCell>
|
||||
<StyledIconTableCell>
|
||||
<StyledIconChevronRight
|
||||
size={theme.icon.size.md}
|
||||
stroke={theme.icon.stroke.sm}
|
||||
/>
|
||||
</StyledIconTableCell>
|
||||
</StyledApisFieldTableRow>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,51 @@
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import styled from '@emotion/styled';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions';
|
||||
import { SettingsServerlessFunctionsFieldItemTableRow } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsFieldItemTableRow';
|
||||
import { ServerlessFunction } from '~/generated-metadata/graphql';
|
||||
import { SettingsServerlessFunctionsTableEmpty } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty';
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
|
||||
const StyledTableRow = styled(TableRow)`
|
||||
grid-template-columns: 312px 132px 68px;
|
||||
`;
|
||||
|
||||
const StyledTableBody = styled(TableBody)`
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
`;
|
||||
|
||||
export const SettingsServerlessFunctionsTable = () => {
|
||||
const { serverlessFunctions } = useGetManyServerlessFunctions();
|
||||
return (
|
||||
<>
|
||||
{serverlessFunctions.length ? (
|
||||
<Table>
|
||||
<StyledTableRow>
|
||||
<TableHeader>Name</TableHeader>
|
||||
<TableHeader>Runtime</TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
</StyledTableRow>
|
||||
<StyledTableBody>
|
||||
{serverlessFunctions.map(
|
||||
(serverlessFunction: ServerlessFunction) => (
|
||||
<SettingsServerlessFunctionsFieldItemTableRow
|
||||
key={serverlessFunction.id}
|
||||
serverlessFunction={serverlessFunction}
|
||||
to={getSettingsPagePath(SettingsPath.ServerlessFunctions, {
|
||||
id: serverlessFunction.id,
|
||||
})}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
</StyledTableBody>
|
||||
</Table>
|
||||
) : (
|
||||
<SettingsServerlessFunctionsTableEmpty />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,43 @@
|
||||
import {
|
||||
AnimatedPlaceholderEmptyContainer,
|
||||
AnimatedPlaceholderEmptySubTitle,
|
||||
AnimatedPlaceholderEmptyTextContainer,
|
||||
AnimatedPlaceholderEmptyTitle,
|
||||
EMPTY_PLACEHOLDER_TRANSITION_PROPS,
|
||||
} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled';
|
||||
import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder';
|
||||
import { IconPlus } from 'twenty-ui';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledEmptyFunctionsContainer = styled.div`
|
||||
height: 60vh;
|
||||
`;
|
||||
|
||||
export const SettingsServerlessFunctionsTableEmpty = () => {
|
||||
return (
|
||||
<StyledEmptyFunctionsContainer>
|
||||
<AnimatedPlaceholderEmptyContainer
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...EMPTY_PLACEHOLDER_TRANSITION_PROPS}
|
||||
>
|
||||
<AnimatedPlaceholder type="emptyFunctions" />
|
||||
<AnimatedPlaceholderEmptyTextContainer>
|
||||
<AnimatedPlaceholderEmptyTitle>
|
||||
Add your first Function
|
||||
</AnimatedPlaceholderEmptyTitle>
|
||||
<AnimatedPlaceholderEmptySubTitle>
|
||||
Add your first Function to get started
|
||||
</AnimatedPlaceholderEmptySubTitle>
|
||||
</AnimatedPlaceholderEmptyTextContainer>
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title="New function"
|
||||
to={getSettingsPagePath(SettingsPath.NewServerlessFunction)}
|
||||
/>
|
||||
</AnimatedPlaceholderEmptyContainer>
|
||||
</StyledEmptyFunctionsContainer>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,60 @@
|
||||
import { H2Title, IconPlayerPlay } from 'twenty-ui';
|
||||
import { CodeEditor } from '@/ui/input/code-editor/components/CodeEditor';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { CoreEditorHeader } from '@/ui/input/code-editor/components/CodeEditorHeader';
|
||||
import styled from '@emotion/styled';
|
||||
import { TabList } from '@/ui/layout/tab/components/TabList';
|
||||
|
||||
const StyledTabList = styled(TabList)`
|
||||
border-bottom: none;
|
||||
`;
|
||||
|
||||
export const SettingsServerlessFunctionCodeEditorTab = ({
|
||||
formValues,
|
||||
handleExecute,
|
||||
onChange,
|
||||
}: {
|
||||
formValues: ServerlessFunctionFormValues;
|
||||
handleExecute: () => void;
|
||||
onChange: (key: string) => (value: string) => void;
|
||||
}) => {
|
||||
const HeaderButton = (
|
||||
<Button
|
||||
title="Test"
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
size="small"
|
||||
Icon={IconPlayerPlay}
|
||||
onClick={handleExecute}
|
||||
/>
|
||||
);
|
||||
|
||||
const TAB_LIST_COMPONENT_ID = 'serverless-function-editor';
|
||||
|
||||
const HeaderTabList = (
|
||||
<StyledTabList
|
||||
tabListId={TAB_LIST_COMPONENT_ID}
|
||||
tabs={[{ id: 'index.ts', title: 'index.ts' }]}
|
||||
/>
|
||||
);
|
||||
|
||||
const Header = (
|
||||
<CoreEditorHeader leftNodes={[HeaderTabList]} rightNodes={[HeaderButton]} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Code your function"
|
||||
description="Write your function (in typescript) below"
|
||||
/>
|
||||
<CodeEditor
|
||||
value={formValues.code}
|
||||
onChange={onChange('code')}
|
||||
header={Header}
|
||||
/>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,62 @@
|
||||
import { H2Title } from 'twenty-ui';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { useState } from 'react';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { SettingsServerlessFunctionNewForm } from '@/settings/serverless-functions/components/SettingsServerlessFunctionNewForm';
|
||||
import { useDeleteOneServerlessFunction } from '@/settings/serverless-functions/hooks/useDeleteOneServerlessFunction';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
export const SettingsServerlessFunctionSettingsTab = ({
|
||||
formValues,
|
||||
serverlessFunctionId,
|
||||
onChange,
|
||||
}: {
|
||||
formValues: ServerlessFunctionFormValues;
|
||||
serverlessFunctionId: string;
|
||||
onChange: (key: string) => (value: string) => void;
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const [isDeleteFunctionModalOpen, setIsDeleteFunctionModalOpen] =
|
||||
useState(false);
|
||||
const { deleteOneServerlessFunction } = useDeleteOneServerlessFunction();
|
||||
|
||||
const deleteFunction = async () => {
|
||||
await deleteOneServerlessFunction({ id: serverlessFunctionId });
|
||||
navigate('/settings/functions');
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<SettingsServerlessFunctionNewForm
|
||||
formValues={formValues}
|
||||
onChange={onChange}
|
||||
/>
|
||||
<Section>
|
||||
<H2Title title="Danger zone" description="Delete this function" />
|
||||
<Button
|
||||
accent="danger"
|
||||
onClick={() => setIsDeleteFunctionModalOpen(true)}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
title="Delete function"
|
||||
/>
|
||||
</Section>
|
||||
<ConfirmationModal
|
||||
confirmationValue={formValues.name}
|
||||
confirmationPlaceholder={formValues.name}
|
||||
isOpen={isDeleteFunctionModalOpen}
|
||||
setIsOpen={setIsDeleteFunctionModalOpen}
|
||||
title="Function Deletion"
|
||||
subtitle={
|
||||
<>
|
||||
This action cannot be undone. This will permanently delete your
|
||||
function. <br /> Please type in the function name to confirm.
|
||||
</>
|
||||
}
|
||||
onConfirmClick={deleteFunction}
|
||||
deleteButtonText="Delete function"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,81 @@
|
||||
import { H2Title, IconPlayerPlay } from 'twenty-ui';
|
||||
import { Section } from '@/ui/layout/section/components/Section';
|
||||
|
||||
import { CodeEditor } from '@/ui/input/code-editor/components/CodeEditor';
|
||||
import styled from '@emotion/styled';
|
||||
import { CoreEditorHeader } from '@/ui/input/code-editor/components/CodeEditorHeader';
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
||||
import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState';
|
||||
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
|
||||
|
||||
const StyledInputsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
export const SettingsServerlessFunctionTestTab = ({
|
||||
handleExecute,
|
||||
}: {
|
||||
handleExecute: () => void;
|
||||
}) => {
|
||||
const settingsServerlessFunctionCodeEditorOutputParams = useRecoilValue(
|
||||
settingsServerlessFunctionCodeEditorOutputParamsState,
|
||||
);
|
||||
const settingsServerlessFunctionOutput = useRecoilValue(
|
||||
settingsServerlessFunctionOutputState,
|
||||
);
|
||||
const [settingsServerlessFunctionInput, setSettingsServerlessFunctionInput] =
|
||||
useRecoilState(settingsServerlessFunctionInputState);
|
||||
|
||||
const InputHeaderButton = (
|
||||
<Button
|
||||
title="Run Function"
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
size="small"
|
||||
Icon={IconPlayerPlay}
|
||||
onClick={handleExecute}
|
||||
/>
|
||||
);
|
||||
|
||||
const InputHeader = (
|
||||
<CoreEditorHeader title={'Input'} rightNodes={[InputHeaderButton]} />
|
||||
);
|
||||
|
||||
const OutputHeaderButton = (
|
||||
<LightCopyIconButton copyText={settingsServerlessFunctionOutput} />
|
||||
);
|
||||
|
||||
const OutputHeader = (
|
||||
<CoreEditorHeader title={'Output'} rightNodes={[OutputHeaderButton]} />
|
||||
);
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Test your function"
|
||||
description='Insert a JSON input, then press "Run" to test your function.'
|
||||
/>
|
||||
<StyledInputsContainer>
|
||||
<CodeEditor
|
||||
value={settingsServerlessFunctionInput}
|
||||
height={200}
|
||||
onChange={setSettingsServerlessFunctionInput}
|
||||
language={'json'}
|
||||
header={InputHeader}
|
||||
/>
|
||||
<CodeEditor
|
||||
value={settingsServerlessFunctionOutput}
|
||||
height={settingsServerlessFunctionCodeEditorOutputParams.height}
|
||||
language={settingsServerlessFunctionCodeEditorOutputParams.language}
|
||||
options={{ readOnly: true, domReadOnly: true }}
|
||||
header={OutputHeader}
|
||||
/>
|
||||
</StyledInputsContainer>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { settingsServerlessFunctionOutputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionOutputState';
|
||||
import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState';
|
||||
|
||||
export const SettingsServerlessFunctionTestTabEffect = () => {
|
||||
const settingsServerlessFunctionOutput = useRecoilValue(
|
||||
settingsServerlessFunctionOutputState,
|
||||
);
|
||||
const setSettingsServerlessFunctionCodeEditorOutputParams = useSetRecoilState(
|
||||
settingsServerlessFunctionCodeEditorOutputParamsState,
|
||||
);
|
||||
try {
|
||||
JSON.parse(settingsServerlessFunctionOutput);
|
||||
setSettingsServerlessFunctionCodeEditorOutputParams({
|
||||
language: 'json',
|
||||
height: 300,
|
||||
});
|
||||
} catch {
|
||||
return <></>;
|
||||
}
|
||||
return <></>;
|
||||
};
|
||||
@ -0,0 +1,15 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const SERVERLESS_FUNCTION_FRAGMENT = gql`
|
||||
fragment ServerlessFunctionFields on ServerlessFunction {
|
||||
id
|
||||
name
|
||||
description
|
||||
sourceCodeHash
|
||||
sourceCodeFullPath
|
||||
runtime
|
||||
syncStatus
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,13 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
||||
|
||||
export const CREATE_ONE_SERVERLESS_FUNCTION = gql`
|
||||
${SERVERLESS_FUNCTION_FRAGMENT}
|
||||
mutation CreateOneServerlessFunctionItem(
|
||||
$input: CreateServerlessFunctionInput!
|
||||
) {
|
||||
createOneServerlessFunction(input: $input) {
|
||||
...ServerlessFunctionFields
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,11 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
||||
|
||||
export const DELETE_ONE_SERVERLESS_FUNCTION = gql`
|
||||
${SERVERLESS_FUNCTION_FRAGMENT}
|
||||
mutation DeleteOneServerlessFunction($input: DeleteServerlessFunctionInput!) {
|
||||
deleteOneServerlessFunction(input: $input) {
|
||||
...ServerlessFunctionFields
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const EXECUTE_ONE_SERVERLESS_FUNCTION = gql`
|
||||
mutation ExecuteOneServerlessFunction($id: UUID!, $payload: JSON!) {
|
||||
executeOneServerlessFunction(id: $id, payload: $payload) {
|
||||
result
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,11 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
||||
|
||||
export const UPDATE_ONE_SERVERLESS_FUNCTION = gql`
|
||||
${SERVERLESS_FUNCTION_FRAGMENT}
|
||||
mutation UpdateOneServerlessFunction($input: UpdateServerlessFunctionInput!) {
|
||||
updateOneServerlessFunction(input: $input) {
|
||||
...ServerlessFunctionFields
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,15 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
||||
|
||||
export const FIND_MANY_SERVERLESS_FUNCTIONS = gql`
|
||||
${SERVERLESS_FUNCTION_FRAGMENT}
|
||||
query GetManyServerlessFunctions {
|
||||
serverlessFunctions(paging: { first: 100 }) {
|
||||
edges {
|
||||
node {
|
||||
...ServerlessFunctionFields
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,11 @@
|
||||
import { gql } from '@apollo/client';
|
||||
import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment';
|
||||
|
||||
export const FIND_ONE_SERVERLESS_FUNCTION = gql`
|
||||
${SERVERLESS_FUNCTION_FRAGMENT}
|
||||
query GetOneServerlessFunction($id: UUID!) {
|
||||
serverlessFunction(id: $id) {
|
||||
...ServerlessFunctionFields
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,34 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
jest.mock(
|
||||
'@/settings/serverless-functions/hooks/useGetOneServerlessFunction',
|
||||
() => ({
|
||||
useGetOneServerlessFunction: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
describe('useServerlessFunctionUpdateFormState', () => {
|
||||
test('should return a form', () => {
|
||||
const serverlessFunctionId = 'serverlessFunctionId';
|
||||
const useGetOneServerlessFunctionMock = jest.requireMock(
|
||||
'@/settings/serverless-functions/hooks/useGetOneServerlessFunction',
|
||||
);
|
||||
useGetOneServerlessFunctionMock.useGetOneServerlessFunction.mockReturnValue(
|
||||
{
|
||||
serverlessFunction: { sourceCodeFullPath: undefined },
|
||||
},
|
||||
);
|
||||
const { result } = renderHook(
|
||||
() => useServerlessFunctionUpdateFormState(serverlessFunctionId),
|
||||
{
|
||||
wrapper: RecoilRoot,
|
||||
},
|
||||
);
|
||||
|
||||
const [formValues] = result.current;
|
||||
|
||||
expect(formValues).toEqual({ name: '', description: '', code: '' });
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,35 @@
|
||||
import { ApolloClient, useMutation } from '@apollo/client';
|
||||
|
||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||
import {
|
||||
CreateServerlessFunctionInput,
|
||||
CreateOneServerlessFunctionItemMutation,
|
||||
CreateOneServerlessFunctionItemMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions';
|
||||
import { CREATE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/createOneServerlessFunction';
|
||||
|
||||
export const useCreateOneServerlessFunction = () => {
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
const [mutate] = useMutation<
|
||||
CreateOneServerlessFunctionItemMutation,
|
||||
CreateOneServerlessFunctionItemMutationVariables
|
||||
>(CREATE_ONE_SERVERLESS_FUNCTION, {
|
||||
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
|
||||
});
|
||||
|
||||
const createOneServerlessFunction = async (
|
||||
input: CreateServerlessFunctionInput,
|
||||
) => {
|
||||
return await mutate({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
awaitRefetchQueries: true,
|
||||
refetchQueries: [getOperationName(FIND_MANY_SERVERLESS_FUNCTIONS) ?? ''],
|
||||
});
|
||||
};
|
||||
|
||||
return { createOneServerlessFunction };
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||
import { ApolloClient, useMutation } from '@apollo/client';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions';
|
||||
import { DELETE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction';
|
||||
import {
|
||||
DeleteServerlessFunctionInput,
|
||||
DeleteOneServerlessFunctionMutation,
|
||||
DeleteOneServerlessFunctionMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useDeleteOneServerlessFunction = () => {
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
const [mutate] = useMutation<
|
||||
DeleteOneServerlessFunctionMutation,
|
||||
DeleteOneServerlessFunctionMutationVariables
|
||||
>(DELETE_ONE_SERVERLESS_FUNCTION, {
|
||||
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
|
||||
});
|
||||
|
||||
const deleteOneServerlessFunction = async (
|
||||
input: DeleteServerlessFunctionInput,
|
||||
) => {
|
||||
return await mutate({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
awaitRefetchQueries: true,
|
||||
refetchQueries: [getOperationName(FIND_MANY_SERVERLESS_FUNCTIONS) ?? ''],
|
||||
});
|
||||
};
|
||||
|
||||
return { deleteOneServerlessFunction };
|
||||
};
|
||||
@ -0,0 +1,30 @@
|
||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||
import { ApolloClient, useMutation } from '@apollo/client';
|
||||
import { EXECUTE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/executeOneServerlessFunction';
|
||||
import {
|
||||
ExecuteOneServerlessFunctionMutation,
|
||||
ExecuteOneServerlessFunctionMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useExecuteOneServerlessFunction = () => {
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
const [mutate] = useMutation<
|
||||
ExecuteOneServerlessFunctionMutation,
|
||||
ExecuteOneServerlessFunctionMutationVariables
|
||||
>(EXECUTE_ONE_SERVERLESS_FUNCTION, {
|
||||
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
|
||||
});
|
||||
|
||||
const executeOneServerlessFunction = async (
|
||||
id: string,
|
||||
payload: object = {},
|
||||
) => {
|
||||
return await mutate({
|
||||
variables: {
|
||||
id,
|
||||
payload,
|
||||
},
|
||||
});
|
||||
};
|
||||
return { executeOneServerlessFunction };
|
||||
};
|
||||
@ -0,0 +1,21 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions';
|
||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||
import {
|
||||
GetManyServerlessFunctionsQuery,
|
||||
GetManyServerlessFunctionsQueryVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useGetManyServerlessFunctions = () => {
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
const { data } = useQuery<
|
||||
GetManyServerlessFunctionsQuery,
|
||||
GetManyServerlessFunctionsQueryVariables
|
||||
>(FIND_MANY_SERVERLESS_FUNCTIONS, {
|
||||
client: apolloMetadataClient ?? undefined,
|
||||
});
|
||||
return {
|
||||
serverlessFunctions:
|
||||
data?.serverlessFunctions?.edges.map(({ node }) => node) || [],
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,23 @@
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||
import { FIND_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunction';
|
||||
import {
|
||||
GetOneServerlessFunctionQuery,
|
||||
GetOneServerlessFunctionQueryVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
export const useGetOneServerlessFunction = (id: string) => {
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
const { data } = useQuery<
|
||||
GetOneServerlessFunctionQuery,
|
||||
GetOneServerlessFunctionQueryVariables
|
||||
>(FIND_ONE_SERVERLESS_FUNCTION, {
|
||||
client: apolloMetadataClient ?? undefined,
|
||||
variables: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
return {
|
||||
serverlessFunction: data?.serverlessFunction || null,
|
||||
};
|
||||
};
|
||||
@ -0,0 +1,57 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
import { useGetOneServerlessFunction } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunction';
|
||||
|
||||
export type ServerlessFunctionNewFormValues = {
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
|
||||
export type ServerlessFunctionFormValues = ServerlessFunctionNewFormValues & {
|
||||
code: string;
|
||||
};
|
||||
|
||||
type SetServerlessFunctionFormValues = Dispatch<
|
||||
SetStateAction<ServerlessFunctionFormValues>
|
||||
>;
|
||||
|
||||
export const useServerlessFunctionUpdateFormState = (
|
||||
serverlessFunctionId: string,
|
||||
): [ServerlessFunctionFormValues, SetServerlessFunctionFormValues] => {
|
||||
const [formValues, setFormValues] = useState<ServerlessFunctionFormValues>({
|
||||
name: '',
|
||||
description: '',
|
||||
code: '',
|
||||
});
|
||||
|
||||
const { serverlessFunction } =
|
||||
useGetOneServerlessFunction(serverlessFunctionId);
|
||||
|
||||
useEffect(() => {
|
||||
const getFileContent = async () => {
|
||||
const resp = await fetch(
|
||||
getFileAbsoluteURI(serverlessFunction?.sourceCodeFullPath),
|
||||
);
|
||||
if (resp.status !== 200) {
|
||||
throw new Error('Network response was not ok');
|
||||
} else {
|
||||
const result = await resp.text();
|
||||
const newState = {
|
||||
code: result,
|
||||
name: serverlessFunction?.name || '',
|
||||
description: serverlessFunction?.description || '',
|
||||
};
|
||||
setFormValues((prevState) => ({
|
||||
...prevState,
|
||||
...newState,
|
||||
}));
|
||||
}
|
||||
};
|
||||
if (isDefined(serverlessFunction?.sourceCodeFullPath)) {
|
||||
getFileContent();
|
||||
}
|
||||
}, [serverlessFunction, setFormValues]);
|
||||
|
||||
return [formValues, setFormValues];
|
||||
};
|
||||
@ -0,0 +1,34 @@
|
||||
import { ApolloClient, useMutation } from '@apollo/client';
|
||||
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
|
||||
import { UPDATE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/updateOneServerlessFunction';
|
||||
import {
|
||||
UpdateServerlessFunctionInput,
|
||||
UpdateOneServerlessFunctionMutation,
|
||||
UpdateOneServerlessFunctionMutationVariables,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { getOperationName } from '@apollo/client/utilities';
|
||||
import { FIND_MANY_SERVERLESS_FUNCTIONS } from '@/settings/serverless-functions/graphql/queries/findManyServerlessFunctions';
|
||||
|
||||
export const useUpdateOneServerlessFunction = () => {
|
||||
const apolloMetadataClient = useApolloMetadataClient();
|
||||
const [mutate] = useMutation<
|
||||
UpdateOneServerlessFunctionMutation,
|
||||
UpdateOneServerlessFunctionMutationVariables
|
||||
>(UPDATE_ONE_SERVERLESS_FUNCTION, {
|
||||
client: apolloMetadataClient ?? ({} as ApolloClient<any>),
|
||||
});
|
||||
|
||||
const updateOneServerlessFunction = async (
|
||||
input: UpdateServerlessFunctionInput,
|
||||
) => {
|
||||
return await mutate({
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
awaitRefetchQueries: true,
|
||||
refetchQueries: [getOperationName(FIND_MANY_SERVERLESS_FUNCTIONS) ?? ''],
|
||||
});
|
||||
};
|
||||
|
||||
return { updateOneServerlessFunction };
|
||||
};
|
||||
@ -0,0 +1,7 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const settingsServerlessFunctionCodeEditorOutputParamsState =
|
||||
createState<{ language: string; height: number }>({
|
||||
key: 'settingsServerlessFunctionCodeEditorOutputParamsState',
|
||||
defaultValue: { language: 'plaintext', height: 64 },
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const settingsServerlessFunctionInputState = createState<string>({
|
||||
key: 'settingsServerlessFunctionInputState',
|
||||
defaultValue: '{}',
|
||||
});
|
||||
@ -0,0 +1,6 @@
|
||||
import { createState } from 'twenty-ui';
|
||||
|
||||
export const settingsServerlessFunctionOutputState = createState<string>({
|
||||
key: 'settingsServerlessFunctionOutputState',
|
||||
defaultValue: 'Enter an input above then press "run Function"',
|
||||
});
|
||||
@ -0,0 +1,15 @@
|
||||
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
|
||||
import { SettingsPath } from '@/types/SettingsPath';
|
||||
|
||||
describe('getSettingsPagePath', () => {
|
||||
test('should compute page path', () => {
|
||||
expect(getSettingsPagePath(SettingsPath.ServerlessFunctions)).toEqual(
|
||||
'/settings/functions',
|
||||
);
|
||||
});
|
||||
test('should compute page path with id', () => {
|
||||
expect(
|
||||
getSettingsPagePath(SettingsPath.ServerlessFunctions, { id: 'id' }),
|
||||
).toEqual('/settings/functions/id');
|
||||
});
|
||||
});
|
||||
@ -2,6 +2,7 @@ import { SettingsPath } from '@/types/SettingsPath';
|
||||
import { isDefined } from '~/utils/isDefined';
|
||||
|
||||
type PathParams = {
|
||||
id?: string;
|
||||
objectSlug?: string;
|
||||
};
|
||||
|
||||
@ -15,5 +16,9 @@ export const getSettingsPagePath = <Path extends SettingsPath>(
|
||||
resultPath = resultPath.replace(':objectSlug', params.objectSlug);
|
||||
}
|
||||
|
||||
if (isDefined(params?.id)) {
|
||||
resultPath = `${resultPath}/${params?.id}`;
|
||||
}
|
||||
|
||||
return resultPath;
|
||||
};
|
||||
|
||||
@ -16,10 +16,13 @@ export enum SettingsPath {
|
||||
ObjectNewFieldStep2 = 'objects/:objectSlug/new-field/step-2',
|
||||
ObjectFieldEdit = 'objects/:objectSlug/:fieldSlug',
|
||||
NewObject = 'objects/new',
|
||||
NewServerlessFunction = 'functions/new',
|
||||
ServerlessFunctionDetail = 'functions/:serverlessFunctionId',
|
||||
WorkspaceMembersPage = 'workspace-members',
|
||||
Workspace = 'workspace',
|
||||
CRMMigration = 'crm-migration',
|
||||
Developers = 'developers',
|
||||
ServerlessFunctions = 'functions',
|
||||
DevelopersNewApiKey = 'api-keys/new',
|
||||
DevelopersApiKeyDetail = 'api-keys/:apiKeyId',
|
||||
Integrations = 'integrations',
|
||||
|
||||
@ -0,0 +1,80 @@
|
||||
import Editor, { Monaco, EditorProps } from '@monaco-editor/react';
|
||||
import { editor } from 'monaco-editor';
|
||||
import { codeEditorTheme } from '@/ui/input/code-editor/theme/CodeEditorTheme';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const DEFAULT_CODE = `export const handler = async (
|
||||
event: object,
|
||||
context: object
|
||||
): Promise<object> => {
|
||||
// Your code here
|
||||
return {};
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledEditor = styled(Editor)`
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-top: none;
|
||||
border-radius: 0 0 ${({ theme }) => theme.border.radius.sm}
|
||||
${({ theme }) => theme.border.radius.sm};
|
||||
`;
|
||||
|
||||
type CodeEditorProps = Omit<EditorProps, 'onChange'> & {
|
||||
header: React.ReactNode;
|
||||
onChange?: (value: string) => void;
|
||||
};
|
||||
|
||||
export const CodeEditor = ({
|
||||
value = DEFAULT_CODE,
|
||||
onChange,
|
||||
language = 'typescript',
|
||||
height = 500,
|
||||
options = undefined,
|
||||
header,
|
||||
}: CodeEditorProps) => {
|
||||
const theme = useTheme();
|
||||
const handleEditorDidMount = (
|
||||
editor: editor.IStandaloneCodeEditor,
|
||||
monaco: Monaco,
|
||||
) => {
|
||||
monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme));
|
||||
monaco.editor.setTheme('codeEditorTheme');
|
||||
};
|
||||
useEffect(() => {
|
||||
const style = document.createElement('style');
|
||||
style.innerHTML = `
|
||||
.monaco-editor .margin .line-numbers {
|
||||
font-weight: bold;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
return () => {
|
||||
document.head.removeChild(style);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<div>
|
||||
{header}
|
||||
<StyledEditor
|
||||
height={height}
|
||||
language={language}
|
||||
value={value}
|
||||
onMount={handleEditorDidMount}
|
||||
onChange={(value?: string) => value && onChange?.(value)}
|
||||
options={{
|
||||
...options,
|
||||
overviewRulerLanes: 0,
|
||||
scrollbar: {
|
||||
vertical: 'hidden',
|
||||
horizontal: 'hidden',
|
||||
},
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,51 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledEditorHeader = styled.div`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.background.transparent.lighter};
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
display: flex;
|
||||
height: ${({ theme }) => theme.spacing(10)};
|
||||
padding: ${({ theme }) => `0 ${theme.spacing(2)}`};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
border-top-right-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const StyledElementContainer = styled.div`
|
||||
align-content: flex-end;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export type CoreEditorHeaderProps = {
|
||||
title?: string;
|
||||
leftNodes?: React.ReactNode[];
|
||||
rightNodes?: React.ReactNode[];
|
||||
};
|
||||
|
||||
export const CoreEditorHeader = ({
|
||||
title,
|
||||
leftNodes,
|
||||
rightNodes,
|
||||
}: CoreEditorHeaderProps) => {
|
||||
return (
|
||||
<StyledEditorHeader>
|
||||
<StyledElementContainer>
|
||||
{leftNodes &&
|
||||
leftNodes.map((leftButton, index) => {
|
||||
return <div key={`left-${index}`}>{leftButton}</div>;
|
||||
})}
|
||||
{title}
|
||||
</StyledElementContainer>
|
||||
<StyledElementContainer>
|
||||
{rightNodes &&
|
||||
rightNodes.map((rightButton, index) => {
|
||||
return <div key={`right-${index}`}>{rightButton}</div>;
|
||||
})}
|
||||
</StyledElementContainer>
|
||||
</StyledEditorHeader>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
import { editor } from 'monaco-editor';
|
||||
import { ThemeType } from 'twenty-ui';
|
||||
|
||||
export const codeEditorTheme = (theme: ThemeType) => {
|
||||
return {
|
||||
base: 'vs' as editor.BuiltinTheme,
|
||||
inherit: true,
|
||||
rules: [
|
||||
{
|
||||
token: '',
|
||||
foreground: theme.code.text.gray,
|
||||
fontStyle: 'bold',
|
||||
},
|
||||
{ token: 'keyword', foreground: theme.code.text.sky },
|
||||
{
|
||||
token: 'delimiter',
|
||||
foreground: theme.code.text.gray,
|
||||
},
|
||||
{ token: 'string', foreground: theme.code.text.pink },
|
||||
{
|
||||
token: 'comment',
|
||||
foreground: theme.code.text.orange,
|
||||
},
|
||||
],
|
||||
colors: {
|
||||
'editor.background': theme.background.secondary,
|
||||
'editorCursor.foreground': theme.font.color.primary,
|
||||
'editorLineNumber.foreground': theme.font.color.extraLight,
|
||||
'editorLineNumber.activeForeground': theme.font.color.light,
|
||||
'editor.lineHighlightBackground': theme.background.tertiary,
|
||||
},
|
||||
};
|
||||
};
|
||||
@ -29,7 +29,6 @@ const StyledTextArea = styled(TextareaAutosize)`
|
||||
line-height: 16px;
|
||||
overflow: auto;
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(3)};
|
||||
resize: none;
|
||||
width: 100%;
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ export const BACKGROUND: Record<string, string> = {
|
||||
emptyTimeline: '/images/placeholders/background/empty_timeline_bg.png',
|
||||
loadingMessages: '/images/placeholders/background/loading_messages_bg.png',
|
||||
loadingAccounts: '/images/placeholders/background/loading_accounts_bg.png',
|
||||
emptyFunctions: '/images/placeholders/background/empty_functions_bg.png',
|
||||
emptyInbox: '/images/placeholders/background/empty_inbox_bg.png',
|
||||
error404: '/images/placeholders/background/404_bg.png',
|
||||
error500: '/images/placeholders/background/500_bg.png',
|
||||
|
||||
@ -11,4 +11,5 @@ export const DARK_BACKGROUND: Record<string, string> = {
|
||||
error500: '/images/placeholders/dark-background/500_bg.png',
|
||||
loadingMessages: '/images/placeholders/background/loading_messages_bg.png',
|
||||
loadingAccounts: '/images/placeholders/background/loading_accounts_bg.png',
|
||||
emptyFunctions: '/images/placeholders/dark-background/empty_functions_bg.png',
|
||||
};
|
||||
|
||||
@ -11,4 +11,5 @@ export const DARK_MOVING_IMAGE: Record<string, string> = {
|
||||
error500: '/images/placeholders/dark-moving-image/500.png',
|
||||
loadingMessages: '/images/placeholders/moving-image/loading_messages.png',
|
||||
loadingAccounts: '/images/placeholders/moving-image/loading_accounts.png',
|
||||
emptyFunctions: '/images/placeholders/dark-moving-image/empty_functions.png',
|
||||
};
|
||||
|
||||
@ -8,6 +8,7 @@ export const MOVING_IMAGE: Record<string, string> = {
|
||||
emptyTimeline: '/images/placeholders/moving-image/empty_timeline.png',
|
||||
loadingMessages: '/images/placeholders/moving-image/loading_messages.png',
|
||||
loadingAccounts: '/images/placeholders/moving-image/loading_accounts.png',
|
||||
emptyFunctions: '/images/placeholders/moving-image/empty_functions.png',
|
||||
emptyInbox: '/images/placeholders/moving-image/empty_inbox.png',
|
||||
error404: '/images/placeholders/moving-image/404.png',
|
||||
error500: '/images/placeholders/moving-image/500.png',
|
||||
|
||||
@ -22,6 +22,7 @@ type TabListProps = {
|
||||
tabListId: string;
|
||||
tabs: SingleTabProps[];
|
||||
loading?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
@ -34,7 +35,12 @@ const StyledContainer = styled.div`
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
export const TabList = ({ tabs, tabListId, loading }: TabListProps) => {
|
||||
export const TabList = ({
|
||||
tabs,
|
||||
tabListId,
|
||||
loading,
|
||||
className,
|
||||
}: TabListProps) => {
|
||||
const initialActiveTabId = tabs.find((tab) => !tab.hide)?.id || '';
|
||||
|
||||
const { activeTabIdState, setActiveTabId } = useTabList(tabListId);
|
||||
@ -48,7 +54,7 @@ export const TabList = ({ tabs, tabListId, loading }: TabListProps) => {
|
||||
return (
|
||||
<TabListScope tabListScopeId={tabListId}>
|
||||
<ScrollWrapper hideY>
|
||||
<StyledContainer>
|
||||
<StyledContainer className={className}>
|
||||
{tabs
|
||||
.filter((tab) => !tab.hide)
|
||||
.map((tab) => (
|
||||
|
||||
@ -5,5 +5,6 @@ export type FeatureFlagKey =
|
||||
| 'IS_AIRTABLE_INTEGRATION_ENABLED'
|
||||
| 'IS_POSTGRESQL_INTEGRATION_ENABLED'
|
||||
| 'IS_STRIPE_INTEGRATION_ENABLED'
|
||||
| 'IS_FUNCTION_SETTINGS_ENABLED'
|
||||
| 'IS_COPILOT_ENABLED'
|
||||
| 'IS_CRM_MIGRATION_ENABLED';
|
||||
|
||||
Reference in New Issue
Block a user