feat: add new ACTOR field type and createdBy standard fields (#6324)
This pull request introduces a new `FieldMetadataType` called `ACTOR`.
The primary objective of this new type is to add an extra column to the
following objects: `person`, `company`, `opportunity`, `note`, `task`,
and all custom objects.
This composite type contains three properties:
- `source`
```typescript
export enum FieldActorSource {
EMAIL = 'EMAIL',
CALENDAR = 'CALENDAR',
API = 'API',
IMPORT = 'IMPORT',
MANUAL = 'MANUAL',
}
```
- `workspaceMemberId`
- This property can be `undefined` in some cases and refers to the
member who created the record.
- `name`
- Serves as a fallback if the `workspaceMember` is deleted and is used
for other source types like `API`.
### Functionality
The pre-hook system has been updated to allow real-time argument
updates. When a record is created, a pre-hook can now compute and update
the arguments accordingly. This enhancement enables the `createdBy`
field to be populated with the correct values based on the
`authContext`.
The `authContext` now includes:
- An optional User entity
- An optional ApiKey entity
- The workspace entity
This provides access to the necessary data for the `createdBy` field.
In the GraphQL API, only the `source` can be specified in the
`createdBy` input. This allows the front-end to specify the source when
creating records from a CSV file.
### Front-End Handling
On the front-end, `orderBy` and `filter` are only applied to the name
property of the `ACTOR` composite type. Currently, we are unable to
apply these operations to the workspace member relation. This means that
if a workspace member changes their first name or last name, there may
be a mismatch because the name will differ from the new one. The name
displayed on the screen is based on the workspace member entity when
available.
### Missing Components
Currently, this PR does not include a `createdBy` value for the `MAIL`
and `CALENDAR` sources. These records are created in a job, and at
present, we only have access to the workspaceId within the job. To
address this, we should use a function similar to
`loadServiceWithContext`, which was recently removed from `TwentyORM`.
This function would allow us to pass the `authContext` to the jobs
without disrupting existing jobs.
Another PR will be created to handle these cases.
### Related Issues
Fixes issue #5155.
### Additional Notes
This PR doesn't include the migrations of the current records and views.
Everything works properly when the database is reset but this part is
still missing for now. We'll add that in another PR.
- There is a minor issue: front-end tests are broken since this commit:
[80c0fc7ff1).
---------
Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
4
.vscode/twenty.code-workspace
vendored
4
.vscode/twenty.code-workspace
vendored
@ -16,6 +16,10 @@
|
|||||||
"name": "packages/twenty-front",
|
"name": "packages/twenty-front",
|
||||||
"path": "../packages/twenty-front"
|
"path": "../packages/twenty-front"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "packages/twenty-ui",
|
||||||
|
"path": "../packages/twenty-ui"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "packages/twenty-postgres",
|
"name": "packages/twenty-postgres",
|
||||||
"path": "../packages/twenty-postgres"
|
"path": "../packages/twenty-postgres"
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import {
|
|||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { ApolloProvider } from '@/apollo/components/ApolloProvider';
|
import { ApolloProvider } from '@/apollo/components/ApolloProvider';
|
||||||
|
import { AuthProvider } from '@/auth/components/AuthProvider';
|
||||||
import { VerifyEffect } from '@/auth/components/VerifyEffect';
|
import { VerifyEffect } from '@/auth/components/VerifyEffect';
|
||||||
import { ChromeExtensionSidecarEffect } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect';
|
import { ChromeExtensionSidecarEffect } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect';
|
||||||
import { ChromeExtensionSidecarProvider } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider';
|
import { ChromeExtensionSidecarProvider } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider';
|
||||||
@ -99,28 +100,30 @@ const ProvidersThatNeedRouterContext = () => {
|
|||||||
<ChromeExtensionSidecarProvider>
|
<ChromeExtensionSidecarProvider>
|
||||||
<UserProviderEffect />
|
<UserProviderEffect />
|
||||||
<UserProvider>
|
<UserProvider>
|
||||||
<ApolloMetadataClientProvider>
|
<AuthProvider>
|
||||||
<ObjectMetadataItemsProvider>
|
<ApolloMetadataClientProvider>
|
||||||
<PrefetchDataProvider>
|
<ObjectMetadataItemsProvider>
|
||||||
<AppThemeProvider>
|
<PrefetchDataProvider>
|
||||||
<SnackBarProvider>
|
<AppThemeProvider>
|
||||||
<DialogManagerScope dialogManagerScopeId="dialog-manager">
|
<SnackBarProvider>
|
||||||
<DialogManager>
|
<DialogManagerScope dialogManagerScopeId="dialog-manager">
|
||||||
<StrictMode>
|
<DialogManager>
|
||||||
<PromiseRejectionEffect />
|
<StrictMode>
|
||||||
<CommandMenuEffect />
|
<PromiseRejectionEffect />
|
||||||
<GotoHotkeysEffect />
|
<CommandMenuEffect />
|
||||||
<PageTitle title={pageTitle} />
|
<GotoHotkeysEffect />
|
||||||
<Outlet />
|
<PageTitle title={pageTitle} />
|
||||||
</StrictMode>
|
<Outlet />
|
||||||
</DialogManager>
|
</StrictMode>
|
||||||
</DialogManagerScope>
|
</DialogManager>
|
||||||
</SnackBarProvider>
|
</DialogManagerScope>
|
||||||
</AppThemeProvider>
|
</SnackBarProvider>
|
||||||
</PrefetchDataProvider>
|
</AppThemeProvider>
|
||||||
<PageChangeEffect />
|
</PrefetchDataProvider>
|
||||||
</ObjectMetadataItemsProvider>
|
<PageChangeEffect />
|
||||||
</ApolloMetadataClientProvider>
|
</ObjectMetadataItemsProvider>
|
||||||
|
</ApolloMetadataClientProvider>
|
||||||
|
</AuthProvider>
|
||||||
</UserProvider>
|
</UserProvider>
|
||||||
</ChromeExtensionSidecarProvider>
|
</ChromeExtensionSidecarProvider>
|
||||||
</ClientConfigProvider>
|
</ClientConfigProvider>
|
||||||
|
|||||||
@ -346,6 +346,7 @@ export type FieldConnection = {
|
|||||||
export enum FieldMetadataType {
|
export enum FieldMetadataType {
|
||||||
Address = 'ADDRESS',
|
Address = 'ADDRESS',
|
||||||
Boolean = 'BOOLEAN',
|
Boolean = 'BOOLEAN',
|
||||||
|
Actor = 'ACTOR',
|
||||||
Currency = 'CURRENCY',
|
Currency = 'CURRENCY',
|
||||||
Date = 'DATE',
|
Date = 'DATE',
|
||||||
DateTime = 'DATE_TIME',
|
DateTime = 'DATE_TIME',
|
||||||
@ -1303,6 +1304,7 @@ export type User = {
|
|||||||
updatedAt: Scalars['DateTime']['output'];
|
updatedAt: Scalars['DateTime']['output'];
|
||||||
userVars: Scalars['JSONObject']['output'];
|
userVars: Scalars['JSONObject']['output'];
|
||||||
workspaceMember?: Maybe<WorkspaceMember>;
|
workspaceMember?: Maybe<WorkspaceMember>;
|
||||||
|
workspaceMembers?: Maybe<Array<WorkspaceMember>>;
|
||||||
workspaces: Array<UserWorkspace>;
|
workspaces: Array<UserWorkspace>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { gql } from '@apollo/client';
|
|
||||||
import * as Apollo from '@apollo/client';
|
import * as Apollo from '@apollo/client';
|
||||||
|
import { gql } from '@apollo/client';
|
||||||
export type Maybe<T> = T | null;
|
export type Maybe<T> = T | null;
|
||||||
export type InputMaybe<T> = Maybe<T>;
|
export type InputMaybe<T> = Maybe<T>;
|
||||||
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
|
||||||
@ -251,6 +251,7 @@ export type FieldConnection = {
|
|||||||
export enum FieldMetadataType {
|
export enum FieldMetadataType {
|
||||||
Address = 'ADDRESS',
|
Address = 'ADDRESS',
|
||||||
Boolean = 'BOOLEAN',
|
Boolean = 'BOOLEAN',
|
||||||
|
Actor = 'ACTOR',
|
||||||
Currency = 'CURRENCY',
|
Currency = 'CURRENCY',
|
||||||
Date = 'DATE',
|
Date = 'DATE',
|
||||||
DateTime = 'DATE_TIME',
|
DateTime = 'DATE_TIME',
|
||||||
@ -1000,6 +1001,7 @@ export type User = {
|
|||||||
updatedAt: Scalars['DateTime'];
|
updatedAt: Scalars['DateTime'];
|
||||||
userVars: Scalars['JSONObject'];
|
userVars: Scalars['JSONObject'];
|
||||||
workspaceMember?: Maybe<WorkspaceMember>;
|
workspaceMember?: Maybe<WorkspaceMember>;
|
||||||
|
workspaceMembers?: Maybe<Array<WorkspaceMember>>;
|
||||||
workspaces: Array<UserWorkspace>;
|
workspaces: Array<UserWorkspace>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1350,7 +1352,7 @@ export type ImpersonateMutationVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
||||||
|
|
||||||
export type RenewTokenMutationVariables = Exact<{
|
export type RenewTokenMutationVariables = Exact<{
|
||||||
appToken: Scalars['String'];
|
appToken: Scalars['String'];
|
||||||
@ -1382,7 +1384,7 @@ export type VerifyMutationVariables = Exact<{
|
|||||||
}>;
|
}>;
|
||||||
|
|
||||||
|
|
||||||
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
|
||||||
|
|
||||||
export type CheckUserExistsQueryVariables = Exact<{
|
export type CheckUserExistsQueryVariables = Exact<{
|
||||||
email: Scalars['String'];
|
email: Scalars['String'];
|
||||||
@ -1443,7 +1445,7 @@ export type GetAisqlQueryQueryVariables = Exact<{
|
|||||||
|
|
||||||
export type GetAisqlQueryQuery = { __typename?: 'Query', getAISQLQuery: { __typename?: 'AISQLQueryResult', sqlQuery: string, sqlQueryResult?: string | null, queryFailedErrorMessage?: string | null } };
|
export type GetAisqlQueryQuery = { __typename?: 'Query', getAISQLQuery: { __typename?: 'AISQLQueryResult', sqlQuery: string, sqlQueryResult?: string | null, queryFailedErrorMessage?: string | null } };
|
||||||
|
|
||||||
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
|
export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
|
||||||
|
|
||||||
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
@ -1460,7 +1462,9 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
|
|||||||
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
|
||||||
|
|
||||||
|
|
||||||
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } };
|
export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, workspaceMembers?: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: WorkspaceActivationStatus, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } };
|
||||||
|
|
||||||
|
export type WorkspaceMemberQueryFragmentFragment = { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } };
|
||||||
|
|
||||||
export type AddUserToWorkspaceMutationVariables = Exact<{
|
export type AddUserToWorkspaceMutationVariables = Exact<{
|
||||||
inviteHash: Scalars['String'];
|
inviteHash: Scalars['String'];
|
||||||
@ -1596,6 +1600,21 @@ export const AuthTokensFragmentFragmentDoc = gql`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
${AuthTokenFragmentFragmentDoc}`;
|
${AuthTokenFragmentFragmentDoc}`;
|
||||||
|
export const WorkspaceMemberQueryFragmentFragmentDoc = gql`
|
||||||
|
fragment WorkspaceMemberQueryFragment on WorkspaceMember {
|
||||||
|
id
|
||||||
|
name {
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
}
|
||||||
|
colorScheme
|
||||||
|
avatarUrl
|
||||||
|
locale
|
||||||
|
timeZone
|
||||||
|
dateFormat
|
||||||
|
timeFormat
|
||||||
|
}
|
||||||
|
`;
|
||||||
export const UserQueryFragmentFragmentDoc = gql`
|
export const UserQueryFragmentFragmentDoc = gql`
|
||||||
fragment UserQueryFragment on User {
|
fragment UserQueryFragment on User {
|
||||||
id
|
id
|
||||||
@ -1606,17 +1625,10 @@ export const UserQueryFragmentFragmentDoc = gql`
|
|||||||
supportUserHash
|
supportUserHash
|
||||||
onboardingStatus
|
onboardingStatus
|
||||||
workspaceMember {
|
workspaceMember {
|
||||||
id
|
...WorkspaceMemberQueryFragment
|
||||||
name {
|
}
|
||||||
firstName
|
workspaceMembers {
|
||||||
lastName
|
...WorkspaceMemberQueryFragment
|
||||||
}
|
|
||||||
colorScheme
|
|
||||||
avatarUrl
|
|
||||||
locale
|
|
||||||
timeZone
|
|
||||||
dateFormat
|
|
||||||
timeFormat
|
|
||||||
}
|
}
|
||||||
defaultWorkspace {
|
defaultWorkspace {
|
||||||
id
|
id
|
||||||
@ -1650,7 +1662,7 @@ export const UserQueryFragmentFragmentDoc = gql`
|
|||||||
}
|
}
|
||||||
userVars
|
userVars
|
||||||
}
|
}
|
||||||
`;
|
${WorkspaceMemberQueryFragmentFragmentDoc}`;
|
||||||
export const GetTimelineCalendarEventsFromCompanyIdDocument = gql`
|
export const GetTimelineCalendarEventsFromCompanyIdDocument = gql`
|
||||||
query GetTimelineCalendarEventsFromCompanyId($companyId: UUID!, $page: Int!, $pageSize: Int!) {
|
query GetTimelineCalendarEventsFromCompanyId($companyId: UUID!, $page: Int!, $pageSize: Int!) {
|
||||||
getTimelineCalendarEventsFromCompanyId(
|
getTimelineCalendarEventsFromCompanyId(
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { filterOutInvalidTimelineActivities } from '@/activities/timelineActivit
|
|||||||
import { groupEventsByMonth } from '@/activities/timelineActivities/utils/groupEventsByMonth';
|
import { groupEventsByMonth } from '@/activities/timelineActivities/utils/groupEventsByMonth';
|
||||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||||
|
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||||
|
|
||||||
type EventListProps = {
|
type EventListProps = {
|
||||||
@ -34,9 +35,12 @@ export const EventList = ({ events, targetableObject }: EventListProps) => {
|
|||||||
objectNameSingular: targetableObject.targetObjectNameSingular,
|
objectNameSingular: targetableObject.targetObjectNameSingular,
|
||||||
}).objectMetadataItem;
|
}).objectMetadataItem;
|
||||||
|
|
||||||
|
const { objectMetadataItems } = useObjectMetadataItems();
|
||||||
|
|
||||||
const filteredEvents = filterOutInvalidTimelineActivities(
|
const filteredEvents = filterOutInvalidTimelineActivities(
|
||||||
events,
|
events,
|
||||||
mainObjectMetadataItem,
|
targetableObject.targetObjectNameSingular,
|
||||||
|
objectMetadataItems,
|
||||||
);
|
);
|
||||||
|
|
||||||
const groupedEvents = groupEventsByMonth(filteredEvents);
|
const groupedEvents = groupEventsByMonth(filteredEvents);
|
||||||
|
|||||||
@ -22,7 +22,9 @@ export const EventRowActivity = ({
|
|||||||
authorFullName,
|
authorFullName,
|
||||||
objectNameSingular,
|
objectNameSingular,
|
||||||
}: EventRowActivityProps & { objectNameSingular: CoreObjectNameSingular }) => {
|
}: EventRowActivityProps & { objectNameSingular: CoreObjectNameSingular }) => {
|
||||||
const [, eventAction] = event.name.split('.');
|
const [eventLinkedObject, eventAction] = event.name.split('.');
|
||||||
|
|
||||||
|
const eventObject = eventLinkedObject.replace('linked-', '');
|
||||||
|
|
||||||
if (!event.linkedRecordId) {
|
if (!event.linkedRecordId) {
|
||||||
throw new Error('Could not find linked record id for event');
|
throw new Error('Could not find linked record id for event');
|
||||||
@ -39,7 +41,9 @@ export const EventRowActivity = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
|
<StyledEventRowItemColumn>{authorFullName}</StyledEventRowItemColumn>
|
||||||
<StyledEventRowItemAction>{eventAction}</StyledEventRowItemAction>
|
<StyledEventRowItemAction>
|
||||||
|
{`${eventAction} a related ${eventObject}`}
|
||||||
|
</StyledEventRowItemAction>
|
||||||
{activityInStore ? (
|
{activityInStore ? (
|
||||||
<StyledLinkedActivity
|
<StyledLinkedActivity
|
||||||
onClick={() => openActivityRightDrawer(event.linkedRecordId)}
|
onClick={() => openActivityRightDrawer(event.linkedRecordId)}
|
||||||
|
|||||||
@ -1,14 +1,34 @@
|
|||||||
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity';
|
||||||
|
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
|
|
||||||
export const filterOutInvalidTimelineActivities = (
|
export const filterOutInvalidTimelineActivities = (
|
||||||
timelineActivities: TimelineActivity[],
|
timelineActivities: TimelineActivity[],
|
||||||
mainObjectMetadataItem: ObjectMetadataItem,
|
mainObjectSingularName: string,
|
||||||
|
objectMetadataItems: ObjectMetadataItem[],
|
||||||
): TimelineActivity[] => {
|
): TimelineActivity[] => {
|
||||||
|
const mainObjectMetadataItem = objectMetadataItems.find(
|
||||||
|
(objectMetadataItem) =>
|
||||||
|
objectMetadataItem.nameSingular === mainObjectSingularName,
|
||||||
|
);
|
||||||
|
|
||||||
|
const noteObjectMetadataItem = objectMetadataItems.find(
|
||||||
|
(objectMetadataItem) =>
|
||||||
|
objectMetadataItem.nameSingular === CoreObjectNameSingular.Note,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!mainObjectMetadataItem || !noteObjectMetadataItem) {
|
||||||
|
throw new Error('Object metadata items not found');
|
||||||
|
}
|
||||||
|
|
||||||
const fieldMetadataItemMap = new Map(
|
const fieldMetadataItemMap = new Map(
|
||||||
mainObjectMetadataItem.fields.map((field) => [field.name, field]),
|
mainObjectMetadataItem.fields.map((field) => [field.name, field]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const noteFieldMetadataItemMap = new Map(
|
||||||
|
noteObjectMetadataItem.fields.map((field) => [field.name, field]),
|
||||||
|
);
|
||||||
|
|
||||||
return timelineActivities.filter((timelineActivity) => {
|
return timelineActivities.filter((timelineActivity) => {
|
||||||
const diff = timelineActivity.properties?.diff;
|
const diff = timelineActivity.properties?.diff;
|
||||||
const canSkipValidation = !diff;
|
const canSkipValidation = !diff;
|
||||||
@ -17,8 +37,14 @@ export const filterOutInvalidTimelineActivities = (
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isNoteOrTask =
|
||||||
|
timelineActivity.name.startsWith('linked-note') ||
|
||||||
|
timelineActivity.name.startsWith('linked-task');
|
||||||
|
|
||||||
const validDiffEntries = Object.entries(diff).filter(([diffKey]) =>
|
const validDiffEntries = Object.entries(diff).filter(([diffKey]) =>
|
||||||
fieldMetadataItemMap.has(diffKey),
|
isNoteOrTask
|
||||||
|
? noteFieldMetadataItemMap.has(diffKey)
|
||||||
|
: fieldMetadataItemMap.has(diffKey),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (validDiffEntries.length === 0) {
|
if (validDiffEntries.length === 0) {
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
|
import { AuthContext } from '@/auth/contexts/AuthContext';
|
||||||
|
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
|
||||||
|
|
||||||
|
export const AuthProvider = ({ children }: React.PropsWithChildren) => {
|
||||||
|
const currentWorkspaceMembers = useRecoilValue(currentWorkspaceMembersState);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AuthContext.Provider
|
||||||
|
value={{
|
||||||
|
currentWorkspaceMembers,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</AuthContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
|
import { createContext } from 'react';
|
||||||
|
|
||||||
|
export type AuthContextType = {
|
||||||
|
currentWorkspaceMembers: CurrentWorkspaceMember[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AuthContext = createContext<AuthContextType>(
|
||||||
|
{} as AuthContextType,
|
||||||
|
);
|
||||||
@ -32,6 +32,7 @@ import {
|
|||||||
} from '~/generated/graphql';
|
} from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
|
||||||
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
|
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
|
||||||
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
|
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
|
||||||
import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
|
import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
|
||||||
@ -47,6 +48,9 @@ export const useAuth = () => {
|
|||||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||||
currentWorkspaceMemberState,
|
currentWorkspaceMemberState,
|
||||||
);
|
);
|
||||||
|
const setCurrentWorkspaceMembers = useSetRecoilState(
|
||||||
|
currentWorkspaceMembersState,
|
||||||
|
);
|
||||||
|
|
||||||
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
||||||
const setIsVerifyPendingState = useSetRecoilState(isVerifyPendingState);
|
const setIsVerifyPendingState = useSetRecoilState(isVerifyPendingState);
|
||||||
@ -109,6 +113,18 @@ export const useAuth = () => {
|
|||||||
|
|
||||||
setCurrentUser(user);
|
setCurrentUser(user);
|
||||||
|
|
||||||
|
if (isDefined(user.workspaceMembers)) {
|
||||||
|
const workspaceMembers = user.workspaceMembers.map(
|
||||||
|
(workspaceMember) => ({
|
||||||
|
...workspaceMember,
|
||||||
|
colorScheme: workspaceMember.colorScheme as ColorScheme,
|
||||||
|
locale: workspaceMember.locale ?? 'en',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
setCurrentWorkspaceMembers(workspaceMembers);
|
||||||
|
}
|
||||||
|
|
||||||
if (isDefined(user.workspaceMember)) {
|
if (isDefined(user.workspaceMember)) {
|
||||||
workspaceMember = {
|
workspaceMember = {
|
||||||
...user.workspaceMember,
|
...user.workspaceMember,
|
||||||
@ -163,10 +179,11 @@ export const useAuth = () => {
|
|||||||
verify,
|
verify,
|
||||||
setTokenPair,
|
setTokenPair,
|
||||||
setCurrentUser,
|
setCurrentUser,
|
||||||
setCurrentWorkspaceMember,
|
|
||||||
setCurrentWorkspace,
|
setCurrentWorkspace,
|
||||||
setWorkspaces,
|
setCurrentWorkspaceMembers,
|
||||||
|
setCurrentWorkspaceMember,
|
||||||
setDateTimeFormat,
|
setDateTimeFormat,
|
||||||
|
setWorkspaces,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
|
import { createState } from 'twenty-ui';
|
||||||
|
|
||||||
|
export const currentWorkspaceMembersState = createState<
|
||||||
|
CurrentWorkspaceMember[]
|
||||||
|
>({
|
||||||
|
key: 'currentWorkspaceMembersState',
|
||||||
|
defaultValue: [],
|
||||||
|
});
|
||||||
@ -1,6 +1,6 @@
|
|||||||
|
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
||||||
import { useRecoilTransactionObserver_UNSTABLE, useRecoilValue } from 'recoil';
|
import { useRecoilTransactionObserver_UNSTABLE, useRecoilValue } from 'recoil';
|
||||||
|
|
||||||
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
|
|
||||||
import { logDebug } from '~/utils/logDebug';
|
import { logDebug } from '~/utils/logDebug';
|
||||||
|
|
||||||
const formatTitle = (stateName: string) => {
|
const formatTitle = (stateName: string) => {
|
||||||
|
|||||||
@ -12,5 +12,6 @@ export const SORTABLE_FIELD_METADATA_TYPES = [
|
|||||||
FieldMetadataType.FullName,
|
FieldMetadataType.FullName,
|
||||||
FieldMetadataType.Rating,
|
FieldMetadataType.Rating,
|
||||||
FieldMetadataType.Currency,
|
FieldMetadataType.Currency,
|
||||||
|
FieldMetadataType.Actor,
|
||||||
FieldMetadataType.Links,
|
FieldMetadataType.Links,
|
||||||
];
|
];
|
||||||
|
|||||||
@ -35,6 +35,7 @@ export const formatFieldMetadataItemsAsFilterDefinitions = ({
|
|||||||
FieldMetadataType.Select,
|
FieldMetadataType.Select,
|
||||||
FieldMetadataType.Currency,
|
FieldMetadataType.Currency,
|
||||||
FieldMetadataType.Rating,
|
FieldMetadataType.Rating,
|
||||||
|
FieldMetadataType.Actor,
|
||||||
].includes(field.type)
|
].includes(field.type)
|
||||||
) {
|
) {
|
||||||
return acc;
|
return acc;
|
||||||
@ -88,6 +89,8 @@ export const getFilterTypeFromFieldType = (fieldType: FieldMetadataType) => {
|
|||||||
return 'ADDRESS';
|
return 'ADDRESS';
|
||||||
case FieldMetadataType.Rating:
|
case FieldMetadataType.Rating:
|
||||||
return 'RATING';
|
return 'RATING';
|
||||||
|
case FieldMetadataType.Actor:
|
||||||
|
return 'ACTOR';
|
||||||
default:
|
default:
|
||||||
return 'TEXT';
|
return 'TEXT';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,14 @@ export const getOrderByForFieldMetadataType = (
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
case FieldMetadataType.Actor:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
[field.name]: {
|
||||||
|
name: direction ?? 'AscNullsLast',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
case FieldMetadataType.Links:
|
case FieldMetadataType.Links:
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@ -147,5 +147,14 @@ ${mapObjectMetadataToGraphQLQuery({
|
|||||||
}`;
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (fieldType === FieldMetadataType.Actor) {
|
||||||
|
return `${field.name}
|
||||||
|
{
|
||||||
|
source
|
||||||
|
workspaceMemberId
|
||||||
|
name
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
return '';
|
return '';
|
||||||
};
|
};
|
||||||
|
|||||||
@ -90,6 +90,10 @@ export type LinksFilter = {
|
|||||||
primaryLinkLabel?: StringFilter;
|
primaryLinkLabel?: StringFilter;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ActorFilter = {
|
||||||
|
name?: StringFilter;
|
||||||
|
};
|
||||||
|
|
||||||
export type LeafFilter =
|
export type LeafFilter =
|
||||||
| UUIDFilter
|
| UUIDFilter
|
||||||
| StringFilter
|
| StringFilter
|
||||||
@ -101,6 +105,7 @@ export type LeafFilter =
|
|||||||
| BooleanFilter
|
| BooleanFilter
|
||||||
| AddressFilter
|
| AddressFilter
|
||||||
| LinksFilter
|
| LinksFilter
|
||||||
|
| ActorFilter
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
export type AndObjectRecordFilter = {
|
export type AndObjectRecordFilter = {
|
||||||
|
|||||||
@ -65,6 +65,7 @@ export const MultipleFiltersDropdownContent = ({
|
|||||||
'LINK',
|
'LINK',
|
||||||
'LINKS',
|
'LINKS',
|
||||||
'ADDRESS',
|
'ADDRESS',
|
||||||
|
'ACTOR',
|
||||||
].includes(filterDefinitionUsedInDropdown.type) && (
|
].includes(filterDefinitionUsedInDropdown.type) && (
|
||||||
<ObjectFilterDropdownTextSearchInput />
|
<ObjectFilterDropdownTextSearchInput />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -13,4 +13,5 @@ export type FilterType =
|
|||||||
| 'ADDRESS'
|
| 'ADDRESS'
|
||||||
| 'SELECT'
|
| 'SELECT'
|
||||||
| 'RATING'
|
| 'RATING'
|
||||||
| 'MULTI_SELECT';
|
| 'MULTI_SELECT'
|
||||||
|
| 'ACTOR';
|
||||||
|
|||||||
@ -28,6 +28,7 @@ describe('getOperandsForFilterType', () => {
|
|||||||
['ADDRESS', [...containsOperands, ...emptyOperands]],
|
['ADDRESS', [...containsOperands, ...emptyOperands]],
|
||||||
['LINK', [...containsOperands, ...emptyOperands]],
|
['LINK', [...containsOperands, ...emptyOperands]],
|
||||||
['LINKS', [...containsOperands, ...emptyOperands]],
|
['LINKS', [...containsOperands, ...emptyOperands]],
|
||||||
|
['ACTOR', [...containsOperands, ...emptyOperands]],
|
||||||
['CURRENCY', [...numberOperands, ...emptyOperands]],
|
['CURRENCY', [...numberOperands, ...emptyOperands]],
|
||||||
['NUMBER', [...numberOperands, ...emptyOperands]],
|
['NUMBER', [...numberOperands, ...emptyOperands]],
|
||||||
['DATE_TIME', [...numberOperands, ...emptyOperands]],
|
['DATE_TIME', [...numberOperands, ...emptyOperands]],
|
||||||
|
|||||||
@ -20,6 +20,7 @@ export const getOperandsForFilterType = (
|
|||||||
case 'PHONE':
|
case 'PHONE':
|
||||||
case 'LINK':
|
case 'LINK':
|
||||||
case 'LINKS':
|
case 'LINKS':
|
||||||
|
case 'ACTOR':
|
||||||
return [
|
return [
|
||||||
ViewFilterOperand.Contains,
|
ViewFilterOperand.Contains,
|
||||||
ViewFilterOperand.DoesNotContain,
|
ViewFilterOperand.DoesNotContain,
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
|
||||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||||
import {
|
import {
|
||||||
|
FieldActorMetadata,
|
||||||
FieldFullNameMetadata,
|
FieldFullNameMetadata,
|
||||||
FieldLinkMetadata,
|
FieldLinkMetadata,
|
||||||
FieldRatingMetadata,
|
FieldRatingMetadata,
|
||||||
FieldSelectMetadata,
|
FieldSelectMetadata,
|
||||||
FieldTextMetadata,
|
FieldTextMetadata
|
||||||
} from '@/object-record/record-field/types/FieldMetadata';
|
} from '@/object-record/record-field/types/FieldMetadata';
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import {
|
import {
|
||||||
@ -79,7 +80,7 @@ export const phoneFieldDefinition = formatFieldMetadataItemAsFieldDefinition({
|
|||||||
objectMetadataItem: mockedPersonObjectMetadataItem,
|
objectMetadataItem: mockedPersonObjectMetadataItem,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ratingfieldDefinition: FieldDefinition<FieldRatingMetadata> = {
|
export const ratingFieldDefinition: FieldDefinition<FieldRatingMetadata> = {
|
||||||
fieldMetadataId,
|
fieldMetadataId,
|
||||||
label: 'Rating',
|
label: 'Rating',
|
||||||
iconName: 'iconName',
|
iconName: 'iconName',
|
||||||
@ -97,3 +98,14 @@ export const booleanFieldDefinition = formatFieldMetadataItemAsFieldDefinition({
|
|||||||
field: booleanFieldMetadataItem!,
|
field: booleanFieldMetadataItem!,
|
||||||
objectMetadataItem: mockedCompanyObjectMetadataItem,
|
objectMetadataItem: mockedCompanyObjectMetadataItem,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const actorFieldDefinition: FieldDefinition<FieldActorMetadata> = {
|
||||||
|
fieldMetadataId,
|
||||||
|
label: 'Created By',
|
||||||
|
iconName: 'restart',
|
||||||
|
type: FieldMetadataType.Actor,
|
||||||
|
defaultValue: { source: 'MANUAL', name: '' },
|
||||||
|
metadata: {
|
||||||
|
fieldName: 'actor',
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -1,11 +1,13 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
import { ActorFieldDisplay } from '@/object-record/record-field/meta-types/display/components/ActorFieldDisplay';
|
||||||
import { BooleanFieldDisplay } from '@/object-record/record-field/meta-types/display/components/BooleanFieldDisplay';
|
import { BooleanFieldDisplay } from '@/object-record/record-field/meta-types/display/components/BooleanFieldDisplay';
|
||||||
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
|
import { LinksFieldDisplay } from '@/object-record/record-field/meta-types/display/components/LinksFieldDisplay';
|
||||||
import { RatingFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RatingFieldDisplay';
|
import { RatingFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RatingFieldDisplay';
|
||||||
import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay';
|
import { RelationFromManyFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RelationFromManyFieldDisplay';
|
||||||
import { RichTextFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RichTextFieldDisplay';
|
import { RichTextFieldDisplay } from '@/object-record/record-field/meta-types/display/components/RichTextFieldDisplay';
|
||||||
import { isFieldIdentifierDisplay } from '@/object-record/record-field/meta-types/display/utils/isFieldIdentifierDisplay';
|
import { isFieldIdentifierDisplay } from '@/object-record/record-field/meta-types/display/utils/isFieldIdentifierDisplay';
|
||||||
|
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
|
||||||
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
||||||
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
import { isFieldDisplayedAsPhone } from '@/object-record/record-field/types/guards/isFieldDisplayedAsPhone';
|
||||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||||
@ -96,5 +98,7 @@ export const FieldDisplay = () => {
|
|||||||
<RatingFieldDisplay />
|
<RatingFieldDisplay />
|
||||||
) : isFieldRichText(fieldDefinition) ? (
|
) : isFieldRichText(fieldDefinition) ? (
|
||||||
<RichTextFieldDisplay />
|
<RichTextFieldDisplay />
|
||||||
|
) : isFieldActor(fieldDefinition) ? (
|
||||||
|
<ActorFieldDisplay />
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,50 @@
|
|||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
import { ReactNode } from 'react';
|
||||||
|
import { RecoilRoot } from 'recoil';
|
||||||
|
|
||||||
|
import {
|
||||||
|
actorFieldDefinition,
|
||||||
|
phoneFieldDefinition,
|
||||||
|
} from '@/object-record/record-field/__mocks__/fieldDefinitions';
|
||||||
|
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
|
||||||
|
import { useIsFieldReadOnly } from '@/object-record/record-field/hooks/useIsFieldReadOnly';
|
||||||
|
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||||
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
|
||||||
|
const entityId = 'entityId';
|
||||||
|
|
||||||
|
const getWrapper =
|
||||||
|
(fieldDefinition: FieldDefinition<FieldMetadata>) =>
|
||||||
|
({ children }: { children: ReactNode }) => (
|
||||||
|
<FieldContext.Provider
|
||||||
|
value={{
|
||||||
|
fieldDefinition,
|
||||||
|
entityId,
|
||||||
|
hotkeyScope: 'hotkeyScope',
|
||||||
|
isLabelIdentifier: false,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<RecoilRoot>{children}</RecoilRoot>
|
||||||
|
</FieldContext.Provider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const ActorWrapper = getWrapper(actorFieldDefinition);
|
||||||
|
const PhoneWrapper = getWrapper(phoneFieldDefinition);
|
||||||
|
|
||||||
|
describe('useIsFieldReadOnly', () => {
|
||||||
|
it('should return true', () => {
|
||||||
|
const { result } = renderHook(() => useIsFieldReadOnly(), {
|
||||||
|
wrapper: ActorWrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false', () => {
|
||||||
|
const { result } = renderHook(() => useIsFieldReadOnly(), {
|
||||||
|
wrapper: PhoneWrapper,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.current).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,14 +1,16 @@
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
|
|
||||||
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
|
||||||
|
import { isFieldRichText } from '@/object-record/record-field/types/guards/isFieldRichText';
|
||||||
import { FieldContext } from '../contexts/FieldContext';
|
import { FieldContext } from '../contexts/FieldContext';
|
||||||
|
|
||||||
export const useIsFieldReadOnly = () => {
|
export const useIsFieldReadOnly = () => {
|
||||||
const { fieldDefinition } = useContext(FieldContext);
|
const { fieldDefinition } = useContext(FieldContext);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
fieldDefinition.type === FieldMetadataType.RichText ||
|
|
||||||
fieldDefinition.metadata.fieldName === 'noteTargets' ||
|
fieldDefinition.metadata.fieldName === 'noteTargets' ||
|
||||||
fieldDefinition.metadata.fieldName === 'taskTargets' // TODO: do something cleaner
|
fieldDefinition.metadata.fieldName === 'taskTargets' ||
|
||||||
|
isFieldActor(fieldDefinition) ||
|
||||||
|
isFieldRichText(fieldDefinition)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,25 @@
|
|||||||
|
import { useActorFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useActorFieldDisplay';
|
||||||
|
import { ActorDisplay } from '@/ui/field/display/components/ActorDisplay';
|
||||||
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
|
||||||
|
export const ActorFieldDisplay = () => {
|
||||||
|
const { fieldValue } = useActorFieldDisplay();
|
||||||
|
|
||||||
|
const name = !fieldValue.workspaceMemberId
|
||||||
|
? fieldValue.name
|
||||||
|
: [
|
||||||
|
fieldValue.workspaceMember?.name.firstName,
|
||||||
|
fieldValue.workspaceMember?.name.lastName,
|
||||||
|
]
|
||||||
|
.filter(isNonEmptyString)
|
||||||
|
.join(' ');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActorDisplay
|
||||||
|
name={name}
|
||||||
|
source={fieldValue.source}
|
||||||
|
avatarUrl={fieldValue.workspaceMember?.avatarUrl}
|
||||||
|
workspaceMemberId={fieldValue.workspaceMemberId}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
import { ActorFieldDisplay } from '@/object-record/record-field/meta-types/display/components/ActorFieldDisplay';
|
||||||
|
import { FieldActorValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { ComponentDecorator } from 'twenty-ui';
|
||||||
|
|
||||||
|
import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDecorator';
|
||||||
|
import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator';
|
||||||
|
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
|
||||||
|
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
|
||||||
|
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'UI/Data/Field/Display/ActorFieldDisplay',
|
||||||
|
decorators: [
|
||||||
|
MemoryRouterDecorator,
|
||||||
|
ChipGeneratorsDecorator,
|
||||||
|
getFieldDecorator('person', 'actor', {
|
||||||
|
name: 'John Doe',
|
||||||
|
source: 'API',
|
||||||
|
workspaceMemberId: undefined,
|
||||||
|
} satisfies FieldActorValue),
|
||||||
|
ComponentDecorator,
|
||||||
|
],
|
||||||
|
component: ActorFieldDisplay,
|
||||||
|
args: {},
|
||||||
|
parameters: {
|
||||||
|
chromatic: { disableSnapshot: true },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof ActorFieldDisplay>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
export const Performance = getProfilingStory({
|
||||||
|
componentName: 'ActorFieldDisplay',
|
||||||
|
averageThresholdInMs: 0.2,
|
||||||
|
numberOfRuns: 20,
|
||||||
|
numberOfTestsPerRun: 100,
|
||||||
|
});
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
import { useContext } from 'react';
|
||||||
|
|
||||||
|
import { FieldActorValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||||
|
|
||||||
|
import { AuthContext } from '@/auth/contexts/AuthContext';
|
||||||
|
import { FieldContext } from '../../contexts/FieldContext';
|
||||||
|
|
||||||
|
export const useActorFieldDisplay = () => {
|
||||||
|
const { entityId, fieldDefinition } = useContext(FieldContext);
|
||||||
|
|
||||||
|
const { currentWorkspaceMembers } = useContext(AuthContext);
|
||||||
|
|
||||||
|
const fieldName = fieldDefinition.metadata.fieldName;
|
||||||
|
|
||||||
|
const fieldValue = useRecordFieldValue<FieldActorValue | undefined>(
|
||||||
|
entityId,
|
||||||
|
fieldName,
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
fieldDefinition,
|
||||||
|
fieldValue: {
|
||||||
|
...fieldValue,
|
||||||
|
workspaceMember: currentWorkspaceMembers?.find(
|
||||||
|
(member) => member.id === fieldValue?.workspaceMemberId,
|
||||||
|
),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -8,6 +8,7 @@ import { isFieldText } from '@/object-record/record-field/types/guards/isFieldTe
|
|||||||
import { useRecordValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
import { useRecordValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
|
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
|
||||||
import { FieldContext } from '../../contexts/FieldContext';
|
import { FieldContext } from '../../contexts/FieldContext';
|
||||||
|
|
||||||
export const useChipFieldDisplay = () => {
|
export const useChipFieldDisplay = () => {
|
||||||
@ -25,7 +26,8 @@ export const useChipFieldDisplay = () => {
|
|||||||
const objectNameSingular =
|
const objectNameSingular =
|
||||||
isFieldText(fieldDefinition) ||
|
isFieldText(fieldDefinition) ||
|
||||||
isFieldFullName(fieldDefinition) ||
|
isFieldFullName(fieldDefinition) ||
|
||||||
isFieldNumber(fieldDefinition)
|
isFieldNumber(fieldDefinition) ||
|
||||||
|
isFieldActor(fieldDefinition)
|
||||||
? fieldDefinition.metadata.objectMetadataNameSingular
|
? fieldDefinition.metadata.objectMetadataNameSingular
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
|
import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode';
|
||||||
import {
|
import {
|
||||||
|
FieldActorValue,
|
||||||
FieldAddressValue,
|
FieldAddressValue,
|
||||||
FieldBooleanValue,
|
FieldBooleanValue,
|
||||||
FieldCurrencyValue,
|
FieldCurrencyValue,
|
||||||
@ -51,6 +52,11 @@ export type FieldAddressDraftValue = {
|
|||||||
addressLng: number | null;
|
addressLng: number | null;
|
||||||
};
|
};
|
||||||
export type FieldJsonDraftValue = string;
|
export type FieldJsonDraftValue = string;
|
||||||
|
export type FieldActorDraftValue = {
|
||||||
|
source: string;
|
||||||
|
workspaceMemberId?: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type FieldInputDraftValue<FieldValue> = FieldValue extends FieldTextValue
|
export type FieldInputDraftValue<FieldValue> = FieldValue extends FieldTextValue
|
||||||
? FieldTextDraftValue
|
? FieldTextDraftValue
|
||||||
@ -88,4 +94,6 @@ export type FieldInputDraftValue<FieldValue> = FieldValue extends FieldTextValue
|
|||||||
? FieldAddressDraftValue
|
? FieldAddressDraftValue
|
||||||
: FieldValue extends FieldJsonValue
|
: FieldValue extends FieldJsonValue
|
||||||
? FieldJsonDraftValue
|
? FieldJsonDraftValue
|
||||||
: never;
|
: FieldValue extends FieldActorValue
|
||||||
|
? FieldActorDraftValue
|
||||||
|
: never;
|
||||||
|
|||||||
@ -142,6 +142,11 @@ export type FieldMultiSelectMetadata = {
|
|||||||
options: { label: string; color: ThemeColor; value: string }[];
|
options: { label: string; color: ThemeColor; value: string }[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FieldActorMetadata = {
|
||||||
|
objectMetadataNameSingular?: string;
|
||||||
|
fieldName: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type FieldMetadata =
|
export type FieldMetadata =
|
||||||
| FieldBooleanMetadata
|
| FieldBooleanMetadata
|
||||||
| FieldCurrencyMetadata
|
| FieldCurrencyMetadata
|
||||||
@ -158,7 +163,8 @@ export type FieldMetadata =
|
|||||||
| FieldMultiSelectMetadata
|
| FieldMultiSelectMetadata
|
||||||
| FieldTextMetadata
|
| FieldTextMetadata
|
||||||
| FieldUuidMetadata
|
| FieldUuidMetadata
|
||||||
| FieldAddressMetadata;
|
| FieldAddressMetadata
|
||||||
|
| FieldActorMetadata;
|
||||||
|
|
||||||
export type FieldTextValue = string;
|
export type FieldTextValue = string;
|
||||||
export type FieldUUidValue = string;
|
export type FieldUUidValue = string;
|
||||||
@ -204,3 +210,9 @@ export type FieldRelationValue<
|
|||||||
|
|
||||||
export type Json = ZodHelperLiteral | { [key: string]: Json } | Json[];
|
export type Json = ZodHelperLiteral | { [key: string]: Json } | Json[];
|
||||||
export type FieldJsonValue = Record<string, Json> | Json[] | null;
|
export type FieldJsonValue = Record<string, Json> | Json[] | null;
|
||||||
|
|
||||||
|
export type FieldActorValue = {
|
||||||
|
source: string;
|
||||||
|
workspaceMemberId?: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { FieldMetadataType } from '~/generated-metadata/graphql';
|
|||||||
|
|
||||||
import { FieldDefinition } from '../FieldDefinition';
|
import { FieldDefinition } from '../FieldDefinition';
|
||||||
import {
|
import {
|
||||||
|
FieldActorMetadata,
|
||||||
FieldAddressMetadata,
|
FieldAddressMetadata,
|
||||||
FieldBooleanMetadata,
|
FieldBooleanMetadata,
|
||||||
FieldCurrencyMetadata,
|
FieldCurrencyMetadata,
|
||||||
@ -63,7 +64,9 @@ type AssertFieldMetadataFunction = <
|
|||||||
? FieldRawJsonMetadata
|
? FieldRawJsonMetadata
|
||||||
: E extends 'RICH_TEXT'
|
: E extends 'RICH_TEXT'
|
||||||
? FieldTextMetadata
|
? FieldTextMetadata
|
||||||
: never,
|
: E extends 'ACTOR'
|
||||||
|
? FieldActorMetadata
|
||||||
|
: never,
|
||||||
>(
|
>(
|
||||||
fieldType: E,
|
fieldType: E,
|
||||||
fieldTypeGuard: (
|
fieldTypeGuard: (
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
|
|
||||||
|
import { FieldDefinition } from '../FieldDefinition';
|
||||||
|
import { FieldActorMetadata, FieldMetadata } from '../FieldMetadata';
|
||||||
|
|
||||||
|
export const isFieldActor = (
|
||||||
|
field: Pick<FieldDefinition<FieldMetadata>, 'type'>,
|
||||||
|
): field is FieldDefinition<FieldActorMetadata> =>
|
||||||
|
field.type === FieldMetadataType.Actor;
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { FieldActorValue } from '../FieldMetadata';
|
||||||
|
|
||||||
|
const actorSchema = z.object({
|
||||||
|
source: z.string(),
|
||||||
|
workspaceMemberId: z.optional(z.string().nullable()),
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const isFieldActorValue = (
|
||||||
|
fieldValue: unknown,
|
||||||
|
): fieldValue is FieldActorValue => actorSchema.safeParse(fieldValue).success;
|
||||||
@ -2,7 +2,7 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { FieldFullNameValue } from '../FieldMetadata';
|
import { FieldFullNameValue } from '../FieldMetadata';
|
||||||
|
|
||||||
const currencySchema = z.object({
|
const fullnameSchema = z.object({
|
||||||
firstName: z.string(),
|
firstName: z.string(),
|
||||||
lastName: z.string(),
|
lastName: z.string(),
|
||||||
});
|
});
|
||||||
@ -10,4 +10,4 @@ const currencySchema = z.object({
|
|||||||
export const isFieldFullNameValue = (
|
export const isFieldFullNameValue = (
|
||||||
fieldValue: unknown,
|
fieldValue: unknown,
|
||||||
): fieldValue is FieldFullNameValue =>
|
): fieldValue is FieldFullNameValue =>
|
||||||
currencySchema.safeParse(fieldValue).success;
|
fullnameSchema.safeParse(fieldValue).success;
|
||||||
|
|||||||
@ -2,6 +2,8 @@ import { isString } from '@sniptt/guards';
|
|||||||
|
|
||||||
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
|
||||||
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { isFieldActor } from '@/object-record/record-field/types/guards/isFieldActor';
|
||||||
|
import { isFieldActorValue } from '@/object-record/record-field/types/guards/isFieldActorValue';
|
||||||
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
|
import { isFieldAddress } from '@/object-record/record-field/types/guards/isFieldAddress';
|
||||||
import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue';
|
import { isFieldAddressValue } from '@/object-record/record-field/types/guards/isFieldAddressValue';
|
||||||
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
import { isFieldBoolean } from '@/object-record/record-field/types/guards/isFieldBoolean';
|
||||||
@ -13,9 +15,9 @@ import { isFieldEmail } from '@/object-record/record-field/types/guards/isFieldE
|
|||||||
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName';
|
||||||
import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue';
|
import { isFieldFullNameValue } from '@/object-record/record-field/types/guards/isFieldFullNameValue';
|
||||||
import { isFieldLink } from '@/object-record/record-field/types/guards/isFieldLink';
|
import { isFieldLink } from '@/object-record/record-field/types/guards/isFieldLink';
|
||||||
|
import { isFieldLinkValue } from '@/object-record/record-field/types/guards/isFieldLinkValue';
|
||||||
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
import { isFieldLinks } from '@/object-record/record-field/types/guards/isFieldLinks';
|
||||||
import { isFieldLinksValue } from '@/object-record/record-field/types/guards/isFieldLinksValue';
|
import { isFieldLinksValue } from '@/object-record/record-field/types/guards/isFieldLinksValue';
|
||||||
import { isFieldLinkValue } from '@/object-record/record-field/types/guards/isFieldLinkValue';
|
|
||||||
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
import { isFieldMultiSelect } from '@/object-record/record-field/types/guards/isFieldMultiSelect';
|
||||||
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
|
import { isFieldMultiSelectValue } from '@/object-record/record-field/types/guards/isFieldMultiSelectValue';
|
||||||
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
|
import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber';
|
||||||
@ -112,6 +114,10 @@ export const isFieldValueEmpty = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFieldActor(fieldDefinition)) {
|
||||||
|
return !isFieldActorValue(fieldValue) || isValueEmpty(fieldValue.name);
|
||||||
|
}
|
||||||
|
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Entity field type not supported in isFieldValueEmpty : ${fieldDefinition.type}}`,
|
`Entity field type not supported in isFieldValueEmpty : ${fieldDefinition.type}}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { isObject } from '@sniptt/guards';
|
|||||||
|
|
||||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||||
import {
|
import {
|
||||||
|
ActorFilter,
|
||||||
AddressFilter,
|
AddressFilter,
|
||||||
AndObjectRecordFilter,
|
AndObjectRecordFilter,
|
||||||
BooleanFilter,
|
BooleanFilter,
|
||||||
@ -256,6 +257,17 @@ export const isRecordMatchingFilter = ({
|
|||||||
value: record[filterKey].amountMicros,
|
value: record[filterKey].amountMicros,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
case FieldMetadataType.Actor: {
|
||||||
|
const actorFilter = filterValue as ActorFilter;
|
||||||
|
|
||||||
|
return (
|
||||||
|
actorFilter.name === undefined ||
|
||||||
|
isMatchingStringFilter({
|
||||||
|
stringFilter: actorFilter.name,
|
||||||
|
value: record[filterKey].name,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
case FieldMetadataType.Relation: {
|
case FieldMetadataType.Relation: {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Not implemented yet, use UUID filter instead on the corredponding "${filterKey}Id" field`,
|
`Not implemented yet, use UUID filter instead on the corredponding "${filterKey}Id" field`,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { isNonEmptyString } from '@sniptt/guards';
|
import { isNonEmptyString } from '@sniptt/guards';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
ActorFilter,
|
||||||
AddressFilter,
|
AddressFilter,
|
||||||
CurrencyFilter,
|
CurrencyFilter,
|
||||||
DateFilter,
|
DateFilter,
|
||||||
@ -212,6 +213,22 @@ const applyEmptyFilters = (
|
|||||||
[correspondingField.name + 'Id']: { is: 'NULL' } as RelationFilter,
|
[correspondingField.name + 'Id']: { is: 'NULL' } as RelationFilter,
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
|
case 'ACTOR':
|
||||||
|
emptyRecordFilter = {
|
||||||
|
or: [
|
||||||
|
{
|
||||||
|
[correspondingField.name]: {
|
||||||
|
name: { ilike: '' },
|
||||||
|
} as ActorFilter,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
[correspondingField.name]: {
|
||||||
|
name: { is: 'NULL' },
|
||||||
|
} as ActorFilter,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unsupported empty filter type ${filterType}`);
|
throw new Error(`Unsupported empty filter type ${filterType}`);
|
||||||
}
|
}
|
||||||
@ -744,6 +761,51 @@ export const turnObjectDropdownFilterIntoQueryFilter = (
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'ACTOR':
|
||||||
|
switch (rawUIFilter.operand) {
|
||||||
|
case ViewFilterOperand.Contains:
|
||||||
|
objectRecordFilters.push({
|
||||||
|
or: [
|
||||||
|
{
|
||||||
|
[correspondingField.name]: {
|
||||||
|
name: {
|
||||||
|
ilike: `%${rawUIFilter.value}%`,
|
||||||
|
},
|
||||||
|
} as ActorFilter,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case ViewFilterOperand.DoesNotContain:
|
||||||
|
objectRecordFilters.push({
|
||||||
|
and: [
|
||||||
|
{
|
||||||
|
not: {
|
||||||
|
[correspondingField.name]: {
|
||||||
|
name: {
|
||||||
|
ilike: `%${rawUIFilter.value}%`,
|
||||||
|
},
|
||||||
|
} as ActorFilter,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case ViewFilterOperand.IsEmpty:
|
||||||
|
case ViewFilterOperand.IsNotEmpty:
|
||||||
|
applyEmptyFilters(
|
||||||
|
rawUIFilter.operand,
|
||||||
|
correspondingField,
|
||||||
|
objectRecordFilters,
|
||||||
|
rawUIFilter.definition.type,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(
|
||||||
|
`Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error('Unknown filter type');
|
throw new Error('Unknown filter type');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types
|
|||||||
import { useInlineCell } from '../hooks/useInlineCell';
|
import { useInlineCell } from '../hooks/useInlineCell';
|
||||||
|
|
||||||
import { RecordInlineCellContainer } from './RecordInlineCellContainer';
|
import { RecordInlineCellContainer } from './RecordInlineCellContainer';
|
||||||
|
import { useIsFieldReadOnly } from '@/object-record/record-field/hooks/useIsFieldReadOnly';
|
||||||
|
|
||||||
type RecordInlineCellProps = {
|
type RecordInlineCellProps = {
|
||||||
readonly?: boolean;
|
readonly?: boolean;
|
||||||
@ -32,8 +33,12 @@ export const RecordInlineCell = ({
|
|||||||
|
|
||||||
const isFieldInputOnly = useIsFieldInputOnly();
|
const isFieldInputOnly = useIsFieldInputOnly();
|
||||||
|
|
||||||
|
const isFieldReadOnly = useIsFieldReadOnly();
|
||||||
|
|
||||||
const { closeInlineCell } = useInlineCell();
|
const { closeInlineCell } = useInlineCell();
|
||||||
|
|
||||||
|
const cellIsReadOnly = readonly || isFieldReadOnly;
|
||||||
|
|
||||||
const handleEnter: FieldInputEvent = (persistField) => {
|
const handleEnter: FieldInputEvent = (persistField) => {
|
||||||
persistField();
|
persistField();
|
||||||
closeInlineCell();
|
closeInlineCell();
|
||||||
@ -72,7 +77,7 @@ export const RecordInlineCell = ({
|
|||||||
return (
|
return (
|
||||||
<FieldFocusContextProvider>
|
<FieldFocusContextProvider>
|
||||||
<RecordInlineCellContainer
|
<RecordInlineCellContainer
|
||||||
readonly={readonly}
|
readonly={cellIsReadOnly}
|
||||||
buttonIcon={buttonIcon}
|
buttonIcon={buttonIcon}
|
||||||
customEditHotkeyScope={
|
customEditHotkeyScope={
|
||||||
isFieldRelation(fieldDefinition)
|
isFieldRelation(fieldDefinition)
|
||||||
@ -100,7 +105,7 @@ export const RecordInlineCell = ({
|
|||||||
onTab={handleTab}
|
onTab={handleTab}
|
||||||
onShiftTab={handleShiftTab}
|
onShiftTab={handleShiftTab}
|
||||||
onClickOutside={handleClickOutside}
|
onClickOutside={handleClickOutside}
|
||||||
isReadOnly={readonly}
|
isReadOnly={cellIsReadOnly}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
displayModeContent={<FieldDisplay />}
|
displayModeContent={<FieldDisplay />}
|
||||||
|
|||||||
@ -78,7 +78,11 @@ export const RecordTableCellSoftFocusMode = ({
|
|||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
Key.Enter,
|
Key.Enter,
|
||||||
() => {
|
() => {
|
||||||
if (!isFieldInputOnly && !isCellReadOnly) {
|
if (isCellReadOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isFieldInputOnly) {
|
||||||
openTableCell();
|
openTableCell();
|
||||||
} else {
|
} else {
|
||||||
toggleEditOnlyInput();
|
toggleEditOnlyInput();
|
||||||
@ -91,6 +95,10 @@ export const RecordTableCellSoftFocusMode = ({
|
|||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
'*',
|
'*',
|
||||||
(keyboardEvent) => {
|
(keyboardEvent) => {
|
||||||
|
if (isCellReadOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!isFieldInputOnly) {
|
if (!isFieldInputOnly) {
|
||||||
const isWritingText =
|
const isWritingText =
|
||||||
!isNonTextWritingKey(keyboardEvent.key) &&
|
!isNonTextWritingKey(keyboardEvent.key) &&
|
||||||
@ -150,6 +158,8 @@ export const RecordTableCellSoftFocusMode = ({
|
|||||||
(!isFirstColumn || !isEmpty) &&
|
(!isFirstColumn || !isEmpty) &&
|
||||||
!isCellReadOnly;
|
!isCellReadOnly;
|
||||||
|
|
||||||
|
const dontShowContent = isEmpty && isCellReadOnly;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RecordTableCellDisplayContainer
|
<RecordTableCellDisplayContainer
|
||||||
@ -157,7 +167,13 @@ export const RecordTableCellSoftFocusMode = ({
|
|||||||
scrollRef={scrollRef}
|
scrollRef={scrollRef}
|
||||||
softFocus
|
softFocus
|
||||||
>
|
>
|
||||||
{editModeContentOnly ? editModeContent : nonEditModeContent}
|
{dontShowContent ? (
|
||||||
|
<></>
|
||||||
|
) : editModeContentOnly ? (
|
||||||
|
editModeContent
|
||||||
|
) : (
|
||||||
|
nonEditModeContent
|
||||||
|
)}
|
||||||
</RecordTableCellDisplayContainer>
|
</RecordTableCellDisplayContainer>
|
||||||
{showButton && (
|
{showButton && (
|
||||||
<RecordTableCellButton onClick={handleButtonClick} Icon={buttonIcon} />
|
<RecordTableCellButton onClick={handleButtonClick} Icon={buttonIcon} />
|
||||||
|
|||||||
@ -30,4 +30,7 @@ export const COMPOSITE_FIELD_IMPORT_LABELS = {
|
|||||||
primaryLinkUrlLabel: 'Link URL',
|
primaryLinkUrlLabel: 'Link URL',
|
||||||
primaryLinkLabelLabel: 'Link Label',
|
primaryLinkLabelLabel: 'Link Label',
|
||||||
} satisfies Partial<CompositeFieldLabels<FieldLinksValue>>,
|
} satisfies Partial<CompositeFieldLabels<FieldLinksValue>>,
|
||||||
|
[FieldMetadataType.Actor]: {
|
||||||
|
sourceLabel: 'Source',
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -160,6 +160,11 @@ export const buildRecordFromImportedStructuredRow = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case FieldMetadataType.Actor:
|
||||||
|
recordToBuild[field.name] = {
|
||||||
|
source: 'IMPORT',
|
||||||
|
};
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
recordToBuild[field.name] = importedFieldValue;
|
recordToBuild[field.name] = importedFieldValue;
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -87,6 +87,12 @@ export const generateEmptyFieldValue = (
|
|||||||
case FieldMetadataType.RichText: {
|
case FieldMetadataType.RichText: {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
case FieldMetadataType.Actor: {
|
||||||
|
return {
|
||||||
|
source: 'MANUAL',
|
||||||
|
name: '',
|
||||||
|
};
|
||||||
|
}
|
||||||
default: {
|
default: {
|
||||||
throw new Error('Unhandled FieldMetadataType');
|
throw new Error('Unhandled FieldMetadataType');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import {
|
|||||||
IconCheck,
|
IconCheck,
|
||||||
IconCoins,
|
IconCoins,
|
||||||
IconComponent,
|
IconComponent,
|
||||||
|
IconCreativeCommonsSa,
|
||||||
IconFilePencil,
|
IconFilePencil,
|
||||||
IconJson,
|
IconJson,
|
||||||
IconKey,
|
IconKey,
|
||||||
@ -137,6 +138,10 @@ export const SETTINGS_FIELD_TYPE_CONFIGS = {
|
|||||||
Icon: IconFilePencil,
|
Icon: IconFilePencil,
|
||||||
defaultValue: { key: 'value' },
|
defaultValue: { key: 'value' },
|
||||||
},
|
},
|
||||||
|
[FieldMetadataType.Actor]: {
|
||||||
|
label: 'Actor',
|
||||||
|
Icon: IconCreativeCommonsSa,
|
||||||
|
},
|
||||||
} as const satisfies Record<
|
} as const satisfies Record<
|
||||||
SettingsSupportedFieldType,
|
SettingsSupportedFieldType,
|
||||||
SettingsFieldTypeConfig
|
SettingsFieldTypeConfig
|
||||||
|
|||||||
@ -0,0 +1,50 @@
|
|||||||
|
import { FieldActorValue } from '@/object-record/record-field/types/FieldMetadata';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
AvatarChip,
|
||||||
|
AvatarChipVariant,
|
||||||
|
IconApi,
|
||||||
|
IconCalendar,
|
||||||
|
IconCsv,
|
||||||
|
IconGmail,
|
||||||
|
} from 'twenty-ui';
|
||||||
|
|
||||||
|
type ActorDisplayProps = Partial<FieldActorValue> & {
|
||||||
|
avatarUrl?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ActorDisplay = ({
|
||||||
|
name,
|
||||||
|
source,
|
||||||
|
workspaceMemberId,
|
||||||
|
avatarUrl,
|
||||||
|
}: ActorDisplayProps) => {
|
||||||
|
const LeftIcon = useMemo(() => {
|
||||||
|
switch (source) {
|
||||||
|
case 'API':
|
||||||
|
return IconApi;
|
||||||
|
case 'IMPORT':
|
||||||
|
return IconCsv;
|
||||||
|
case 'EMAIL':
|
||||||
|
return IconGmail;
|
||||||
|
case 'CALENDAR':
|
||||||
|
return IconCalendar;
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}, [source]);
|
||||||
|
|
||||||
|
const isIconInverted = source === 'API' || source === 'IMPORT';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AvatarChip
|
||||||
|
placeholderColorSeed={workspaceMemberId ?? undefined}
|
||||||
|
name={name ?? ''}
|
||||||
|
avatarType={workspaceMemberId ? 'rounded' : 'squared'}
|
||||||
|
LeftIcon={LeftIcon}
|
||||||
|
avatarUrl={avatarUrl ?? undefined}
|
||||||
|
isIconInverted={isIconInverted}
|
||||||
|
variant={AvatarChipVariant.Transparent}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
|
||||||
|
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
|
||||||
|
|
||||||
|
import { ActorDisplay } from '@/ui/field/display/components/ActorDisplay';
|
||||||
|
import { CatalogDecorator } from 'twenty-ui';
|
||||||
|
|
||||||
|
const meta: Meta = {
|
||||||
|
title: 'UI/Display/ActorDisplay',
|
||||||
|
component: ActorDisplay,
|
||||||
|
args: {
|
||||||
|
name: 'John Doe',
|
||||||
|
},
|
||||||
|
decorators: [ComponentWithRouterDecorator],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default meta;
|
||||||
|
|
||||||
|
type Story = StoryObj<typeof ActorDisplay>;
|
||||||
|
|
||||||
|
export const Default: Story = {};
|
||||||
|
|
||||||
|
export const Catalog: Story = {
|
||||||
|
decorators: [CatalogDecorator],
|
||||||
|
parameters: {
|
||||||
|
catalog: {
|
||||||
|
dimensions: [
|
||||||
|
{
|
||||||
|
name: 'source',
|
||||||
|
values: ['API', 'IMPORT', 'EMAIL', 'CALENDAR', 'MANUAL'],
|
||||||
|
props: (source: string) => ({ source }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'workspaceMemberId',
|
||||||
|
values: [null, '123'],
|
||||||
|
props: (workspaceMemberId: string) => ({ workspaceMemberId }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'avatarUrl',
|
||||||
|
values: [null, 'https://picsum.photos/16'],
|
||||||
|
props: (avatarUrl: string) => ({ avatarUrl }),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
@ -2,6 +2,7 @@ import { useEffect, useState } from 'react';
|
|||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { currentUserState } from '@/auth/states/currentUserState';
|
import { currentUserState } from '@/auth/states/currentUserState';
|
||||||
|
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
|
||||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||||
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState';
|
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState';
|
||||||
@ -13,6 +14,7 @@ import { detectTimeZone } from '@/localization/utils/detectTimeZone';
|
|||||||
import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDateFormatFromWorkspaceDateFormat';
|
import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDateFormatFromWorkspaceDateFormat';
|
||||||
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
|
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
|
||||||
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
||||||
|
import { WorkspaceMember } from '~/generated-metadata/graphql';
|
||||||
import { useGetCurrentUserQuery } from '~/generated/graphql';
|
import { useGetCurrentUserQuery } from '~/generated/graphql';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
@ -31,6 +33,9 @@ export const UserProviderEffect = () => {
|
|||||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||||
currentWorkspaceMemberState,
|
currentWorkspaceMemberState,
|
||||||
);
|
);
|
||||||
|
const setCurrentWorkspaceMembers = useSetRecoilState(
|
||||||
|
currentWorkspaceMembersState,
|
||||||
|
);
|
||||||
|
|
||||||
const { loading: queryLoading, data: queryData } = useGetCurrentUserQuery({
|
const { loading: queryLoading, data: queryData } = useGetCurrentUserQuery({
|
||||||
skip: isCurrentUserLoaded,
|
skip: isCurrentUserLoaded,
|
||||||
@ -47,15 +52,26 @@ export const UserProviderEffect = () => {
|
|||||||
setCurrentUser(queryData.currentUser);
|
setCurrentUser(queryData.currentUser);
|
||||||
setCurrentWorkspace(queryData.currentUser.defaultWorkspace);
|
setCurrentWorkspace(queryData.currentUser.defaultWorkspace);
|
||||||
|
|
||||||
const { workspaceMember, workspaces: userWorkspaces } =
|
const {
|
||||||
queryData.currentUser;
|
workspaceMember,
|
||||||
|
workspaceMembers,
|
||||||
|
workspaces: userWorkspaces,
|
||||||
|
} = queryData.currentUser;
|
||||||
|
|
||||||
if (isDefined(workspaceMember)) {
|
const affectDefaultValuesOnEmptyWorkspaceMemberFields = (
|
||||||
setCurrentWorkspaceMember({
|
workspaceMember: WorkspaceMember,
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
...workspaceMember,
|
...workspaceMember,
|
||||||
colorScheme: (workspaceMember.colorScheme as ColorScheme) ?? 'Light',
|
colorScheme: (workspaceMember.colorScheme as ColorScheme) ?? 'Light',
|
||||||
locale: workspaceMember.locale ?? 'en',
|
locale: workspaceMember.locale ?? 'en',
|
||||||
});
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isDefined(workspaceMember)) {
|
||||||
|
setCurrentWorkspaceMember(
|
||||||
|
affectDefaultValuesOnEmptyWorkspaceMemberFields(workspaceMember),
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: factorize
|
// TODO: factorize
|
||||||
setDateTimeFormat({
|
setDateTimeFormat({
|
||||||
@ -72,6 +88,13 @@ export const UserProviderEffect = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isDefined(workspaceMembers)) {
|
||||||
|
setCurrentWorkspaceMembers(
|
||||||
|
workspaceMembers.map(affectDefaultValuesOnEmptyWorkspaceMemberFields) ??
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isDefined(userWorkspaces)) {
|
if (isDefined(userWorkspaces)) {
|
||||||
const workspaces = userWorkspaces
|
const workspaces = userWorkspaces
|
||||||
.map(({ workspace }) => workspace)
|
.map(({ workspace }) => workspace)
|
||||||
@ -81,6 +104,7 @@ export const UserProviderEffect = () => {
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
setCurrentUser,
|
setCurrentUser,
|
||||||
|
setCurrentWorkspaceMembers,
|
||||||
isLoading,
|
isLoading,
|
||||||
queryLoading,
|
queryLoading,
|
||||||
setCurrentWorkspace,
|
setCurrentWorkspace,
|
||||||
|
|||||||
@ -10,17 +10,10 @@ export const USER_QUERY_FRAGMENT = gql`
|
|||||||
supportUserHash
|
supportUserHash
|
||||||
onboardingStatus
|
onboardingStatus
|
||||||
workspaceMember {
|
workspaceMember {
|
||||||
id
|
...WorkspaceMemberQueryFragment
|
||||||
name {
|
}
|
||||||
firstName
|
workspaceMembers {
|
||||||
lastName
|
...WorkspaceMemberQueryFragment
|
||||||
}
|
|
||||||
colorScheme
|
|
||||||
avatarUrl
|
|
||||||
locale
|
|
||||||
timeZone
|
|
||||||
dateFormat
|
|
||||||
timeFormat
|
|
||||||
}
|
}
|
||||||
defaultWorkspace {
|
defaultWorkspace {
|
||||||
id
|
id
|
||||||
|
|||||||
@ -0,0 +1,17 @@
|
|||||||
|
import { gql } from '@apollo/client';
|
||||||
|
|
||||||
|
export const USER_QUERY_FRAGMENT = gql`
|
||||||
|
fragment WorkspaceMemberQueryFragment on WorkspaceMember {
|
||||||
|
id
|
||||||
|
name {
|
||||||
|
firstName
|
||||||
|
lastName
|
||||||
|
}
|
||||||
|
colorScheme
|
||||||
|
avatarUrl
|
||||||
|
locale
|
||||||
|
timeZone
|
||||||
|
dateFormat
|
||||||
|
timeFormat
|
||||||
|
}
|
||||||
|
`;
|
||||||
@ -164,6 +164,7 @@ export const SettingsObjectNewFieldStep2 = () => {
|
|||||||
FieldMetadataType.Link,
|
FieldMetadataType.Link,
|
||||||
FieldMetadataType.Numeric,
|
FieldMetadataType.Numeric,
|
||||||
FieldMetadataType.RichText,
|
FieldMetadataType.RichText,
|
||||||
|
FieldMetadataType.Actor,
|
||||||
] as const
|
] as const
|
||||||
).filter(isDefined);
|
).filter(isDefined);
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { useEffect } from 'react';
|
|
||||||
import { Decorator } from '@storybook/react';
|
import { Decorator } from '@storybook/react';
|
||||||
|
import { useEffect } from 'react';
|
||||||
import { useRecoilCallback } from 'recoil';
|
import { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition';
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -72,6 +72,8 @@ export default defineConfig(({ command, mode }) => {
|
|||||||
'**/RecordTableCellTd.tsx',
|
'**/RecordTableCellTd.tsx',
|
||||||
'**/RecordTableTd.tsx',
|
'**/RecordTableTd.tsx',
|
||||||
'**/RecordTableHeaderDragDropColumn.tsx',
|
'**/RecordTableHeaderDragDropColumn.tsx',
|
||||||
|
'**/ActorDisplay.tsx',
|
||||||
|
'**/AvatarChip.tsx',
|
||||||
],
|
],
|
||||||
babelOptions: {
|
babelOptions: {
|
||||||
presets: ['@babel/preset-typescript', '@babel/preset-react'],
|
presets: ['@babel/preset-typescript', '@babel/preset-react'],
|
||||||
|
|||||||
4
packages/twenty-server/@types/express.d.ts
vendored
4
packages/twenty-server/@types/express.d.ts
vendored
@ -1,9 +1,11 @@
|
|||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity';
|
||||||
|
|
||||||
declare module 'express-serve-static-core' {
|
declare module 'express-serve-static-core' {
|
||||||
interface Request {
|
interface Request {
|
||||||
user?: User;
|
user?: User | null;
|
||||||
|
apiKey?: ApiKeyWorkspaceEntity | null;
|
||||||
workspace?: Workspace;
|
workspace?: Workspace;
|
||||||
workspaceId?: string;
|
workspaceId?: string;
|
||||||
cacheVersion?: string | null;
|
cacheVersion?: string | null;
|
||||||
|
|||||||
@ -32,6 +32,7 @@
|
|||||||
"langchain": "^0.2.6",
|
"langchain": "^0.2.6",
|
||||||
"langfuse-langchain": "^3.11.2",
|
"langfuse-langchain": "^3.11.2",
|
||||||
"lodash.differencewith": "^4.5.0",
|
"lodash.differencewith": "^4.5.0",
|
||||||
|
"lodash.merge": "^4.6.2",
|
||||||
"lodash.omitby": "^4.6.0",
|
"lodash.omitby": "^4.6.0",
|
||||||
"lodash.uniq": "^4.5.0",
|
"lodash.uniq": "^4.5.0",
|
||||||
"lodash.uniqby": "^4.7.0",
|
"lodash.uniqby": "^4.7.0",
|
||||||
@ -48,6 +49,7 @@
|
|||||||
"@types/lodash.isempty": "^4.4.7",
|
"@types/lodash.isempty": "^4.4.7",
|
||||||
"@types/lodash.isequal": "^4.5.8",
|
"@types/lodash.isequal": "^4.5.8",
|
||||||
"@types/lodash.isobject": "^3.0.7",
|
"@types/lodash.isobject": "^3.0.7",
|
||||||
|
"@types/lodash.merge": "^4.6.9",
|
||||||
"@types/lodash.omit": "^4.5.9",
|
"@types/lodash.omit": "^4.5.9",
|
||||||
"@types/lodash.omitby": "^4.6.9",
|
"@types/lodash.omitby": "^4.6.9",
|
||||||
"@types/lodash.snakecase": "^4.1.7",
|
"@types/lodash.snakecase": "^4.1.7",
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import { EntityManager } from 'typeorm';
|
import { EntityManager } from 'typeorm';
|
||||||
|
|
||||||
|
import { DEV_SEED_WORKSPACE_MEMBER_IDS } from 'src/database/typeorm-seeds/workspace/workspace-members';
|
||||||
|
|
||||||
const tableName = 'company';
|
const tableName = 'company';
|
||||||
|
|
||||||
export const DEV_SEED_COMPANY_IDS = {
|
export const DEV_SEED_COMPANY_IDS = {
|
||||||
@ -36,6 +38,9 @@ export const seedCompanies = async (
|
|||||||
'addressAddressPostcode',
|
'addressAddressPostcode',
|
||||||
'addressAddressCountry',
|
'addressAddressCountry',
|
||||||
'position',
|
'position',
|
||||||
|
'createdBySource',
|
||||||
|
'createdByWorkspaceMemberId',
|
||||||
|
'createdByName',
|
||||||
])
|
])
|
||||||
.orIgnore()
|
.orIgnore()
|
||||||
.values([
|
.values([
|
||||||
@ -50,6 +55,9 @@ export const seedCompanies = async (
|
|||||||
addressAddressPostcode: null,
|
addressAddressPostcode: null,
|
||||||
addressAddressCountry: 'Ireland',
|
addressAddressCountry: 'Ireland',
|
||||||
position: 1,
|
position: 1,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.FACEBOOK,
|
id: DEV_SEED_COMPANY_IDS.FACEBOOK,
|
||||||
@ -62,6 +70,9 @@ export const seedCompanies = async (
|
|||||||
addressAddressPostcode: null,
|
addressAddressPostcode: null,
|
||||||
addressAddressCountry: null,
|
addressAddressCountry: null,
|
||||||
position: 2,
|
position: 2,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.QONTO,
|
id: DEV_SEED_COMPANY_IDS.QONTO,
|
||||||
@ -74,6 +85,9 @@ export const seedCompanies = async (
|
|||||||
addressAddressPostcode: '75009',
|
addressAddressPostcode: '75009',
|
||||||
addressAddressCountry: 'France',
|
addressAddressCountry: 'France',
|
||||||
position: 3,
|
position: 3,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.MICROSOFT,
|
id: DEV_SEED_COMPANY_IDS.MICROSOFT,
|
||||||
@ -86,6 +100,9 @@ export const seedCompanies = async (
|
|||||||
addressAddressPostcode: null,
|
addressAddressPostcode: null,
|
||||||
addressAddressCountry: null,
|
addressAddressCountry: null,
|
||||||
position: 4,
|
position: 4,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.AIRBNB,
|
id: DEV_SEED_COMPANY_IDS.AIRBNB,
|
||||||
@ -98,6 +115,9 @@ export const seedCompanies = async (
|
|||||||
addressAddressPostcode: '94103',
|
addressAddressPostcode: '94103',
|
||||||
addressAddressCountry: 'United States',
|
addressAddressCountry: 'United States',
|
||||||
position: 5,
|
position: 5,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.GOOGLE,
|
id: DEV_SEED_COMPANY_IDS.GOOGLE,
|
||||||
@ -110,6 +130,9 @@ export const seedCompanies = async (
|
|||||||
addressAddressPostcode: '94102',
|
addressAddressPostcode: '94102',
|
||||||
addressAddressCountry: 'United States',
|
addressAddressCountry: 'United States',
|
||||||
position: 6,
|
position: 6,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.NETFLIX,
|
id: DEV_SEED_COMPANY_IDS.NETFLIX,
|
||||||
@ -122,6 +145,9 @@ export const seedCompanies = async (
|
|||||||
addressAddressPostcode: '94110',
|
addressAddressPostcode: '94110',
|
||||||
addressAddressCountry: 'United States',
|
addressAddressCountry: 'United States',
|
||||||
position: 7,
|
position: 7,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.LIBEO,
|
id: DEV_SEED_COMPANY_IDS.LIBEO,
|
||||||
@ -134,6 +160,9 @@ export const seedCompanies = async (
|
|||||||
addressAddressPostcode: null,
|
addressAddressPostcode: null,
|
||||||
addressAddressCountry: null,
|
addressAddressCountry: null,
|
||||||
position: 8,
|
position: 8,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.CLAAP,
|
id: DEV_SEED_COMPANY_IDS.CLAAP,
|
||||||
@ -146,6 +175,9 @@ export const seedCompanies = async (
|
|||||||
addressAddressPostcode: null,
|
addressAddressPostcode: null,
|
||||||
addressAddressCountry: null,
|
addressAddressCountry: null,
|
||||||
position: 9,
|
position: 9,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.HASURA,
|
id: DEV_SEED_COMPANY_IDS.HASURA,
|
||||||
@ -158,6 +190,9 @@ export const seedCompanies = async (
|
|||||||
addressAddressPostcode: null,
|
addressAddressPostcode: null,
|
||||||
addressAddressCountry: null,
|
addressAddressCountry: null,
|
||||||
position: 10,
|
position: 10,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.WEWORK,
|
id: DEV_SEED_COMPANY_IDS.WEWORK,
|
||||||
@ -170,6 +205,9 @@ export const seedCompanies = async (
|
|||||||
addressAddressPostcode: null,
|
addressAddressPostcode: null,
|
||||||
addressAddressCountry: null,
|
addressAddressCountry: null,
|
||||||
position: 11,
|
position: 11,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.SAMSUNG,
|
id: DEV_SEED_COMPANY_IDS.SAMSUNG,
|
||||||
@ -182,6 +220,9 @@ export const seedCompanies = async (
|
|||||||
addressAddressPostcode: null,
|
addressAddressPostcode: null,
|
||||||
addressAddressCountry: null,
|
addressAddressCountry: null,
|
||||||
position: 12,
|
position: 12,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_COMPANY_IDS.ALGOLIA,
|
id: DEV_SEED_COMPANY_IDS.ALGOLIA,
|
||||||
@ -194,6 +235,9 @@ export const seedCompanies = async (
|
|||||||
addressAddressPostcode: null,
|
addressAddressPostcode: null,
|
||||||
addressAddressCountry: null,
|
addressAddressCountry: null,
|
||||||
position: 13,
|
position: 13,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: null,
|
||||||
|
createdByName: '',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
.execute();
|
.execute();
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { EntityManager } from 'typeorm';
|
|||||||
|
|
||||||
import { DEV_SEED_COMPANY_IDS } from 'src/database/typeorm-seeds/workspace/companies';
|
import { DEV_SEED_COMPANY_IDS } from 'src/database/typeorm-seeds/workspace/companies';
|
||||||
import { DEV_SEED_PERSON_IDS } from 'src/database/typeorm-seeds/workspace/people';
|
import { DEV_SEED_PERSON_IDS } from 'src/database/typeorm-seeds/workspace/people';
|
||||||
|
import { DEV_SEED_WORKSPACE_MEMBER_IDS } from 'src/database/typeorm-seeds/workspace/workspace-members';
|
||||||
|
|
||||||
const tableName = 'opportunity';
|
const tableName = 'opportunity';
|
||||||
|
|
||||||
@ -29,6 +30,9 @@ export const seedOpportunity = async (
|
|||||||
'position',
|
'position',
|
||||||
'pointOfContactId',
|
'pointOfContactId',
|
||||||
'companyId',
|
'companyId',
|
||||||
|
'createdBySource',
|
||||||
|
'createdByWorkspaceMemberId',
|
||||||
|
'createdByName',
|
||||||
])
|
])
|
||||||
.orIgnore()
|
.orIgnore()
|
||||||
.values([
|
.values([
|
||||||
@ -42,6 +46,9 @@ export const seedOpportunity = async (
|
|||||||
position: 1,
|
position: 1,
|
||||||
pointOfContactId: DEV_SEED_PERSON_IDS.CHRISTOPH,
|
pointOfContactId: DEV_SEED_PERSON_IDS.CHRISTOPH,
|
||||||
companyId: DEV_SEED_COMPANY_IDS.LINKEDIN,
|
companyId: DEV_SEED_COMPANY_IDS.LINKEDIN,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.JONY,
|
||||||
|
createdByName: 'Jony Ive',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_OPPORTUNITY_IDS.OPPORTUNITY_2,
|
id: DEV_SEED_OPPORTUNITY_IDS.OPPORTUNITY_2,
|
||||||
@ -53,6 +60,9 @@ export const seedOpportunity = async (
|
|||||||
position: 2,
|
position: 2,
|
||||||
pointOfContactId: DEV_SEED_PERSON_IDS.CHRISTOPHER_G,
|
pointOfContactId: DEV_SEED_PERSON_IDS.CHRISTOPHER_G,
|
||||||
companyId: DEV_SEED_COMPANY_IDS.FACEBOOK,
|
companyId: DEV_SEED_COMPANY_IDS.FACEBOOK,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.JONY,
|
||||||
|
createdByName: 'Jony Ive',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_OPPORTUNITY_IDS.OPPORTUNITY_3,
|
id: DEV_SEED_OPPORTUNITY_IDS.OPPORTUNITY_3,
|
||||||
@ -64,6 +74,9 @@ export const seedOpportunity = async (
|
|||||||
position: 3,
|
position: 3,
|
||||||
pointOfContactId: DEV_SEED_PERSON_IDS.NICHOLAS,
|
pointOfContactId: DEV_SEED_PERSON_IDS.NICHOLAS,
|
||||||
companyId: DEV_SEED_COMPANY_IDS.MICROSOFT,
|
companyId: DEV_SEED_COMPANY_IDS.MICROSOFT,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.JONY,
|
||||||
|
createdByName: 'Jony Ive',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_OPPORTUNITY_IDS.OPPORTUNITY_4,
|
id: DEV_SEED_OPPORTUNITY_IDS.OPPORTUNITY_4,
|
||||||
@ -75,6 +88,9 @@ export const seedOpportunity = async (
|
|||||||
position: 4,
|
position: 4,
|
||||||
pointOfContactId: DEV_SEED_PERSON_IDS.MATTHEW,
|
pointOfContactId: DEV_SEED_PERSON_IDS.MATTHEW,
|
||||||
companyId: DEV_SEED_COMPANY_IDS.MICROSOFT,
|
companyId: DEV_SEED_COMPANY_IDS.MICROSOFT,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: null,
|
||||||
|
createdByName: '',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
.execute();
|
.execute();
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { EntityManager } from 'typeorm';
|
import { EntityManager } from 'typeorm';
|
||||||
|
|
||||||
import { DEV_SEED_COMPANY_IDS } from 'src/database/typeorm-seeds/workspace/companies';
|
import { DEV_SEED_COMPANY_IDS } from 'src/database/typeorm-seeds/workspace/companies';
|
||||||
|
import { DEV_SEED_WORKSPACE_MEMBER_IDS } from 'src/database/typeorm-seeds/workspace/workspace-members';
|
||||||
|
|
||||||
const tableName = 'person';
|
const tableName = 'person';
|
||||||
|
|
||||||
@ -38,6 +39,9 @@ export const seedPeople = async (
|
|||||||
'companyId',
|
'companyId',
|
||||||
'email',
|
'email',
|
||||||
'position',
|
'position',
|
||||||
|
'createdBySource',
|
||||||
|
'createdByWorkspaceMemberId',
|
||||||
|
'createdByName',
|
||||||
])
|
])
|
||||||
.orIgnore()
|
.orIgnore()
|
||||||
.values([
|
.values([
|
||||||
@ -50,6 +54,9 @@ export const seedPeople = async (
|
|||||||
companyId: DEV_SEED_COMPANY_IDS.LINKEDIN,
|
companyId: DEV_SEED_COMPANY_IDS.LINKEDIN,
|
||||||
email: 'christoph.calisto@linkedin.com',
|
email: 'christoph.calisto@linkedin.com',
|
||||||
position: 1,
|
position: 1,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_PERSON_IDS.SYLVIE,
|
id: DEV_SEED_PERSON_IDS.SYLVIE,
|
||||||
@ -60,6 +67,9 @@ export const seedPeople = async (
|
|||||||
companyId: DEV_SEED_COMPANY_IDS.LINKEDIN,
|
companyId: DEV_SEED_COMPANY_IDS.LINKEDIN,
|
||||||
email: 'sylvie.palmer@linkedin.com',
|
email: 'sylvie.palmer@linkedin.com',
|
||||||
position: 2,
|
position: 2,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_PERSON_IDS.CHRISTOPHER_G,
|
id: DEV_SEED_PERSON_IDS.CHRISTOPHER_G,
|
||||||
@ -70,6 +80,9 @@ export const seedPeople = async (
|
|||||||
companyId: DEV_SEED_COMPANY_IDS.QONTO,
|
companyId: DEV_SEED_COMPANY_IDS.QONTO,
|
||||||
email: 'christopher.gonzalez@qonto.com',
|
email: 'christopher.gonzalez@qonto.com',
|
||||||
position: 3,
|
position: 3,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_PERSON_IDS.ASHLEY,
|
id: DEV_SEED_PERSON_IDS.ASHLEY,
|
||||||
@ -80,6 +93,9 @@ export const seedPeople = async (
|
|||||||
companyId: DEV_SEED_COMPANY_IDS.QONTO,
|
companyId: DEV_SEED_COMPANY_IDS.QONTO,
|
||||||
email: 'ashley.parker@qonto.com',
|
email: 'ashley.parker@qonto.com',
|
||||||
position: 4,
|
position: 4,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_PERSON_IDS.NICHOLAS,
|
id: DEV_SEED_PERSON_IDS.NICHOLAS,
|
||||||
@ -90,6 +106,9 @@ export const seedPeople = async (
|
|||||||
companyId: DEV_SEED_COMPANY_IDS.MICROSOFT,
|
companyId: DEV_SEED_COMPANY_IDS.MICROSOFT,
|
||||||
email: 'nicholas.wright@microsoft.com',
|
email: 'nicholas.wright@microsoft.com',
|
||||||
position: 5,
|
position: 5,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_PERSON_IDS.ISABELLA,
|
id: DEV_SEED_PERSON_IDS.ISABELLA,
|
||||||
@ -100,6 +119,9 @@ export const seedPeople = async (
|
|||||||
companyId: DEV_SEED_COMPANY_IDS.MICROSOFT,
|
companyId: DEV_SEED_COMPANY_IDS.MICROSOFT,
|
||||||
email: 'isabella.scott@microsoft.com',
|
email: 'isabella.scott@microsoft.com',
|
||||||
position: 6,
|
position: 6,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_PERSON_IDS.MATTHEW,
|
id: DEV_SEED_PERSON_IDS.MATTHEW,
|
||||||
@ -110,6 +132,9 @@ export const seedPeople = async (
|
|||||||
companyId: DEV_SEED_COMPANY_IDS.MICROSOFT,
|
companyId: DEV_SEED_COMPANY_IDS.MICROSOFT,
|
||||||
email: 'matthew.green@microsoft.com',
|
email: 'matthew.green@microsoft.com',
|
||||||
position: 7,
|
position: 7,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_PERSON_IDS.ELIZABETH,
|
id: DEV_SEED_PERSON_IDS.ELIZABETH,
|
||||||
@ -120,6 +145,9 @@ export const seedPeople = async (
|
|||||||
companyId: DEV_SEED_COMPANY_IDS.AIRBNB,
|
companyId: DEV_SEED_COMPANY_IDS.AIRBNB,
|
||||||
email: 'elizabeth.baker@airbnb.com',
|
email: 'elizabeth.baker@airbnb.com',
|
||||||
position: 8,
|
position: 8,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_PERSON_IDS.CHRISTOPHER_N,
|
id: DEV_SEED_PERSON_IDS.CHRISTOPHER_N,
|
||||||
@ -130,6 +158,9 @@ export const seedPeople = async (
|
|||||||
companyId: DEV_SEED_COMPANY_IDS.AIRBNB,
|
companyId: DEV_SEED_COMPANY_IDS.AIRBNB,
|
||||||
email: 'christopher.nelson@airbnb.com',
|
email: 'christopher.nelson@airbnb.com',
|
||||||
position: 9,
|
position: 9,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_PERSON_IDS.AVERY,
|
id: DEV_SEED_PERSON_IDS.AVERY,
|
||||||
@ -140,6 +171,9 @@ export const seedPeople = async (
|
|||||||
companyId: DEV_SEED_COMPANY_IDS.AIRBNB,
|
companyId: DEV_SEED_COMPANY_IDS.AIRBNB,
|
||||||
email: 'avery.carter@airbnb.com',
|
email: 'avery.carter@airbnb.com',
|
||||||
position: 10,
|
position: 10,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_PERSON_IDS.ETHAN,
|
id: DEV_SEED_PERSON_IDS.ETHAN,
|
||||||
@ -150,6 +184,9 @@ export const seedPeople = async (
|
|||||||
companyId: DEV_SEED_COMPANY_IDS.GOOGLE,
|
companyId: DEV_SEED_COMPANY_IDS.GOOGLE,
|
||||||
email: 'ethan.mitchell@google.com',
|
email: 'ethan.mitchell@google.com',
|
||||||
position: 11,
|
position: 11,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_PERSON_IDS.MADISON,
|
id: DEV_SEED_PERSON_IDS.MADISON,
|
||||||
@ -160,6 +197,9 @@ export const seedPeople = async (
|
|||||||
companyId: DEV_SEED_COMPANY_IDS.GOOGLE,
|
companyId: DEV_SEED_COMPANY_IDS.GOOGLE,
|
||||||
email: 'madison.perez@google.com',
|
email: 'madison.perez@google.com',
|
||||||
position: 12,
|
position: 12,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_PERSON_IDS.BERTRAND,
|
id: DEV_SEED_PERSON_IDS.BERTRAND,
|
||||||
@ -170,6 +210,9 @@ export const seedPeople = async (
|
|||||||
companyId: DEV_SEED_COMPANY_IDS.GOOGLE,
|
companyId: DEV_SEED_COMPANY_IDS.GOOGLE,
|
||||||
email: 'bertrand.voulzy@google.com',
|
email: 'bertrand.voulzy@google.com',
|
||||||
position: 13,
|
position: 13,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_PERSON_IDS.LOUIS,
|
id: DEV_SEED_PERSON_IDS.LOUIS,
|
||||||
@ -180,6 +223,9 @@ export const seedPeople = async (
|
|||||||
companyId: DEV_SEED_COMPANY_IDS.GOOGLE,
|
companyId: DEV_SEED_COMPANY_IDS.GOOGLE,
|
||||||
email: 'louis.duss@google.com',
|
email: 'louis.duss@google.com',
|
||||||
position: 14,
|
position: 14,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: DEV_SEED_WORKSPACE_MEMBER_IDS.TIM,
|
||||||
|
createdByName: 'Tim Apple',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: DEV_SEED_PERSON_IDS.LORIE,
|
id: DEV_SEED_PERSON_IDS.LORIE,
|
||||||
@ -190,6 +236,9 @@ export const seedPeople = async (
|
|||||||
companyId: DEV_SEED_COMPANY_IDS.GOOGLE,
|
companyId: DEV_SEED_COMPANY_IDS.GOOGLE,
|
||||||
email: 'lorie.vladim@google.com',
|
email: 'lorie.vladim@google.com',
|
||||||
position: 15,
|
position: 15,
|
||||||
|
createdBySource: 'MANUAL',
|
||||||
|
createdByWorkspaceMemberId: null,
|
||||||
|
createdByName: '',
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
.execute();
|
.execute();
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type';
|
||||||
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity';
|
||||||
|
|
||||||
@ -196,6 +197,16 @@ const fieldRichTextMock = {
|
|||||||
defaultValue: null,
|
defaultValue: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fieldActorMock = {
|
||||||
|
name: 'fieldActor',
|
||||||
|
type: FieldMetadataType.ACTOR,
|
||||||
|
isNullable: true,
|
||||||
|
defaultValue: {
|
||||||
|
source: FieldActorSource.MANUAL,
|
||||||
|
name: '',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const fields = [
|
export const fields = [
|
||||||
fieldUuidMock,
|
fieldUuidMock,
|
||||||
fieldTextMock,
|
fieldTextMock,
|
||||||
@ -218,6 +229,7 @@ export const fields = [
|
|||||||
fieldAddressMock,
|
fieldAddressMock,
|
||||||
fieldRawJsonMock,
|
fieldRawJsonMock,
|
||||||
fieldRichTextMock,
|
fieldRichTextMock,
|
||||||
|
fieldActorMock,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const objectMetadataItemMock = {
|
export const objectMetadataItemMock = {
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { JsonWebTokenError, TokenExpiredError } from 'jsonwebtoken';
|
|||||||
import { useThrottler } from 'src/engine/api/graphql/graphql-config/hooks/use-throttler';
|
import { useThrottler } from 'src/engine/api/graphql/graphql-config/hooks/use-throttler';
|
||||||
import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.factory';
|
import { WorkspaceSchemaFactory } from 'src/engine/api/graphql/workspace-schema.factory';
|
||||||
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
|
import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
|
||||||
import { JwtData } from 'src/engine/core-modules/auth/types/jwt-data.type';
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module';
|
import { CoreEngineModule } from 'src/engine/core-modules/core-engine.module';
|
||||||
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
|
import { useGraphQLErrorHandlerHook } from 'src/engine/core-modules/graphql/hooks/use-graphql-error-handler.hook';
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
@ -65,7 +65,7 @@ export class GraphQLConfigService
|
|||||||
autoSchemaFile: true,
|
autoSchemaFile: true,
|
||||||
include: [CoreEngineModule],
|
include: [CoreEngineModule],
|
||||||
conditionalSchema: async (context) => {
|
conditionalSchema: async (context) => {
|
||||||
let user: User | undefined;
|
let user: User | null | undefined;
|
||||||
let workspace: Workspace | undefined;
|
let workspace: Workspace | undefined;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -136,7 +136,7 @@ export class GraphQLConfigService
|
|||||||
|
|
||||||
async createSchema(
|
async createSchema(
|
||||||
context: YogaDriverServerContext<'express'> & YogaInitialContext,
|
context: YogaDriverServerContext<'express'> & YogaInitialContext,
|
||||||
data: JwtData,
|
data: AuthContext,
|
||||||
): Promise<GraphQLSchemaWithContext<YogaDriverServerContext<'express'>>> {
|
): Promise<GraphQLSchemaWithContext<YogaDriverServerContext<'express'>>> {
|
||||||
// Create a new contextId for each request
|
// Create a new contextId for each request
|
||||||
const contextId = ContextIdFactory.create();
|
const contextId = ContextIdFactory.create();
|
||||||
@ -155,9 +155,6 @@ export class GraphQLConfigService
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return await workspaceFactory.createGraphQLSchema(
|
return await workspaceFactory.createGraphQLSchema(data);
|
||||||
data.workspace.id,
|
|
||||||
data.user?.id,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -19,7 +19,7 @@ describe('QueryRunnerArgsFactory', () => {
|
|||||||
{ name: 'testNumber', type: FieldMetadataType.NUMBER },
|
{ name: 'testNumber', type: FieldMetadataType.NUMBER },
|
||||||
] as FieldMetadataInterface[],
|
] as FieldMetadataInterface[],
|
||||||
objectMetadataItem: { isCustom: true, nameSingular: 'test' },
|
objectMetadataItem: { isCustom: true, nameSingular: 'test' },
|
||||||
workspaceId,
|
authContext: { workspace: { id: workspaceId } },
|
||||||
} as WorkspaceQueryRunnerOptions;
|
} as WorkspaceQueryRunnerOptions;
|
||||||
|
|
||||||
let factory: QueryRunnerArgsFactory;
|
let factory: QueryRunnerArgsFactory;
|
||||||
@ -149,6 +149,7 @@ describe('QueryRunnerArgsFactory', () => {
|
|||||||
{ name: 'testNumber', type: FieldMetadataType.NUMBER },
|
{ name: 'testNumber', type: FieldMetadataType.NUMBER },
|
||||||
] as FieldMetadataInterface[],
|
] as FieldMetadataInterface[],
|
||||||
objectMetadataItem: { isCustom: true, nameSingular: 'test' },
|
objectMetadataItem: { isCustom: true, nameSingular: 'test' },
|
||||||
|
authContext: { workspace: { id: workspaceId } },
|
||||||
} as WorkspaceQueryRunnerOptions;
|
} as WorkspaceQueryRunnerOptions;
|
||||||
|
|
||||||
const args = {
|
const args = {
|
||||||
|
|||||||
@ -129,7 +129,7 @@ export class QueryRunnerArgsFactory {
|
|||||||
isCustom: options.objectMetadataItem.isCustom,
|
isCustom: options.objectMetadataItem.isCustom,
|
||||||
nameSingular: options.objectMetadataItem.nameSingular,
|
nameSingular: options.objectMetadataItem.nameSingular,
|
||||||
},
|
},
|
||||||
options.workspaceId,
|
options.authContext.workspace.id,
|
||||||
argPositionBackfillInput.argIndex,
|
argPositionBackfillInput.argIndex,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@ -157,7 +157,7 @@ export class QueryRunnerArgsFactory {
|
|||||||
isCustom: options.objectMetadataItem.isCustom,
|
isCustom: options.objectMetadataItem.isCustom,
|
||||||
nameSingular: options.objectMetadataItem.nameSingular,
|
nameSingular: options.objectMetadataItem.nameSingular,
|
||||||
},
|
},
|
||||||
options.workspaceId,
|
options.authContext.workspace.id,
|
||||||
argPositionBackfillInput.argIndex,
|
argPositionBackfillInput.argIndex,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -3,9 +3,10 @@ import { GraphQLResolveInfo } from 'graphql';
|
|||||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
|
||||||
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
|
|
||||||
export interface WorkspaceQueryRunnerOptions {
|
export interface WorkspaceQueryRunnerOptions {
|
||||||
workspaceId: string;
|
authContext: AuthContext;
|
||||||
userId: string | undefined;
|
|
||||||
info: GraphQLResolveInfo;
|
info: GraphQLResolveInfo;
|
||||||
objectMetadataItem: ObjectMetadataInterface;
|
objectMetadataItem: ObjectMetadataInterface;
|
||||||
fieldMetadataCollection: FieldMetadataInterface[];
|
fieldMetadataCollection: FieldMetadataInterface[];
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { ResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
import { ResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
|
|
||||||
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
|
|
||||||
export interface WorkspaceQueryHookInstance {
|
export interface WorkspaceQueryHookInstance {
|
||||||
execute(
|
execute(
|
||||||
userId: string | undefined,
|
authContext: AuthContext,
|
||||||
workspaceId: string,
|
objectName: string,
|
||||||
payload: ResolverArgs,
|
payload: ResolverArgs,
|
||||||
): Promise<void>;
|
): Promise<ResolverArgs>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { Module } from '@nestjs/core/injector/module';
|
import { Module } from '@nestjs/core/injector/module';
|
||||||
|
|
||||||
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||||
|
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
|
|
||||||
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||||
|
|
||||||
@ -36,8 +37,23 @@ export class WorkspaceQueryHookStorage {
|
|||||||
|
|
||||||
getWorkspaceQueryPreHookInstances(
|
getWorkspaceQueryPreHookInstances(
|
||||||
key: WorkspaceQueryHookKey,
|
key: WorkspaceQueryHookKey,
|
||||||
): WorkspaceQueryHookData<WorkspaceQueryHookInstance>[] | undefined {
|
): WorkspaceQueryHookData<WorkspaceQueryHookInstance>[] {
|
||||||
return this.preHookInstances.get(key);
|
const methodName = key.split('.')?.[1] as
|
||||||
|
| WorkspaceResolverBuilderMethodNames
|
||||||
|
| undefined;
|
||||||
|
let wildcardInstances: WorkspaceQueryHookData<WorkspaceQueryHookInstance>[] =
|
||||||
|
[];
|
||||||
|
|
||||||
|
if (!methodName) {
|
||||||
|
throw new Error(`Can't split workspace query hook key: ${key}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrive wildcard pre-hook instances
|
||||||
|
if (this.preHookInstances.has(`*.${methodName}`)) {
|
||||||
|
wildcardInstances = this.preHookInstances.get(`*.${methodName}`)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...wildcardInstances, ...(this.preHookInstances.get(key) ?? [])];
|
||||||
}
|
}
|
||||||
|
|
||||||
registerWorkspaceQueryPostHookInstance(
|
registerWorkspaceQueryPostHookInstance(
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
|
||||||
import { DiscoveryService, ModuleRef, createContextId } from '@nestjs/core';
|
import { DiscoveryService, ModuleRef, createContextId } from '@nestjs/core';
|
||||||
import { Module } from '@nestjs/core/injector/module';
|
|
||||||
import { Injector } from '@nestjs/core/injector/injector';
|
import { Injector } from '@nestjs/core/injector/injector';
|
||||||
|
import { Module } from '@nestjs/core/injector/module';
|
||||||
|
|
||||||
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
import { WorkspaceQueryHookInstance } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/interfaces/workspace-query-hook.interface';
|
||||||
|
|
||||||
import { WorkspaceQueryHookMetadataAccessor } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook-metadata.accessor';
|
|
||||||
import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage';
|
|
||||||
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||||
|
import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage';
|
||||||
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
|
import { WorkspaceQueryHookType } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
|
||||||
|
import { WorkspaceQueryHookMetadataAccessor } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook-metadata.accessor';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceQueryHookExplorer implements OnModuleInit {
|
export class WorkspaceQueryHookExplorer implements OnModuleInit {
|
||||||
@ -73,7 +73,7 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
|
|||||||
instance: object,
|
instance: object,
|
||||||
host: Module,
|
host: Module,
|
||||||
isRequestScoped: boolean,
|
isRequestScoped: boolean,
|
||||||
) {
|
): Promise<ReturnType<WorkspaceQueryHookInstance['execute']>> {
|
||||||
const methodName = 'execute';
|
const methodName = 'execute';
|
||||||
|
|
||||||
if (isRequestScoped) {
|
if (isRequestScoped) {
|
||||||
@ -83,7 +83,7 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
|
|||||||
this.moduleRef.registerRequestByContextId(
|
this.moduleRef.registerRequestByContextId(
|
||||||
{
|
{
|
||||||
req: {
|
req: {
|
||||||
workspaceId: payload?.[1],
|
workspaceId: payload?.[0].workspace.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
contextId,
|
contextId,
|
||||||
@ -97,9 +97,9 @@ export class WorkspaceQueryHookExplorer implements OnModuleInit {
|
|||||||
contextId,
|
contextId,
|
||||||
);
|
);
|
||||||
|
|
||||||
await contextInstance[methodName].call(contextInstance, ...payload);
|
return contextInstance[methodName].call(contextInstance, ...payload);
|
||||||
} else {
|
} else {
|
||||||
await instance[methodName].call(instance, ...payload);
|
return instance[methodName].call(instance, ...payload);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,11 +1,14 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import merge from 'lodash.merge';
|
||||||
|
|
||||||
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
import { WorkspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
|
|
||||||
import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage';
|
import { WorkspaceQueryHookStorage } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/storage/workspace-query-hook.storage';
|
||||||
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
import { WorkspaceQueryHookKey } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/decorators/workspace-query-hook.decorator';
|
||||||
import { WorkspaceQueryHookExplorer } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer';
|
import { WorkspaceQueryHookExplorer } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.explorer';
|
||||||
import { WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
|
import { WorkspacePreQueryHookPayload } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type';
|
||||||
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceQueryHookService {
|
export class WorkspaceQueryHookService {
|
||||||
@ -17,27 +20,33 @@ export class WorkspaceQueryHookService {
|
|||||||
public async executePreQueryHooks<
|
public async executePreQueryHooks<
|
||||||
T extends WorkspaceResolverBuilderMethodNames,
|
T extends WorkspaceResolverBuilderMethodNames,
|
||||||
>(
|
>(
|
||||||
userId: string | undefined,
|
authContext: AuthContext,
|
||||||
workspaceId: string,
|
// TODO: We should allow wildcard for object name
|
||||||
objectName: string,
|
objectName: string,
|
||||||
methodName: T,
|
methodName: T,
|
||||||
payload: WorkspacePreQueryHookPayload<T>,
|
payload: WorkspacePreQueryHookPayload<T>,
|
||||||
): Promise<void> {
|
): Promise<WorkspacePreQueryHookPayload<T>> {
|
||||||
const key: WorkspaceQueryHookKey = `${objectName}.${methodName}`;
|
const key: WorkspaceQueryHookKey = `${objectName}.${methodName}`;
|
||||||
const preHookInstances =
|
const preHookInstances =
|
||||||
this.workspaceQueryHookStorage.getWorkspaceQueryPreHookInstances(key);
|
this.workspaceQueryHookStorage.getWorkspaceQueryPreHookInstances(key);
|
||||||
|
|
||||||
if (!preHookInstances) {
|
if (!preHookInstances) {
|
||||||
return;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const preHookInstance of preHookInstances) {
|
for (const preHookInstance of preHookInstances) {
|
||||||
await this.workspaceQueryHookExplorer.handleHook(
|
// Deep merge all return of handleHook into payload before returning it
|
||||||
[userId, workspaceId, payload],
|
const hookPayload = await this.workspaceQueryHookExplorer.handleHook(
|
||||||
|
[authContext, objectName, payload],
|
||||||
preHookInstance.instance,
|
preHookInstance.instance,
|
||||||
preHookInstance.host,
|
preHookInstance.host,
|
||||||
preHookInstance.isRequestScoped,
|
preHookInstance.isRequestScoped,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: Is it really a good idea ?
|
||||||
|
payload = merge(payload, hookPayload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return payload;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,7 +52,6 @@ import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.
|
|||||||
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util';
|
||||||
import { isQueryTimeoutError } from 'src/engine/utils/query-timeout.util';
|
import { isQueryTimeoutError } from 'src/engine/utils/query-timeout.util';
|
||||||
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service';
|
||||||
import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PGGraphQLMutation,
|
PGGraphQLMutation,
|
||||||
@ -90,11 +89,19 @@ export class WorkspaceQueryRunnerService {
|
|||||||
args: FindManyResolverArgs<Filter, OrderBy>,
|
args: FindManyResolverArgs<Filter, OrderBy>,
|
||||||
options: WorkspaceQueryRunnerOptions,
|
options: WorkspaceQueryRunnerOptions,
|
||||||
): Promise<IConnection<Record> | undefined> {
|
): Promise<IConnection<Record> | undefined> {
|
||||||
const { workspaceId, userId, objectMetadataItem } = options;
|
const { authContext, objectMetadataItem } = options;
|
||||||
const start = performance.now();
|
const start = performance.now();
|
||||||
|
|
||||||
|
const hookedArgs =
|
||||||
|
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||||
|
authContext,
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
'findMany',
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
|
||||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||||
args,
|
hookedArgs,
|
||||||
options,
|
options,
|
||||||
ResolverArgsType.FindMany,
|
ResolverArgsType.FindMany,
|
||||||
)) as FindManyResolverArgs<Filter, OrderBy>;
|
)) as FindManyResolverArgs<Filter, OrderBy>;
|
||||||
@ -104,15 +111,7 @@ export class WorkspaceQueryRunnerService {
|
|||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
const result = await this.execute(query, authContext.workspace.id);
|
||||||
userId,
|
|
||||||
workspaceId,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
'findMany',
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await this.execute(query, workspaceId);
|
|
||||||
const end = performance.now();
|
const end = performance.now();
|
||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
@ -125,7 +124,7 @@ export class WorkspaceQueryRunnerService {
|
|||||||
result,
|
result,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
'',
|
'',
|
||||||
workspaceId,
|
authContext.workspace.id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -142,10 +141,18 @@ export class WorkspaceQueryRunnerService {
|
|||||||
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
const { workspaceId, userId, objectMetadataItem } = options;
|
const { authContext, objectMetadataItem } = options;
|
||||||
|
|
||||||
|
const hookedArgs =
|
||||||
|
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||||
|
authContext,
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
'findOne',
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
|
||||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||||
args,
|
hookedArgs,
|
||||||
options,
|
options,
|
||||||
ResolverArgsType.FindOne,
|
ResolverArgsType.FindOne,
|
||||||
)) as FindOneResolverArgs<Filter>;
|
)) as FindOneResolverArgs<Filter>;
|
||||||
@ -155,20 +162,12 @@ export class WorkspaceQueryRunnerService {
|
|||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
const result = await this.execute(query, authContext.workspace.id);
|
||||||
userId,
|
|
||||||
workspaceId,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
'findOne',
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await this.execute(query, workspaceId);
|
|
||||||
const parsedResult = await this.parseResult<IConnection<Record>>(
|
const parsedResult = await this.parseResult<IConnection<Record>>(
|
||||||
result,
|
result,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
'',
|
'',
|
||||||
workspaceId,
|
authContext.workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
return parsedResult?.edges?.[0]?.node;
|
return parsedResult?.edges?.[0]?.node;
|
||||||
@ -192,10 +191,18 @@ export class WorkspaceQueryRunnerService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { workspaceId, userId, objectMetadataItem } = options;
|
const { authContext, objectMetadataItem } = options;
|
||||||
|
|
||||||
|
const hookedArgs =
|
||||||
|
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||||
|
authContext,
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
'findDuplicates',
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
|
||||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||||
args,
|
hookedArgs,
|
||||||
options,
|
options,
|
||||||
ResolverArgsType.FindDuplicates,
|
ResolverArgsType.FindDuplicates,
|
||||||
)) as FindDuplicatesResolverArgs<TRecord>;
|
)) as FindDuplicatesResolverArgs<TRecord>;
|
||||||
@ -206,7 +213,7 @@ export class WorkspaceQueryRunnerService {
|
|||||||
existingRecords = await this.duplicateService.findExistingRecords(
|
existingRecords = await this.duplicateService.findExistingRecords(
|
||||||
computedArgs.ids,
|
computedArgs.ids,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
workspaceId,
|
authContext.workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!existingRecords || existingRecords.length === 0) {
|
if (!existingRecords || existingRecords.length === 0) {
|
||||||
@ -223,21 +230,13 @@ export class WorkspaceQueryRunnerService {
|
|||||||
existingRecords,
|
existingRecords,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
const result = await this.execute(query, authContext.workspace.id);
|
||||||
userId,
|
|
||||||
workspaceId,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
'findDuplicates',
|
|
||||||
computedArgs,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await this.execute(query, workspaceId);
|
|
||||||
|
|
||||||
return this.parseResult<IConnection<TRecord>>(
|
return this.parseResult<IConnection<TRecord>>(
|
||||||
result,
|
result,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
'',
|
'',
|
||||||
workspaceId,
|
authContext.workspace.id,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -246,7 +245,7 @@ export class WorkspaceQueryRunnerService {
|
|||||||
args: CreateManyResolverArgs<Partial<Record>>,
|
args: CreateManyResolverArgs<Partial<Record>>,
|
||||||
options: WorkspaceQueryRunnerOptions,
|
options: WorkspaceQueryRunnerOptions,
|
||||||
): Promise<Record[] | undefined> {
|
): Promise<Record[] | undefined> {
|
||||||
const { workspaceId, userId, objectMetadataItem } = options;
|
const { authContext, objectMetadataItem } = options;
|
||||||
|
|
||||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||||
|
|
||||||
@ -260,33 +259,33 @@ export class WorkspaceQueryRunnerService {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const hookedArgs =
|
||||||
|
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||||
|
authContext,
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
'createMany',
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
|
||||||
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
const computedArgs = (await this.queryRunnerArgsFactory.create(
|
||||||
args,
|
hookedArgs,
|
||||||
options,
|
options,
|
||||||
ResolverArgsType.CreateMany,
|
ResolverArgsType.CreateMany,
|
||||||
)) as CreateManyResolverArgs<Record>;
|
)) as CreateManyResolverArgs<Record>;
|
||||||
|
|
||||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
|
||||||
userId,
|
|
||||||
workspaceId,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
'createMany',
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.createMany(
|
const query = await this.workspaceQueryBuilderFactory.createMany(
|
||||||
computedArgs,
|
computedArgs,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await this.execute(query, workspaceId);
|
const result = await this.execute(query, authContext.workspace.id);
|
||||||
|
|
||||||
const parsedResults = (
|
const parsedResults = (
|
||||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||||
result,
|
result,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
'insertInto',
|
'insertInto',
|
||||||
workspaceId,
|
authContext.workspace.id,
|
||||||
)
|
)
|
||||||
)?.records;
|
)?.records;
|
||||||
|
|
||||||
@ -299,8 +298,8 @@ export class WorkspaceQueryRunnerService {
|
|||||||
parsedResults.forEach((record) => {
|
parsedResults.forEach((record) => {
|
||||||
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.created`, {
|
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.created`, {
|
||||||
name: `${objectMetadataItem.nameSingular}.created`,
|
name: `${objectMetadataItem.nameSingular}.created`,
|
||||||
workspaceId,
|
workspaceId: authContext.workspace.id,
|
||||||
userId,
|
userId: authContext.user?.id,
|
||||||
recordId: record.id,
|
recordId: record.id,
|
||||||
objectMetadata: objectMetadataItem,
|
objectMetadata: objectMetadataItem,
|
||||||
properties: {
|
properties: {
|
||||||
@ -325,7 +324,7 @@ export class WorkspaceQueryRunnerService {
|
|||||||
? await this.duplicateService.findExistingRecords(
|
? await this.duplicateService.findExistingRecords(
|
||||||
ids as string[],
|
ids as string[],
|
||||||
options.objectMetadataItem,
|
options.objectMetadataItem,
|
||||||
options.workspaceId,
|
options.authContext.workspace.id,
|
||||||
)
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
@ -381,10 +380,10 @@ export class WorkspaceQueryRunnerService {
|
|||||||
args: UpdateOneResolverArgs<Partial<Record>>,
|
args: UpdateOneResolverArgs<Partial<Record>>,
|
||||||
options: WorkspaceQueryRunnerOptions,
|
options: WorkspaceQueryRunnerOptions,
|
||||||
): Promise<Record | undefined> {
|
): Promise<Record | undefined> {
|
||||||
const { workspaceId, userId, objectMetadataItem } = options;
|
const { authContext, objectMetadataItem } = options;
|
||||||
const repository =
|
const repository =
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||||
workspaceId,
|
authContext.workspace.id,
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -402,27 +401,27 @@ export class WorkspaceQueryRunnerService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hookedArgs =
|
||||||
|
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||||
|
authContext,
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
'updateOne',
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.updateOne(
|
const query = await this.workspaceQueryBuilderFactory.updateOne(
|
||||||
args,
|
hookedArgs,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
const result = await this.execute(query, authContext.workspace.id);
|
||||||
userId,
|
|
||||||
workspaceId,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
'updateOne',
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await this.execute(query, workspaceId);
|
|
||||||
|
|
||||||
const parsedResults = (
|
const parsedResults = (
|
||||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||||
result,
|
result,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
'update',
|
'update',
|
||||||
workspaceId,
|
authContext.workspace.id,
|
||||||
)
|
)
|
||||||
)?.records;
|
)?.records;
|
||||||
|
|
||||||
@ -434,8 +433,8 @@ export class WorkspaceQueryRunnerService {
|
|||||||
|
|
||||||
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.updated`, {
|
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.updated`, {
|
||||||
name: `${objectMetadataItem.nameSingular}.updated`,
|
name: `${objectMetadataItem.nameSingular}.updated`,
|
||||||
workspaceId,
|
workspaceId: authContext.workspace.id,
|
||||||
userId,
|
userId: authContext.user?.id,
|
||||||
recordId: existingRecord.id,
|
recordId: existingRecord.id,
|
||||||
objectMetadata: objectMetadataItem,
|
objectMetadata: objectMetadataItem,
|
||||||
properties: {
|
properties: {
|
||||||
@ -452,10 +451,10 @@ export class WorkspaceQueryRunnerService {
|
|||||||
args: UpdateManyResolverArgs<Partial<Record>>,
|
args: UpdateManyResolverArgs<Partial<Record>>,
|
||||||
options: WorkspaceQueryRunnerOptions,
|
options: WorkspaceQueryRunnerOptions,
|
||||||
): Promise<Record[] | undefined> {
|
): Promise<Record[] | undefined> {
|
||||||
const { userId, workspaceId, objectMetadataItem } = options;
|
const { authContext, objectMetadataItem } = options;
|
||||||
const repository =
|
const repository =
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||||
workspaceId,
|
authContext.workspace.id,
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -471,27 +470,31 @@ export class WorkspaceQueryRunnerService {
|
|||||||
const maximumRecordAffected = this.environmentService.get(
|
const maximumRecordAffected = this.environmentService.get(
|
||||||
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
|
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
|
||||||
);
|
);
|
||||||
const query = await this.workspaceQueryBuilderFactory.updateMany(args, {
|
|
||||||
...options,
|
|
||||||
atMost: maximumRecordAffected,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
const hookedArgs =
|
||||||
userId,
|
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||||
workspaceId,
|
authContext,
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
'updateMany',
|
'updateMany',
|
||||||
args,
|
args,
|
||||||
|
);
|
||||||
|
|
||||||
|
const query = await this.workspaceQueryBuilderFactory.updateMany(
|
||||||
|
hookedArgs,
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
atMost: maximumRecordAffected,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await this.execute(query, workspaceId);
|
const result = await this.execute(query, authContext.workspace.id);
|
||||||
|
|
||||||
const parsedResults = (
|
const parsedResults = (
|
||||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||||
result,
|
result,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
'update',
|
'update',
|
||||||
workspaceId,
|
authContext.workspace.id,
|
||||||
)
|
)
|
||||||
)?.records;
|
)?.records;
|
||||||
|
|
||||||
@ -514,8 +517,8 @@ export class WorkspaceQueryRunnerService {
|
|||||||
|
|
||||||
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.updated`, {
|
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.updated`, {
|
||||||
name: `${objectMetadataItem.nameSingular}.updated`,
|
name: `${objectMetadataItem.nameSingular}.updated`,
|
||||||
workspaceId,
|
workspaceId: authContext.workspace.id,
|
||||||
userId,
|
userId: authContext.user?.id,
|
||||||
recordId: existingRecord.id,
|
recordId: existingRecord.id,
|
||||||
objectMetadata: objectMetadataItem,
|
objectMetadata: objectMetadataItem,
|
||||||
properties: {
|
properties: {
|
||||||
@ -536,34 +539,38 @@ export class WorkspaceQueryRunnerService {
|
|||||||
args: DeleteManyResolverArgs<Filter>,
|
args: DeleteManyResolverArgs<Filter>,
|
||||||
options: WorkspaceQueryRunnerOptions,
|
options: WorkspaceQueryRunnerOptions,
|
||||||
): Promise<Record[] | undefined> {
|
): Promise<Record[] | undefined> {
|
||||||
const { workspaceId, userId, objectMetadataItem } = options;
|
const { authContext, objectMetadataItem } = options;
|
||||||
|
|
||||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||||
|
|
||||||
const maximumRecordAffected = this.environmentService.get(
|
const maximumRecordAffected = this.environmentService.get(
|
||||||
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
|
'MUTATION_MAXIMUM_AFFECTED_RECORDS',
|
||||||
);
|
);
|
||||||
const query = await this.workspaceQueryBuilderFactory.deleteMany(args, {
|
|
||||||
...options,
|
|
||||||
atMost: maximumRecordAffected,
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
const hookedArgs =
|
||||||
userId,
|
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||||
workspaceId,
|
authContext,
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
'deleteMany',
|
'deleteMany',
|
||||||
args,
|
args,
|
||||||
|
);
|
||||||
|
|
||||||
|
const query = await this.workspaceQueryBuilderFactory.deleteMany(
|
||||||
|
hookedArgs,
|
||||||
|
{
|
||||||
|
...options,
|
||||||
|
atMost: maximumRecordAffected,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await this.execute(query, workspaceId);
|
const result = await this.execute(query, authContext.workspace.id);
|
||||||
|
|
||||||
const parsedResults = (
|
const parsedResults = (
|
||||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||||
result,
|
result,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
'deleteFrom',
|
'deleteFrom',
|
||||||
workspaceId,
|
authContext.workspace.id,
|
||||||
)
|
)
|
||||||
)?.records;
|
)?.records;
|
||||||
|
|
||||||
@ -576,8 +583,8 @@ export class WorkspaceQueryRunnerService {
|
|||||||
parsedResults.forEach((record) => {
|
parsedResults.forEach((record) => {
|
||||||
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
|
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
|
||||||
name: `${objectMetadataItem.nameSingular}.deleted`,
|
name: `${objectMetadataItem.nameSingular}.deleted`,
|
||||||
workspaceId,
|
workspaceId: authContext.workspace.id,
|
||||||
userId,
|
userId: authContext.user?.id,
|
||||||
recordId: record.id,
|
recordId: record.id,
|
||||||
objectMetadata: objectMetadataItem,
|
objectMetadata: objectMetadataItem,
|
||||||
properties: {
|
properties: {
|
||||||
@ -593,18 +600,26 @@ export class WorkspaceQueryRunnerService {
|
|||||||
args: DeleteOneResolverArgs,
|
args: DeleteOneResolverArgs,
|
||||||
options: WorkspaceQueryRunnerOptions,
|
options: WorkspaceQueryRunnerOptions,
|
||||||
): Promise<Record | undefined> {
|
): Promise<Record | undefined> {
|
||||||
const { workspaceId, userId, objectMetadataItem } = options;
|
const { authContext, objectMetadataItem } = options;
|
||||||
const repository =
|
const repository =
|
||||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
await this.twentyORMGlobalManager.getRepositoryForWorkspace(
|
||||||
workspaceId,
|
authContext.workspace.id,
|
||||||
objectMetadataItem.nameSingular,
|
objectMetadataItem.nameSingular,
|
||||||
);
|
);
|
||||||
|
|
||||||
assertMutationNotOnRemoteObject(objectMetadataItem);
|
assertMutationNotOnRemoteObject(objectMetadataItem);
|
||||||
assertIsValidUuid(args.id);
|
assertIsValidUuid(args.id);
|
||||||
|
|
||||||
|
const hookedArgs =
|
||||||
|
await this.workspaceQueryHookService.executePreQueryHooks(
|
||||||
|
authContext,
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
'deleteOne',
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.deleteOne(
|
const query = await this.workspaceQueryBuilderFactory.deleteOne(
|
||||||
args,
|
hookedArgs,
|
||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -612,22 +627,14 @@ export class WorkspaceQueryRunnerService {
|
|||||||
where: { id: args.id },
|
where: { id: args.id },
|
||||||
});
|
});
|
||||||
|
|
||||||
await this.workspaceQueryHookService.executePreQueryHooks(
|
const result = await this.execute(query, authContext.workspace.id);
|
||||||
userId,
|
|
||||||
workspaceId,
|
|
||||||
objectMetadataItem.nameSingular,
|
|
||||||
'deleteOne',
|
|
||||||
args,
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await this.execute(query, workspaceId);
|
|
||||||
|
|
||||||
const parsedResults = (
|
const parsedResults = (
|
||||||
await this.parseResult<PGGraphQLMutation<Record>>(
|
await this.parseResult<PGGraphQLMutation<Record>>(
|
||||||
result,
|
result,
|
||||||
objectMetadataItem,
|
objectMetadataItem,
|
||||||
'deleteFrom',
|
'deleteFrom',
|
||||||
workspaceId,
|
authContext.workspace.id,
|
||||||
)
|
)
|
||||||
)?.records;
|
)?.records;
|
||||||
|
|
||||||
@ -639,8 +646,8 @@ export class WorkspaceQueryRunnerService {
|
|||||||
|
|
||||||
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
|
this.eventEmitter.emit(`${objectMetadataItem.nameSingular}.deleted`, {
|
||||||
name: `${objectMetadataItem.nameSingular}.deleted`,
|
name: `${objectMetadataItem.nameSingular}.deleted`,
|
||||||
workspaceId,
|
workspaceId: authContext.workspace.id,
|
||||||
userId,
|
userId: authContext.user?.id,
|
||||||
recordId: args.id,
|
recordId: args.id,
|
||||||
objectMetadata: objectMetadataItem,
|
objectMetadata: objectMetadataItem,
|
||||||
properties: {
|
properties: {
|
||||||
@ -808,7 +815,7 @@ export class WorkspaceQueryRunnerService {
|
|||||||
CallWebhookJobsJob.name,
|
CallWebhookJobsJob.name,
|
||||||
{
|
{
|
||||||
record: jobData,
|
record: jobData,
|
||||||
workspaceId: options.workspaceId,
|
workspaceId: options.authContext.workspace.id,
|
||||||
operation,
|
operation,
|
||||||
objectMetadataItem: options.objectMetadataItem,
|
objectMetadataItem: options.objectMetadataItem,
|
||||||
},
|
},
|
||||||
@ -816,65 +823,4 @@ export class WorkspaceQueryRunnerService {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleDeleteWorkspaceMember(
|
|
||||||
id: string,
|
|
||||||
workspaceId: string,
|
|
||||||
objectMetadataItem: ObjectMetadataInterface,
|
|
||||||
) {
|
|
||||||
if (objectMetadataItem.nameSingular !== 'workspaceMember') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspaceMemberResult = await this.executeAndParse<IRecord>(
|
|
||||||
`
|
|
||||||
query {
|
|
||||||
workspaceMemberCollection(filter: {id: {eq: "${id}"}}) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
userId: userId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
objectMetadataItem,
|
|
||||||
'',
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return workspaceMemberResult.edges?.[0]?.node;
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleDeleteBlocklistItem(
|
|
||||||
id: string,
|
|
||||||
workspaceId: string,
|
|
||||||
objectMetadataItem: ObjectMetadataInterface,
|
|
||||||
) {
|
|
||||||
if (objectMetadataItem.standardId !== STANDARD_OBJECT_IDS.blocklist) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const blocklistItemResult = await this.executeAndParse<IRecord>(
|
|
||||||
`
|
|
||||||
query {
|
|
||||||
blocklistCollection(filter: {id: {eq: "${id}"}}) {
|
|
||||||
edges {
|
|
||||||
node {
|
|
||||||
handle
|
|
||||||
workspaceMember {
|
|
||||||
id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
objectMetadataItem,
|
|
||||||
'',
|
|
||||||
workspaceId,
|
|
||||||
);
|
|
||||||
|
|
||||||
return blocklistItemResult.edges?.[0]?.node;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,12 +25,11 @@ export class CreateManyResolverFactory
|
|||||||
): Resolver<CreateManyResolverArgs> {
|
): Resolver<CreateManyResolverArgs> {
|
||||||
const internalContext = context;
|
const internalContext = context;
|
||||||
|
|
||||||
return async (_source, args, context, info) => {
|
return (_source, args, context, info) => {
|
||||||
try {
|
try {
|
||||||
return await this.workspaceQueryRunnerService.createMany(args, {
|
return this.workspaceQueryRunnerService.createMany(args, {
|
||||||
|
authContext: internalContext.authContext,
|
||||||
objectMetadataItem: internalContext.objectMetadataItem,
|
objectMetadataItem: internalContext.objectMetadataItem,
|
||||||
workspaceId: internalContext.workspaceId,
|
|
||||||
userId: internalContext.userId,
|
|
||||||
info,
|
info,
|
||||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||||
|
|||||||
@ -25,12 +25,11 @@ export class CreateOneResolverFactory
|
|||||||
): Resolver<CreateOneResolverArgs> {
|
): Resolver<CreateOneResolverArgs> {
|
||||||
const internalContext = context;
|
const internalContext = context;
|
||||||
|
|
||||||
return async (_source, args, context, info) => {
|
return (_source, args, context, info) => {
|
||||||
try {
|
try {
|
||||||
return await this.workspaceQueryRunnerService.createOne(args, {
|
return this.workspaceQueryRunnerService.createOne(args, {
|
||||||
|
authContext: internalContext.authContext,
|
||||||
objectMetadataItem: internalContext.objectMetadataItem,
|
objectMetadataItem: internalContext.objectMetadataItem,
|
||||||
workspaceId: internalContext.workspaceId,
|
|
||||||
userId: internalContext.userId,
|
|
||||||
info,
|
info,
|
||||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||||
|
|||||||
@ -25,12 +25,11 @@ export class DeleteManyResolverFactory
|
|||||||
): Resolver<DeleteManyResolverArgs> {
|
): Resolver<DeleteManyResolverArgs> {
|
||||||
const internalContext = context;
|
const internalContext = context;
|
||||||
|
|
||||||
return async (_source, args, context, info) => {
|
return (_source, args, context, info) => {
|
||||||
try {
|
try {
|
||||||
return await this.workspaceQueryRunnerService.deleteMany(args, {
|
return this.workspaceQueryRunnerService.deleteMany(args, {
|
||||||
|
authContext: internalContext.authContext,
|
||||||
objectMetadataItem: internalContext.objectMetadataItem,
|
objectMetadataItem: internalContext.objectMetadataItem,
|
||||||
workspaceId: internalContext.workspaceId,
|
|
||||||
userId: internalContext.userId,
|
|
||||||
info,
|
info,
|
||||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||||
|
|||||||
@ -25,12 +25,11 @@ export class DeleteOneResolverFactory
|
|||||||
): Resolver<DeleteOneResolverArgs> {
|
): Resolver<DeleteOneResolverArgs> {
|
||||||
const internalContext = context;
|
const internalContext = context;
|
||||||
|
|
||||||
return async (_source, args, context, info) => {
|
return (_source, args, context, info) => {
|
||||||
try {
|
try {
|
||||||
return await this.workspaceQueryRunnerService.deleteOne(args, {
|
return this.workspaceQueryRunnerService.deleteOne(args, {
|
||||||
|
authContext: internalContext.authContext,
|
||||||
objectMetadataItem: internalContext.objectMetadataItem,
|
objectMetadataItem: internalContext.objectMetadataItem,
|
||||||
workspaceId: internalContext.workspaceId,
|
|
||||||
userId: internalContext.userId,
|
|
||||||
info,
|
info,
|
||||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||||
|
|||||||
@ -31,12 +31,11 @@ export class ExecuteQuickActionOnOneResolverFactory
|
|||||||
): Resolver<ExecuteQuickActionOnOneResolverArgs> {
|
): Resolver<ExecuteQuickActionOnOneResolverArgs> {
|
||||||
const internalContext = context;
|
const internalContext = context;
|
||||||
|
|
||||||
return async (_source, args, context, info) => {
|
return (_source, args, context, info) => {
|
||||||
try {
|
try {
|
||||||
return await this.executeQuickActionOnOne(args, {
|
return this.executeQuickActionOnOne(args, {
|
||||||
|
authContext: internalContext.authContext,
|
||||||
objectMetadataItem: internalContext.objectMetadataItem,
|
objectMetadataItem: internalContext.objectMetadataItem,
|
||||||
userId: internalContext.userId,
|
|
||||||
workspaceId: internalContext.workspaceId,
|
|
||||||
info,
|
info,
|
||||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||||
@ -55,7 +54,7 @@ export class ExecuteQuickActionOnOneResolverFactory
|
|||||||
case 'company': {
|
case 'company': {
|
||||||
await this.quickActionsService.executeQuickActionOnCompany(
|
await this.quickActionsService.executeQuickActionOnCompany(
|
||||||
args.id,
|
args.id,
|
||||||
options.workspaceId,
|
options.authContext.workspace.id,
|
||||||
options.objectMetadataItem,
|
options.objectMetadataItem,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@ -63,7 +62,7 @@ export class ExecuteQuickActionOnOneResolverFactory
|
|||||||
case 'person': {
|
case 'person': {
|
||||||
await this.quickActionsService.createCompanyFromPerson(
|
await this.quickActionsService.createCompanyFromPerson(
|
||||||
args.id,
|
args.id,
|
||||||
options.workspaceId,
|
options.authContext.workspace.id,
|
||||||
options.objectMetadataCollection,
|
options.objectMetadataCollection,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@ -25,12 +25,11 @@ export class FindDuplicatesResolverFactory
|
|||||||
): Resolver<FindDuplicatesResolverArgs> {
|
): Resolver<FindDuplicatesResolverArgs> {
|
||||||
const internalContext = context;
|
const internalContext = context;
|
||||||
|
|
||||||
return async (_source, args, context, info) => {
|
return (_source, args, context, info) => {
|
||||||
try {
|
try {
|
||||||
return await this.workspaceQueryRunnerService.findDuplicates(args, {
|
return this.workspaceQueryRunnerService.findDuplicates(args, {
|
||||||
|
authContext: internalContext.authContext,
|
||||||
objectMetadataItem: internalContext.objectMetadataItem,
|
objectMetadataItem: internalContext.objectMetadataItem,
|
||||||
workspaceId: internalContext.workspaceId,
|
|
||||||
userId: internalContext.userId,
|
|
||||||
info,
|
info,
|
||||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||||
|
|||||||
@ -25,12 +25,11 @@ export class FindManyResolverFactory
|
|||||||
): Resolver<FindManyResolverArgs> {
|
): Resolver<FindManyResolverArgs> {
|
||||||
const internalContext = context;
|
const internalContext = context;
|
||||||
|
|
||||||
return async (_source, args, context, info) => {
|
return (_source, args, context, info) => {
|
||||||
try {
|
try {
|
||||||
return await this.workspaceQueryRunnerService.findMany(args, {
|
return this.workspaceQueryRunnerService.findMany(args, {
|
||||||
|
authContext: internalContext.authContext,
|
||||||
objectMetadataItem: internalContext.objectMetadataItem,
|
objectMetadataItem: internalContext.objectMetadataItem,
|
||||||
workspaceId: internalContext.workspaceId,
|
|
||||||
userId: internalContext.userId,
|
|
||||||
info,
|
info,
|
||||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||||
|
|||||||
@ -25,12 +25,11 @@ export class FindOneResolverFactory
|
|||||||
): Resolver<FindOneResolverArgs> {
|
): Resolver<FindOneResolverArgs> {
|
||||||
const internalContext = context;
|
const internalContext = context;
|
||||||
|
|
||||||
return async (_source, args, context, info) => {
|
return (_source, args, context, info) => {
|
||||||
try {
|
try {
|
||||||
return await this.workspaceQueryRunnerService.findOne(args, {
|
return this.workspaceQueryRunnerService.findOne(args, {
|
||||||
|
authContext: internalContext.authContext,
|
||||||
objectMetadataItem: internalContext.objectMetadataItem,
|
objectMetadataItem: internalContext.objectMetadataItem,
|
||||||
workspaceId: internalContext.workspaceId,
|
|
||||||
userId: internalContext.userId,
|
|
||||||
info,
|
info,
|
||||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||||
|
|||||||
@ -25,12 +25,11 @@ export class UpdateManyResolverFactory
|
|||||||
): Resolver<UpdateManyResolverArgs> {
|
): Resolver<UpdateManyResolverArgs> {
|
||||||
const internalContext = context;
|
const internalContext = context;
|
||||||
|
|
||||||
return async (_source, args, context, info) => {
|
return (_source, args, context, info) => {
|
||||||
try {
|
try {
|
||||||
return await this.workspaceQueryRunnerService.updateMany(args, {
|
return this.workspaceQueryRunnerService.updateMany(args, {
|
||||||
|
authContext: internalContext.authContext,
|
||||||
objectMetadataItem: internalContext.objectMetadataItem,
|
objectMetadataItem: internalContext.objectMetadataItem,
|
||||||
workspaceId: internalContext.workspaceId,
|
|
||||||
userId: internalContext.userId,
|
|
||||||
info,
|
info,
|
||||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||||
|
|||||||
@ -25,12 +25,11 @@ export class UpdateOneResolverFactory
|
|||||||
): Resolver<UpdateOneResolverArgs> {
|
): Resolver<UpdateOneResolverArgs> {
|
||||||
const internalContext = context;
|
const internalContext = context;
|
||||||
|
|
||||||
return async (_source, args, context, info) => {
|
return (_source, args, context, info) => {
|
||||||
try {
|
try {
|
||||||
return await this.workspaceQueryRunnerService.updateOne(args, {
|
return this.workspaceQueryRunnerService.updateOne(args, {
|
||||||
|
authContext: internalContext.authContext,
|
||||||
objectMetadataItem: internalContext.objectMetadataItem,
|
objectMetadataItem: internalContext.objectMetadataItem,
|
||||||
workspaceId: internalContext.workspaceId,
|
|
||||||
userId: internalContext.userId,
|
|
||||||
info,
|
info,
|
||||||
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
fieldMetadataCollection: internalContext.fieldMetadataCollection,
|
||||||
objectMetadataCollection: internalContext.objectMetadataCollection,
|
objectMetadataCollection: internalContext.objectMetadataCollection,
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { getResolverName } from 'src/engine/utils/get-resolver-name.util';
|
|||||||
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory';
|
||||||
import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory';
|
import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory';
|
||||||
import { ExecuteQuickActionOnOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/execute-quick-action-on-one-resolver.factory';
|
import { ExecuteQuickActionOnOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/execute-quick-action-on-one-resolver.factory';
|
||||||
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
|
|
||||||
import { FindDuplicatesResolverFactory } from './factories/find-duplicates-resolver.factory';
|
import { FindDuplicatesResolverFactory } from './factories/find-duplicates-resolver.factory';
|
||||||
import { FindManyResolverFactory } from './factories/find-many-resolver.factory';
|
import { FindManyResolverFactory } from './factories/find-many-resolver.factory';
|
||||||
@ -40,8 +41,7 @@ export class WorkspaceResolverFactory {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async create(
|
async create(
|
||||||
workspaceId: string,
|
authContext: AuthContext,
|
||||||
userId: string | undefined,
|
|
||||||
objectMetadataCollection: ObjectMetadataInterface[],
|
objectMetadataCollection: ObjectMetadataInterface[],
|
||||||
workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods,
|
workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods,
|
||||||
): Promise<IResolvers> {
|
): Promise<IResolvers> {
|
||||||
@ -82,8 +82,7 @@ export class WorkspaceResolverFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolvers.Query[resolverName] = resolverFactory.create({
|
resolvers.Query[resolverName] = resolverFactory.create({
|
||||||
workspaceId,
|
authContext,
|
||||||
userId,
|
|
||||||
objectMetadataItem: objectMetadata,
|
objectMetadataItem: objectMetadata,
|
||||||
fieldMetadataCollection: objectMetadata.fields,
|
fieldMetadataCollection: objectMetadata.fields,
|
||||||
objectMetadataCollection: objectMetadataCollection,
|
objectMetadataCollection: objectMetadataCollection,
|
||||||
@ -106,8 +105,7 @@ export class WorkspaceResolverFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolvers.Mutation[resolverName] = resolverFactory.create({
|
resolvers.Mutation[resolverName] = resolverFactory.create({
|
||||||
workspaceId,
|
authContext,
|
||||||
userId,
|
|
||||||
objectMetadataItem: objectMetadata,
|
objectMetadataItem: objectMetadata,
|
||||||
fieldMetadataCollection: objectMetadata.fields,
|
fieldMetadataCollection: objectMetadata.fields,
|
||||||
objectMetadataCollection: objectMetadataCollection,
|
objectMetadataCollection: objectMetadataCollection,
|
||||||
|
|||||||
@ -0,0 +1,99 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { GraphQLEnumType } from 'graphql';
|
||||||
|
|
||||||
|
import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface';
|
||||||
|
import {
|
||||||
|
CompositeProperty,
|
||||||
|
CompositeType,
|
||||||
|
} from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
|
||||||
|
|
||||||
|
import { pascalCase } from 'src/utils/pascal-case';
|
||||||
|
import {
|
||||||
|
FieldMetadataComplexOption,
|
||||||
|
FieldMetadataDefaultOption,
|
||||||
|
} from 'src/engine/metadata-modules/field-metadata/dtos/options.input';
|
||||||
|
import { isEnumFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-enum-field-metadata-type.util';
|
||||||
|
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
|
||||||
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
import { computeCompositePropertyTarget } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-property-target.util';
|
||||||
|
|
||||||
|
export interface EnumTypeDefinition {
|
||||||
|
target: string;
|
||||||
|
type: GraphQLEnumType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class CompositeEnumTypeDefinitionFactory {
|
||||||
|
private readonly logger = new Logger(EnumTypeDefinitionFactory.name);
|
||||||
|
|
||||||
|
public create(
|
||||||
|
compositeType: CompositeType,
|
||||||
|
options: WorkspaceBuildSchemaOptions,
|
||||||
|
): EnumTypeDefinition[] {
|
||||||
|
const enumTypeDefinitions: EnumTypeDefinition[] = [];
|
||||||
|
|
||||||
|
for (const compositeProperty of compositeType.properties) {
|
||||||
|
if (!isEnumFieldMetadataType(compositeProperty.type)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = computeCompositePropertyTarget(
|
||||||
|
compositeType.type,
|
||||||
|
compositeProperty,
|
||||||
|
);
|
||||||
|
|
||||||
|
enumTypeDefinitions.push({
|
||||||
|
target,
|
||||||
|
type: this.generateEnum(compositeType.type, compositeProperty, options),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return enumTypeDefinitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
private generateEnum(
|
||||||
|
compositeType: FieldMetadataType,
|
||||||
|
compositeProperty: CompositeProperty,
|
||||||
|
options: WorkspaceBuildSchemaOptions,
|
||||||
|
): GraphQLEnumType {
|
||||||
|
// FixMe: It's a hack until Typescript get fixed on union types for reduce function
|
||||||
|
// https://github.com/microsoft/TypeScript/issues/36390
|
||||||
|
const enumOptions = compositeProperty.options as Array<
|
||||||
|
FieldMetadataDefaultOption | FieldMetadataComplexOption
|
||||||
|
>;
|
||||||
|
|
||||||
|
if (!enumOptions) {
|
||||||
|
this.logger.error(
|
||||||
|
`Enum options are not defined for ${compositeProperty.name}`,
|
||||||
|
{
|
||||||
|
compositeProperty,
|
||||||
|
options,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`Enum options are not defined for ${compositeProperty.name}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new GraphQLEnumType({
|
||||||
|
name: `${pascalCase(compositeType.toString())}${pascalCase(
|
||||||
|
compositeProperty.name,
|
||||||
|
)}Enum`,
|
||||||
|
description: compositeProperty.description,
|
||||||
|
values: enumOptions.reduce(
|
||||||
|
(acc, enumOption) => {
|
||||||
|
// Key must match this regex: /^[_A-Za-z][_0-9A-Za-z]+$/
|
||||||
|
acc[enumOption.value] = {
|
||||||
|
value: enumOption.value,
|
||||||
|
description: enumOption.label,
|
||||||
|
};
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as { [key: string]: { value: string; description: string } },
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,9 +12,15 @@ import {
|
|||||||
InputTypeDefinition,
|
InputTypeDefinition,
|
||||||
InputTypeDefinitionKind,
|
InputTypeDefinitionKind,
|
||||||
} from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
|
} from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory';
|
||||||
|
import { computeCompositePropertyTarget } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-property-target.util';
|
||||||
|
|
||||||
import { InputTypeFactory } from './input-type.factory';
|
import { InputTypeFactory } from './input-type.factory';
|
||||||
|
|
||||||
|
const hiddenAllowListKind = [
|
||||||
|
InputTypeDefinitionKind.Create,
|
||||||
|
InputTypeDefinitionKind.Update,
|
||||||
|
];
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CompositeInputTypeDefinitionFactory {
|
export class CompositeInputTypeDefinitionFactory {
|
||||||
private readonly logger = new Logger(
|
private readonly logger = new Logger(
|
||||||
@ -58,12 +64,19 @@ export class CompositeInputTypeDefinitionFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Skip hidden fields
|
// Skip hidden fields
|
||||||
if (property.hidden === true || property.hidden === 'input') {
|
if (
|
||||||
|
property.hidden === true ||
|
||||||
|
(property.hidden === 'input' && hiddenAllowListKind.includes(kind))
|
||||||
|
) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const target = computeCompositePropertyTarget(
|
||||||
|
compositeType.type,
|
||||||
|
property,
|
||||||
|
);
|
||||||
const type = this.inputTypeFactory.create(
|
const type = this.inputTypeFactory.create(
|
||||||
property.name,
|
target,
|
||||||
property.type,
|
property.type,
|
||||||
kind,
|
kind,
|
||||||
options,
|
options,
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import {
|
|||||||
ObjectTypeDefinitionKind,
|
ObjectTypeDefinitionKind,
|
||||||
} from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory';
|
} from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory';
|
||||||
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util';
|
||||||
|
import { computeCompositePropertyTarget } from 'src/engine/api/graphql/workspace-schema-builder/utils/compute-composite-property-target.util';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class CompositeObjectTypeDefinitionFactory {
|
export class CompositeObjectTypeDefinitionFactory {
|
||||||
@ -62,8 +63,12 @@ export class CompositeObjectTypeDefinitionFactory {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const target = computeCompositePropertyTarget(
|
||||||
|
compositeType.type,
|
||||||
|
property,
|
||||||
|
);
|
||||||
const type = this.outputTypeFactory.create(
|
const type = this.outputTypeFactory.create(
|
||||||
property.name,
|
target,
|
||||||
property.type,
|
property.type,
|
||||||
kind,
|
kind,
|
||||||
options,
|
options,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
|
import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/enum-type-definition.factory';
|
||||||
import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory';
|
import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory';
|
||||||
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
|
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
|
||||||
|
import { CompositeEnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-enum-type-definition.factory';
|
||||||
|
|
||||||
import { ArgsFactory } from './args.factory';
|
import { ArgsFactory } from './args.factory';
|
||||||
import { InputTypeFactory } from './input-type.factory';
|
import { InputTypeFactory } from './input-type.factory';
|
||||||
@ -27,6 +28,7 @@ export const workspaceSchemaBuilderFactories = [
|
|||||||
ObjectTypeDefinitionFactory,
|
ObjectTypeDefinitionFactory,
|
||||||
CompositeObjectTypeDefinitionFactory,
|
CompositeObjectTypeDefinitionFactory,
|
||||||
EnumTypeDefinitionFactory,
|
EnumTypeDefinitionFactory,
|
||||||
|
CompositeEnumTypeDefinitionFactory,
|
||||||
RelationTypeFactory,
|
RelationTypeFactory,
|
||||||
ExtendObjectTypeDefinitionFactory,
|
ExtendObjectTypeDefinitionFactory,
|
||||||
ConnectionTypeFactory,
|
ConnectionTypeFactory,
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface';
|
||||||
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface';
|
||||||
|
|
||||||
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
|
|
||||||
export interface WorkspaceSchemaBuilderContext {
|
export interface WorkspaceSchemaBuilderContext {
|
||||||
workspaceId: string;
|
authContext: AuthContext;
|
||||||
userId: string | undefined;
|
|
||||||
objectMetadataItem: ObjectMetadataInterface;
|
objectMetadataItem: ObjectMetadataInterface;
|
||||||
fieldMetadataCollection: FieldMetadataInterface[];
|
fieldMetadataCollection: FieldMetadataInterface[];
|
||||||
objectMetadataCollection: ObjectMetadataInterface[];
|
objectMetadataCollection: ObjectMetadataInterface[];
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { EnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-sche
|
|||||||
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
import { compositeTypeDefintions } from 'src/engine/metadata-modules/field-metadata/composite-types';
|
||||||
import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory';
|
import { CompositeObjectTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-object-type-definition.factory';
|
||||||
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
|
import { CompositeInputTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-input-type-definition.factory';
|
||||||
|
import { CompositeEnumTypeDefinitionFactory } from 'src/engine/api/graphql/workspace-schema-builder/factories/composite-enum-type-definition.factory';
|
||||||
|
|
||||||
import { TypeDefinitionsStorage } from './storages/type-definitions.storage';
|
import { TypeDefinitionsStorage } from './storages/type-definitions.storage';
|
||||||
import {
|
import {
|
||||||
@ -32,6 +33,7 @@ export class TypeDefinitionsGenerator {
|
|||||||
private readonly objectTypeDefinitionFactory: ObjectTypeDefinitionFactory,
|
private readonly objectTypeDefinitionFactory: ObjectTypeDefinitionFactory,
|
||||||
private readonly compositeObjectTypeDefinitionFactory: CompositeObjectTypeDefinitionFactory,
|
private readonly compositeObjectTypeDefinitionFactory: CompositeObjectTypeDefinitionFactory,
|
||||||
private readonly enumTypeDefinitionFactory: EnumTypeDefinitionFactory,
|
private readonly enumTypeDefinitionFactory: EnumTypeDefinitionFactory,
|
||||||
|
private readonly compositeEnumTypeDefinitionFactory: CompositeEnumTypeDefinitionFactory,
|
||||||
private readonly inputTypeDefinitionFactory: InputTypeDefinitionFactory,
|
private readonly inputTypeDefinitionFactory: InputTypeDefinitionFactory,
|
||||||
private readonly compositeInputTypeDefinitionFactory: CompositeInputTypeDefinitionFactory,
|
private readonly compositeInputTypeDefinitionFactory: CompositeInputTypeDefinitionFactory,
|
||||||
private readonly edgeTypeDefinitionFactory: EdgeTypeDefinitionFactory,
|
private readonly edgeTypeDefinitionFactory: EdgeTypeDefinitionFactory,
|
||||||
@ -62,10 +64,24 @@ export class TypeDefinitionsGenerator {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Generate composite types first because they can be used in metadata objects
|
// Generate composite types first because they can be used in metadata objects
|
||||||
|
this.generateCompositeEnumTypeDefs(compositeTypeCollection, options);
|
||||||
this.generateCompositeObjectTypeDefs(compositeTypeCollection, options);
|
this.generateCompositeObjectTypeDefs(compositeTypeCollection, options);
|
||||||
this.generateCompositeInputTypeDefs(compositeTypeCollection, options);
|
this.generateCompositeInputTypeDefs(compositeTypeCollection, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private generateCompositeEnumTypeDefs(
|
||||||
|
compositeTypes: CompositeType[],
|
||||||
|
options: WorkspaceBuildSchemaOptions,
|
||||||
|
) {
|
||||||
|
const enumTypeDefs = compositeTypes
|
||||||
|
.map((compositeType) =>
|
||||||
|
this.compositeEnumTypeDefinitionFactory.create(compositeType, options),
|
||||||
|
)
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
this.typeDefinitionsStorage.addEnumTypes(enumTypeDefs);
|
||||||
|
}
|
||||||
|
|
||||||
private generateCompositeObjectTypeDefs(
|
private generateCompositeObjectTypeDefs(
|
||||||
compositeTypes: CompositeType[],
|
compositeTypes: CompositeType[],
|
||||||
options: WorkspaceBuildSchemaOptions,
|
options: WorkspaceBuildSchemaOptions,
|
||||||
|
|||||||
@ -0,0 +1,10 @@
|
|||||||
|
import { CompositeProperty } from 'src/engine/metadata-modules/field-metadata/interfaces/composite-type.interface';
|
||||||
|
|
||||||
|
import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity';
|
||||||
|
|
||||||
|
export const computeCompositePropertyTarget = (
|
||||||
|
type: FieldMetadataType,
|
||||||
|
compositeProperty: CompositeProperty,
|
||||||
|
): string => {
|
||||||
|
return `${type.toString()}->${compositeProperty.name}`;
|
||||||
|
};
|
||||||
@ -11,6 +11,7 @@ import { ScalarsExplorerService } from 'src/engine/api/graphql/services/scalars-
|
|||||||
import { WorkspaceGraphQLSchemaFactory } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory';
|
import { WorkspaceGraphQLSchemaFactory } from 'src/engine/api/graphql/workspace-schema-builder/workspace-graphql-schema.factory';
|
||||||
import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories';
|
import { workspaceResolverBuilderMethodNames } from 'src/engine/api/graphql/workspace-resolver-builder/factories/factories';
|
||||||
import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory';
|
import { WorkspaceResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory';
|
||||||
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WorkspaceSchemaFactory {
|
export class WorkspaceSchemaFactory {
|
||||||
@ -23,17 +24,14 @@ export class WorkspaceSchemaFactory {
|
|||||||
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
private readonly workspaceCacheStorageService: WorkspaceCacheStorageService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async createGraphQLSchema(
|
async createGraphQLSchema(authContext: AuthContext): Promise<GraphQLSchema> {
|
||||||
workspaceId: string | undefined,
|
if (!authContext.workspace?.id) {
|
||||||
userId: string | undefined,
|
|
||||||
): Promise<GraphQLSchema> {
|
|
||||||
if (!workspaceId) {
|
|
||||||
return new GraphQLSchema({});
|
return new GraphQLSchema({});
|
||||||
}
|
}
|
||||||
|
|
||||||
const dataSourcesMetadata =
|
const dataSourcesMetadata =
|
||||||
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
|
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
|
||||||
workspaceId,
|
authContext.workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Can'f find any data sources for this workspace
|
// Can'f find any data sources for this workspace
|
||||||
@ -42,30 +40,37 @@ export class WorkspaceSchemaFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate cache version
|
// Validate cache version
|
||||||
await this.workspaceCacheStorageService.validateCacheVersion(workspaceId);
|
await this.workspaceCacheStorageService.validateCacheVersion(
|
||||||
|
authContext.workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
// Get object metadata from cache
|
// Get object metadata from cache
|
||||||
let objectMetadataCollection =
|
let objectMetadataCollection =
|
||||||
await this.workspaceCacheStorageService.getObjectMetadataCollection(
|
await this.workspaceCacheStorageService.getObjectMetadataCollection(
|
||||||
workspaceId,
|
authContext.workspace.id,
|
||||||
);
|
);
|
||||||
|
|
||||||
// If object metadata is not cached, get it from the database
|
// If object metadata is not cached, get it from the database
|
||||||
if (!objectMetadataCollection) {
|
if (!objectMetadataCollection) {
|
||||||
objectMetadataCollection =
|
objectMetadataCollection =
|
||||||
await this.objectMetadataService.findManyWithinWorkspace(workspaceId);
|
await this.objectMetadataService.findManyWithinWorkspace(
|
||||||
|
authContext.workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
await this.workspaceCacheStorageService.setObjectMetadataCollection(
|
await this.workspaceCacheStorageService.setObjectMetadataCollection(
|
||||||
workspaceId,
|
authContext.workspace.id,
|
||||||
objectMetadataCollection,
|
objectMetadataCollection,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get typeDefs from cache
|
// Get typeDefs from cache
|
||||||
let typeDefs =
|
let typeDefs = await this.workspaceCacheStorageService.getTypeDefs(
|
||||||
await this.workspaceCacheStorageService.getTypeDefs(workspaceId);
|
authContext.workspace.id,
|
||||||
|
);
|
||||||
let usedScalarNames =
|
let usedScalarNames =
|
||||||
await this.workspaceCacheStorageService.getUsedScalarNames(workspaceId);
|
await this.workspaceCacheStorageService.getUsedScalarNames(
|
||||||
|
authContext.workspace.id,
|
||||||
|
);
|
||||||
|
|
||||||
// If typeDefs are not cached, generate them
|
// If typeDefs are not cached, generate them
|
||||||
if (!typeDefs || !usedScalarNames) {
|
if (!typeDefs || !usedScalarNames) {
|
||||||
@ -80,18 +85,17 @@ export class WorkspaceSchemaFactory {
|
|||||||
typeDefs = printSchema(autoGeneratedSchema);
|
typeDefs = printSchema(autoGeneratedSchema);
|
||||||
|
|
||||||
await this.workspaceCacheStorageService.setTypeDefs(
|
await this.workspaceCacheStorageService.setTypeDefs(
|
||||||
workspaceId,
|
authContext.workspace.id,
|
||||||
typeDefs,
|
typeDefs,
|
||||||
);
|
);
|
||||||
await this.workspaceCacheStorageService.setUsedScalarNames(
|
await this.workspaceCacheStorageService.setUsedScalarNames(
|
||||||
workspaceId,
|
authContext.workspace.id,
|
||||||
usedScalarNames,
|
usedScalarNames,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoGeneratedResolvers = await this.workspaceResolverFactory.create(
|
const autoGeneratedResolvers = await this.workspaceResolverFactory.create(
|
||||||
workspaceId,
|
authContext,
|
||||||
userId,
|
|
||||||
objectMetadataCollection,
|
objectMetadataCollection,
|
||||||
workspaceResolverBuilderMethodNames,
|
workspaceResolverBuilderMethodNames,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -135,5 +135,14 @@ export const mapFieldMetadataToGraphqlQuery = (
|
|||||||
addressLng
|
addressLng
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
} else if (fieldType === FieldMetadataType.ACTOR) {
|
||||||
|
return `
|
||||||
|
${field.name}
|
||||||
|
{
|
||||||
|
source
|
||||||
|
workspaceMemberId
|
||||||
|
name
|
||||||
|
}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -39,6 +39,7 @@ import {
|
|||||||
AppToken,
|
AppToken,
|
||||||
AppTokenType,
|
AppTokenType,
|
||||||
} from 'src/engine/core-modules/app-token/app-token.entity';
|
} from 'src/engine/core-modules/app-token/app-token.entity';
|
||||||
|
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||||
|
|
||||||
import { TokenService } from './token.service';
|
import { TokenService } from './token.service';
|
||||||
|
|
||||||
@ -131,7 +132,7 @@ export class AuthService {
|
|||||||
const workspaceMember = await this.userService.loadWorkspaceMember(user);
|
const workspaceMember = await this.userService.loadWorkspaceMember(user);
|
||||||
|
|
||||||
if (workspaceMember) {
|
if (workspaceMember) {
|
||||||
user.workspaceMember = workspaceMember;
|
user.workspaceMember = workspaceMember as WorkspaceMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
const accessToken = await this.tokenService.generateAccessToken(user.id);
|
const accessToken = await this.tokenService.generateAccessToken(user.id);
|
||||||
|
|||||||
@ -42,7 +42,7 @@ import { ValidatePasswordResetToken } from 'src/engine/core-modules/auth/dto/val
|
|||||||
import { EmailService } from 'src/engine/integrations/email/email.service';
|
import { EmailService } from 'src/engine/integrations/email/email.service';
|
||||||
import { InvalidatePassword } from 'src/engine/core-modules/auth/dto/invalidate-password.entity';
|
import { InvalidatePassword } from 'src/engine/core-modules/auth/dto/invalidate-password.entity';
|
||||||
import { EmailPasswordResetLink } from 'src/engine/core-modules/auth/dto/email-password-reset-link.entity';
|
import { EmailPasswordResetLink } from 'src/engine/core-modules/auth/dto/email-password-reset-link.entity';
|
||||||
import { JwtData } from 'src/engine/core-modules/auth/types/jwt-data.type';
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { ExchangeAuthCodeInput } from 'src/engine/core-modules/auth/dto/exchange-auth-code.input';
|
import { ExchangeAuthCodeInput } from 'src/engine/core-modules/auth/dto/exchange-auth-code.input';
|
||||||
import { ExchangeAuthCode } from 'src/engine/core-modules/auth/dto/exchange-auth-code.entity';
|
import { ExchangeAuthCode } from 'src/engine/core-modules/auth/dto/exchange-auth-code.entity';
|
||||||
@ -208,7 +208,7 @@ export class TokenService {
|
|||||||
return !!token;
|
return !!token;
|
||||||
}
|
}
|
||||||
|
|
||||||
async validateToken(request: Request): Promise<JwtData> {
|
async validateToken(request: Request): Promise<AuthContext> {
|
||||||
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
|
const token = ExtractJwt.fromAuthHeaderAsBearerToken()(request);
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
@ -219,11 +219,11 @@ export class TokenService {
|
|||||||
this.environmentService.get('ACCESS_TOKEN_SECRET'),
|
this.environmentService.get('ACCESS_TOKEN_SECRET'),
|
||||||
);
|
);
|
||||||
|
|
||||||
const { user, workspace } = await this.jwtStrategy.validate(
|
const { user, apiKey, workspace } = await this.jwtStrategy.validate(
|
||||||
decoded as JwtPayload,
|
decoded as JwtPayload,
|
||||||
);
|
);
|
||||||
|
|
||||||
return { user, workspace };
|
return { user, apiKey, workspace };
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyLoginToken(loginToken: string): Promise<string> {
|
async verifyLoginToken(loginToken: string): Promise<string> {
|
||||||
|
|||||||
@ -15,9 +15,10 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|||||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||||
import { assert } from 'src/utils/assert';
|
import { assert } from 'src/utils/assert';
|
||||||
|
import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type';
|
||||||
|
import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity';
|
||||||
|
|
||||||
export type JwtPayload = { sub: string; workspaceId: string; jti?: string };
|
export type JwtPayload = { sub: string; workspaceId: string; jti?: string };
|
||||||
export type PassportUser = { user?: User; workspace: Workspace };
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||||
@ -37,15 +38,24 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async validate(payload: JwtPayload): Promise<PassportUser> {
|
async validate(payload: JwtPayload): Promise<AuthContext> {
|
||||||
const workspace = await this.workspaceRepository.findOneBy({
|
const workspace = await this.workspaceRepository.findOneBy({
|
||||||
id: payload.workspaceId ?? payload.sub,
|
id: payload.workspaceId ?? payload.sub,
|
||||||
});
|
});
|
||||||
|
let user: User | null = null;
|
||||||
|
let apiKey: ApiKeyWorkspaceEntity | null = null;
|
||||||
|
|
||||||
if (!workspace) {
|
if (!workspace) {
|
||||||
throw new UnauthorizedException();
|
throw new UnauthorizedException();
|
||||||
}
|
}
|
||||||
if (payload.jti) {
|
if (payload.jti) {
|
||||||
|
// TODO: Check why it's not working
|
||||||
|
// const apiKeyRepository =
|
||||||
|
// await this.twentyORMGlobalManager.getRepositoryForWorkspace<ApiKeyWorkspaceEntity>(
|
||||||
|
// workspace.id,
|
||||||
|
// 'apiKey',
|
||||||
|
// );
|
||||||
|
|
||||||
const dataSourceMetadata =
|
const dataSourceMetadata =
|
||||||
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceIdOrFail(
|
||||||
workspace.id,
|
workspace.id,
|
||||||
@ -54,20 +64,20 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|||||||
const workspaceDataSource =
|
const workspaceDataSource =
|
||||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
||||||
|
|
||||||
const apiKey = await workspaceDataSource?.query(
|
const res = await workspaceDataSource?.query(
|
||||||
`SELECT * FROM ${dataSourceMetadata.schema}."apiKey" WHERE id = $1`,
|
`SELECT * FROM ${dataSourceMetadata.schema}."apiKey" WHERE id = $1`,
|
||||||
[payload.jti],
|
[payload.jti],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
apiKey = res?.[0];
|
||||||
|
|
||||||
assert(
|
assert(
|
||||||
apiKey.length === 1 && !apiKey?.[0].revokedAt,
|
apiKey && !apiKey.revokedAt,
|
||||||
'This API Key is revoked',
|
'This API Key is revoked',
|
||||||
ForbiddenException,
|
ForbiddenException,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let user;
|
|
||||||
|
|
||||||
if (payload.workspaceId) {
|
if (payload.workspaceId) {
|
||||||
user = await this.userRepository.findOne({
|
user = await this.userRepository.findOne({
|
||||||
where: { id: payload.sub },
|
where: { id: payload.sub },
|
||||||
@ -78,6 +88,6 @@ export class JwtAuthStrategy extends PassportStrategy(Strategy, 'jwt') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { user, workspace };
|
return { user, apiKey, workspace };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
|
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
|
import { ApiKeyWorkspaceEntity } from 'src/modules/api-key/standard-objects/api-key.workspace-entity';
|
||||||
|
|
||||||
|
export type AuthContext = {
|
||||||
|
user?: User | null | undefined;
|
||||||
|
apiKey?: ApiKeyWorkspaceEntity | null | undefined;
|
||||||
|
workspace: Workspace;
|
||||||
|
};
|
||||||
@ -1,7 +0,0 @@
|
|||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
|
||||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
|
||||||
|
|
||||||
export type JwtData = {
|
|
||||||
user?: User | undefined;
|
|
||||||
workspace: Workspace;
|
|
||||||
};
|
|
||||||
@ -137,6 +137,21 @@ describe('computeSchemaComponents', () => {
|
|||||||
fieldRichText: {
|
fieldRichText: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
},
|
},
|
||||||
|
fieldActor: {
|
||||||
|
properties: {
|
||||||
|
source: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
workspaceMemberId: {
|
||||||
|
type: 'string',
|
||||||
|
format: 'uuid',
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
type: 'object',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'ObjectName with Relations': {
|
'ObjectName with Relations': {
|
||||||
|
|||||||
@ -71,6 +71,7 @@ const getSchemaComponentsProperties = (
|
|||||||
case FieldMetadataType.CURRENCY:
|
case FieldMetadataType.CURRENCY:
|
||||||
case FieldMetadataType.FULL_NAME:
|
case FieldMetadataType.FULL_NAME:
|
||||||
case FieldMetadataType.ADDRESS:
|
case FieldMetadataType.ADDRESS:
|
||||||
|
case FieldMetadataType.ACTOR:
|
||||||
itemProperty = {
|
itemProperty = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: compositeTypeDefintions
|
properties: compositeTypeDefintions
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-
|
|||||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||||
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
||||||
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
|
|
||||||
describe('UserService', () => {
|
describe('UserService', () => {
|
||||||
let service: UserService;
|
let service: UserService;
|
||||||
@ -40,6 +41,10 @@ describe('UserService', () => {
|
|||||||
provide: WorkspaceService,
|
provide: WorkspaceService,
|
||||||
useValue: {},
|
useValue: {},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: TwentyORMGlobalManager,
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
|
|||||||
@ -1,18 +1,22 @@
|
|||||||
import { EventEmitter2 } from '@nestjs/event-emitter';
|
import { EventEmitter2 } from '@nestjs/event-emitter';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
|
||||||
|
import assert from 'assert';
|
||||||
|
|
||||||
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
|
||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
|
|
||||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||||
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
|
||||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||||
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
|
||||||
|
import {
|
||||||
|
Workspace,
|
||||||
|
WorkspaceActivationStatus,
|
||||||
|
} from 'src/engine/core-modules/workspace/workspace.entity';
|
||||||
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
|
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
|
||||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
|
||||||
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
|
||||||
|
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||||
import { assert } from 'src/utils/assert';
|
|
||||||
|
|
||||||
export class UserService extends TypeOrmQueryService<User> {
|
export class UserService extends TypeOrmQueryService<User> {
|
||||||
constructor(
|
constructor(
|
||||||
@ -22,73 +26,46 @@ export class UserService extends TypeOrmQueryService<User> {
|
|||||||
private readonly typeORMService: TypeORMService,
|
private readonly typeORMService: TypeORMService,
|
||||||
private readonly eventEmitter: EventEmitter2,
|
private readonly eventEmitter: EventEmitter2,
|
||||||
private readonly workspaceService: WorkspaceService,
|
private readonly workspaceService: WorkspaceService,
|
||||||
|
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||||
) {
|
) {
|
||||||
super(userRepository);
|
super(userRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadWorkspaceMember(user: User) {
|
async loadWorkspaceMember(user: User) {
|
||||||
const dataSourcesMetadata =
|
if (
|
||||||
await this.dataSourceService.getDataSourcesMetadataFromWorkspaceId(
|
user.defaultWorkspace.activationStatus !==
|
||||||
user.defaultWorkspace.id,
|
WorkspaceActivationStatus.ACTIVE
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const workspaceMemberRepository =
|
||||||
|
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
|
||||||
|
user.defaultWorkspaceId,
|
||||||
|
'workspaceMember',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!dataSourcesMetadata.length) {
|
const workspaceMember = await workspaceMemberRepository.findOne({
|
||||||
return;
|
where: {
|
||||||
}
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (dataSourcesMetadata.length > 1) {
|
return workspaceMember;
|
||||||
throw new Error(
|
|
||||||
`user '${user.id}' default workspace '${user.defaultWorkspace.id}' has multiple data source metadata`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dataSourceMetadata = dataSourcesMetadata[0];
|
|
||||||
|
|
||||||
const workspaceDataSource =
|
|
||||||
await this.typeORMService.connectToDataSource(dataSourceMetadata);
|
|
||||||
|
|
||||||
const workspaceMembers = await workspaceDataSource?.query(
|
|
||||||
`SELECT * FROM ${dataSourceMetadata.schema}."workspaceMember" WHERE "userId" = '${user.id}'`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!workspaceMembers.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(
|
|
||||||
workspaceMembers.length === 1,
|
|
||||||
'WorkspaceMember not found or too many found',
|
|
||||||
);
|
|
||||||
|
|
||||||
const userWorkspaceMember = new WorkspaceMember();
|
|
||||||
|
|
||||||
userWorkspaceMember.id = workspaceMembers[0].id;
|
|
||||||
userWorkspaceMember.colorScheme = workspaceMembers[0].colorScheme;
|
|
||||||
userWorkspaceMember.locale = workspaceMembers[0].locale;
|
|
||||||
userWorkspaceMember.avatarUrl = workspaceMembers[0].avatarUrl;
|
|
||||||
userWorkspaceMember.name = {
|
|
||||||
firstName: workspaceMembers[0].nameFirstName,
|
|
||||||
lastName: workspaceMembers[0].nameLastName,
|
|
||||||
};
|
|
||||||
userWorkspaceMember.timeZone = workspaceMembers[0].timeZone;
|
|
||||||
userWorkspaceMember.dateFormat = workspaceMembers[0].dateFormat;
|
|
||||||
userWorkspaceMember.timeFormat = workspaceMembers[0].timeFormat;
|
|
||||||
|
|
||||||
return userWorkspaceMember;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadWorkspaceMembers(dataSource: DataSourceEntity) {
|
async loadWorkspaceMembers(workspace: Workspace) {
|
||||||
const workspaceDataSource =
|
if (workspace.activationStatus !== WorkspaceActivationStatus.ACTIVE) {
|
||||||
await this.typeORMService.connectToDataSource(dataSource);
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
return await workspaceDataSource?.query(
|
const workspaceMemberRepository =
|
||||||
`
|
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
|
||||||
SELECT *
|
workspace.id,
|
||||||
FROM ${dataSource.schema}."workspaceMember" AS s
|
'workspaceMember',
|
||||||
INNER JOIN core.user AS u
|
);
|
||||||
ON s."userId" = u.id
|
|
||||||
`,
|
return workspaceMemberRepository.find();
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteUser(userId: string): Promise<User> {
|
async deleteUser(userId: string): Promise<User> {
|
||||||
@ -136,6 +113,7 @@ export class UserService extends TypeOrmQueryService<User> {
|
|||||||
payload.properties = {
|
payload.properties = {
|
||||||
before: workspaceMember,
|
before: workspaceMember,
|
||||||
};
|
};
|
||||||
|
payload.name = 'workspaceMember.deleted';
|
||||||
payload.recordId = workspaceMember.id;
|
payload.recordId = workspaceMember.id;
|
||||||
|
|
||||||
this.eventEmitter.emit('workspaceMember.deleted', payload);
|
this.eventEmitter.emit('workspaceMember.deleted', payload);
|
||||||
|
|||||||
@ -112,9 +112,7 @@ export class UserResolver {
|
|||||||
@ResolveField(() => WorkspaceMember, {
|
@ResolveField(() => WorkspaceMember, {
|
||||||
nullable: true,
|
nullable: true,
|
||||||
})
|
})
|
||||||
async workspaceMember(
|
async workspaceMember(@Parent() user: User): Promise<WorkspaceMember | null> {
|
||||||
@Parent() user: User,
|
|
||||||
): Promise<WorkspaceMember | undefined> {
|
|
||||||
const workspaceMember = await this.userService.loadWorkspaceMember(user);
|
const workspaceMember = await this.userService.loadWorkspaceMember(user);
|
||||||
|
|
||||||
if (workspaceMember && workspaceMember.avatarUrl) {
|
if (workspaceMember && workspaceMember.avatarUrl) {
|
||||||
@ -126,7 +124,31 @@ export class UserResolver {
|
|||||||
workspaceMember.avatarUrl = `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`;
|
workspaceMember.avatarUrl = `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return workspaceMember;
|
// TODO: Fix typing disrepency between Entity and DTO
|
||||||
|
return workspaceMember as WorkspaceMember | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => [WorkspaceMember], {
|
||||||
|
nullable: true,
|
||||||
|
})
|
||||||
|
async workspaceMembers(@Parent() user: User): Promise<WorkspaceMember[]> {
|
||||||
|
const workspaceMembers = await this.userService.loadWorkspaceMembers(
|
||||||
|
user.defaultWorkspace,
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const workspaceMember of workspaceMembers) {
|
||||||
|
if (workspaceMember.avatarUrl) {
|
||||||
|
const avatarUrlToken = await this.fileService.encodeFileToken({
|
||||||
|
workspace_member_id: workspaceMember.id,
|
||||||
|
workspace_id: user.defaultWorkspaceId,
|
||||||
|
});
|
||||||
|
|
||||||
|
workspaceMember.avatarUrl = `${workspaceMember.avatarUrl}?token=${avatarUrlToken}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Fix typing disrepency between Entity and DTO
|
||||||
|
return workspaceMembers as WorkspaceMember[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ResolveField(() => String, {
|
@ResolveField(() => String, {
|
||||||
|
|||||||
@ -8,5 +8,5 @@ export interface ExceptionHandlerOptions {
|
|||||||
name: string;
|
name: string;
|
||||||
};
|
};
|
||||||
document?: string;
|
document?: string;
|
||||||
user?: ExceptionHandlerUser;
|
user?: ExceptionHandlerUser | null;
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user