diff --git a/docs/docs/contributor/server/others/zapier.mdx b/docs/docs/contributor/server/others/zapier.mdx
index 7d5dbf5e6..6ae4bc259 100644
--- a/docs/docs/contributor/server/others/zapier.mdx
+++ b/docs/docs/contributor/server/others/zapier.mdx
@@ -37,7 +37,7 @@ From the `packages/twenty-zapier` folder, run:
```bash
cp .env.example .env
```
-Run the application locally, go to [http://localhost:3000/settings/apis](http://localhost:3000/settings/apis), and generate an API key.
+Run the application locally, go to [http://localhost:3000/settings/developers/api-keys](http://localhost:3000/settings/developers/api-keys), and generate an API key.
Replace the **YOUR_API_KEY** value in the `.env` file with the API key you just generated.
diff --git a/front/.eslintrc.js b/front/.eslintrc.js
index 9432bdbf4..9829bbfa8 100644
--- a/front/.eslintrc.js
+++ b/front/.eslintrc.js
@@ -107,7 +107,7 @@ module.exports = {
'message': 'Icon imports are only allowed for `@/ui/icon`',
},
{
- 'group': ['react-hotkeys-hook'],
+ 'group': ['react-hotkeys-web-hook'],
"importNames": ["useHotkeys"],
'message': 'Please use the custom wrapper: `useScopedHotkeys`',
},
diff --git a/front/src/App.tsx b/front/src/App.tsx
index 205dea85d..5b21d7f98 100644
--- a/front/src/App.tsx
+++ b/front/src/App.tsx
@@ -21,6 +21,9 @@ import { SettingsNewObject } from '~/pages/settings/data-model/SettingsNewObject
import { SettingsObjectDetail } from '~/pages/settings/data-model/SettingsObjectDetail';
import { SettingsObjectEdit } from '~/pages/settings/data-model/SettingsObjectEdit';
import { SettingsObjects } from '~/pages/settings/data-model/SettingsObjects';
+import { SettingsDevelopersApiKeyDetail } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail';
+import { SettingsDevelopersApiKeys } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeys';
+import { SettingsDevelopersApiKeysNew } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew';
import { SettingsExperience } from '~/pages/settings/SettingsExperience';
import { SettingsProfile } from '~/pages/settings/SettingsProfile';
import { SettingsWorkspace } from '~/pages/settings/SettingsWorkspace';
@@ -31,7 +34,6 @@ import { getPageTitleFromPath } from '~/utils/title-utils';
import { ObjectTablePage } from './modules/metadata/components/ObjectTablePage';
import { SettingsObjectNewFieldStep1 } from './pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1';
import { SettingsObjectNewFieldStep2 } from './pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2';
-import { SettingsApis } from './pages/settings/SettingsApis';
export const App = () => {
const { pathname } = useLocation();
@@ -97,7 +99,25 @@ export const App = () => {
path={SettingsPath.NewObject}
element={}
/>
- } />
+
+ }
+ />
+ }
+ />
+ }
+ />
+
+ }
+ />
}
diff --git a/front/src/generated-metadata/graphql.ts b/front/src/generated-metadata/graphql.ts
index 794d888c7..19cdc4da7 100644
--- a/front/src/generated-metadata/graphql.ts
+++ b/front/src/generated-metadata/graphql.ts
@@ -760,6 +760,15 @@ export enum ViewType {
Table = 'Table'
}
+export type WebHook = {
+ __typename?: 'WebHook';
+ createdAt: Scalars['DateTime']['output'];
+ id: Scalars['ID']['output'];
+ operation: Scalars['String']['output'];
+ targetUrl: Scalars['String']['output'];
+ updatedAt: Scalars['DateTime']['output'];
+};
+
export type Workspace = {
__typename?: 'Workspace';
Attachment?: Maybe>;
@@ -783,6 +792,7 @@ export type Workspace = {
viewFilters?: Maybe>;
viewSorts?: Maybe>;
views?: Maybe>;
+ webHooks?: Maybe>;
workspaceMember?: Maybe>;
};
diff --git a/front/src/generated/graphql.tsx b/front/src/generated/graphql.tsx
index 7ccb56bf9..54859b2f4 100644
--- a/front/src/generated/graphql.tsx
+++ b/front/src/generated/graphql.tsx
@@ -482,6 +482,13 @@ export enum ApiKeyScalarFieldEnum {
WorkspaceId = 'workspaceId'
}
+export type ApiKeyToken = {
+ __typename?: 'ApiKeyToken';
+ expiresAt: Scalars['DateTime'];
+ id: Scalars['String'];
+ token: Scalars['String'];
+};
+
export type ApiKeyUpdateManyWithoutWorkspaceNestedInput = {
connect?: InputMaybe>;
disconnect?: InputMaybe>;
@@ -1392,7 +1399,7 @@ export type Mutation = {
createManyViewFilter: AffectedRows;
createManyViewSort: AffectedRows;
createOneActivity: Activity;
- createOneApiKey: AuthToken;
+ createOneApiKey: ApiKeyToken;
createOneComment: Comment;
createOneCompany: Company;
createOneField: Field;
@@ -1402,6 +1409,7 @@ export type Mutation = {
createOnePipelineStage: PipelineStage;
createOneView: View;
createOneViewField: ViewField;
+ createOneWebHook: WebHook;
deleteCurrentWorkspace: Workspace;
deleteFavorite: Favorite;
deleteManyActivities: AffectedRows;
@@ -1415,6 +1423,7 @@ export type Mutation = {
deleteOneObject: ObjectDeleteResponse;
deleteOnePipelineStage: PipelineStage;
deleteOneView: View;
+ deleteOneWebHook: WebHook;
deleteUserAccount: User;
deleteWorkspaceMember: WorkspaceMember;
impersonate: Verify;
@@ -1559,6 +1568,11 @@ export type MutationCreateOneViewFieldArgs = {
};
+export type MutationCreateOneWebHookArgs = {
+ data: WebHookCreateInput;
+};
+
+
export type MutationDeleteFavoriteArgs = {
where: FavoriteWhereInput;
};
@@ -1609,6 +1623,11 @@ export type MutationDeleteOneViewArgs = {
};
+export type MutationDeleteOneWebHookArgs = {
+ where: WebHookWhereUniqueInput;
+};
+
+
export type MutationDeleteWorkspaceMemberArgs = {
where: WorkspaceMemberWhereUniqueInput;
};
@@ -2523,6 +2542,7 @@ export type Query = {
findManyViewField: Array;
findManyViewFilter: Array;
findManyViewSort: Array;
+ findManyWebHook: Array;
findManyWorkspaceMember: Array;
findUniqueCompany: Company;
findUniquePerson: Person;
@@ -2662,6 +2682,16 @@ export type QueryFindManyViewSortArgs = {
};
+export type QueryFindManyWebHookArgs = {
+ cursor?: InputMaybe;
+ distinct?: InputMaybe>;
+ orderBy?: InputMaybe>;
+ skip?: InputMaybe;
+ take?: InputMaybe;
+ where?: InputMaybe;
+};
+
+
export type QueryFindManyWorkspaceMemberArgs = {
cursor?: InputMaybe;
distinct?: InputMaybe>;
@@ -3392,6 +3422,62 @@ export type ViewWhereUniqueInput = {
id?: InputMaybe;
};
+export type WebHook = {
+ __typename?: 'WebHook';
+ createdAt: Scalars['DateTime'];
+ id: Scalars['ID'];
+ operation: Scalars['String'];
+ targetUrl: Scalars['String'];
+ updatedAt: Scalars['DateTime'];
+};
+
+export type WebHookCreateInput = {
+ createdAt?: InputMaybe;
+ id?: InputMaybe;
+ operation: Scalars['String'];
+ targetUrl: Scalars['String'];
+ updatedAt?: InputMaybe;
+};
+
+export type WebHookOrderByWithRelationInput = {
+ createdAt?: InputMaybe;
+ id?: InputMaybe;
+ operation?: InputMaybe;
+ targetUrl?: InputMaybe;
+ updatedAt?: InputMaybe;
+};
+
+export enum WebHookScalarFieldEnum {
+ CreatedAt = 'createdAt',
+ DeletedAt = 'deletedAt',
+ Id = 'id',
+ Operation = 'operation',
+ TargetUrl = 'targetUrl',
+ UpdatedAt = 'updatedAt',
+ WorkspaceId = 'workspaceId'
+}
+
+export type WebHookUpdateManyWithoutWorkspaceNestedInput = {
+ connect?: InputMaybe>;
+ disconnect?: InputMaybe>;
+ set?: InputMaybe>;
+};
+
+export type WebHookWhereInput = {
+ AND?: InputMaybe>;
+ NOT?: InputMaybe>;
+ OR?: InputMaybe>;
+ createdAt?: InputMaybe;
+ id?: InputMaybe;
+ operation?: InputMaybe;
+ targetUrl?: InputMaybe;
+ updatedAt?: InputMaybe;
+};
+
+export type WebHookWhereUniqueInput = {
+ id?: InputMaybe;
+};
+
export type Workspace = {
__typename?: 'Workspace';
Attachment?: Maybe>;
@@ -3415,6 +3501,7 @@ export type Workspace = {
viewFilters?: Maybe>;
viewSorts?: Maybe>;
views?: Maybe>;
+ webHooks?: Maybe>;
workspaceMember?: Maybe>;
};
@@ -3590,6 +3677,7 @@ export type WorkspaceUpdateInput = {
viewFilters?: InputMaybe;
viewSorts?: InputMaybe;
views?: InputMaybe;
+ webHooks?: InputMaybe;
workspaceMember?: InputMaybe;
};
@@ -4118,6 +4206,27 @@ export type SearchUserQueryVariables = Exact<{
export type SearchUserQuery = { __typename?: 'Query', searchResults: Array<{ __typename?: 'User', avatarUrl?: string | null, id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null }> };
+export type DeleteOneApiKeyMutationVariables = Exact<{
+ apiKeyId: Scalars['String'];
+}>;
+
+
+export type DeleteOneApiKeyMutation = { __typename?: 'Mutation', revokeOneApiKey: { __typename?: 'ApiKey', id: string } };
+
+export type InsertOneApiKeyMutationVariables = Exact<{
+ data: ApiKeyCreateInput;
+}>;
+
+
+export type InsertOneApiKeyMutation = { __typename?: 'Mutation', createOneApiKey: { __typename?: 'ApiKeyToken', id: string, token: string, expiresAt: string } };
+
+export type GetApiKeyQueryVariables = Exact<{
+ apiKeyId: Scalars['String'];
+}>;
+
+
+export type GetApiKeyQuery = { __typename?: 'Query', findManyApiKey: Array<{ __typename?: 'ApiKey', id: string, name: string, expiresAt?: string | null }> };
+
export type UserFieldsFragmentFragment = { __typename?: 'User', id: string, email: string, displayName: string, firstName?: string | null, lastName?: string | null };
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
@@ -6771,6 +6880,111 @@ export function useSearchUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions
export type SearchUserQueryHookResult = ReturnType;
export type SearchUserLazyQueryHookResult = ReturnType;
export type SearchUserQueryResult = Apollo.QueryResult;
+export const DeleteOneApiKeyDocument = gql`
+ mutation DeleteOneApiKey($apiKeyId: String!) {
+ revokeOneApiKey(where: {id: $apiKeyId}) {
+ id
+ }
+}
+ `;
+export type DeleteOneApiKeyMutationFn = Apollo.MutationFunction;
+
+/**
+ * __useDeleteOneApiKeyMutation__
+ *
+ * To run a mutation, you first call `useDeleteOneApiKeyMutation` within a React component and pass it any options that fit your needs.
+ * When your component renders, `useDeleteOneApiKeyMutation` returns a tuple that includes:
+ * - A mutate function that you can call at any time to execute the mutation
+ * - An object with fields that represent the current status of the mutation's execution
+ *
+ * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
+ *
+ * @example
+ * const [deleteOneApiKeyMutation, { data, loading, error }] = useDeleteOneApiKeyMutation({
+ * variables: {
+ * apiKeyId: // value for 'apiKeyId'
+ * },
+ * });
+ */
+export function useDeleteOneApiKeyMutation(baseOptions?: Apollo.MutationHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useMutation(DeleteOneApiKeyDocument, options);
+ }
+export type DeleteOneApiKeyMutationHookResult = ReturnType;
+export type DeleteOneApiKeyMutationResult = Apollo.MutationResult;
+export type DeleteOneApiKeyMutationOptions = Apollo.BaseMutationOptions;
+export const InsertOneApiKeyDocument = gql`
+ mutation InsertOneApiKey($data: ApiKeyCreateInput!) {
+ createOneApiKey(data: $data) {
+ id
+ token
+ expiresAt
+ }
+}
+ `;
+export type InsertOneApiKeyMutationFn = Apollo.MutationFunction;
+
+/**
+ * __useInsertOneApiKeyMutation__
+ *
+ * To run a mutation, you first call `useInsertOneApiKeyMutation` within a React component and pass it any options that fit your needs.
+ * When your component renders, `useInsertOneApiKeyMutation` returns a tuple that includes:
+ * - A mutate function that you can call at any time to execute the mutation
+ * - An object with fields that represent the current status of the mutation's execution
+ *
+ * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
+ *
+ * @example
+ * const [insertOneApiKeyMutation, { data, loading, error }] = useInsertOneApiKeyMutation({
+ * variables: {
+ * data: // value for 'data'
+ * },
+ * });
+ */
+export function useInsertOneApiKeyMutation(baseOptions?: Apollo.MutationHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useMutation(InsertOneApiKeyDocument, options);
+ }
+export type InsertOneApiKeyMutationHookResult = ReturnType;
+export type InsertOneApiKeyMutationResult = Apollo.MutationResult;
+export type InsertOneApiKeyMutationOptions = Apollo.BaseMutationOptions;
+export const GetApiKeyDocument = gql`
+ query GetApiKey($apiKeyId: String!) {
+ findManyApiKey(where: {id: {equals: $apiKeyId}}) {
+ id
+ name
+ expiresAt
+ }
+}
+ `;
+
+/**
+ * __useGetApiKeyQuery__
+ *
+ * To run a query within a React component, call `useGetApiKeyQuery` and pass it any options that fit your needs.
+ * When your component renders, `useGetApiKeyQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useGetApiKeyQuery({
+ * variables: {
+ * apiKeyId: // value for 'apiKeyId'
+ * },
+ * });
+ */
+export function useGetApiKeyQuery(baseOptions: Apollo.QueryHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useQuery(GetApiKeyDocument, options);
+ }
+export function useGetApiKeyLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useLazyQuery(GetApiKeyDocument, options);
+ }
+export type GetApiKeyQueryHookResult = ReturnType;
+export type GetApiKeyLazyQueryHookResult = ReturnType;
+export type GetApiKeyQueryResult = Apollo.QueryResult;
export const DeleteUserAccountDocument = gql`
mutation DeleteUserAccount {
deleteUserAccount {
diff --git a/front/src/modules/companies/board/components/CompanyBoard.tsx b/front/src/modules/companies/board/components/CompanyBoard.tsx
index 717f308a2..f4aa76ac3 100644
--- a/front/src/modules/companies/board/components/CompanyBoard.tsx
+++ b/front/src/modules/companies/board/components/CompanyBoard.tsx
@@ -24,7 +24,7 @@ export const CompanyBoard = ({
onEditColumnTitle,
}: CompanyBoardProps) => {
// TODO: we can store objectId and fieldDefinitions in the ViewBarContext
- // And then use the useBoardViews hook wherever we need it in the board
+ // And then use the useBoardViews web-hook wherever we need it in the board
const { createView, deleteView, submitCurrentView, updateView } =
useBoardViews({
objectId: 'company',
diff --git a/front/src/modules/settings/components/SettingsNavbar.tsx b/front/src/modules/settings/components/SettingsNavbar.tsx
index b65beb434..7bdba5db0 100644
--- a/front/src/modules/settings/components/SettingsNavbar.tsx
+++ b/front/src/modules/settings/components/SettingsNavbar.tsx
@@ -41,7 +41,7 @@ export const SettingsNavbar = () => {
end: false,
});
const isDevelopersSettingsActive = !!useMatch({
- path: useResolvedPath('/settings/api').pathname,
+ path: useResolvedPath('/settings/developers/api-keys').pathname,
end: true,
});
@@ -104,7 +104,7 @@ export const SettingsNavbar = () => {
{isDevelopersSettingsEnabled && (
diff --git a/front/src/modules/settings/developers/components/ApiKeyInput.tsx b/front/src/modules/settings/developers/components/ApiKeyInput.tsx
new file mode 100644
index 000000000..36639f35f
--- /dev/null
+++ b/front/src/modules/settings/developers/components/ApiKeyInput.tsx
@@ -0,0 +1,51 @@
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+
+import { IconCopy } from '@/ui/display/icon';
+import { useSnackBar } from '@/ui/feedback/snack-bar/hooks/useSnackBar';
+import { Button } from '@/ui/input/button/components/Button';
+import { TextInput } from '@/ui/input/components/TextInput';
+import { beautifyDateDiff } from '~/utils/date-utils';
+
+const StyledContainer = styled.div`
+ display: flex;
+ flex-direction: row;
+`;
+
+const StyledLinkContainer = styled.div`
+ flex: 1;
+ margin-right: ${({ theme }) => theme.spacing(2)};
+`;
+
+type ApiKeyInputProps = { expiresAt?: string | null; apiKey: string };
+
+export const ApiKeyInput = ({ expiresAt, apiKey }: ApiKeyInputProps) => {
+ const theme = useTheme();
+ const computeInfo = () => {
+ if (!expiresAt) {
+ return '';
+ }
+ return `This key will expire in ${beautifyDateDiff(expiresAt)}`;
+ };
+
+ const { enqueueSnackBar } = useSnackBar();
+ return (
+
+
+
+
+
+ );
+};
diff --git a/front/src/modules/settings/developers/components/SettingsApisFieldItemTableRow.tsx b/front/src/modules/settings/developers/components/SettingsApiKeysFieldItemTableRow.tsx
similarity index 96%
rename from front/src/modules/settings/developers/components/SettingsApisFieldItemTableRow.tsx
rename to front/src/modules/settings/developers/components/SettingsApiKeysFieldItemTableRow.tsx
index 60b8237f4..aab2c4faf 100644
--- a/front/src/modules/settings/developers/components/SettingsApisFieldItemTableRow.tsx
+++ b/front/src/modules/settings/developers/components/SettingsApiKeysFieldItemTableRow.tsx
@@ -25,7 +25,7 @@ const StyledIconChevronRight = styled(IconChevronRight)`
color: ${({ theme }) => theme.font.color.tertiary};
`;
-export const SettingsApisFieldItemTableRow = ({
+export const SettingsApiKeysFieldItemTableRow = ({
fieldItem,
}: {
fieldItem: ApisFiedlItem;
diff --git a/front/src/modules/settings/developers/components/__stories__/ApiKeyInput.stories.tsx b/front/src/modules/settings/developers/components/__stories__/ApiKeyInput.stories.tsx
new file mode 100644
index 000000000..c0375de22
--- /dev/null
+++ b/front/src/modules/settings/developers/components/__stories__/ApiKeyInput.stories.tsx
@@ -0,0 +1,19 @@
+import { Meta, StoryObj } from '@storybook/react';
+
+import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput';
+import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
+
+const meta: Meta = {
+ title: 'Pages/Settings/Developers/ApiKeys/ApiKeyInput',
+ component: ApiKeyInput,
+ decorators: [ComponentDecorator],
+ args: {
+ expiresAt: '2123-11-06T23:59:59.825Z',
+ apiKey:
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0d2VudHktN2VkOWQyMTItMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNjk4MTQyODgyLCJleHAiOjE2OTk0MDE1OTksImp0aSI6ImMyMmFiNjQxLTVhOGYtNGQwMC1iMDkzLTk3MzUwYTM2YzZkOSJ9.JIe2TX5IXrdNl3n-kRFp3jyfNUE7unzXZLAzm2Gxl98',
+ },
+};
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/front/src/modules/settings/developers/components/__stories__/SettingsApiKeysFieldItemTableRow.stories.tsx b/front/src/modules/settings/developers/components/__stories__/SettingsApiKeysFieldItemTableRow.stories.tsx
new file mode 100644
index 000000000..60306dae0
--- /dev/null
+++ b/front/src/modules/settings/developers/components/__stories__/SettingsApiKeysFieldItemTableRow.stories.tsx
@@ -0,0 +1,23 @@
+import { Meta, StoryObj } from '@storybook/react';
+
+import { SettingsApiKeysFieldItemTableRow } from '@/settings/developers/components/SettingsApiKeysFieldItemTableRow';
+import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
+
+const meta: Meta = {
+ title: 'Pages/Settings/Developers/ApiKeys/SettingsApiKeysFieldItemTableRow',
+ component: SettingsApiKeysFieldItemTableRow,
+ decorators: [ComponentDecorator],
+ args: {
+ fieldItem: {
+ id: '3f4a42e8-b81f-4f8c-9c20-1602e6b34791',
+ name: 'Zapier Api Key',
+ type: 'internal',
+ expiration: 'In 3 days',
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/front/src/modules/settings/developers/constants/expirationDates.ts b/front/src/modules/settings/developers/constants/expirationDates.ts
new file mode 100644
index 000000000..746661e7a
--- /dev/null
+++ b/front/src/modules/settings/developers/constants/expirationDates.ts
@@ -0,0 +1,11 @@
+export const ExpirationDates: {
+ value: number;
+ label: string;
+}[] = [
+ { label: '15 days', value: 15 },
+ { label: '30 days', value: 30 },
+ { label: '90 days', value: 90 },
+ { label: '1 year', value: 365 },
+ { label: '2 years', value: 2 * 365 },
+ { label: 'Never', value: 10 * 365 },
+];
diff --git a/front/src/modules/settings/developers/graphql/mutations/deleteOneApiKey.ts b/front/src/modules/settings/developers/graphql/mutations/deleteOneApiKey.ts
new file mode 100644
index 000000000..68c53127a
--- /dev/null
+++ b/front/src/modules/settings/developers/graphql/mutations/deleteOneApiKey.ts
@@ -0,0 +1,9 @@
+import { gql } from '@apollo/client';
+
+export const DELETE_ONE_API_KEY = gql`
+ mutation DeleteOneApiKey($apiKeyId: String!) {
+ revokeOneApiKey(where: { id: $apiKeyId }) {
+ id
+ }
+ }
+`;
diff --git a/front/src/modules/settings/developers/graphql/mutations/insertOneApiKey.ts b/front/src/modules/settings/developers/graphql/mutations/insertOneApiKey.ts
new file mode 100644
index 000000000..63f4898c8
--- /dev/null
+++ b/front/src/modules/settings/developers/graphql/mutations/insertOneApiKey.ts
@@ -0,0 +1,11 @@
+import { gql } from '@apollo/client';
+
+export const INSERT_ONE_API_KEY = gql`
+ mutation InsertOneApiKey($data: ApiKeyCreateInput!) {
+ createOneApiKey(data: $data) {
+ id
+ token
+ expiresAt
+ }
+ }
+`;
diff --git a/front/src/modules/settings/developers/graphql/queries/getApiKey.ts b/front/src/modules/settings/developers/graphql/queries/getApiKey.ts
new file mode 100644
index 000000000..2c91682cf
--- /dev/null
+++ b/front/src/modules/settings/developers/graphql/queries/getApiKey.ts
@@ -0,0 +1,11 @@
+import { gql } from '@apollo/client';
+
+export const GET_API_KEY = gql`
+ query GetApiKey($apiKeyId: String!) {
+ findManyApiKey(where: { id: { equals: $apiKeyId } }) {
+ id
+ name
+ expiresAt
+ }
+ }
+`;
diff --git a/front/src/modules/settings/developers/states/generatedApiKeyState.ts b/front/src/modules/settings/developers/states/generatedApiKeyState.ts
new file mode 100644
index 000000000..73701d1e2
--- /dev/null
+++ b/front/src/modules/settings/developers/states/generatedApiKeyState.ts
@@ -0,0 +1,6 @@
+import { atom } from 'recoil';
+
+export const generatedApiKeyState = atom({
+ key: 'generatedApiKeyState',
+ default: null,
+});
diff --git a/front/src/modules/settings/profile/components/NameFields.tsx b/front/src/modules/settings/profile/components/NameFields.tsx
index 0364b41b5..16ef32796 100644
--- a/front/src/modules/settings/profile/components/NameFields.tsx
+++ b/front/src/modules/settings/profile/components/NameFields.tsx
@@ -36,7 +36,7 @@ export const NameFields = ({
const [updateUser] = useUpdateUserMutation();
- // TODO: Enhance this with react-hook-form (https://www.react-hook-form.com)
+ // TODO: Enhance this with react-web-hook-form (https://www.react-hook-form.com)
const debouncedUpdate = debounce(async () => {
if (onFirstNameUpdate) {
onFirstNameUpdate(firstName);
diff --git a/front/src/modules/settings/workspace/components/NameField.tsx b/front/src/modules/settings/workspace/components/NameField.tsx
index ad0393131..8f0401eaf 100644
--- a/front/src/modules/settings/workspace/components/NameField.tsx
+++ b/front/src/modules/settings/workspace/components/NameField.tsx
@@ -34,7 +34,7 @@ export const NameField = ({
const [updateWorkspace] = useUpdateWorkspaceMutation();
- // TODO: Enhance this with react-hook-form (https://www.react-hook-form.com)
+ // TODO: Enhance this with react-web-hook-form (https://www.react-hook-form.com)
// eslint-disable-next-line react-hooks/exhaustive-deps
const debouncedUpdate = useCallback(
debounce(async (name: string) => {
diff --git a/front/src/modules/types/AppPath.ts b/front/src/modules/types/AppPath.ts
index 24ea00f44..9c0a033b4 100644
--- a/front/src/modules/types/AppPath.ts
+++ b/front/src/modules/types/AppPath.ts
@@ -20,6 +20,7 @@ export enum AppPath {
ObjectTablePage = '/objects/:objectNamePlural',
SettingsCatchAll = `/settings/*`,
+ DevelopersCatchAll = `/developers/*`,
// Impersonate
Impersonate = '/impersonate/:userId',
diff --git a/front/src/modules/types/SettingsPath.ts b/front/src/modules/types/SettingsPath.ts
index 24cfa4b2f..3053e625f 100644
--- a/front/src/modules/types/SettingsPath.ts
+++ b/front/src/modules/types/SettingsPath.ts
@@ -9,5 +9,7 @@ export enum SettingsPath {
NewObject = 'objects/new',
WorkspaceMembersPage = 'workspace-members',
Workspace = 'workspace',
- Apis = 'apis',
+ Developers = 'api-keys',
+ DevelopersNewApiKey = 'api-keys/new',
+ DevelopersApiKeyDetail = 'api-keys/:apiKeyId',
}
diff --git a/front/src/modules/ui/data/field/contexts/FieldContext.ts b/front/src/modules/ui/data/field/contexts/FieldContext.ts
index ebd651efa..1f85b25da 100644
--- a/front/src/modules/ui/data/field/contexts/FieldContext.ts
+++ b/front/src/modules/ui/data/field/contexts/FieldContext.ts
@@ -5,7 +5,7 @@ import { FieldMetadata } from '../types/FieldMetadata';
export type GenericFieldContextType = {
fieldDefinition: FieldDefinition;
- // TODO: add better typing for mutation hook
+ // TODO: add better typing for mutation web-hook
useUpdateEntityMutation: () => [(params: any) => void, any];
entityId: string;
recoilScopeId: string;
diff --git a/front/src/modules/ui/input/components/Select.tsx b/front/src/modules/ui/input/components/Select.tsx
index 8dad26379..6fb077372 100644
--- a/front/src/modules/ui/input/components/Select.tsx
+++ b/front/src/modules/ui/input/components/Select.tsx
@@ -11,7 +11,7 @@ import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem';
import { SelectHotkeyScope } from '../types/SelectHotkeyScope';
-export type SelectProps = {
+export type SelectProps = {
dropdownScopeId: string;
onChange: (value: Value) => void;
options: { value: Value; label: string; Icon?: IconComponent }[];
@@ -38,7 +38,7 @@ const StyledLabel = styled.div`
gap: ${({ theme }) => theme.spacing(1)};
`;
-export const Select = ({
+export const Select = ({
dropdownScopeId,
onChange,
options,
diff --git a/front/src/modules/ui/input/components/TextInput.tsx b/front/src/modules/ui/input/components/TextInput.tsx
index f5d210b07..406145148 100644
--- a/front/src/modules/ui/input/components/TextInput.tsx
+++ b/front/src/modules/ui/input/components/TextInput.tsx
@@ -25,6 +25,7 @@ export type TextInputComponentProps = Omit<
> & {
className?: string;
label?: string;
+ info?: string;
onChange?: (text: string) => void;
fullWidth?: boolean;
disableHotkeys?: boolean;
@@ -45,10 +46,16 @@ const StyledLabel = styled.span`
text-transform: uppercase;
`;
+const StyledInfo = styled.span`
+ color: ${({ theme }) => theme.font.color.light};
+ font-size: ${({ theme }) => theme.font.size.sm};
+ font-weight: ${({ theme }) => theme.font.weight.regular};
+ margin-top: ${({ theme }) => theme.spacing(1)};
+`;
+
const StyledInputContainer = styled.div`
display: flex;
flex-direction: row;
-
width: 100%;
`;
@@ -113,6 +120,7 @@ const TextInputComponent = (
{
className,
label,
+ info,
value,
onChange,
onFocus,
@@ -204,6 +212,7 @@ const TextInputComponent = (
)}
+ {info && {info}}
{error && {error}}
);
diff --git a/front/src/modules/ui/input/components/__stories__/Select.stories.tsx b/front/src/modules/ui/input/components/__stories__/Select.stories.tsx
index 678e30fd4..ae118a00a 100644
--- a/front/src/modules/ui/input/components/__stories__/Select.stories.tsx
+++ b/front/src/modules/ui/input/components/__stories__/Select.stories.tsx
@@ -6,11 +6,11 @@ import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
import { Select, SelectProps } from '../Select';
-type RenderProps = SelectProps;
+type RenderProps = SelectProps;
const Render = (args: RenderProps) => {
const [value, setValue] = useState(args.value);
- const handleChange = (value: string) => {
+ const handleChange = (value: string | number) => {
args.onChange?.(value);
setValue(value);
};
diff --git a/front/src/modules/ui/input/components/__stories__/TextInput.stories.tsx b/front/src/modules/ui/input/components/__stories__/TextInput.stories.tsx
index 74dc2e96b..5838f0e1f 100644
--- a/front/src/modules/ui/input/components/__stories__/TextInput.stories.tsx
+++ b/front/src/modules/ui/input/components/__stories__/TextInput.stories.tsx
@@ -38,3 +38,7 @@ export const Filled: Story = {
export const Disabled: Story = {
args: { disabled: true, value: 'Tim' },
};
+
+export const WithInfo: Story = {
+ args: { info: 'Some info displayed below the input', value: 'Tim' },
+};
diff --git a/front/src/modules/ui/layout/board/states/selectors/boardColumnTotalsFamilySelector.ts b/front/src/modules/ui/layout/board/states/selectors/boardColumnTotalsFamilySelector.ts
index 2664bb29a..5a70fef68 100644
--- a/front/src/modules/ui/layout/board/states/selectors/boardColumnTotalsFamilySelector.ts
+++ b/front/src/modules/ui/layout/board/states/selectors/boardColumnTotalsFamilySelector.ts
@@ -4,7 +4,7 @@ import { companyProgressesFamilyState } from '@/companies/states/companyProgress
import { boardCardIdsByColumnIdFamilyState } from '../boardCardIdsByColumnIdFamilyState';
-// TODO: this state should be computed during the synchronization hook and put in a generic
+// TODO: this state should be computed during the synchronization web-hook and put in a generic
// boardColumnTotalsFamilyState indexed by columnId.
export const boardColumnTotalsFamilySelector = selectorFamily({
key: 'boardColumnTotalsFamilySelector',
diff --git a/front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutsideArrayOfRef.test.tsx b/front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutsideArrayOfRef.test.tsx
index d96d092df..29c5cf111 100644
--- a/front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutsideArrayOfRef.test.tsx
+++ b/front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useListenClickOutsideArrayOfRef.test.tsx
@@ -22,7 +22,7 @@ const TestComponentDomMode = () => {
);
};
-test('useListenClickOutside hook works in dom mode', async () => {
+test('useListenClickOutside web-hook works in dom mode', async () => {
const { getByText } = render();
const inside = getByText('Inside');
const inside2 = getByText('Inside 2');
diff --git a/front/src/pages/auth/CreateProfile.tsx b/front/src/pages/auth/CreateProfile.tsx
index e6ce80d4c..0e4d1b635 100644
--- a/front/src/pages/auth/CreateProfile.tsx
+++ b/front/src/pages/auth/CreateProfile.tsx
@@ -136,7 +136,7 @@ export const CreateProfile = () => {
title="Name"
description="Your name as it will be displayed on the app"
/>
- {/* TODO: When react-hook-form is added to edit page we should create a dedicated component with context */}
+ {/* TODO: When react-web-hook-form is added to edit page we should create a dedicated component with context */}
= {
- title: 'Pages/Settings/SettingsApi',
- component: SettingsApis,
- decorators: [PageDecorator],
- args: { routePath: '/settings/apis' },
- parameters: {
- msw: graphqlMocks,
- },
-};
-
-export default meta;
-
-export type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx b/front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx
new file mode 100644
index 000000000..1acc6ec09
--- /dev/null
+++ b/front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx
@@ -0,0 +1,76 @@
+import { useNavigate, useParams } from 'react-router-dom';
+import { useRecoilState } from 'recoil';
+
+import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
+import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
+import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput';
+import { generatedApiKeyState } from '@/settings/developers/states/generatedApiKeyState';
+import { IconSettings, IconTrash } from '@/ui/display/icon';
+import { H2Title } from '@/ui/display/typography/components/H2Title';
+import { Button } from '@/ui/input/button/components/Button';
+import { TextInput } from '@/ui/input/components/TextInput';
+import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
+import { Section } from '@/ui/layout/section/components/Section';
+import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
+import {
+ useDeleteOneApiKeyMutation,
+ useGetApiKeyQuery,
+} from '~/generated/graphql';
+
+export const SettingsDevelopersApiKeyDetail = () => {
+ const navigate = useNavigate();
+ const { apiKeyId = '' } = useParams();
+ const [generatedApiKey] = useRecoilState(generatedApiKeyState);
+ const apiKeyQuery = useGetApiKeyQuery({
+ variables: {
+ apiKeyId,
+ },
+ });
+ const [deleteApiKey] = useDeleteOneApiKeyMutation();
+ const deleteIntegration = async () => {
+ await deleteApiKey({ variables: { apiKeyId } });
+ navigate('/settings/developers/api-keys');
+ };
+ const { expiresAt, name } = apiKeyQuery.data?.findManyApiKey[0] || {};
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/front/src/pages/settings/SettingsApis.tsx b/front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeys.tsx
similarity index 89%
rename from front/src/pages/settings/SettingsApis.tsx
rename to front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeys.tsx
index c113cb87e..9b1fbe540 100644
--- a/front/src/pages/settings/SettingsApis.tsx
+++ b/front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeys.tsx
@@ -2,7 +2,7 @@ import { useNavigate } from 'react-router-dom';
import styled from '@emotion/styled';
import { objectSettingsWidth } from '@/settings/data-model/constants/objectSettings';
-import { SettingsApisFieldItemTableRow } from '@/settings/developers/components/SettingsApisFieldItemTableRow';
+import { SettingsApiKeysFieldItemTableRow } from '@/settings/developers/components/SettingsApiKeysFieldItemTableRow';
import { activeApiKeyItems } from '@/settings/developers/constants/mockObjects';
import { IconPlus, IconSettings } from '@/ui/display/icon';
import { H1Title } from '@/ui/display/typography/components/H1Title';
@@ -34,7 +34,7 @@ const StyledH1Title = styled(H1Title)`
margin-bottom: 0;
`;
-export const SettingsApis = () => {
+export const SettingsDevelopersApiKeys = () => {
const navigate = useNavigate();
return (
@@ -48,7 +48,7 @@ export const SettingsApis = () => {
accent="blue"
size="small"
onClick={() => {
- navigate('/');
+ navigate('/settings/developers/api-keys/new');
}}
/>
@@ -64,7 +64,7 @@ export const SettingsApis = () => {
{activeApiKeyItems.map((fieldItem) => (
-
diff --git a/front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx b/front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx
new file mode 100644
index 000000000..548f3cb56
--- /dev/null
+++ b/front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx
@@ -0,0 +1,100 @@
+import { useState } from 'react';
+import { useNavigate } from 'react-router-dom';
+import { DateTime } from 'luxon';
+import { useRecoilState } from 'recoil';
+
+import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
+import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
+import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
+import { ExpirationDates } from '@/settings/developers/constants/expirationDates';
+import { generatedApiKeyState } from '@/settings/developers/states/generatedApiKeyState';
+import { IconSettings } from '@/ui/display/icon';
+import { H2Title } from '@/ui/display/typography/components/H2Title';
+import { Select } from '@/ui/input/components/Select';
+import { TextInput } from '@/ui/input/components/TextInput';
+import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
+import { Section } from '@/ui/layout/section/components/Section';
+import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
+import { useInsertOneApiKeyMutation } from '~/generated/graphql';
+
+export const SettingsDevelopersApiKeysNew = () => {
+ const [insertOneApiKey] = useInsertOneApiKeyMutation();
+ const navigate = useNavigate();
+ const [, setGeneratedApiKey] = useRecoilState(generatedApiKeyState);
+ const [formValues, setFormValues] = useState<{
+ name: string;
+ expirationDate: number;
+ }>({
+ expirationDate: ExpirationDates[0].value,
+ name: '',
+ });
+ const onSave = async () => {
+ const apiKey = await insertOneApiKey({
+ variables: {
+ data: {
+ name: formValues.name,
+ expiresAt: DateTime.now()
+ .plus({ days: formValues.expirationDate })
+ .toISODate(),
+ },
+ },
+ });
+ setGeneratedApiKey(apiKey.data?.createOneApiKey?.token);
+ navigate(
+ `/settings/developers/api-keys/${apiKey.data?.createOneApiKey?.id}`,
+ );
+ };
+ const canSave = !!formValues.name;
+ return (
+
+
+
+
+ {
+ navigate('/settings/developers/api-keys');
+ }}
+ onSave={onSave}
+ />
+
+
+
+ {
+ setFormValues((prevState) => ({
+ ...prevState,
+ name: value,
+ }));
+ }}
+ fullWidth
+ />
+
+
+
+
+
+
+ );
+};
diff --git a/front/src/pages/settings/developers/api-keys/__stories__/SettingsDevelopersApiKeys.stories.tsx b/front/src/pages/settings/developers/api-keys/__stories__/SettingsDevelopersApiKeys.stories.tsx
new file mode 100644
index 000000000..ebd547063
--- /dev/null
+++ b/front/src/pages/settings/developers/api-keys/__stories__/SettingsDevelopersApiKeys.stories.tsx
@@ -0,0 +1,29 @@
+import { Meta, StoryObj } from '@storybook/react';
+
+import { SettingsDevelopersApiKeys } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeys';
+import {
+ PageDecorator,
+ PageDecoratorArgs,
+} from '~/testing/decorators/PageDecorator';
+import { graphqlMocks } from '~/testing/graphqlMocks';
+import { sleep } from '~/testing/sleep';
+
+const meta: Meta = {
+ title: 'Pages/Settings/Developers/ApiKeys/SettingsDevelopersApiKeys',
+ component: SettingsDevelopersApiKeys,
+ decorators: [PageDecorator],
+ args: { routePath: '/settings/developers/api-keys' },
+ parameters: {
+ msw: graphqlMocks,
+ },
+};
+
+export default meta;
+
+export type Story = StoryObj;
+
+export const Default: Story = {
+ play: async ({}) => {
+ await sleep(100);
+ },
+};
diff --git a/front/src/pages/settings/developers/api-keys/__stories__/SettingsDevelopersApiKeysDetail.stories.tsx b/front/src/pages/settings/developers/api-keys/__stories__/SettingsDevelopersApiKeysDetail.stories.tsx
new file mode 100644
index 000000000..a01b14b8d
--- /dev/null
+++ b/front/src/pages/settings/developers/api-keys/__stories__/SettingsDevelopersApiKeysDetail.stories.tsx
@@ -0,0 +1,32 @@
+import { Meta, StoryObj } from '@storybook/react';
+
+import { SettingsDevelopersApiKeyDetail } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail';
+import {
+ PageDecorator,
+ PageDecoratorArgs,
+} from '~/testing/decorators/PageDecorator';
+import { graphqlMocks } from '~/testing/graphqlMocks';
+import { mockedApiKeyToken } from '~/testing/mock-data/api-keys';
+import { sleep } from '~/testing/sleep';
+
+const meta: Meta = {
+ title: 'Pages/Settings/Developers/ApiKeys/SettingsDevelopersApiKeyDetail',
+ component: SettingsDevelopersApiKeyDetail,
+ decorators: [PageDecorator],
+ args: {
+ routePath: '/settings/apis/f7c6d736-8fcd-4e9c-ab99-28f6a9031570',
+ state: mockedApiKeyToken,
+ },
+ parameters: {
+ msw: graphqlMocks,
+ },
+};
+export default meta;
+
+export type Story = StoryObj;
+
+export const Default: Story = {
+ play: async ({}) => {
+ await sleep(100);
+ },
+};
diff --git a/front/src/pages/settings/developers/api-keys/__stories__/SettingsDevelopersApiKeysNew.stories.tsx b/front/src/pages/settings/developers/api-keys/__stories__/SettingsDevelopersApiKeysNew.stories.tsx
new file mode 100644
index 000000000..5ae23f8d3
--- /dev/null
+++ b/front/src/pages/settings/developers/api-keys/__stories__/SettingsDevelopersApiKeysNew.stories.tsx
@@ -0,0 +1,29 @@
+import { Meta, StoryObj } from '@storybook/react';
+
+import { SettingsDevelopersApiKeysNew } from '~/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew';
+import {
+ PageDecorator,
+ PageDecoratorArgs,
+} from '~/testing/decorators/PageDecorator';
+import { graphqlMocks } from '~/testing/graphqlMocks';
+import { sleep } from '~/testing/sleep';
+
+const meta: Meta = {
+ title: 'Pages/Settings/Developers/ApiKeys/SettingsDevelopersApiKeysNew',
+ component: SettingsDevelopersApiKeysNew,
+ decorators: [PageDecorator],
+ args: { routePath: '/settings/developers/api-keys/new' },
+ parameters: {
+ msw: graphqlMocks,
+ },
+};
+
+export default meta;
+
+export type Story = StoryObj;
+
+export const Default: Story = {
+ play: async ({}) => {
+ await sleep(100);
+ },
+};
diff --git a/front/src/testing/decorators/PageDecorator.tsx b/front/src/testing/decorators/PageDecorator.tsx
index ebd6d5a95..714e12e09 100644
--- a/front/src/testing/decorators/PageDecorator.tsx
+++ b/front/src/testing/decorators/PageDecorator.tsx
@@ -8,23 +8,41 @@ import { UserProvider } from '~/modules/users/components/UserProvider';
import { FullHeightStorybookLayout } from '../FullHeightStorybookLayout';
-export type PageDecoratorArgs = { routePath: string; routeParams: RouteParams };
+export type PageDecoratorArgs = {
+ routePath: string;
+ routeParams: RouteParams;
+ state?: string;
+};
type RouteParams = {
[param: string]: string;
};
-const computeLocation = (routePath: string, routeParams: RouteParams) =>
- routePath.replace(/:(\w+)/g, (paramName) => routeParams[paramName] ?? '');
+const computeLocation = (
+ routePath: string,
+ routeParams: RouteParams,
+ state?: string,
+) => {
+ return {
+ pathname: routePath.replace(
+ /:(\w+)/g,
+ (paramName) => routeParams[paramName] ?? '',
+ ),
+ state,
+ };
+};
export const PageDecorator: Decorator<{
routePath: string;
routeParams: RouteParams;
+ state?: string;
}> = (Story, { args }) => (
diff --git a/front/src/testing/graphqlMocks.ts b/front/src/testing/graphqlMocks.ts
index 36f231152..7a0d092b9 100644
--- a/front/src/testing/graphqlMocks.ts
+++ b/front/src/testing/graphqlMocks.ts
@@ -17,6 +17,7 @@ import { SEARCH_ACTIVITY_QUERY } from '@/search/graphql/queries/searchActivityQu
import { SEARCH_COMPANY_QUERY } from '@/search/graphql/queries/searchCompanyQuery';
import { SEARCH_PEOPLE_QUERY } from '@/search/graphql/queries/searchPeopleQuery';
import { SEARCH_USER_QUERY } from '@/search/graphql/queries/searchUserQuery';
+import { GET_API_KEY } from '@/settings/developers/graphql/queries/getApiKey';
import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser';
import { GET_VIEW_FIELDS } from '@/views/graphql/queries/getViewFields';
import { GET_VIEWS } from '@/views/graphql/queries/getViews';
@@ -30,6 +31,7 @@ import {
SearchUserQuery,
ViewType,
} from '~/generated/graphql';
+import { mockedApiKeys } from '~/testing/mock-data/api-keys';
import { mockedActivities, mockedTasks } from './mock-data/activities';
import {
@@ -278,6 +280,13 @@ export const graphqlMocks = [
}),
);
}),
+ graphql.query(getOperationName(GET_API_KEY) ?? '', (req, res, ctx) => {
+ return res(
+ ctx.data({
+ findManyApiKey: mockedApiKeys[0],
+ }),
+ );
+ }),
graphql.mutation(
getOperationName(CREATE_ACTIVITY_WITH_COMMENT) ?? '',
(req, res, ctx) => {
diff --git a/front/src/testing/mock-data/api-keys.ts b/front/src/testing/mock-data/api-keys.ts
new file mode 100644
index 000000000..f9d7bd961
--- /dev/null
+++ b/front/src/testing/mock-data/api-keys.ts
@@ -0,0 +1,18 @@
+import { ApiKey } from '~/generated/graphql';
+
+type MockedApiKey = Pick<
+ ApiKey,
+ 'id' | 'name' | 'createdAt' | 'updatedAt' | 'expiresAt' | '__typename'
+>;
+export const mockedApiKeyToken =
+ 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0d2VudHktN2VkOWQyMTItMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNjk4MDkzMDU0LCJleHAiOjE2OTkzMTUxOTksImp0aSI6IjY0Njg3ZWNmLWFhYzktNDNmYi1hY2I4LTE1M2QzNzgwYmIzMSJ9.JkQ3u7aRiqOFQkgHcC-mgCU37096HRSo40A_9X8gEng';
+export const mockedApiKeys: Array = [
+ {
+ id: 'f7c6d736-8fcd-4e9c-ab99-28f6a9031570',
+ name: 'Zapier Integration',
+ createdAt: '2023-04-26T10:12:42.33625+00:00',
+ updatedAt: '2023-04-26T10:23:42.33625+00:00',
+ expiresAt: '2100-11-06T23:59:59.825Z',
+ __typename: 'ApiKey',
+ },
+];
diff --git a/front/src/utils/__tests__/date-utils.test.ts b/front/src/utils/__tests__/date-utils.test.ts
index 90c4225bd..e287c8bb2 100644
--- a/front/src/utils/__tests__/date-utils.test.ts
+++ b/front/src/utils/__tests__/date-utils.test.ts
@@ -2,6 +2,7 @@ import { formatDistanceToNow } from 'date-fns';
import { DateTime } from 'luxon';
import {
+ beautifyDateDiff,
beautifyExactDate,
beautifyExactDateTime,
beautifyPastDateAbsolute,
@@ -237,3 +238,47 @@ describe('hasDatePassed', () => {
expect(result).toEqual(false);
});
});
+
+describe('beautifyDateDiff', () => {
+ it('should return the correct date diff', () => {
+ const date = '2023-11-05T00:00:00.000Z';
+ const dateToCompareWith = '2023-11-01T00:00:00.000Z';
+ const result = beautifyDateDiff(date, dateToCompareWith);
+ expect(result).toEqual('4 days');
+ });
+ it('should return the correct date diff for large diff', () => {
+ const date = '2033-11-05T00:00:00.000Z';
+ const dateToCompareWith = '2023-11-01T00:00:00.000Z';
+ const result = beautifyDateDiff(date, dateToCompareWith);
+ expect(result).toEqual('10 years and 4 days');
+ });
+ it('should return the correct date for negative diff', () => {
+ const date = '2013-11-05T00:00:00.000Z';
+ const dateToCompareWith = '2023-11-01T00:00:00.000Z';
+ const result = beautifyDateDiff(date, dateToCompareWith);
+ expect(result).toEqual('-9 years and -361 days');
+ });
+ it('should return the correct date diff for large diff', () => {
+ const date = '2033-11-01T00:00:00.000Z';
+ const dateToCompareWith = '2023-11-01T00:00:00.000Z';
+ const result = beautifyDateDiff(date, dateToCompareWith);
+ expect(result).toEqual('10 years');
+ });
+ it('should return the proper english date diff', () => {
+ const date = '2024-11-02T00:00:00.000Z';
+ const dateToCompareWith = '2023-11-01T00:00:00.000Z';
+ const result = beautifyDateDiff(date, dateToCompareWith);
+ expect(result).toEqual('1 year and 1 day');
+ });
+ it('should round date diff', () => {
+ const date = '2024-11-03T14:04:43.421Z';
+ const dateToCompareWith = '2023-11-01T00:00:00.000Z';
+ const result = beautifyDateDiff(date, dateToCompareWith);
+ expect(result).toEqual('1 year and 2 days');
+ });
+ it('should compare to now', () => {
+ const date = '2200-11-01T00:00:00.000Z';
+ const result = beautifyDateDiff(date);
+ expect(result).toContain('years');
+ });
+});
diff --git a/front/src/utils/date-utils.ts b/front/src/utils/date-utils.ts
index 5b782a0fe..d82e2fbb8 100644
--- a/front/src/utils/date-utils.ts
+++ b/front/src/utils/date-utils.ts
@@ -108,3 +108,17 @@ export const hasDatePassed = (date: Date | string | number) => {
return false;
}
};
+
+export const beautifyDateDiff = (date: string, dateToCompareWith?: string) => {
+ const dateDiff = DateTime.fromISO(date).diff(
+ dateToCompareWith ? DateTime.fromISO(dateToCompareWith) : DateTime.now(),
+ ['years', 'days'],
+ );
+ let result = '';
+ if (dateDiff.years) result = result + `${dateDiff.years} year`;
+ if (![0, 1].includes(dateDiff.years)) result = result + 's';
+ if (dateDiff.years && dateDiff.days) result = result + ' and ';
+ if (dateDiff.days) result = result + `${Math.floor(dateDiff.days)} day`;
+ if (![0, 1].includes(dateDiff.days)) result = result + 's';
+ return result;
+};
diff --git a/packages/twenty-zapier/src/authentication.ts b/packages/twenty-zapier/src/authentication.ts
index 413010502..9a17ce474 100644
--- a/packages/twenty-zapier/src/authentication.ts
+++ b/packages/twenty-zapier/src/authentication.ts
@@ -20,7 +20,7 @@ export default {
label: 'Api Key',
type: 'string',
helpText:
- 'Create the api key in [your twenty workspace](https://app.twenty.com/settings/apis)',
+ 'Create the api-keys key in [your twenty workspace](https://app.twenty.com/settings/developers/api-keys)',
},
],
connectionLabel: '{{data.currentWorkspace.displayName}}',
diff --git a/packages/twenty-zapier/src/test/triggers/company.test.ts b/packages/twenty-zapier/src/test/triggers/company.test.ts
index 5df4259af..54d22da8b 100644
--- a/packages/twenty-zapier/src/test/triggers/company.test.ts
+++ b/packages/twenty-zapier/src/test/triggers/company.test.ts
@@ -19,12 +19,12 @@ describe('triggers.company', () => {
requestDb(
z,
bundle,
- `query findManyHook {findManyHook(where: {id: {equals: "${result.id}"}}){id operation}}`,
+ `query findManyWebHook {findManyWebHook(where: {id: {equals: "${result.id}"}}){id operation}}`,
),
bundle,
);
- expect(checkDbResult.data.findManyHook.length).toEqual(1);
- expect(checkDbResult.data.findManyHook[0].operation).toEqual(
+ expect(checkDbResult.data.findManyWebHook.length).toEqual(1);
+ expect(checkDbResult.data.findManyWebHook[0].operation).toEqual(
'createOneCompany',
);
});
@@ -48,13 +48,13 @@ describe('triggers.company', () => {
requestDb(
z,
bundle,
- `query findManyHook {findManyHook(where: {id: {equals: "${result.id}"}}){id}}`,
+ `query findManyWebHook {findManyWebHook(where: {id: {equals: "${result.id}"}}){id}}`,
),
bundle,
);
- expect(checkDbResult.data.findManyHook.length).toEqual(0);
+ expect(checkDbResult.data.findManyWebHook.length).toEqual(0);
});
- test('should load company from hook', async () => {
+ test('should load company from web-hook', async () => {
const bundle = {
cleanedRequest: {
id: 'd6ccb1d1-a90b-4822-a992-a0dd946592c9',
diff --git a/packages/twenty-zapier/src/triggers/company.ts b/packages/twenty-zapier/src/triggers/company.ts
index 96464f94f..df6a710e0 100644
--- a/packages/twenty-zapier/src/triggers/company.ts
+++ b/packages/twenty-zapier/src/triggers/company.ts
@@ -7,22 +7,22 @@ const performSubscribe = async (z: ZObject, bundle: Bundle) => {
const result = await requestDb(
z,
bundle,
- `mutation createOneHook {createOneHook(data:{${handleQueryParams(
+ `mutation createOneWebHook {createOneWebHook(data:{${handleQueryParams(
data,
)}}) {id}}`,
);
- return result.data.createOneHook;
+ return result.data.createOneWebHook;
};
const performUnsubscribe = async (z: ZObject, bundle: Bundle) => {
const data = { id: bundle.subscribeData?.id };
const result = await requestDb(
z,
bundle,
- `mutation deleteOneHook {deleteOneHook(where:{${handleQueryParams(
+ `mutation deleteOneWebHook {deleteOneWebHook(where:{${handleQueryParams(
data,
)}}) {id}}`,
);
- return result.data.deleteOneHook;
+ return result.data.deleteOneWebHook;
};
const perform = (z: ZObject, bundle: Bundle) => {
return [bundle.cleanedRequest];
@@ -55,7 +55,7 @@ export default {
},
operation: {
inputFields: [],
- type: 'hook',
+ type: 'web-hook',
performSubscribe,
performUnsubscribe,
perform,
diff --git a/server/src/ability/ability.factory.ts b/server/src/ability/ability.factory.ts
index 6a5bdd759..576da8fd2 100644
--- a/server/src/ability/ability.factory.ts
+++ b/server/src/ability/ability.factory.ts
@@ -7,7 +7,6 @@ import {
ActivityTarget,
Attachment,
ApiKey,
- Hook,
Comment,
Company,
Favorite,
@@ -22,6 +21,7 @@ import {
ViewField,
ViewFilter,
ViewSort,
+ WebHook,
Workspace,
WorkspaceMember,
} from '@prisma/client';
@@ -36,7 +36,7 @@ type SubjectsAbility = Subjects<{
Comment: Comment;
Company: Company;
Favorite: Favorite;
- Hook: Hook;
+ WebHook: WebHook;
Person: Person;
Pipeline: Pipeline;
PipelineProgress: PipelineProgress;
@@ -83,10 +83,10 @@ export class AbilityFactory {
can(AbilityAction.Create, 'ApiKey');
can(AbilityAction.Update, 'ApiKey', { workspaceId: workspace.id });
- // Hook
- can(AbilityAction.Read, 'Hook', { workspaceId: workspace.id });
- can(AbilityAction.Create, 'Hook');
- can(AbilityAction.Delete, 'Hook', { workspaceId: workspace.id });
+ // WebHook
+ can(AbilityAction.Read, 'WebHook', { workspaceId: workspace.id });
+ can(AbilityAction.Create, 'WebHook');
+ can(AbilityAction.Delete, 'WebHook', { workspaceId: workspace.id });
// Workspace
can(AbilityAction.Read, 'Workspace');
diff --git a/server/src/ability/ability.module.ts b/server/src/ability/ability.module.ts
index 4f13ffc7c..744e2c817 100644
--- a/server/src/ability/ability.module.ts
+++ b/server/src/ability/ability.module.ts
@@ -2,6 +2,11 @@ import { Module } from '@nestjs/common';
import { AbilityFactory } from 'src/ability/ability.factory';
import { PrismaService } from 'src/database/prisma.service';
+import {
+ CreateWebHookAbilityHandler,
+ DeleteWebHookAbilityHandler,
+ ReadWebHookAbilityHandler,
+} from 'src/ability/handlers/web-hook.ability-handler';
import {
CreateUserAbilityHandler,
@@ -129,11 +134,6 @@ import {
ManageApiKeyAbilityHandler,
ReadApiKeyAbilityHandler,
} from './handlers/api-key.ability-handler';
-import {
- CreateHookAbilityHandler,
- DeleteHookAbilityHandler,
- ReadHookAbilityHandler,
-} from './handlers/hook.ability-handler';
@Module({
providers: [
@@ -247,9 +247,9 @@ import {
CreateApiKeyAbilityHandler,
UpdateApiKeyAbilityHandler,
// Hook
- CreateHookAbilityHandler,
- DeleteHookAbilityHandler,
- ReadHookAbilityHandler,
+ CreateWebHookAbilityHandler,
+ DeleteWebHookAbilityHandler,
+ ReadWebHookAbilityHandler,
],
exports: [
AbilityFactory,
@@ -360,9 +360,9 @@ import {
CreateApiKeyAbilityHandler,
UpdateApiKeyAbilityHandler,
// Hook
- CreateHookAbilityHandler,
- DeleteHookAbilityHandler,
- ReadHookAbilityHandler,
+ CreateWebHookAbilityHandler,
+ DeleteWebHookAbilityHandler,
+ ReadWebHookAbilityHandler,
],
})
export class AbilityModule {}
diff --git a/server/src/ability/handlers/hook.ability-handler.ts b/server/src/ability/handlers/web-hook.ability-handler.ts
similarity index 73%
rename from server/src/ability/handlers/hook.ability-handler.ts
rename to server/src/ability/handlers/web-hook.ability-handler.ts
index 1c2574b55..c2b73fd65 100644
--- a/server/src/ability/handlers/hook.ability-handler.ts
+++ b/server/src/ability/handlers/web-hook.ability-handler.ts
@@ -16,14 +16,14 @@ import { AbilityAction } from 'src/ability/ability.action';
import { assert } from 'src/utils/assert';
@Injectable()
-export class CreateHookAbilityHandler implements IAbilityHandler {
+export class CreateWebHookAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
const allowed = await relationAbilityChecker(
- 'Hook',
+ 'WebHook',
ability,
this.prismaService.client,
args,
@@ -31,27 +31,27 @@ export class CreateHookAbilityHandler implements IAbilityHandler {
if (!allowed) {
return false;
}
- return ability.can(AbilityAction.Create, 'Hook');
+ return ability.can(AbilityAction.Create, 'WebHook');
}
}
@Injectable()
-export class DeleteHookAbilityHandler implements IAbilityHandler {
+export class DeleteWebHookAbilityHandler implements IAbilityHandler {
constructor(private readonly prismaService: PrismaService) {}
async handle(ability: AppAbility, context: ExecutionContext) {
const gqlContext = GqlExecutionContext.create(context);
const args = gqlContext.getArgs();
- const hook = await this.prismaService.client.hook.findFirst({
+ const hook = await this.prismaService.client.webHook.findFirst({
where: args.where,
});
assert(hook, '', NotFoundException);
- return ability.can(AbilityAction.Delete, subject('Hook', hook));
+ return ability.can(AbilityAction.Delete, subject('WebHook', hook));
}
}
@Injectable()
-export class ReadHookAbilityHandler implements IAbilityHandler {
+export class ReadWebHookAbilityHandler implements IAbilityHandler {
async handle(ability: AppAbility) {
- return ability.can(AbilityAction.Read, 'Hook');
+ return ability.can(AbilityAction.Read, 'WebHook');
}
}
diff --git a/server/src/core/api-key/api-key.resolver.ts b/server/src/core/api-key/api-key.resolver.ts
index 96e7ec259..a4cec079c 100644
--- a/server/src/core/api-key/api-key.resolver.ts
+++ b/server/src/core/api-key/api-key.resolver.ts
@@ -19,7 +19,7 @@ import {
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory';
-import { AuthToken } from 'src/core/auth/dto/token.entity';
+import { ApiKeyToken } from 'src/core/auth/dto/token.entity';
import { ApiKeyService } from './api-key.service';
@@ -28,13 +28,13 @@ import { ApiKeyService } from './api-key.service';
export class ApiKeyResolver {
constructor(private readonly apiKeyService: ApiKeyService) {}
- @Mutation(() => AuthToken)
+ @Mutation(() => ApiKeyToken)
@UseGuards(AbilityGuard)
@CheckAbilities(CreateApiKeyAbilityHandler)
async createOneApiKey(
@Args() args: CreateOneApiKeyArgs,
@AuthWorkspace() { id: workspaceId }: Workspace,
- ): Promise {
+ ): Promise {
return await this.apiKeyService.generateApiKeyToken(
workspaceId,
args.data.name,
diff --git a/server/src/core/api-key/api-key.service.ts b/server/src/core/api-key/api-key.service.ts
index 2c0879ada..38e25ff49 100644
--- a/server/src/core/api-key/api-key.service.ts
+++ b/server/src/core/api-key/api-key.service.ts
@@ -5,7 +5,7 @@ import { addMilliseconds, addSeconds } from 'date-fns';
import ms from 'ms';
import { PrismaService } from 'src/database/prisma.service';
-import { AuthToken } from 'src/core/auth/dto/token.entity';
+import { ApiKeyToken } from 'src/core/auth/dto/token.entity';
import { assert } from 'src/utils/assert';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
@@ -28,7 +28,7 @@ export class ApiKeyService {
workspaceId: string,
name: string,
expiresAt?: Date | string,
- ): Promise {
+ ): Promise {
const secret = this.environmentService.getAccessTokenSecret();
let expiresIn: string | number;
let expirationDate: Date;
@@ -52,6 +52,7 @@ export class ApiKeyService {
},
});
return {
+ id,
token: this.jwtService.sign(jwtPayload, {
secret,
expiresIn,
diff --git a/server/src/core/auth/dto/token.entity.ts b/server/src/core/auth/dto/token.entity.ts
index a1b8b6f7a..2487a9f94 100644
--- a/server/src/core/auth/dto/token.entity.ts
+++ b/server/src/core/auth/dto/token.entity.ts
@@ -9,6 +9,18 @@ export class AuthToken {
expiresAt: Date;
}
+@ObjectType()
+export class ApiKeyToken {
+ @Field(() => String)
+ id: string;
+
+ @Field(() => String)
+ token: string;
+
+ @Field(() => Date)
+ expiresAt: Date;
+}
+
@ObjectType()
export class AuthTokenPair {
@Field(() => AuthToken)
diff --git a/server/src/core/core.module.ts b/server/src/core/core.module.ts
index fec8c726d..d73271367 100644
--- a/server/src/core/core.module.ts
+++ b/server/src/core/core.module.ts
@@ -1,5 +1,7 @@
import { Module } from '@nestjs/common';
+import { WebHookModule } from 'src/core/web-hook/web-hook.module';
+
import { UserModule } from './user/user.module';
import { CommentModule } from './comment/comment.module';
import { CompanyModule } from './company/company.module';
@@ -15,7 +17,6 @@ import { ActivityModule } from './activity/activity.module';
import { ViewModule } from './view/view.module';
import { FavoriteModule } from './favorite/favorite.module';
import { ApiKeyModule } from './api-key/api-key.module';
-import { HookModule } from './hook/hook.module';
@Module({
imports: [
@@ -34,7 +35,7 @@ import { HookModule } from './hook/hook.module';
ViewModule,
FavoriteModule,
ApiKeyModule,
- HookModule,
+ WebHookModule,
],
exports: [
AuthModule,
@@ -48,7 +49,7 @@ import { HookModule } from './hook/hook.module';
AttachmentModule,
FavoriteModule,
ApiKeyModule,
- HookModule,
+ WebHookModule,
],
})
export class CoreModule {}
diff --git a/server/src/core/hook/hook.module.ts b/server/src/core/web-hook/web-hook.module.ts
similarity index 62%
rename from server/src/core/hook/hook.module.ts
rename to server/src/core/web-hook/web-hook.module.ts
index c7ee4f69a..f54f86fb0 100644
--- a/server/src/core/hook/hook.module.ts
+++ b/server/src/core/web-hook/web-hook.module.ts
@@ -2,11 +2,10 @@ import { Module } from '@nestjs/common';
import { PrismaModule } from 'src/database/prisma.module';
import { AbilityModule } from 'src/ability/ability.module';
-
-import { HookResolver } from './hook.resolver';
+import { WebHookResolver } from 'src/core/web-hook/web-hook.resolver';
@Module({
imports: [PrismaModule, AbilityModule],
- providers: [HookResolver],
+ providers: [WebHookResolver],
})
-export class HookModule {}
+export class WebHookModule {}
diff --git a/server/src/core/hook/hook.resolver.ts b/server/src/core/web-hook/web-hook.resolver.ts
similarity index 52%
rename from server/src/core/hook/hook.resolver.ts
rename to server/src/core/web-hook/web-hook.resolver.ts
index 06c928776..0cf67a53a 100644
--- a/server/src/core/hook/hook.resolver.ts
+++ b/server/src/core/web-hook/web-hook.resolver.ts
@@ -4,35 +4,35 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { accessibleBy } from '@casl/prisma';
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
-import { Hook } from 'src/core/@generated/hook/hook.model';
import { AbilityGuard } from 'src/guards/ability.guard';
import { CheckAbilities } from 'src/decorators/check-abilities.decorator';
import {
- CreateHookAbilityHandler,
- DeleteHookAbilityHandler,
- ReadHookAbilityHandler,
-} from 'src/ability/handlers/hook.ability-handler';
-import { CreateOneHookArgs } from 'src/core/@generated/hook/create-one-hook.args';
+ CreateWebHookAbilityHandler,
+ DeleteWebHookAbilityHandler,
+ ReadWebHookAbilityHandler,
+} from 'src/ability/handlers/web-hook.ability-handler';
import { PrismaService } from 'src/database/prisma.service';
import { AuthWorkspace } from 'src/decorators/auth-workspace.decorator';
import { Workspace } from 'src/core/@generated/workspace/workspace.model';
-import { DeleteOneHookArgs } from 'src/core/@generated/hook/delete-one-hook.args';
-import { FindManyHookArgs } from 'src/core/@generated/hook/find-many-hook.args';
import { UserAbility } from 'src/decorators/user-ability.decorator';
import { AppAbility } from 'src/ability/ability.factory';
+import { CreateOneWebHookArgs } from 'src/core/@generated/web-hook/create-one-web-hook.args';
+import { DeleteOneWebHookArgs } from 'src/core/@generated/web-hook/delete-one-web-hook.args';
+import { FindManyWebHookArgs } from 'src/core/@generated/web-hook/find-many-web-hook.args';
+import { WebHook } from 'src/core/@generated/web-hook/web-hook.model';
@UseGuards(JwtAuthGuard)
-@Resolver(() => Hook)
-export class HookResolver {
+@Resolver(() => WebHook)
+export class WebHookResolver {
constructor(private readonly prismaService: PrismaService) {}
- @Mutation(() => Hook)
+ @Mutation(() => WebHook)
@UseGuards(AbilityGuard)
- @CheckAbilities(CreateHookAbilityHandler)
- async createOneHook(
- @Args() args: CreateOneHookArgs,
+ @CheckAbilities(CreateWebHookAbilityHandler)
+ async createOneWebHook(
+ @Args() args: CreateOneWebHookArgs,
@AuthWorkspace() { id: workspaceId }: Workspace,
- ): Promise {
- return this.prismaService.client.hook.create({
+ ): Promise {
+ return this.prismaService.client.webHook.create({
data: {
...args.data,
...{ workspace: { connect: { id: workspaceId } } },
@@ -40,31 +40,31 @@ export class HookResolver {
});
}
- @Mutation(() => Hook, { nullable: false })
+ @Mutation(() => WebHook, { nullable: false })
@UseGuards(AbilityGuard)
- @CheckAbilities(DeleteHookAbilityHandler)
- async deleteOneHook(@Args() args: DeleteOneHookArgs): Promise {
- const hookToDelete = this.prismaService.client.hook.findUnique({
+ @CheckAbilities(DeleteWebHookAbilityHandler)
+ async deleteOneWebHook(@Args() args: DeleteOneWebHookArgs): Promise {
+ const hookToDelete = this.prismaService.client.webHook.findUnique({
where: args.where,
});
if (!hookToDelete) {
throw new NotFoundException();
}
- return await this.prismaService.client.hook.delete({
+ return await this.prismaService.client.webHook.delete({
where: args.where,
});
}
- @Query(() => [Hook])
+ @Query(() => [WebHook])
@UseGuards(AbilityGuard)
- @CheckAbilities(ReadHookAbilityHandler)
- async findManyHook(
- @Args() args: FindManyHookArgs,
+ @CheckAbilities(ReadWebHookAbilityHandler)
+ async findManyWebHook(
+ @Args() args: FindManyWebHookArgs,
@UserAbility() ability: AppAbility,
) {
const filterOptions = [accessibleBy(ability).WorkspaceMember];
if (args.where) filterOptions.push(args.where);
- return this.prismaService.client.hook.findMany({
+ return this.prismaService.client.webHook.findMany({
...args,
where: { AND: filterOptions },
});
diff --git a/server/src/database/migrations/20231024123425_rename_hooks_table_to_web_hooks/migration.sql b/server/src/database/migrations/20231024123425_rename_hooks_table_to_web_hooks/migration.sql
new file mode 100644
index 000000000..43ef82be9
--- /dev/null
+++ b/server/src/database/migrations/20231024123425_rename_hooks_table_to_web_hooks/migration.sql
@@ -0,0 +1,27 @@
+/*
+ Warnings:
+
+ - You are about to drop the `hooks` table. If the table is not empty, all the data it contains will be lost.
+
+*/
+-- DropForeignKey
+ALTER TABLE "hooks" DROP CONSTRAINT "hooks_workspaceId_fkey";
+
+-- DropTable
+DROP TABLE "hooks";
+
+-- CreateTable
+CREATE TABLE "web_hooks" (
+ "id" TEXT NOT NULL,
+ "workspaceId" TEXT NOT NULL,
+ "targetUrl" TEXT NOT NULL,
+ "operation" TEXT NOT NULL,
+ "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ "updatedAt" TIMESTAMP(3) NOT NULL,
+ "deletedAt" TIMESTAMP(3),
+
+ CONSTRAINT "web_hooks_pkey" PRIMARY KEY ("id")
+);
+
+-- AddForeignKey
+ALTER TABLE "web_hooks" ADD CONSTRAINT "web_hooks_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "workspaces"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
diff --git a/server/src/database/schema.prisma b/server/src/database/schema.prisma
index 9084aee00..a2bae9b5f 100644
--- a/server/src/database/schema.prisma
+++ b/server/src/database/schema.prisma
@@ -179,7 +179,7 @@ model Workspace {
views View[]
viewSorts ViewSort[]
apiKeys ApiKey[]
- hooks Hook[]
+ webHooks WebHook[]
/// @TypeGraphQL.omit(input: true, output: true)
deletedAt DateTime?
@@ -910,7 +910,7 @@ model ApiKey {
@@map("api_keys")
}
-model Hook {
+model WebHook {
/// @Validator.IsString()
/// @Validator.IsOptional()
id String @id @default(uuid())
@@ -925,5 +925,5 @@ model Hook {
/// @TypeGraphQL.omit(input: true, output: true)
deletedAt DateTime?
- @@map("hooks")
+ @@map("web_hooks")
}
diff --git a/server/src/utils/prisma-select/model-select-map.ts b/server/src/utils/prisma-select/model-select-map.ts
index 11026d502..f187e7c9a 100644
--- a/server/src/utils/prisma-select/model-select-map.ts
+++ b/server/src/utils/prisma-select/model-select-map.ts
@@ -22,5 +22,5 @@ export type ModelSelectMap = {
ViewSort: Prisma.ViewSortSelect;
ViewField: Prisma.ViewFieldSelect;
ApiKey: Prisma.ApiKeySelect;
- Hook: Prisma.HookSelect;
+ WebHook: Prisma.WebHookSelect;
};