Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
12bd695c59 | ||
|
|
17347736be | ||
|
|
dfe0b5bfd4 | ||
|
|
186d5b8faa | ||
|
|
011afa6011 | ||
|
|
a2fa941ce3 | ||
|
|
898713bd49 | ||
|
|
4658d44d8b | ||
|
|
a596e7d904 | ||
|
|
91f2f08995 | ||
|
|
6c65d26ced | ||
|
|
bfb83e93b2 | ||
|
|
20d9244639 | ||
|
|
51e3eddb29 | ||
|
|
27db8bbae4 | ||
|
|
84ba7eb8dc | ||
|
|
a3d73740a1 | ||
|
|
0d3c7a47af | ||
|
|
2dbdd72e65 | ||
|
|
f20d04eb6e | ||
|
|
f72898063a |
@@ -4,7 +4,7 @@
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<h2 align="center" >The #1 Open-Source CRM</h2>
|
||||
<h2 align="center">The #1 Open-Source CRM</h2>
|
||||
|
||||
<p align="center"><a href="https://twenty.com"><img src="./packages/twenty-website/public/images/readme/globe-icon.svg" width="12" height="12"/> Website</a> · <a href="https://docs.twenty.com"><img src="./packages/twenty-website/public/images/readme/book-icon.svg" width="12" height="12"/> Documentation</a> · <a href="https://github.com/orgs/twentyhq/projects/1"><img src="./packages/twenty-website/public/images/readme/map-icon.svg" width="12" height="12"/> Roadmap </a> · <a href="https://discord.gg/cx5n4Jzs57"><img src="./packages/twenty-website/public/images/readme/discord-icon.svg" width="12" height="12"/> Discord</a> · <a href="https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty"><img src="./packages/twenty-website/public/images/readme/figma-icon.webp" width="12" height="12"/> Figma</a></p>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "2.10.0",
|
||||
"version": "2.11.0",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-client-sdk",
|
||||
"version": "2.10.0",
|
||||
"version": "2.11.0",
|
||||
"sideEffects": false,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
|
||||
@@ -1434,11 +1434,6 @@ type EnterpriseSubscriptionStatusDTO {
|
||||
isCancellationScheduled: Boolean!
|
||||
}
|
||||
|
||||
type Analytics {
|
||||
"""Boolean that confirms query was dispatched"""
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type VerificationRecord {
|
||||
type: String!
|
||||
key: String!
|
||||
@@ -2621,6 +2616,11 @@ type SendEmailOutput {
|
||||
error: String
|
||||
}
|
||||
|
||||
type Analytics {
|
||||
"""Boolean that confirms query was dispatched"""
|
||||
success: Boolean!
|
||||
}
|
||||
|
||||
type EventLogRecord {
|
||||
event: String!
|
||||
timestamp: DateTime!
|
||||
@@ -3007,7 +3007,6 @@ type Query {
|
||||
): IndexConnection!
|
||||
findManyAgents: [Agent!]!
|
||||
findOneAgent(input: AgentIdInput!): Agent!
|
||||
myConnectedAccounts: [ConnectedAccountPublicDTO!]!
|
||||
getRoles: [Role!]!
|
||||
getToolIndex: [ToolIndexEntry!]!
|
||||
getToolInputSchema(toolName: String!): JSON
|
||||
@@ -3028,6 +3027,7 @@ type Query {
|
||||
getViewGroup(id: String!): ViewGroup
|
||||
myMessageFolders(messageChannelId: UUID): [MessageFolder!]!
|
||||
myMessageChannels(connectedAccountId: UUID): [MessageChannel!]!
|
||||
myConnectedAccounts: [ConnectedAccountPublicDTO!]!
|
||||
myCalendarChannels(connectedAccountId: UUID): [CalendarChannel!]!
|
||||
minimalMetadata: MinimalMetadata!
|
||||
appConnections(filter: ListAppConnectionsInput): [AppConnection!]!
|
||||
@@ -3200,8 +3200,6 @@ type Mutation {
|
||||
updateApiKey(input: UpdateApiKeyInput!): ApiKey
|
||||
revokeApiKey(input: RevokeApiKeyInput!): ApiKey
|
||||
assignRoleToApiKey(apiKeyId: UUID!, roleId: UUID!): Boolean!
|
||||
createObjectEvent(event: String!, recordId: UUID!, objectMetadataId: UUID!, properties: JSON): Analytics!
|
||||
trackAnalytics(type: AnalyticsType!, name: String, event: String, properties: JSON): Analytics!
|
||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess!
|
||||
skipBookOnboardingStep: OnboardingStepSuccess!
|
||||
checkoutSession(recurringInterval: SubscriptionInterval!, plan: BillingPlanKey! = PRO, requirePaymentMethod: Boolean! = true, successUrlPath: String): BillingSession!
|
||||
@@ -3254,7 +3252,6 @@ type Mutation {
|
||||
createOneAgent(input: CreateAgentInput!): Agent!
|
||||
updateOneAgent(input: UpdateAgentInput!): Agent!
|
||||
deleteOneAgent(input: AgentIdInput!): Agent!
|
||||
deleteConnectedAccount(id: UUID!): ConnectedAccountPublicDTO!
|
||||
updateWorkspaceMemberRole(workspaceMemberId: UUID!, roleId: UUID!): WorkspaceMember!
|
||||
createOneRole(createRoleInput: CreateRoleInput!): Role!
|
||||
updateOneRole(updateRoleInput: UpdateRoleInput!): Role!
|
||||
@@ -3283,6 +3280,7 @@ type Mutation {
|
||||
updateMessageChannel(input: UpdateMessageChannelInput!): MessageChannel!
|
||||
createEmailGroupChannel(input: CreateEmailGroupChannelInput!): CreateEmailGroupChannelOutput!
|
||||
deleteEmailGroupChannel(id: UUID!): MessageChannel!
|
||||
deleteConnectedAccount(id: UUID!): ConnectedAccountPublicDTO!
|
||||
updateCalendarChannel(input: UpdateCalendarChannelInput!): CalendarChannel!
|
||||
createChatThread: AgentChatThread!
|
||||
sendChatMessage(threadId: UUID!, text: String!, messageId: UUID!, browsingContext: JSON, modelId: String, fileAttachments: [FileAttachmentInput!]): SendChatMessageResult!
|
||||
@@ -3344,6 +3342,8 @@ type Mutation {
|
||||
createSAMLIdentityProvider(input: SetupSAMLSsoInput!): SetupSso!
|
||||
deleteSSOIdentityProvider(input: DeleteSsoInput!): DeleteSso!
|
||||
editSSOIdentityProvider(input: EditSsoInput!): EditSso!
|
||||
createObjectEvent(event: String!, recordId: UUID!, objectMetadataId: UUID!, properties: JSON): Analytics!
|
||||
trackAnalytics(type: AnalyticsType!, name: String, event: String, properties: JSON): Analytics!
|
||||
duplicateDashboard(id: UUID!): DuplicatedDashboard!
|
||||
impersonate(userId: UUID!, workspaceId: UUID!): Impersonate!
|
||||
sendEmail(input: SendEmailInput!): SendEmailOutput!
|
||||
@@ -3360,7 +3360,7 @@ type Mutation {
|
||||
syncMarketplaceCatalog: Boolean!
|
||||
createDevelopmentApplication(universalIdentifier: String!, name: String!): DevelopmentApplication!
|
||||
generateApplicationToken(applicationId: UUID!): ApplicationTokenPair!
|
||||
syncApplication(manifest: JSON!): WorkspaceMigration!
|
||||
syncApplication(manifest: JSON!, dryRun: Boolean): WorkspaceMigration!
|
||||
uploadApplicationFile(file: Upload!, applicationUniversalIdentifier: String!, fileFolder: FileFolder!, filePath: String!): File!
|
||||
upgradeApplication(appRegistrationId: String!, targetVersion: String!): Boolean!
|
||||
renewApplicationToken(applicationRefreshToken: String!): ApplicationTokenPair!
|
||||
@@ -3716,11 +3716,6 @@ input RevokeApiKeyInput {
|
||||
id: UUID!
|
||||
}
|
||||
|
||||
enum AnalyticsType {
|
||||
PAGEVIEW
|
||||
TRACK
|
||||
}
|
||||
|
||||
input CreateApprovedAccessDomainInput {
|
||||
domain: String!
|
||||
email: String!
|
||||
@@ -4430,6 +4425,11 @@ input EditSsoInput {
|
||||
status: SSOIdentityProviderStatus!
|
||||
}
|
||||
|
||||
enum AnalyticsType {
|
||||
PAGEVIEW
|
||||
TRACK
|
||||
}
|
||||
|
||||
input SendEmailInput {
|
||||
connectedAccountId: String!
|
||||
to: String!
|
||||
@@ -4498,6 +4498,7 @@ type Subscription {
|
||||
onEventSubscription(eventStreamId: String!): EventSubscription
|
||||
logicFunctionLogs(input: LogicFunctionLogsInput!): LogicFunctionLogs!
|
||||
onAgentChatEvent(threadId: UUID!): AgentChatEvent!
|
||||
eventLogsLive(table: EventLogTable!): [EventLogRecord!]
|
||||
}
|
||||
|
||||
input LogicFunctionLogsInput {
|
||||
|
||||
@@ -1091,12 +1091,6 @@ export interface EnterpriseSubscriptionStatusDTO {
|
||||
__typename: 'EnterpriseSubscriptionStatusDTO'
|
||||
}
|
||||
|
||||
export interface Analytics {
|
||||
/** Boolean that confirms query was dispatched */
|
||||
success: Scalars['Boolean']
|
||||
__typename: 'Analytics'
|
||||
}
|
||||
|
||||
export interface VerificationRecord {
|
||||
type: Scalars['String']
|
||||
key: Scalars['String']
|
||||
@@ -2314,6 +2308,12 @@ export interface SendEmailOutput {
|
||||
__typename: 'SendEmailOutput'
|
||||
}
|
||||
|
||||
export interface Analytics {
|
||||
/** Boolean that confirms query was dispatched */
|
||||
success: Scalars['Boolean']
|
||||
__typename: 'Analytics'
|
||||
}
|
||||
|
||||
export interface EventLogRecord {
|
||||
event: Scalars['String']
|
||||
timestamp: Scalars['DateTime']
|
||||
@@ -2613,7 +2613,6 @@ export interface Query {
|
||||
indexMetadatas: IndexConnection
|
||||
findManyAgents: Agent[]
|
||||
findOneAgent: Agent
|
||||
myConnectedAccounts: ConnectedAccountPublicDTO[]
|
||||
getRoles: Role[]
|
||||
getToolIndex: ToolIndexEntry[]
|
||||
getToolInputSchema?: Scalars['JSON']
|
||||
@@ -2625,6 +2624,7 @@ export interface Query {
|
||||
getViewGroup?: ViewGroup
|
||||
myMessageFolders: MessageFolder[]
|
||||
myMessageChannels: MessageChannel[]
|
||||
myConnectedAccounts: ConnectedAccountPublicDTO[]
|
||||
myCalendarChannels: CalendarChannel[]
|
||||
minimalMetadata: MinimalMetadata
|
||||
appConnections: AppConnection[]
|
||||
@@ -2724,8 +2724,6 @@ export interface Mutation {
|
||||
updateApiKey?: ApiKey
|
||||
revokeApiKey?: ApiKey
|
||||
assignRoleToApiKey: Scalars['Boolean']
|
||||
createObjectEvent: Analytics
|
||||
trackAnalytics: Analytics
|
||||
skipSyncEmailOnboardingStep: OnboardingStepSuccess
|
||||
skipBookOnboardingStep: OnboardingStepSuccess
|
||||
checkoutSession: BillingSession
|
||||
@@ -2778,7 +2776,6 @@ export interface Mutation {
|
||||
createOneAgent: Agent
|
||||
updateOneAgent: Agent
|
||||
deleteOneAgent: Agent
|
||||
deleteConnectedAccount: ConnectedAccountPublicDTO
|
||||
updateWorkspaceMemberRole: WorkspaceMember
|
||||
createOneRole: Role
|
||||
updateOneRole: Role
|
||||
@@ -2807,6 +2804,7 @@ export interface Mutation {
|
||||
updateMessageChannel: MessageChannel
|
||||
createEmailGroupChannel: CreateEmailGroupChannelOutput
|
||||
deleteEmailGroupChannel: MessageChannel
|
||||
deleteConnectedAccount: ConnectedAccountPublicDTO
|
||||
updateCalendarChannel: CalendarChannel
|
||||
createChatThread: AgentChatThread
|
||||
sendChatMessage: SendChatMessageResult
|
||||
@@ -2868,6 +2866,8 @@ export interface Mutation {
|
||||
createSAMLIdentityProvider: SetupSso
|
||||
deleteSSOIdentityProvider: DeleteSso
|
||||
editSSOIdentityProvider: EditSso
|
||||
createObjectEvent: Analytics
|
||||
trackAnalytics: Analytics
|
||||
duplicateDashboard: DuplicatedDashboard
|
||||
impersonate: Impersonate
|
||||
sendEmail: SendEmailOutput
|
||||
@@ -2892,16 +2892,17 @@ export interface Mutation {
|
||||
__typename: 'Mutation'
|
||||
}
|
||||
|
||||
export type AnalyticsType = 'PAGEVIEW' | 'TRACK'
|
||||
|
||||
export type WorkspaceMigrationActionType = 'delete' | 'create' | 'update'
|
||||
|
||||
export type AnalyticsType = 'PAGEVIEW' | 'TRACK'
|
||||
|
||||
export type FileFolder = 'ProfilePicture' | 'WorkspaceLogo' | 'Attachment' | 'PersonPicture' | 'CorePicture' | 'File' | 'AgentChat' | 'BuiltLogicFunction' | 'BuiltFrontComponent' | 'PublicAsset' | 'Source' | 'FilesField' | 'Dependencies' | 'Workflow' | 'EmailAttachment' | 'AppTarball' | 'GeneratedSdkClient'
|
||||
|
||||
export interface Subscription {
|
||||
onEventSubscription?: EventSubscription
|
||||
logicFunctionLogs: LogicFunctionLogs
|
||||
onAgentChatEvent: AgentChatEvent
|
||||
eventLogsLive?: EventLogRecord[]
|
||||
__typename: 'Subscription'
|
||||
}
|
||||
|
||||
@@ -4049,13 +4050,6 @@ export interface EnterpriseSubscriptionStatusDTOGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface AnalyticsGenqlSelection{
|
||||
/** Boolean that confirms query was dispatched */
|
||||
success?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface VerificationRecordGenqlSelection{
|
||||
type?: boolean | number
|
||||
key?: boolean | number
|
||||
@@ -5364,6 +5358,13 @@ export interface SendEmailOutputGenqlSelection{
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface AnalyticsGenqlSelection{
|
||||
/** Boolean that confirms query was dispatched */
|
||||
success?: boolean | number
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
|
||||
export interface EventLogRecordGenqlSelection{
|
||||
event?: boolean | number
|
||||
timestamp?: boolean | number
|
||||
@@ -5671,7 +5672,6 @@ export interface QueryGenqlSelection{
|
||||
filter: IndexFilter} })
|
||||
findManyAgents?: AgentGenqlSelection
|
||||
findOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} })
|
||||
myConnectedAccounts?: ConnectedAccountPublicDTOGenqlSelection
|
||||
getRoles?: RoleGenqlSelection
|
||||
getToolIndex?: ToolIndexEntryGenqlSelection
|
||||
getToolInputSchema?: { __args: {toolName: Scalars['String']} }
|
||||
@@ -5689,6 +5689,7 @@ export interface QueryGenqlSelection{
|
||||
getViewGroup?: (ViewGroupGenqlSelection & { __args: {id: Scalars['String']} })
|
||||
myMessageFolders?: (MessageFolderGenqlSelection & { __args?: {messageChannelId?: (Scalars['UUID'] | null)} })
|
||||
myMessageChannels?: (MessageChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
myConnectedAccounts?: ConnectedAccountPublicDTOGenqlSelection
|
||||
myCalendarChannels?: (CalendarChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
minimalMetadata?: MinimalMetadataGenqlSelection
|
||||
appConnections?: (AppConnectionGenqlSelection & { __args?: {filter?: (ListAppConnectionsInput | null)} })
|
||||
@@ -5811,8 +5812,6 @@ export interface MutationGenqlSelection{
|
||||
updateApiKey?: (ApiKeyGenqlSelection & { __args: {input: UpdateApiKeyInput} })
|
||||
revokeApiKey?: (ApiKeyGenqlSelection & { __args: {input: RevokeApiKeyInput} })
|
||||
assignRoleToApiKey?: { __args: {apiKeyId: Scalars['UUID'], roleId: Scalars['UUID']} }
|
||||
createObjectEvent?: (AnalyticsGenqlSelection & { __args: {event: Scalars['String'], recordId: Scalars['UUID'], objectMetadataId: Scalars['UUID'], properties?: (Scalars['JSON'] | null)} })
|
||||
trackAnalytics?: (AnalyticsGenqlSelection & { __args: {type: AnalyticsType, name?: (Scalars['String'] | null), event?: (Scalars['String'] | null), properties?: (Scalars['JSON'] | null)} })
|
||||
skipSyncEmailOnboardingStep?: OnboardingStepSuccessGenqlSelection
|
||||
skipBookOnboardingStep?: OnboardingStepSuccessGenqlSelection
|
||||
checkoutSession?: (BillingSessionGenqlSelection & { __args: {recurringInterval: SubscriptionInterval, plan: BillingPlanKey, requirePaymentMethod: Scalars['Boolean'], successUrlPath?: (Scalars['String'] | null)} })
|
||||
@@ -5865,7 +5864,6 @@ export interface MutationGenqlSelection{
|
||||
createOneAgent?: (AgentGenqlSelection & { __args: {input: CreateAgentInput} })
|
||||
updateOneAgent?: (AgentGenqlSelection & { __args: {input: UpdateAgentInput} })
|
||||
deleteOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} })
|
||||
deleteConnectedAccount?: (ConnectedAccountPublicDTOGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
updateWorkspaceMemberRole?: (WorkspaceMemberGenqlSelection & { __args: {workspaceMemberId: Scalars['UUID'], roleId: Scalars['UUID']} })
|
||||
createOneRole?: (RoleGenqlSelection & { __args: {createRoleInput: CreateRoleInput} })
|
||||
updateOneRole?: (RoleGenqlSelection & { __args: {updateRoleInput: UpdateRoleInput} })
|
||||
@@ -5894,6 +5892,7 @@ export interface MutationGenqlSelection{
|
||||
updateMessageChannel?: (MessageChannelGenqlSelection & { __args: {input: UpdateMessageChannelInput} })
|
||||
createEmailGroupChannel?: (CreateEmailGroupChannelOutputGenqlSelection & { __args: {input: CreateEmailGroupChannelInput} })
|
||||
deleteEmailGroupChannel?: (MessageChannelGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
deleteConnectedAccount?: (ConnectedAccountPublicDTOGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
updateCalendarChannel?: (CalendarChannelGenqlSelection & { __args: {input: UpdateCalendarChannelInput} })
|
||||
createChatThread?: AgentChatThreadGenqlSelection
|
||||
sendChatMessage?: (SendChatMessageResultGenqlSelection & { __args: {threadId: Scalars['UUID'], text: Scalars['String'], messageId: Scalars['UUID'], browsingContext?: (Scalars['JSON'] | null), modelId?: (Scalars['String'] | null), fileAttachments?: (FileAttachmentInput[] | null)} })
|
||||
@@ -5955,6 +5954,8 @@ export interface MutationGenqlSelection{
|
||||
createSAMLIdentityProvider?: (SetupSsoGenqlSelection & { __args: {input: SetupSAMLSsoInput} })
|
||||
deleteSSOIdentityProvider?: (DeleteSsoGenqlSelection & { __args: {input: DeleteSsoInput} })
|
||||
editSSOIdentityProvider?: (EditSsoGenqlSelection & { __args: {input: EditSsoInput} })
|
||||
createObjectEvent?: (AnalyticsGenqlSelection & { __args: {event: Scalars['String'], recordId: Scalars['UUID'], objectMetadataId: Scalars['UUID'], properties?: (Scalars['JSON'] | null)} })
|
||||
trackAnalytics?: (AnalyticsGenqlSelection & { __args: {type: AnalyticsType, name?: (Scalars['String'] | null), event?: (Scalars['String'] | null), properties?: (Scalars['JSON'] | null)} })
|
||||
duplicateDashboard?: (DuplicatedDashboardGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
impersonate?: (ImpersonateGenqlSelection & { __args: {userId: Scalars['UUID'], workspaceId: Scalars['UUID']} })
|
||||
sendEmail?: (SendEmailOutputGenqlSelection & { __args: {input: SendEmailInput} })
|
||||
@@ -5972,7 +5973,7 @@ export interface MutationGenqlSelection{
|
||||
syncMarketplaceCatalog?: boolean | number
|
||||
createDevelopmentApplication?: (DevelopmentApplicationGenqlSelection & { __args: {universalIdentifier: Scalars['String'], name: Scalars['String']} })
|
||||
generateApplicationToken?: (ApplicationTokenPairGenqlSelection & { __args: {applicationId: Scalars['UUID']} })
|
||||
syncApplication?: (WorkspaceMigrationGenqlSelection & { __args: {manifest: Scalars['JSON']} })
|
||||
syncApplication?: (WorkspaceMigrationGenqlSelection & { __args: {manifest: Scalars['JSON'], dryRun?: (Scalars['Boolean'] | null)} })
|
||||
uploadApplicationFile?: (FileGenqlSelection & { __args: {file: Scalars['Upload'], applicationUniversalIdentifier: Scalars['String'], fileFolder: FileFolder, filePath: Scalars['String']} })
|
||||
upgradeApplication?: { __args: {appRegistrationId: Scalars['String'], targetVersion: Scalars['String']} }
|
||||
renewApplicationToken?: (ApplicationTokenPairGenqlSelection & { __args: {applicationRefreshToken: Scalars['String']} })
|
||||
@@ -6356,6 +6357,7 @@ export interface SubscriptionGenqlSelection{
|
||||
onEventSubscription?: (EventSubscriptionGenqlSelection & { __args: {eventStreamId: Scalars['String']} })
|
||||
logicFunctionLogs?: (LogicFunctionLogsGenqlSelection & { __args: {input: LogicFunctionLogsInput} })
|
||||
onAgentChatEvent?: (AgentChatEventGenqlSelection & { __args: {threadId: Scalars['UUID']} })
|
||||
eventLogsLive?: (EventLogRecordGenqlSelection & { __args: {table: EventLogTable} })
|
||||
__typename?: boolean | number
|
||||
__scalar?: boolean | number
|
||||
}
|
||||
@@ -7003,14 +7005,6 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const Analytics_possibleTypes: string[] = ['Analytics']
|
||||
export const isAnalytics = (obj?: { __typename?: any } | null): obj is Analytics => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isAnalytics"')
|
||||
return Analytics_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const VerificationRecord_possibleTypes: string[] = ['VerificationRecord']
|
||||
export const isVerificationRecord = (obj?: { __typename?: any } | null): obj is VerificationRecord => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isVerificationRecord"')
|
||||
@@ -8139,6 +8133,14 @@ export interface LogicFunctionLogsInput {applicationId?: (Scalars['UUID'] | null
|
||||
|
||||
|
||||
|
||||
const Analytics_possibleTypes: string[] = ['Analytics']
|
||||
export const isAnalytics = (obj?: { __typename?: any } | null): obj is Analytics => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isAnalytics"')
|
||||
return Analytics_possibleTypes.includes(obj.__typename)
|
||||
}
|
||||
|
||||
|
||||
|
||||
const EventLogRecord_possibleTypes: string[] = ['EventLogRecord']
|
||||
export const isEventLogRecord = (obj?: { __typename?: any } | null): obj is EventLogRecord => {
|
||||
if (!obj?.__typename) throw new Error('__typename is missing in "isEventLogRecord"')
|
||||
@@ -8990,17 +8992,17 @@ export const enumUsageOperationType = {
|
||||
WEB_SEARCH: 'WEB_SEARCH' as const
|
||||
}
|
||||
|
||||
export const enumAnalyticsType = {
|
||||
PAGEVIEW: 'PAGEVIEW' as const,
|
||||
TRACK: 'TRACK' as const
|
||||
}
|
||||
|
||||
export const enumWorkspaceMigrationActionType = {
|
||||
delete: 'delete' as const,
|
||||
create: 'create' as const,
|
||||
update: 'update' as const
|
||||
}
|
||||
|
||||
export const enumAnalyticsType = {
|
||||
PAGEVIEW: 'PAGEVIEW' as const,
|
||||
TRACK: 'TRACK' as const
|
||||
}
|
||||
|
||||
export const enumFileFolder = {
|
||||
ProfilePicture: 'ProfilePicture' as const,
|
||||
WorkspaceLogo: 'WorkspaceLogo' as const,
|
||||
|
||||
@@ -6,10 +6,10 @@ services:
|
||||
volumes:
|
||||
- server-local-data:/app/packages/twenty-server/.local-storage
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "3010:3000"
|
||||
environment:
|
||||
NODE_PORT: 3000
|
||||
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default
|
||||
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/${PG_DATABASE_NAME}
|
||||
SERVER_URL: ${SERVER_URL}
|
||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
||||
DISABLE_DB_MIGRATIONS: ${DISABLE_DB_MIGRATIONS}
|
||||
@@ -46,11 +46,6 @@ services:
|
||||
# EMAIL_SMTP_USER: ${EMAIL_SMTP_USER:-}
|
||||
# EMAIL_SMTP_PASSWORD: ${EMAIL_SMTP_PASSWORD:-}
|
||||
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: curl --fail http://localhost:3000/healthz
|
||||
interval: 5s
|
||||
@@ -64,7 +59,7 @@ services:
|
||||
- server-local-data:/app/packages/twenty-server/.local-storage
|
||||
command: ["yarn", "worker:prod"]
|
||||
environment:
|
||||
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default
|
||||
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/${PG_DATABASE_NAME}
|
||||
SERVER_URL: ${SERVER_URL}
|
||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
||||
DISABLE_DB_MIGRATIONS: "true" # it already runs on the server
|
||||
@@ -101,38 +96,7 @@ services:
|
||||
# EMAIL_SMTP_USER: ${EMAIL_SMTP_USER:-}
|
||||
# EMAIL_SMTP_PASSWORD: ${EMAIL_SMTP_PASSWORD:-}
|
||||
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
server:
|
||||
condition: service_healthy
|
||||
restart: always
|
||||
|
||||
db:
|
||||
image: postgres:16
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
environment:
|
||||
POSTGRES_DB: ${PG_DATABASE_NAME:-default}
|
||||
POSTGRES_PASSWORD: ${PG_DATABASE_PASSWORD:-postgres}
|
||||
POSTGRES_USER: ${PG_DATABASE_USER:-postgres}
|
||||
healthcheck:
|
||||
test: pg_isready -U ${PG_DATABASE_USER:-postgres} -h localhost -d postgres
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
restart: always
|
||||
|
||||
redis:
|
||||
image: redis
|
||||
restart: always
|
||||
command: ["--maxmemory-policy", "noeviction"]
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
server-local-data:
|
||||
|
||||
|
Before Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 69 KiB |
@@ -3315,6 +3315,7 @@ export type MutationStopAgentChatStreamArgs = {
|
||||
|
||||
|
||||
export type MutationSyncApplicationArgs = {
|
||||
dryRun?: InputMaybe<Scalars['Boolean']>;
|
||||
manifest: Scalars['JSON'];
|
||||
};
|
||||
|
||||
@@ -4932,12 +4933,18 @@ export type StandardOverrides = {
|
||||
|
||||
export type Subscription = {
|
||||
__typename?: 'Subscription';
|
||||
eventLogsLive?: Maybe<Array<EventLogRecord>>;
|
||||
logicFunctionLogs: LogicFunctionLogs;
|
||||
onAgentChatEvent: AgentChatEvent;
|
||||
onEventSubscription?: Maybe<EventSubscription>;
|
||||
};
|
||||
|
||||
|
||||
export type SubscriptionEventLogsLiveArgs = {
|
||||
table: EventLogTable;
|
||||
};
|
||||
|
||||
|
||||
export type SubscriptionLogicFunctionLogsArgs = {
|
||||
input: LogicFunctionLogsInput;
|
||||
};
|
||||
@@ -7403,7 +7410,14 @@ export type EventLogsQueryVariables = Exact<{
|
||||
}>;
|
||||
|
||||
|
||||
export type EventLogsQuery = { __typename?: 'Query', eventLogs: { __typename?: 'EventLogQueryResult', totalCount: number, records: Array<{ __typename?: 'EventLogRecord', event: string, timestamp: string, userId?: string | null, properties?: any | null, recordId?: string | null, objectMetadataId?: string | null, isCustom?: boolean | null }>, pageInfo: { __typename?: 'EventLogPageInfo', endCursor?: string | null, hasNextPage: boolean } } };
|
||||
export type EventLogsQuery = { __typename?: 'Query', eventLogs: { __typename?: 'EventLogQueryResult', totalCount: number, records: Array<{ __typename?: 'EventLogRecord', event: string, timestamp: string, userId?: string | null, properties?: any | null, recordId?: string | null, objectMetadataId?: string | null }>, pageInfo: { __typename?: 'EventLogPageInfo', endCursor?: string | null, hasNextPage: boolean } } };
|
||||
|
||||
export type EventLogsLiveSubscriptionVariables = Exact<{
|
||||
table: EventLogTable;
|
||||
}>;
|
||||
|
||||
|
||||
export type EventLogsLiveSubscription = { __typename?: 'Subscription', eventLogsLive?: Array<{ __typename?: 'EventLogRecord', event: string, timestamp: string, userId?: string | null, properties?: any | null, recordId?: string | null, objectMetadataId?: string | null }> | null };
|
||||
|
||||
export type UpdateLabPublicFeatureFlagMutationVariables = Exact<{
|
||||
input: UpdateLabPublicFeatureFlagInput;
|
||||
@@ -8254,7 +8268,8 @@ export const SetEnterpriseKeyDocument = {"kind":"Document","definitions":[{"kind
|
||||
export const EnterpriseCheckoutSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EnterpriseCheckoutSession"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"billingInterval"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enterpriseCheckoutSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"billingInterval"},"value":{"kind":"Variable","name":{"kind":"Name","value":"billingInterval"}}}]}]}}]} as unknown as DocumentNode<EnterpriseCheckoutSessionQuery, EnterpriseCheckoutSessionQueryVariables>;
|
||||
export const EnterprisePortalSessionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EnterprisePortalSession"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"returnUrlPath"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enterprisePortalSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"returnUrlPath"},"value":{"kind":"Variable","name":{"kind":"Name","value":"returnUrlPath"}}}]}]}}]} as unknown as DocumentNode<EnterprisePortalSessionQuery, EnterprisePortalSessionQueryVariables>;
|
||||
export const EnterpriseSubscriptionStatusDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EnterpriseSubscriptionStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enterpriseSubscriptionStatus"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"licensee"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"cancelAt"}},{"kind":"Field","name":{"kind":"Name","value":"currentPeriodEnd"}},{"kind":"Field","name":{"kind":"Name","value":"isCancellationScheduled"}}]}}]}}]} as unknown as DocumentNode<EnterpriseSubscriptionStatusQuery, EnterpriseSubscriptionStatusQueryVariables>;
|
||||
export const EventLogsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventLogs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EventLogQueryInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"eventLogs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}},{"kind":"Field","name":{"kind":"Name","value":"properties"}},{"kind":"Field","name":{"kind":"Name","value":"recordId"}},{"kind":"Field","name":{"kind":"Name","value":"objectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}}]}}]}}]}}]} as unknown as DocumentNode<EventLogsQuery, EventLogsQueryVariables>;
|
||||
export const EventLogsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"EventLogs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EventLogQueryInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"eventLogs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"records"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}},{"kind":"Field","name":{"kind":"Name","value":"properties"}},{"kind":"Field","name":{"kind":"Name","value":"recordId"}},{"kind":"Field","name":{"kind":"Name","value":"objectMetadataId"}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"endCursor"}},{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}}]}}]}}]}}]} as unknown as DocumentNode<EventLogsQuery, EventLogsQueryVariables>;
|
||||
export const EventLogsLiveDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"subscription","name":{"kind":"Name","value":"EventLogsLive"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"table"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EventLogTable"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"eventLogsLive"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"table"},"value":{"kind":"Variable","name":{"kind":"Name","value":"table"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"event"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"userId"}},{"kind":"Field","name":{"kind":"Name","value":"properties"}},{"kind":"Field","name":{"kind":"Name","value":"recordId"}},{"kind":"Field","name":{"kind":"Name","value":"objectMetadataId"}}]}}]}}]} as unknown as DocumentNode<EventLogsLiveSubscription, EventLogsLiveSubscriptionVariables>;
|
||||
export const UpdateLabPublicFeatureFlagDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateLabPublicFeatureFlag"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateLabPublicFeatureFlagInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateLabPublicFeatureFlag"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}}]}}]} as unknown as DocumentNode<UpdateLabPublicFeatureFlagMutation, UpdateLabPublicFeatureFlagMutationVariables>;
|
||||
export const UploadWorkspaceMemberProfilePictureDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UploadWorkspaceMemberProfilePicture"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"file"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Upload"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"uploadWorkspaceMemberProfilePicture"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"file"},"value":{"kind":"Variable","name":{"kind":"Name","value":"file"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]} as unknown as DocumentNode<UploadWorkspaceMemberProfilePictureMutation, UploadWorkspaceMemberProfilePictureMutationVariables>;
|
||||
export const UpdateUserEmailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateUserEmail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"newEmail"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"verifyEmailRedirectPath"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateUserEmail"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"newEmail"},"value":{"kind":"Variable","name":{"kind":"Name","value":"newEmail"}}},{"kind":"Argument","name":{"kind":"Name","value":"verifyEmailRedirectPath"},"value":{"kind":"Variable","name":{"kind":"Name","value":"verifyEmailRedirectPath"}}}]}]}}]} as unknown as DocumentNode<UpdateUserEmailMutation, UpdateUserEmailMutationVariables>;
|
||||
|
||||
@@ -8,6 +8,7 @@ import { originalDragSelectionComponentState } from '@/object-record/record-drag
|
||||
|
||||
import { RECORD_INDEX_REMOVE_SORTING_MODAL_ID } from '@/object-record/record-index/constants/RecordIndexRemoveSortingModalId';
|
||||
import { currentRecordSortsComponentState } from '@/object-record/record-sort/states/currentRecordSortsComponentState';
|
||||
import { getBoardCardDropBehavior } from '@/object-record/record-board/utils/getBoardCardDropBehavior';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { useAtomComponentSelectorCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentSelectorCallbackState';
|
||||
import { useAtomComponentStateCallbackState } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateCallbackState';
|
||||
@@ -82,8 +83,13 @@ export const RecordBoardDragDropContext = ({
|
||||
}
|
||||
|
||||
const existingRecordSorts = store.get(currentRecordSorts);
|
||||
const boardCardDropBehavior = getBoardCardDropBehavior({
|
||||
hasRecordSorts: existingRecordSorts.length > 0,
|
||||
sourceDroppableId: result.source.droppableId,
|
||||
destinationDroppableId: result.destination.droppableId,
|
||||
});
|
||||
|
||||
if (existingRecordSorts.length > 0) {
|
||||
if (boardCardDropBehavior.shouldBlockDrop) {
|
||||
store.set(isRecordBoardDropProcessingCallbackState, false);
|
||||
endRecordDrag();
|
||||
openModal(RECORD_INDEX_REMOVE_SORTING_MODAL_ID);
|
||||
@@ -91,7 +97,9 @@ export const RecordBoardDragDropContext = ({
|
||||
}
|
||||
|
||||
try {
|
||||
processBoardCardDrop(result, originalDragSelection);
|
||||
processBoardCardDrop(result, originalDragSelection, {
|
||||
shouldUpdatePosition: boardCardDropBehavior.shouldUpdatePosition,
|
||||
});
|
||||
} catch (error) {
|
||||
store.set(isRecordBoardDropProcessingCallbackState, false);
|
||||
endRecordDrag();
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import { getBoardCardDropBehavior } from '@/object-record/record-board/utils/getBoardCardDropBehavior';
|
||||
|
||||
describe('getBoardCardDropBehavior', () => {
|
||||
it('should block same-column drops when record sorting is active', () => {
|
||||
expect(
|
||||
getBoardCardDropBehavior({
|
||||
hasRecordSorts: true,
|
||||
sourceDroppableId: 'new',
|
||||
destinationDroppableId: 'new',
|
||||
}),
|
||||
).toEqual({
|
||||
shouldBlockDrop: true,
|
||||
shouldUpdatePosition: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow cross-column drops without position updates when record sorting is active', () => {
|
||||
expect(
|
||||
getBoardCardDropBehavior({
|
||||
hasRecordSorts: true,
|
||||
sourceDroppableId: 'new',
|
||||
destinationDroppableId: 'won',
|
||||
}),
|
||||
).toEqual({
|
||||
shouldBlockDrop: false,
|
||||
shouldUpdatePosition: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow drops with position updates when record sorting is inactive', () => {
|
||||
expect(
|
||||
getBoardCardDropBehavior({
|
||||
hasRecordSorts: false,
|
||||
sourceDroppableId: 'new',
|
||||
destinationDroppableId: 'won',
|
||||
}),
|
||||
).toEqual({
|
||||
shouldBlockDrop: false,
|
||||
shouldUpdatePosition: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
export const getBoardCardDropBehavior = ({
|
||||
hasRecordSorts,
|
||||
sourceDroppableId,
|
||||
destinationDroppableId,
|
||||
}: {
|
||||
hasRecordSorts: boolean;
|
||||
sourceDroppableId: string;
|
||||
destinationDroppableId: string;
|
||||
}) => {
|
||||
const isMovingInsideSameRecordGroup =
|
||||
sourceDroppableId === destinationDroppableId;
|
||||
|
||||
return {
|
||||
shouldBlockDrop: hasRecordSorts && isMovingInsideSameRecordGroup,
|
||||
shouldUpdatePosition: !hasRecordSorts,
|
||||
};
|
||||
};
|
||||
@@ -40,9 +40,15 @@ export const useProcessBoardCardDrop = () => {
|
||||
);
|
||||
|
||||
const processBoardCardDrop = useCallback(
|
||||
(boardCardDropResult: DropResult, selectedRecordIds: string[]) => {
|
||||
(
|
||||
boardCardDropResult: DropResult,
|
||||
selectedRecordIds: string[],
|
||||
options?: { shouldUpdatePosition?: boolean },
|
||||
) => {
|
||||
if (!isDefined(selectFieldMetadataItem)) return;
|
||||
|
||||
const shouldUpdatePosition = options?.shouldUpdatePosition ?? true;
|
||||
|
||||
processGroupDrop({
|
||||
groupDropResult: boardCardDropResult,
|
||||
store,
|
||||
@@ -51,7 +57,10 @@ export const useProcessBoardCardDrop = () => {
|
||||
recordIndexRecordIdsByGroupCallbackFamilyState,
|
||||
onUpdateRecord: ({ recordId, position }, targetRecordGroupValue) => {
|
||||
updateDroppedRecordOnBoard(
|
||||
{ recordId, position },
|
||||
{
|
||||
recordId,
|
||||
position: shouldUpdatePosition ? position : undefined,
|
||||
},
|
||||
targetRecordGroupValue,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -45,10 +45,6 @@ export const useUpdateDroppedRecordOnBoard = () => {
|
||||
recordStoreFamilyState.atomFamily(recordId),
|
||||
) as Record<string, unknown> | null | undefined;
|
||||
|
||||
if (!isDefined(newPosition)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isDefined(initialRecord)) {
|
||||
return;
|
||||
}
|
||||
@@ -94,7 +90,10 @@ export const useUpdateDroppedRecordOnBoard = () => {
|
||||
|
||||
const isSamePosition = initialRecord.position === newPosition;
|
||||
|
||||
if (movingInsideSameRecordGroup && isSamePosition) {
|
||||
if (
|
||||
movingInsideSameRecordGroup &&
|
||||
(!isDefined(newPosition) || isSamePosition)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -126,25 +125,32 @@ export const useUpdateDroppedRecordOnBoard = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const targetGroupRecordsWithIds = extractRecordPositions(
|
||||
currentRecordIdsInTargetRecordGroup,
|
||||
store,
|
||||
);
|
||||
if (isDefined(newPosition)) {
|
||||
const targetGroupRecordsWithIds = extractRecordPositions(
|
||||
currentRecordIdsInTargetRecordGroup,
|
||||
store,
|
||||
);
|
||||
|
||||
const newTargetRecordGroupWithIds = [
|
||||
...targetGroupRecordsWithIds,
|
||||
{
|
||||
id: recordId,
|
||||
position: newPosition,
|
||||
},
|
||||
];
|
||||
const newTargetRecordGroupWithIds = [
|
||||
...targetGroupRecordsWithIds,
|
||||
{
|
||||
id: recordId,
|
||||
position: newPosition,
|
||||
},
|
||||
];
|
||||
|
||||
newTargetRecordGroupWithIds.sort(sortByProperty('position', 'asc'));
|
||||
newTargetRecordGroupWithIds.sort(sortByProperty('position', 'asc'));
|
||||
|
||||
store.set(
|
||||
recordIndexRecordIdsByGroupCallbackFamilyState(targetRecordGroupId),
|
||||
newTargetRecordGroupWithIds.map((record) => record.id),
|
||||
);
|
||||
store.set(
|
||||
recordIndexRecordIdsByGroupCallbackFamilyState(targetRecordGroupId),
|
||||
newTargetRecordGroupWithIds.map((record) => record.id),
|
||||
);
|
||||
} else {
|
||||
store.set(
|
||||
recordIndexRecordIdsByGroupCallbackFamilyState(targetRecordGroupId),
|
||||
[...currentRecordIdsInTargetRecordGroup, recordId],
|
||||
);
|
||||
}
|
||||
|
||||
upsertRecordsInStore({
|
||||
partialRecords: [
|
||||
@@ -155,7 +161,7 @@ export const useUpdateDroppedRecordOnBoard = () => {
|
||||
(initialRecord as { __typename?: string })?.__typename ??
|
||||
'Record',
|
||||
[selectFieldMetadataItem.name]: targetRecordGroupValue,
|
||||
position: newPosition,
|
||||
...(isDefined(newPosition) && { position: newPosition }),
|
||||
} as ObjectRecord,
|
||||
],
|
||||
});
|
||||
@@ -164,7 +170,7 @@ export const useUpdateDroppedRecordOnBoard = () => {
|
||||
idToUpdate: recordId,
|
||||
updateOneRecordInput: {
|
||||
[selectFieldMetadataItem.name]: targetRecordGroupValue,
|
||||
position: newPosition,
|
||||
...(isDefined(newPosition) && { position: newPosition }),
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 40 KiB |
@@ -1,4 +1 @@
|
||||
export {
|
||||
SettingsDatePickerInput as EventLogDatePickerInput,
|
||||
type SettingsDatePickerInputProps as EventLogDatePickerInputProps,
|
||||
} from '@/settings/components/SettingsDatePickerInput';
|
||||
export { SettingsDatePickerInput as EventLogDatePickerInput } from '@/settings/components/SettingsDatePickerInput';
|
||||
|
||||
@@ -12,18 +12,16 @@ import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { useScrollWrapperHTMLElement } from '@/ui/utilities/scroll/hooks/useScrollWrapperHTMLElement';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
||||
|
||||
import {
|
||||
type EventLogRecord,
|
||||
EventLogTable,
|
||||
type EventLogTable,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
import {
|
||||
type ColumnConfig,
|
||||
getColumnsForEventLogTable,
|
||||
} from '@/settings/event-logs/utils/getColumnsForEventLogTable';
|
||||
import { EventLogJsonCell } from '@/settings/event-logs/components/EventLogJsonCell';
|
||||
|
||||
type EventLogResultsTableProps = {
|
||||
records: EventLogRecord[];
|
||||
@@ -106,9 +104,6 @@ export const EventLogResultsTable = ({
|
||||
const { theme } = useContext(ThemeContext);
|
||||
const { t } = useLingui();
|
||||
|
||||
const showObjectEventColumns = selectedTable === EventLogTable.OBJECT_EVENT;
|
||||
const showApplicationLogColumns =
|
||||
selectedTable === EventLogTable.APPLICATION_LOG;
|
||||
const baseColumns = getColumnsForEventLogTable(selectedTable);
|
||||
|
||||
const [columnWidths, setColumnWidths] = useState<Record<string, number>>(() =>
|
||||
@@ -117,7 +112,6 @@ export const EventLogResultsTable = ({
|
||||
|
||||
const [resizingColumn, setResizingColumn] = useState<string | null>(null);
|
||||
|
||||
// Reset column widths when switching tables to avoid undefined widths for new columns
|
||||
useEffect(() => {
|
||||
setColumnWidths(
|
||||
Object.fromEntries(baseColumns.map((col) => [col.id, col.defaultWidth])),
|
||||
@@ -242,85 +236,21 @@ export const EventLogResultsTable = ({
|
||||
</StyledResizableHeaderContainer>
|
||||
))}
|
||||
</TableRow>
|
||||
{records.map((record, index) => (
|
||||
{records.map((record) => (
|
||||
<TableRow
|
||||
key={`${record.timestamp}-${record.event}-${index}`}
|
||||
key={`${record.timestamp}-${record.event}`}
|
||||
gridTemplateColumns={gridTemplateColumns}
|
||||
>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{record.event}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{beautifyPastDateRelativeToNow(record.timestamp)}
|
||||
</TableCell>
|
||||
{showApplicationLogColumns ? (
|
||||
<>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{record.properties?.level ?? '-'}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{record.properties?.message ?? '-'}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{record.properties?.executionId ?? '-'}
|
||||
</TableCell>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{record.userId ?? '-'}
|
||||
</TableCell>
|
||||
{showObjectEventColumns && (
|
||||
<>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{record.recordId ?? '-'}
|
||||
</TableCell>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{record.objectMetadataId ?? '-'}
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
<EventLogJsonCell value={record.properties} />
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
{baseColumns.map((column) => (
|
||||
<TableCell
|
||||
key={column.id}
|
||||
overflow="hidden"
|
||||
textOverflow="ellipsis"
|
||||
whiteSpace="nowrap"
|
||||
>
|
||||
{column.renderCell(record)}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</Table>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
@@ -8,34 +10,26 @@ type EventLogTableSelectorProps = {
|
||||
onChange: (value: EventLogTable) => void;
|
||||
};
|
||||
|
||||
const TABLE_LABELS: Record<EventLogTable, MessageDescriptor> = {
|
||||
[EventLogTable.PAGEVIEW]: msg`Page Views`,
|
||||
[EventLogTable.WORKSPACE_EVENT]: msg`Workspace Events`,
|
||||
[EventLogTable.OBJECT_EVENT]: msg`Object Events`,
|
||||
[EventLogTable.USAGE_EVENT]: msg`Usage Events`,
|
||||
[EventLogTable.APPLICATION_LOG]: msg`Application Logs`,
|
||||
};
|
||||
|
||||
export const EventLogTableSelector = ({
|
||||
value,
|
||||
onChange,
|
||||
}: EventLogTableSelectorProps) => {
|
||||
const { t } = useLingui();
|
||||
|
||||
const options = [
|
||||
{
|
||||
value: EventLogTable.PAGEVIEW,
|
||||
label: t`Page Views`,
|
||||
},
|
||||
{
|
||||
value: EventLogTable.WORKSPACE_EVENT,
|
||||
label: t`Workspace Events`,
|
||||
},
|
||||
{
|
||||
value: EventLogTable.OBJECT_EVENT,
|
||||
label: t`Object Events`,
|
||||
},
|
||||
{
|
||||
value: EventLogTable.USAGE_EVENT,
|
||||
label: t`Usage Events`,
|
||||
},
|
||||
{
|
||||
value: EventLogTable.APPLICATION_LOG,
|
||||
label: t`Application Logs`,
|
||||
},
|
||||
];
|
||||
const options = (
|
||||
Object.entries(TABLE_LABELS) as [EventLogTable, MessageDescriptor][]
|
||||
).map(([table, label]) => ({
|
||||
value: table,
|
||||
label: t(label),
|
||||
}));
|
||||
|
||||
return (
|
||||
<Select
|
||||
|
||||
@@ -1,24 +1,51 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { useMemo, useState } from 'react';
|
||||
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { isClickHouseConfiguredState } from '@/client-config/states/isClickHouseConfiguredState';
|
||||
import { SettingsEmptyPlaceholder } from '@/settings/components/SettingsEmptyPlaceholder';
|
||||
import { SettingsEnterpriseFeatureGateCard } from '@/settings/components/SettingsEnterpriseFeatureGateCard';
|
||||
import { SettingsOptionCardContentButton } from '@/settings/components/SettingsOptions/SettingsOptionCardContentButton';
|
||||
import { EventLogFilters } from '@/settings/event-logs/components/EventLogFilters';
|
||||
import { EventLogResultsTable } from '@/settings/event-logs/components/EventLogResultsTable';
|
||||
import { EventLogTableSelector } from '@/settings/event-logs/components/EventLogTableSelector';
|
||||
import { useEventLogsLiveStream } from '@/settings/event-logs/hooks/useEventLogsLiveStream';
|
||||
import { useEventLogs } from '@/settings/event-logs/hooks/useQueryEventLogs';
|
||||
import { type EventLogFiltersState } from '@/settings/event-logs/types/EventLogFiltersState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { IconRefresh } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
import {
|
||||
IconArrowUp,
|
||||
IconLock,
|
||||
IconPlayerPause,
|
||||
IconPlayerPlay,
|
||||
} from 'twenty-ui/display';
|
||||
import { Button, IconButton } from 'twenty-ui/input';
|
||||
import { Card } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
import { EventLogTable } from '~/generated-metadata/graphql';
|
||||
import {
|
||||
BillingEntitlementKey,
|
||||
EventLogTable,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { isGraphqlErrorOfType } from '~/utils/is-graphql-error-of-type.util';
|
||||
|
||||
const StyledRoot = styled.div`
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[6]};
|
||||
margin: 0 auto;
|
||||
max-width: 760px;
|
||||
min-height: 0;
|
||||
padding: ${themeCssVariables.spacing[6]} ${themeCssVariables.spacing[8]}
|
||||
${themeCssVariables.spacing[8]};
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledCardContent = styled.div`
|
||||
display: flex;
|
||||
@@ -40,8 +67,10 @@ const StyledSelectorGrow = styled.div`
|
||||
|
||||
const StyledResults = styled.div`
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
min-height: 0;
|
||||
`;
|
||||
|
||||
const StyledRecordCount = styled.span`
|
||||
@@ -50,10 +79,9 @@ const StyledRecordCount = styled.span`
|
||||
font-size: ${themeCssVariables.font.size.sm};
|
||||
`;
|
||||
|
||||
// The results table scrolls internally and loads more as you reach the bottom,
|
||||
// so it needs a bounded height.
|
||||
const StyledTableWrapper = styled.div`
|
||||
height: 480px;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
@@ -64,45 +92,64 @@ export const SettingsLogs = () => {
|
||||
|
||||
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
|
||||
const isClickHouseConfigured = useAtomStateValue(isClickHouseConfiguredState);
|
||||
const billing = useAtomStateValue(billingState);
|
||||
const navigateSettings = useNavigateSettings();
|
||||
|
||||
const hasEnterpriseAccess =
|
||||
currentWorkspace?.hasValidSignedEnterpriseKey === true;
|
||||
const isBillingEnabled = billing?.isBillingEnabled ?? false;
|
||||
const hasAuditLogsEntitlement =
|
||||
currentWorkspace?.billingEntitlements?.some(
|
||||
(entitlement) =>
|
||||
entitlement.key === BillingEntitlementKey.AUDIT_LOGS &&
|
||||
entitlement.value,
|
||||
) === true;
|
||||
|
||||
const [selectedTable, setSelectedTable] = useState<EventLogTable>(
|
||||
EventLogTable.PAGEVIEW,
|
||||
);
|
||||
const [filters, setFilters] = useState<EventLogFiltersState>({});
|
||||
const [isPaused, setIsPaused] = useState(false);
|
||||
|
||||
const isApplicationLog = selectedTable === EventLogTable.APPLICATION_LOG;
|
||||
const canQuery =
|
||||
isClickHouseConfigured && (isApplicationLog || hasEnterpriseAccess);
|
||||
isClickHouseConfigured && (isApplicationLog || hasAuditLogsEntitlement);
|
||||
|
||||
const {
|
||||
records,
|
||||
totalCount,
|
||||
hasNextPage,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
loadMore,
|
||||
} = useEventLogs(
|
||||
{
|
||||
table: selectedTable,
|
||||
filters: {
|
||||
eventType: filters.eventType,
|
||||
userWorkspaceId: filters.userWorkspaceId,
|
||||
dateRange: filters.dateRange
|
||||
? {
|
||||
start: filters.dateRange.start?.toISOString(),
|
||||
end: filters.dateRange.end?.toISOString(),
|
||||
}
|
||||
: undefined,
|
||||
recordId: filters.recordId,
|
||||
objectMetadataId: filters.objectMetadataId,
|
||||
const { records, totalCount, hasNextPage, loading, error, loadMore } =
|
||||
useEventLogs(
|
||||
{
|
||||
table: selectedTable,
|
||||
filters: {
|
||||
eventType: filters.eventType,
|
||||
userWorkspaceId: filters.userWorkspaceId,
|
||||
dateRange: filters.dateRange
|
||||
? {
|
||||
start: filters.dateRange.start?.toISOString(),
|
||||
end: filters.dateRange.end?.toISOString(),
|
||||
}
|
||||
: undefined,
|
||||
recordId: filters.recordId,
|
||||
objectMetadataId: filters.objectMetadataId,
|
||||
},
|
||||
first: RECORDS_PER_PAGE,
|
||||
},
|
||||
first: RECORDS_PER_PAGE,
|
||||
},
|
||||
{ skip: !canQuery },
|
||||
{ skip: !canQuery },
|
||||
);
|
||||
|
||||
const hasActiveFilters =
|
||||
isDefined(filters.eventType) ||
|
||||
isDefined(filters.userWorkspaceId) ||
|
||||
isDefined(filters.recordId) ||
|
||||
isDefined(filters.objectMetadataId) ||
|
||||
isDefined(filters.dateRange?.start) ||
|
||||
isDefined(filters.dateRange?.end);
|
||||
|
||||
const liveRecords = useEventLogsLiveStream({
|
||||
table: selectedTable,
|
||||
enabled: !isPaused && !hasActiveFilters && canQuery,
|
||||
});
|
||||
|
||||
const displayedRecords = useMemo(
|
||||
() => [...liveRecords, ...records],
|
||||
[liveRecords, records],
|
||||
);
|
||||
|
||||
const handleTableChange = (table: EventLogTable) => {
|
||||
@@ -114,39 +161,63 @@ export const SettingsLogs = () => {
|
||||
setFilters(newFilters);
|
||||
};
|
||||
|
||||
const renderUpgradeCard = () => (
|
||||
<Card rounded>
|
||||
<SettingsOptionCardContentButton
|
||||
Icon={IconLock}
|
||||
title={t`Upgrade to access audit logs`}
|
||||
description={t`Only application logs are available on your current plan. Other log types require an Enterprise subscription.`}
|
||||
Button={
|
||||
<Button
|
||||
title={t`Upgrade`}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
size="small"
|
||||
Icon={IconArrowUp}
|
||||
onClick={() =>
|
||||
navigateSettings(
|
||||
isBillingEnabled
|
||||
? SettingsPath.Billing
|
||||
: SettingsPath.AdminPanelEnterprise,
|
||||
)
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
||||
const renderResults = () => {
|
||||
if (!isApplicationLog && !hasEnterpriseAccess) {
|
||||
return (
|
||||
<SettingsEnterpriseFeatureGateCard
|
||||
title={t`Enterprise feature`}
|
||||
description={t`Upgrade to Enterprise to access this log type.`}
|
||||
buttonTitle={t`Activate`}
|
||||
/>
|
||||
);
|
||||
if (!isApplicationLog && !hasAuditLogsEntitlement) {
|
||||
return renderUpgradeCard();
|
||||
}
|
||||
|
||||
if (!isClickHouseConfigured) {
|
||||
return (
|
||||
<SettingsEmptyPlaceholder>
|
||||
{t`Audit logs require ClickHouse to be configured. Please contact your administrator.`}
|
||||
{t`Logs require ClickHouse to be configured. Please contact your administrator.`}
|
||||
</SettingsEmptyPlaceholder>
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(error)) {
|
||||
if (isGraphqlErrorOfType(error, 'NO_ENTITLEMENT')) {
|
||||
return renderUpgradeCard();
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingsEmptyPlaceholder>
|
||||
{t`Something went wrong while loading audit logs. Please try again.`}
|
||||
{t`Something went wrong while loading logs. Please try again.`}
|
||||
</SettingsEmptyPlaceholder>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledResults>
|
||||
<StyledRecordCount>{t`${records.length} of ${totalCount}`}</StyledRecordCount>
|
||||
<StyledRecordCount>{t`${displayedRecords.length} of ${totalCount + liveRecords.length}`}</StyledRecordCount>
|
||||
<StyledTableWrapper>
|
||||
<EventLogResultsTable
|
||||
records={records}
|
||||
records={displayedRecords}
|
||||
loading={loading}
|
||||
hasNextPage={hasNextPage}
|
||||
onLoadMore={loadMore}
|
||||
@@ -158,7 +229,7 @@ export const SettingsLogs = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledRoot>
|
||||
<Card rounded fullWidth>
|
||||
<StyledCardContent>
|
||||
<StyledSelectorRow>
|
||||
@@ -168,17 +239,15 @@ export const SettingsLogs = () => {
|
||||
onChange={handleTableChange}
|
||||
/>
|
||||
</StyledSelectorGrow>
|
||||
<IconButton
|
||||
Icon={IconRefresh}
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
ariaLabel={t`Refresh`}
|
||||
onClick={() => {
|
||||
if (canQuery) {
|
||||
void refetch();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{canQuery && (
|
||||
<IconButton
|
||||
Icon={isPaused ? IconPlayerPlay : IconPlayerPause}
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
ariaLabel={isPaused ? t`Resume` : t`Pause`}
|
||||
onClick={() => setIsPaused((previous) => !previous)}
|
||||
/>
|
||||
)}
|
||||
</StyledSelectorRow>
|
||||
<EventLogFilters
|
||||
table={selectedTable}
|
||||
@@ -189,6 +258,6 @@ export const SettingsLogs = () => {
|
||||
</Card>
|
||||
|
||||
{renderResults()}
|
||||
</>
|
||||
</StyledRoot>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,7 +10,6 @@ export const GET_EVENT_LOGS = gql`
|
||||
properties
|
||||
recordId
|
||||
objectMetadataId
|
||||
isCustom
|
||||
}
|
||||
totalCount
|
||||
pageInfo {
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const EVENT_LOGS_LIVE_SUBSCRIPTION = gql`
|
||||
subscription EventLogsLive($table: EventLogTable!) {
|
||||
eventLogsLive(table: $table) {
|
||||
event
|
||||
timestamp
|
||||
userId
|
||||
properties
|
||||
recordId
|
||||
objectMetadataId
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,68 @@
|
||||
import { print, type ExecutionResult } from 'graphql';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { EVENT_LOGS_LIVE_SUBSCRIPTION } from '@/settings/event-logs/graphql/subscriptions/EventLogsLiveSubscription';
|
||||
import { sseClientState } from '@/sse-db-event/states/sseClientState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { captureException } from '@sentry/react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
type EventLogRecord,
|
||||
type EventLogTable,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type EventLogsLivePayload = {
|
||||
eventLogsLive: EventLogRecord[] | null;
|
||||
};
|
||||
|
||||
const EVENT_LOGS_LIVE_SUBSCRIPTION_QUERY = print(EVENT_LOGS_LIVE_SUBSCRIPTION);
|
||||
|
||||
export const useEventLogsLiveStream = ({
|
||||
table,
|
||||
enabled,
|
||||
}: {
|
||||
table: EventLogTable;
|
||||
enabled: boolean;
|
||||
}): EventLogRecord[] => {
|
||||
const sseClient = useAtomStateValue(sseClientState);
|
||||
const [liveRecords, setLiveRecords] = useState<EventLogRecord[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setLiveRecords([]);
|
||||
}, [table]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled) {
|
||||
setLiveRecords([]);
|
||||
}
|
||||
}, [enabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || !isDefined(sseClient)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dispose = sseClient.subscribe<EventLogsLivePayload>(
|
||||
{
|
||||
query: EVENT_LOGS_LIVE_SUBSCRIPTION_QUERY,
|
||||
variables: { table },
|
||||
},
|
||||
{
|
||||
next: (value: ExecutionResult<EventLogsLivePayload>) => {
|
||||
const incoming = value.data?.eventLogsLive;
|
||||
|
||||
if (isDefined(incoming) && incoming.length > 0) {
|
||||
setLiveRecords((previous) => [...incoming, ...previous]);
|
||||
}
|
||||
},
|
||||
error: (error) => captureException(error),
|
||||
complete: () => {},
|
||||
},
|
||||
);
|
||||
|
||||
return () => dispose();
|
||||
}, [enabled, sseClient, table]);
|
||||
|
||||
return liveRecords;
|
||||
};
|
||||
@@ -20,7 +20,7 @@ export const useEventLogs = (
|
||||
input: EventLogQueryInput,
|
||||
options?: { skip?: boolean },
|
||||
) => {
|
||||
const { data, loading, error, refetch, fetchMore } = useQuery<
|
||||
const { data, loading, error, fetchMore } = useQuery<
|
||||
EventLogsData,
|
||||
EventLogsVariables
|
||||
>(GET_EVENT_LOGS, {
|
||||
@@ -70,7 +70,6 @@ export const useEventLogs = (
|
||||
hasNextPage,
|
||||
loading,
|
||||
error,
|
||||
refetch,
|
||||
loadMore,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
|
||||
import { EventLogTable } from '~/generated-metadata/graphql';
|
||||
|
||||
export type ColumnConfig = {
|
||||
id: string;
|
||||
label: MessageDescriptor;
|
||||
minWidth: number;
|
||||
defaultWidth: number;
|
||||
};
|
||||
|
||||
const DEFAULT_COLUMNS: ColumnConfig[] = [
|
||||
{ id: 'event', label: msg`Event`, minWidth: 100, defaultWidth: 200 },
|
||||
{
|
||||
id: 'timestamp',
|
||||
label: msg`Timestamp`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 150,
|
||||
},
|
||||
{ id: 'userId', label: msg`User`, minWidth: 100, defaultWidth: 150 },
|
||||
{
|
||||
id: 'properties',
|
||||
label: msg`Properties`,
|
||||
minWidth: 200,
|
||||
defaultWidth: 400,
|
||||
},
|
||||
];
|
||||
|
||||
const OBJECT_EVENT_COLUMNS: ColumnConfig[] = [
|
||||
{ id: 'event', label: msg`Event`, minWidth: 100, defaultWidth: 180 },
|
||||
{
|
||||
id: 'timestamp',
|
||||
label: msg`Timestamp`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 130,
|
||||
},
|
||||
{ id: 'userId', label: msg`User`, minWidth: 100, defaultWidth: 130 },
|
||||
{
|
||||
id: 'recordId',
|
||||
label: msg`Record ID`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 130,
|
||||
},
|
||||
{
|
||||
id: 'objectMetadataId',
|
||||
label: msg`Object ID`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 130,
|
||||
},
|
||||
{
|
||||
id: 'properties',
|
||||
label: msg`Properties`,
|
||||
minWidth: 150,
|
||||
defaultWidth: 300,
|
||||
},
|
||||
];
|
||||
|
||||
const USAGE_EVENT_COLUMNS: ColumnConfig[] = [
|
||||
{
|
||||
id: 'event',
|
||||
label: msg`Resource Type`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 130,
|
||||
},
|
||||
{
|
||||
id: 'timestamp',
|
||||
label: msg`Timestamp`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 140,
|
||||
},
|
||||
{ id: 'userId', label: msg`User`, minWidth: 100, defaultWidth: 130 },
|
||||
{
|
||||
id: 'properties',
|
||||
label: msg`Details`,
|
||||
minWidth: 200,
|
||||
defaultWidth: 400,
|
||||
},
|
||||
];
|
||||
|
||||
const APPLICATION_LOG_COLUMNS: ColumnConfig[] = [
|
||||
{
|
||||
id: 'event',
|
||||
label: msg`Function`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 160,
|
||||
},
|
||||
{
|
||||
id: 'timestamp',
|
||||
label: msg`Timestamp`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 140,
|
||||
},
|
||||
{ id: 'level', label: msg`Level`, minWidth: 60, defaultWidth: 80 },
|
||||
{
|
||||
id: 'message',
|
||||
label: msg`Message`,
|
||||
minWidth: 200,
|
||||
defaultWidth: 400,
|
||||
},
|
||||
{
|
||||
id: 'executionId',
|
||||
label: msg`Execution ID`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 140,
|
||||
},
|
||||
];
|
||||
|
||||
const COLUMNS_BY_TABLE: Record<EventLogTable, ColumnConfig[]> = {
|
||||
[EventLogTable.OBJECT_EVENT]: OBJECT_EVENT_COLUMNS,
|
||||
[EventLogTable.USAGE_EVENT]: USAGE_EVENT_COLUMNS,
|
||||
[EventLogTable.APPLICATION_LOG]: APPLICATION_LOG_COLUMNS,
|
||||
[EventLogTable.WORKSPACE_EVENT]: DEFAULT_COLUMNS,
|
||||
[EventLogTable.PAGEVIEW]: DEFAULT_COLUMNS,
|
||||
};
|
||||
|
||||
export const getColumnsForEventLogTable = (
|
||||
table: EventLogTable,
|
||||
): ColumnConfig[] => {
|
||||
return COLUMNS_BY_TABLE[table] ?? DEFAULT_COLUMNS;
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
import { type ReactNode } from 'react';
|
||||
import { msg } from '@lingui/core/macro';
|
||||
import { type MessageDescriptor } from '@lingui/core';
|
||||
|
||||
import { EventLogJsonCell } from '@/settings/event-logs/components/EventLogJsonCell';
|
||||
import {
|
||||
type EventLogRecord,
|
||||
EventLogTable,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
||||
|
||||
export type ColumnConfig = {
|
||||
id: string;
|
||||
label: MessageDescriptor;
|
||||
minWidth: number;
|
||||
defaultWidth: number;
|
||||
renderCell: (record: EventLogRecord) => ReactNode;
|
||||
};
|
||||
|
||||
const EVENT_COLUMN: ColumnConfig = {
|
||||
id: 'event',
|
||||
label: msg`Event`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 200,
|
||||
renderCell: (record) => record.event,
|
||||
};
|
||||
|
||||
const TIMESTAMP_COLUMN: ColumnConfig = {
|
||||
id: 'timestamp',
|
||||
label: msg`Timestamp`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 150,
|
||||
renderCell: (record) => beautifyPastDateRelativeToNow(record.timestamp),
|
||||
};
|
||||
|
||||
const USER_COLUMN: ColumnConfig = {
|
||||
id: 'userId',
|
||||
label: msg`User`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 150,
|
||||
renderCell: (record) => record.userId ?? '-',
|
||||
};
|
||||
|
||||
const PROPERTIES_COLUMN: ColumnConfig = {
|
||||
id: 'properties',
|
||||
label: msg`Properties`,
|
||||
minWidth: 200,
|
||||
defaultWidth: 400,
|
||||
renderCell: (record) => <EventLogJsonCell value={record.properties} />,
|
||||
};
|
||||
|
||||
const DEFAULT_COLUMNS: ColumnConfig[] = [
|
||||
EVENT_COLUMN,
|
||||
TIMESTAMP_COLUMN,
|
||||
USER_COLUMN,
|
||||
PROPERTIES_COLUMN,
|
||||
];
|
||||
|
||||
const OBJECT_EVENT_COLUMNS: ColumnConfig[] = [
|
||||
{ ...EVENT_COLUMN, defaultWidth: 180 },
|
||||
{ ...TIMESTAMP_COLUMN, defaultWidth: 130 },
|
||||
{ ...USER_COLUMN, defaultWidth: 130 },
|
||||
{
|
||||
id: 'recordId',
|
||||
label: msg`Record ID`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 130,
|
||||
renderCell: (record) => record.recordId ?? '-',
|
||||
},
|
||||
{
|
||||
id: 'objectMetadataId',
|
||||
label: msg`Object ID`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 130,
|
||||
renderCell: (record) => record.objectMetadataId ?? '-',
|
||||
},
|
||||
{ ...PROPERTIES_COLUMN, minWidth: 150, defaultWidth: 300 },
|
||||
];
|
||||
|
||||
const USAGE_EVENT_COLUMNS: ColumnConfig[] = [
|
||||
{ ...EVENT_COLUMN, label: msg`Resource Type`, defaultWidth: 130 },
|
||||
{ ...TIMESTAMP_COLUMN, defaultWidth: 140 },
|
||||
{ ...USER_COLUMN, defaultWidth: 130 },
|
||||
{ ...PROPERTIES_COLUMN, label: msg`Details` },
|
||||
];
|
||||
|
||||
const APPLICATION_LOG_COLUMNS: ColumnConfig[] = [
|
||||
{ ...EVENT_COLUMN, label: msg`Function`, defaultWidth: 160 },
|
||||
{ ...TIMESTAMP_COLUMN, defaultWidth: 140 },
|
||||
{
|
||||
id: 'level',
|
||||
label: msg`Level`,
|
||||
minWidth: 60,
|
||||
defaultWidth: 80,
|
||||
renderCell: (record) => record.properties?.level ?? '-',
|
||||
},
|
||||
{
|
||||
id: 'message',
|
||||
label: msg`Message`,
|
||||
minWidth: 200,
|
||||
defaultWidth: 400,
|
||||
renderCell: (record) => record.properties?.message ?? '-',
|
||||
},
|
||||
{
|
||||
id: 'executionId',
|
||||
label: msg`Execution ID`,
|
||||
minWidth: 100,
|
||||
defaultWidth: 140,
|
||||
renderCell: (record) => record.properties?.executionId ?? '-',
|
||||
},
|
||||
];
|
||||
|
||||
const COLUMNS_BY_TABLE: Record<EventLogTable, ColumnConfig[]> = {
|
||||
[EventLogTable.OBJECT_EVENT]: OBJECT_EVENT_COLUMNS,
|
||||
[EventLogTable.USAGE_EVENT]: USAGE_EVENT_COLUMNS,
|
||||
[EventLogTable.APPLICATION_LOG]: APPLICATION_LOG_COLUMNS,
|
||||
[EventLogTable.WORKSPACE_EVENT]: DEFAULT_COLUMNS,
|
||||
[EventLogTable.PAGEVIEW]: DEFAULT_COLUMNS,
|
||||
};
|
||||
|
||||
export const getColumnsForEventLogTable = (
|
||||
table: EventLogTable,
|
||||
): ColumnConfig[] => {
|
||||
return COLUMNS_BY_TABLE[table] ?? DEFAULT_COLUMNS;
|
||||
};
|
||||
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 110 KiB |
@@ -1,169 +0,0 @@
|
||||
import { useContext, useState } from 'react';
|
||||
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
import { GenericDropdownContentWidth } from '@/ui/layout/dropdown/constants/GenericDropdownContentWidth';
|
||||
import { styled } from '@linaria/react';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { H2Title, IconCopy } from 'twenty-ui/display';
|
||||
import { CodeEditor, IconButton } from 'twenty-ui/input';
|
||||
import { Card, CardContent, Section } from 'twenty-ui/layout';
|
||||
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
|
||||
const StyledCoverImage = styled.div`
|
||||
background-position: center;
|
||||
background-size: cover;
|
||||
height: 160px;
|
||||
overflow: hidden;
|
||||
`;
|
||||
|
||||
const StyledConfigButtonsContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
position: absolute;
|
||||
right: ${themeCssVariables.spacing[3]};
|
||||
top: ${themeCssVariables.spacing[3]};
|
||||
z-index: 1;
|
||||
`;
|
||||
|
||||
const StyledCoverCardContent = styled(CardContent)`
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
const StyledCopyButton = styled(IconButton)`
|
||||
background-color: ${themeCssVariables.background.transparent.lighter};
|
||||
border: 1px solid ${themeCssVariables.border.color.medium};
|
||||
`;
|
||||
|
||||
const StyledEditorContainer = styled.div`
|
||||
.monaco-editor,
|
||||
.monaco-editor .overflow-guard {
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
.monaco-editor .line-hover {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
`;
|
||||
|
||||
type McpAuthMethod = 'oauth' | 'api-key';
|
||||
|
||||
export const SettingsAiMCP = () => {
|
||||
const { t } = useLingui();
|
||||
const { copyToClipboard } = useCopyToClipboard();
|
||||
const [authMethod, setAuthMethod] = useState<McpAuthMethod>('oauth');
|
||||
const { colorScheme } = useContext(ThemeContext);
|
||||
const coverImage =
|
||||
colorScheme === 'light'
|
||||
? '/images/ai/ai-mcp-cover-light.svg'
|
||||
: '/images/ai/ai-mcp-cover-dark.svg';
|
||||
|
||||
const oauthConfig = JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
twenty: {
|
||||
type: 'streamable-http',
|
||||
url: `${REACT_APP_SERVER_BASE_URL}/mcp`,
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
const apiKeyConfig = JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
twenty: {
|
||||
type: 'streamable-http',
|
||||
url: `${REACT_APP_SERVER_BASE_URL}/mcp`,
|
||||
headers: {
|
||||
Authorization: 'Bearer [API_KEY]',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
|
||||
const isOAuth = authMethod === 'oauth';
|
||||
const activeConfig = isOAuth ? oauthConfig : apiKeyConfig;
|
||||
const editorHeight = isOAuth ? 170 : 230;
|
||||
|
||||
const codeEditorOptions = {
|
||||
readOnly: true,
|
||||
domReadOnly: true,
|
||||
renderLineHighlight: 'none' as const,
|
||||
renderLineHighlightOnlyWhenFocus: false,
|
||||
lineNumbers: 'off' as const,
|
||||
folding: false,
|
||||
selectionHighlight: false,
|
||||
occurrencesHighlight: 'off' as const,
|
||||
scrollBeyondLastLine: false,
|
||||
hover: {
|
||||
enabled: false,
|
||||
},
|
||||
guides: {
|
||||
indentation: false,
|
||||
bracketPairs: false,
|
||||
bracketPairsHorizontal: false,
|
||||
},
|
||||
padding: {
|
||||
top: 12,
|
||||
},
|
||||
};
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`MCP Server`}
|
||||
description={t`Access your workspace data from your favorite MCP client like Claude Desktop, Windsurf or Cursor.`}
|
||||
/>
|
||||
<Card rounded>
|
||||
<StyledCoverCardContent divider>
|
||||
<StyledCoverImage
|
||||
style={{ backgroundImage: `url('${coverImage}')` }}
|
||||
/>
|
||||
</StyledCoverCardContent>
|
||||
<StyledCoverCardContent>
|
||||
<StyledEditorContainer style={{ position: 'relative' }}>
|
||||
<StyledConfigButtonsContainer>
|
||||
<Select
|
||||
dropdownId="mcp-auth-method-select"
|
||||
value={authMethod}
|
||||
onChange={(value) => setAuthMethod(value as McpAuthMethod)}
|
||||
options={[
|
||||
{ label: t`OAuth`, value: 'oauth' },
|
||||
{ label: t`API Key`, value: 'api-key' },
|
||||
]}
|
||||
selectSizeVariant="small"
|
||||
dropdownWidth={GenericDropdownContentWidth.Medium}
|
||||
dropdownOffset={{ x: 0, y: 4 }}
|
||||
/>
|
||||
<StyledCopyButton
|
||||
Icon={IconCopy}
|
||||
onClick={() => {
|
||||
copyToClipboard(
|
||||
activeConfig,
|
||||
t`MCP Configuration copied to clipboard`,
|
||||
);
|
||||
}}
|
||||
size="small"
|
||||
/>
|
||||
</StyledConfigButtonsContainer>
|
||||
<CodeEditor
|
||||
value={activeConfig}
|
||||
language="json"
|
||||
options={codeEditorOptions}
|
||||
height={editorHeight}
|
||||
/>
|
||||
</StyledEditorContainer>
|
||||
</StyledCoverCardContent>
|
||||
</Card>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 92 KiB |
@@ -50,10 +50,6 @@ export const SettingsGeneral = () => {
|
||||
);
|
||||
|
||||
const renderActiveTabContent = () => {
|
||||
if (activeTabId === GENERAL_TAB_LOGS) {
|
||||
return <SettingsLogs />;
|
||||
}
|
||||
|
||||
if (activeTabId === GENERAL_TAB_SECURITY) {
|
||||
return <SettingsSecuritySettings />;
|
||||
}
|
||||
@@ -97,7 +93,13 @@ export const SettingsGeneral = () => {
|
||||
}
|
||||
links={[{ children: t`Workspace` }, { children: t`General` }]}
|
||||
>
|
||||
<SettingsPageContainer>{renderActiveTabContent()}</SettingsPageContainer>
|
||||
{activeTabId === GENERAL_TAB_LOGS ? (
|
||||
<SettingsLogs />
|
||||
) : (
|
||||
<SettingsPageContainer>
|
||||
{renderActiveTabContent()}
|
||||
</SettingsPageContainer>
|
||||
)}
|
||||
</SettingsPageLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 88 KiB |
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-sdk",
|
||||
"version": "2.10.0",
|
||||
"version": "2.11.0",
|
||||
"sideEffects": false,
|
||||
"bin": {
|
||||
"twenty": "dist/cli.cjs"
|
||||
|
||||
@@ -7,6 +7,7 @@ import chalk from 'chalk';
|
||||
export type AppDevOnceCommandOptions = {
|
||||
appPath?: string;
|
||||
verbose?: boolean;
|
||||
dryRun?: boolean;
|
||||
};
|
||||
|
||||
export class AppDevOnceCommand {
|
||||
@@ -17,12 +18,17 @@ export class AppDevOnceCommand {
|
||||
|
||||
const remoteName = ConfigService.getActiveRemote();
|
||||
|
||||
console.log(chalk.blue(`Syncing application on ${remoteName}...`));
|
||||
console.log(
|
||||
chalk.blue(
|
||||
`${options.dryRun ? 'Previewing application diff' : 'Syncing application'} on ${remoteName}...`,
|
||||
),
|
||||
);
|
||||
console.log(chalk.gray(`App path: ${appPath}\n`));
|
||||
|
||||
const result = await appDevOnce({
|
||||
appPath,
|
||||
verbose: options.verbose,
|
||||
dryRun: options.dryRun,
|
||||
onProgress: (message) => console.log(chalk.gray(message)),
|
||||
});
|
||||
|
||||
@@ -31,6 +37,16 @@ export class AppDevOnceCommand {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (options.dryRun) {
|
||||
console.log(
|
||||
chalk.green(
|
||||
`\n✓ Dry run complete for ${result.data.applicationDisplayName} — no changes were applied`,
|
||||
),
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.green(
|
||||
`\n✓ Synced ${result.data.applicationDisplayName} (${result.data.fileCount} file${result.data.fileCount === 1 ? '' : 's'})`,
|
||||
|
||||
@@ -22,6 +22,7 @@ export const registerDevCommands = (program: Command): void => {
|
||||
verbose?: boolean;
|
||||
debug?: boolean;
|
||||
debounceMs?: string;
|
||||
dryRun?: boolean;
|
||||
},
|
||||
) => {
|
||||
const commonOptions = {
|
||||
@@ -33,7 +34,10 @@ export const registerDevCommands = (program: Command): void => {
|
||||
};
|
||||
|
||||
if (options.once) {
|
||||
await devOnceCommand.execute(commonOptions);
|
||||
await devOnceCommand.execute({
|
||||
...commonOptions,
|
||||
dryRun: options.dryRun,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -48,6 +52,10 @@ export const registerDevCommands = (program: Command): void => {
|
||||
'-o, --once',
|
||||
'Build and sync once, then exit (useful for CI, scripts, and pre-commit hooks)',
|
||||
)
|
||||
.option(
|
||||
'--dry-run',
|
||||
'Preview the metadata changes without applying them (requires --once)',
|
||||
)
|
||||
.option('--debounceMs <ms>', 'Debounce in ms (default: 2 000)')
|
||||
.option('-v, --verbose', 'Show detailed logs')
|
||||
.option('-d, --debug', 'Show detailed logs (alias for --verbose)')
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import path from 'path';
|
||||
import { OUTPUT_DIR, type Manifest } from 'twenty-shared/application';
|
||||
import { type SyncAction } from 'twenty-shared/metadata';
|
||||
|
||||
import { ApiService } from '@/cli/utilities/api/api-service';
|
||||
import {
|
||||
@@ -13,6 +14,7 @@ import { manifestUpdateChecksums } from '@/cli/utilities/build/manifest/manifest
|
||||
import { writeManifestToOutput } from '@/cli/utilities/build/manifest/manifest-writer';
|
||||
import { ClientService } from '@/cli/utilities/client/client-service';
|
||||
import { ConfigService } from '@/cli/utilities/config/config-service';
|
||||
import { formatSyncActionsSummary } from '@/cli/utilities/dev/orchestrator/steps/format-sync-actions-summary';
|
||||
import { formatManifestValidationErrors } from '@/cli/utilities/error/format-manifest-validation-errors';
|
||||
import { serializeError } from '@/cli/utilities/error/serialize-error';
|
||||
import { FileUploader } from '@/cli/utilities/file/file-uploader';
|
||||
@@ -22,6 +24,7 @@ import { APP_ERROR_CODES, type CommandResult } from '@/cli/types';
|
||||
export type AppDevOnceOptions = {
|
||||
appPath: string;
|
||||
verbose?: boolean;
|
||||
dryRun?: boolean;
|
||||
onProgress?: (message: string) => void;
|
||||
};
|
||||
|
||||
@@ -32,10 +35,19 @@ export type AppDevOnceResult = {
|
||||
applicationUniversalIdentifier: string;
|
||||
};
|
||||
|
||||
const reportMetadataChanges = (
|
||||
data: { actions: SyncAction[] },
|
||||
onProgress?: (message: string) => void,
|
||||
): void => {
|
||||
for (const event of formatSyncActionsSummary(data.actions)) {
|
||||
onProgress?.(event.message);
|
||||
}
|
||||
};
|
||||
|
||||
const innerAppDevOnce = async (
|
||||
options: AppDevOnceOptions,
|
||||
): Promise<CommandResult<AppDevOnceResult>> => {
|
||||
const { appPath, onProgress, verbose } = options;
|
||||
const { appPath, onProgress, verbose, dryRun } = options;
|
||||
|
||||
onProgress?.('Checking server...');
|
||||
|
||||
@@ -120,6 +132,47 @@ const innerAppDevOnce = async (
|
||||
|
||||
await writeManifestToOutput(appPath, manifest);
|
||||
|
||||
if (dryRun) {
|
||||
onProgress?.(
|
||||
'Computing metadata diff (dry run, nothing will be applied)...',
|
||||
);
|
||||
|
||||
const dryRunResult = await apiService.syncApplication(manifest, {
|
||||
dryRun: true,
|
||||
});
|
||||
|
||||
if (!dryRunResult.success) {
|
||||
const errorEvents = verbose
|
||||
? null
|
||||
: formatManifestValidationErrors(dryRunResult.error);
|
||||
|
||||
const message = errorEvents
|
||||
? errorEvents.map((event) => event.message).join('\n')
|
||||
: `Dry run failed with error: ${serializeError(dryRunResult.error)}`;
|
||||
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: APP_ERROR_CODES.SYNC_FAILED,
|
||||
message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
reportMetadataChanges(dryRunResult.data, onProgress);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
outputDir: path.join(appPath, OUTPUT_DIR),
|
||||
fileCount: buildResult.builtFileInfos.size,
|
||||
applicationDisplayName: manifest.application.displayName,
|
||||
applicationUniversalIdentifier:
|
||||
manifest.application.universalIdentifier,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
onProgress?.('Registering application...');
|
||||
|
||||
const configService = new ConfigService();
|
||||
@@ -212,6 +265,8 @@ const innerAppDevOnce = async (
|
||||
};
|
||||
}
|
||||
|
||||
reportMetadataChanges(syncResult.data, onProgress);
|
||||
|
||||
onProgress?.('Generating API client...');
|
||||
|
||||
try {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { FileApi } from '@/cli/utilities/api/file-api';
|
||||
import { LogicFunctionApi } from '@/cli/utilities/api/logic-function-api';
|
||||
import { SchemaApi } from '@/cli/utilities/api/schema-api';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { type SyncAction } from 'twenty-shared/metadata';
|
||||
|
||||
type ApiServiceOptions = {
|
||||
disableInterceptors?: boolean;
|
||||
@@ -72,8 +73,16 @@ export class ApiService {
|
||||
return this.applicationApi.createDevelopmentApplication(...args);
|
||||
}
|
||||
|
||||
syncApplication(manifest: Manifest): Promise<ApiResponse> {
|
||||
return this.applicationApi.syncApplication(manifest);
|
||||
syncApplication(
|
||||
manifest: Manifest,
|
||||
options?: { dryRun?: boolean },
|
||||
): Promise<
|
||||
ApiResponse<{
|
||||
applicationUniversalIdentifier: string;
|
||||
actions: SyncAction[];
|
||||
}>
|
||||
> {
|
||||
return this.applicationApi.syncApplication(manifest, options);
|
||||
}
|
||||
|
||||
uninstallApplication(universalIdentifier: string): Promise<ApiResponse> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { type ApiResponse } from '@/cli/utilities/api/api-response-type';
|
||||
import axios, { type AxiosInstance, type AxiosResponse } from 'axios';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
import { type SyncAction } from 'twenty-shared/metadata';
|
||||
|
||||
export class ApplicationApi {
|
||||
constructor(private readonly client: AxiosInstance) {}
|
||||
@@ -251,18 +252,26 @@ export class ApplicationApi {
|
||||
}
|
||||
}
|
||||
|
||||
async syncApplication(manifest: Manifest): Promise<ApiResponse> {
|
||||
async syncApplication(
|
||||
manifest: Manifest,
|
||||
options?: { dryRun?: boolean },
|
||||
): Promise<
|
||||
ApiResponse<{
|
||||
applicationUniversalIdentifier: string;
|
||||
actions: SyncAction[];
|
||||
}>
|
||||
> {
|
||||
try {
|
||||
const mutation = `
|
||||
mutation SyncApplication($manifest: JSON!) {
|
||||
syncApplication(manifest: $manifest) {
|
||||
mutation SyncApplication($manifest: JSON!, $dryRun: Boolean) {
|
||||
syncApplication(manifest: $manifest, dryRun: $dryRun) {
|
||||
applicationUniversalIdentifier
|
||||
actions
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const variables = { manifest };
|
||||
const variables = { manifest, dryRun: options?.dryRun ?? false };
|
||||
|
||||
const response: AxiosResponse = await this.client.post(
|
||||
'/metadata',
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
import { type SyncAction } from 'twenty-shared/metadata';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { formatSyncActionsSummary } from '@/cli/utilities/dev/orchestrator/steps/format-sync-actions-summary';
|
||||
|
||||
describe('formatSyncActionsSummary', () => {
|
||||
it('reports no changes when the actions list is empty', () => {
|
||||
expect(formatSyncActionsSummary([])).toEqual([
|
||||
{ message: 'No metadata changes', status: 'info' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('reports no changes when actions are missing from the response', () => {
|
||||
expect(formatSyncActionsSummary(undefined)).toEqual([
|
||||
{ message: 'No metadata changes', status: 'info' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('summarizes created, updated and deleted actions with their identifiers', () => {
|
||||
const events = formatSyncActionsSummary([
|
||||
{
|
||||
type: 'create',
|
||||
metadataName: 'objectMetadata',
|
||||
flatEntity: {
|
||||
universalIdentifier: 'uid-object',
|
||||
nameSingular: 'rocket',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'create',
|
||||
metadataName: 'fieldMetadata',
|
||||
flatEntity: {
|
||||
universalIdentifier: 'uid-field',
|
||||
name: 'timelineActivities',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'update',
|
||||
metadataName: 'fieldMetadata',
|
||||
universalIdentifier: 'uid-updated-field',
|
||||
},
|
||||
{
|
||||
type: 'delete',
|
||||
metadataName: 'pageLayout',
|
||||
universalIdentifier: 'uid-page-layout',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(events).toEqual([
|
||||
{
|
||||
message: 'Metadata changes: 2 created, 1 updated, 1 deleted',
|
||||
status: 'info',
|
||||
},
|
||||
{ message: ' created objectMetadata rocket', status: 'info' },
|
||||
{ message: ' created fieldMetadata timelineActivities', status: 'info' },
|
||||
{ message: ' updated fieldMetadata uid-updated-field', status: 'info' },
|
||||
{ message: ' deleted pageLayout uid-page-layout', status: 'info' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('falls back to the universal identifier when a created entity has no name', () => {
|
||||
const events = formatSyncActionsSummary([
|
||||
{
|
||||
type: 'create',
|
||||
metadataName: 'fieldMetadata',
|
||||
flatEntity: { universalIdentifier: 'uid-nameless' },
|
||||
},
|
||||
]);
|
||||
|
||||
expect(events).toEqual([
|
||||
{ message: 'Metadata changes: 1 created', status: 'info' },
|
||||
{ message: ' created fieldMetadata uid-nameless', status: 'info' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('truncates the detail lines when there are more changes than the display limit', () => {
|
||||
const actions: SyncAction[] = Array.from({ length: 55 }, (_, index) => ({
|
||||
type: 'create' as const,
|
||||
metadataName: 'fieldMetadata' as const,
|
||||
flatEntity: {
|
||||
universalIdentifier: `uid-${index}`,
|
||||
name: `field${index}`,
|
||||
},
|
||||
}));
|
||||
|
||||
const events = formatSyncActionsSummary(actions);
|
||||
|
||||
expect(events[0]).toEqual({
|
||||
message: 'Metadata changes: 55 created',
|
||||
status: 'info',
|
||||
});
|
||||
expect(events).toHaveLength(52);
|
||||
expect(events[51]).toEqual({
|
||||
message: ' …and 5 more change(s)',
|
||||
status: 'info',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,85 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { type ApiService } from '@/cli/utilities/api/api-service';
|
||||
import { OrchestratorState } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
|
||||
import { SyncApplicationOrchestratorStep } from '@/cli/utilities/dev/orchestrator/steps/sync-application-orchestrator-step';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
|
||||
vi.mock('@/cli/utilities/build/manifest/manifest-update-checksums', () => ({
|
||||
manifestUpdateChecksums: ({ manifest }: { manifest: unknown }) => manifest,
|
||||
}));
|
||||
|
||||
vi.mock('@/cli/utilities/build/manifest/manifest-writer', () => ({
|
||||
writeManifestToOutput: vi.fn(),
|
||||
}));
|
||||
|
||||
const buildStep = (
|
||||
syncApplication: ApiService['syncApplication'],
|
||||
): { state: OrchestratorState; step: SyncApplicationOrchestratorStep } => {
|
||||
const state = new OrchestratorState({ appPath: '/tmp/app' });
|
||||
|
||||
const apiService = { syncApplication } as unknown as ApiService;
|
||||
|
||||
const step = new SyncApplicationOrchestratorStep({
|
||||
apiService,
|
||||
state,
|
||||
notify: () => {},
|
||||
});
|
||||
|
||||
return { state, step };
|
||||
};
|
||||
|
||||
const executeInput = {
|
||||
manifest: {
|
||||
application: { displayName: 'Demo' },
|
||||
} as unknown as Manifest,
|
||||
builtFileInfos: new Map(),
|
||||
appPath: '/tmp/app',
|
||||
};
|
||||
|
||||
describe('SyncApplicationOrchestratorStep', () => {
|
||||
it('renders the applied metadata changes on a successful sync', async () => {
|
||||
const syncApplication = vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
data: {
|
||||
applicationUniversalIdentifier: 'app-uid',
|
||||
actions: [
|
||||
{
|
||||
type: 'create',
|
||||
metadataName: 'fieldMetadata',
|
||||
flatEntity: {
|
||||
universalIdentifier: 'uid-field',
|
||||
name: 'timelineActivities',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const { state, step } = buildStep(syncApplication);
|
||||
|
||||
await step.execute(executeInput);
|
||||
|
||||
const messages = state.events.map((event) => event.message);
|
||||
|
||||
expect(messages).toContain('Metadata changes: 1 created');
|
||||
expect(messages).toContain(' created fieldMetadata timelineActivities');
|
||||
expect(messages).toContain('✓ Synced');
|
||||
});
|
||||
|
||||
it('reports no metadata changes when the sync applies nothing', async () => {
|
||||
const syncApplication = vi.fn().mockResolvedValue({
|
||||
success: true,
|
||||
data: { applicationUniversalIdentifier: 'app-uid', actions: [] },
|
||||
});
|
||||
|
||||
const { state, step } = buildStep(syncApplication);
|
||||
|
||||
await step.execute(executeInput);
|
||||
|
||||
const messages = state.events.map((event) => event.message);
|
||||
|
||||
expect(messages).toContain('No metadata changes');
|
||||
expect(messages).toContain('✓ Synced');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import { type OrchestratorStateStepEvent } from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
|
||||
import { type SyncAction } from 'twenty-shared/metadata';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
const MAX_DETAIL_LINES = 50;
|
||||
|
||||
const VERB_BY_TYPE = {
|
||||
create: 'created',
|
||||
update: 'updated',
|
||||
delete: 'deleted',
|
||||
} as const;
|
||||
|
||||
const getActionLabel = (action: SyncAction): string => {
|
||||
if (action.type === 'create') {
|
||||
return (
|
||||
action.flatEntity?.name ??
|
||||
action.flatEntity?.nameSingular ??
|
||||
action.flatEntity?.universalIdentifier ??
|
||||
'unknown'
|
||||
);
|
||||
}
|
||||
|
||||
return action.universalIdentifier;
|
||||
};
|
||||
|
||||
export const formatSyncActionsSummary = (
|
||||
actions: SyncAction[] | undefined,
|
||||
): OrchestratorStateStepEvent[] => {
|
||||
const definedActions = actions ?? [];
|
||||
|
||||
if (definedActions.length === 0) {
|
||||
return [{ message: 'No metadata changes', status: 'info' }];
|
||||
}
|
||||
|
||||
const counts = { create: 0, update: 0, delete: 0 };
|
||||
|
||||
for (const action of definedActions) {
|
||||
counts[action.type] += 1;
|
||||
}
|
||||
|
||||
const summaryParts = [
|
||||
counts.create > 0 ? `${counts.create} created` : null,
|
||||
counts.update > 0 ? `${counts.update} updated` : null,
|
||||
counts.delete > 0 ? `${counts.delete} deleted` : null,
|
||||
].filter(isDefined);
|
||||
|
||||
const events: OrchestratorStateStepEvent[] = [
|
||||
{ message: `Metadata changes: ${summaryParts.join(', ')}`, status: 'info' },
|
||||
];
|
||||
|
||||
const visibleActions = definedActions.slice(0, MAX_DETAIL_LINES);
|
||||
|
||||
for (const action of visibleActions) {
|
||||
events.push({
|
||||
message: ` ${VERB_BY_TYPE[action.type]} ${action.metadataName} ${getActionLabel(action)}`,
|
||||
status: 'info',
|
||||
});
|
||||
}
|
||||
|
||||
const hiddenCount = definedActions.length - visibleActions.length;
|
||||
|
||||
if (hiddenCount > 0) {
|
||||
events.push({
|
||||
message: ` …and ${hiddenCount} more change(s)`,
|
||||
status: 'info',
|
||||
});
|
||||
}
|
||||
|
||||
return events;
|
||||
};
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type OrchestratorStateStepEvent,
|
||||
type OrchestratorStateSyncStatus,
|
||||
} from '@/cli/utilities/dev/orchestrator/dev-mode-orchestrator-state';
|
||||
import { formatSyncActionsSummary } from '@/cli/utilities/dev/orchestrator/steps/format-sync-actions-summary';
|
||||
import { formatManifestValidationErrors } from '@/cli/utilities/error/format-manifest-validation-errors';
|
||||
import { serializeError } from '@/cli/utilities/error/serialize-error';
|
||||
import { type Manifest } from 'twenty-shared/application';
|
||||
@@ -69,6 +70,9 @@ export class SyncApplicationOrchestratorStep {
|
||||
const syncResult = await this.apiService.syncApplication(manifest);
|
||||
|
||||
if (syncResult.success) {
|
||||
const syncData = syncResult.data;
|
||||
|
||||
events.push(...formatSyncActionsSummary(syncData.actions));
|
||||
events.push({ message: '✓ Synced', status: 'success' });
|
||||
step.output = { syncStatus: 'synced', error: null };
|
||||
step.status = 'done';
|
||||
|
||||
@@ -35,7 +35,6 @@ FRONTEND_URL=http://localhost:3001
|
||||
# AUTH_GOOGLE_APIS_CALLBACK_URL=http://localhost:3000/auth/google-apis/get-access-token
|
||||
# CODE_INTERPRETER_TYPE=LOCAL
|
||||
# LOGIC_FUNCTION_TYPE=LOCAL
|
||||
# LOGIC_FUNCTION_LOGS_ENABLED=true
|
||||
# STORAGE_TYPE=local
|
||||
# STORAGE_LOCAL_PATH=.local-storage
|
||||
# SUPPORT_DRIVER=front
|
||||
|
||||
@@ -105,6 +105,10 @@ describe('ClickHouseService', () => {
|
||||
table: 'test_table',
|
||||
values: testData,
|
||||
format: 'JSONEachRow',
|
||||
clickhouse_settings: {
|
||||
async_insert: 1,
|
||||
wait_for_async_insert: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -28,10 +28,6 @@ export class ClickHouseService implements OnModuleInit, OnModuleDestroy {
|
||||
response: true,
|
||||
request: true,
|
||||
},
|
||||
clickhouse_settings: {
|
||||
async_insert: 1,
|
||||
wait_for_async_insert: 1,
|
||||
},
|
||||
application: 'twenty',
|
||||
log: { level: ClickHouseLogLevel.OFF },
|
||||
});
|
||||
@@ -88,10 +84,6 @@ export class ClickHouseService implements OnModuleInit, OnModuleDestroy {
|
||||
response: true,
|
||||
request: true,
|
||||
},
|
||||
clickhouse_settings: {
|
||||
async_insert: 1,
|
||||
wait_for_async_insert: 1,
|
||||
},
|
||||
application: 'twenty',
|
||||
log: { level: ClickHouseLogLevel.OFF },
|
||||
});
|
||||
@@ -282,6 +274,10 @@ export class ClickHouseService implements OnModuleInit, OnModuleDestroy {
|
||||
table,
|
||||
values: chunk,
|
||||
format: 'JSONEachRow',
|
||||
clickhouse_settings: {
|
||||
async_insert: 1,
|
||||
wait_for_async_insert: 1,
|
||||
},
|
||||
});
|
||||
chunk = [];
|
||||
currentSizeBytes = 0;
|
||||
|
||||
@@ -12,3 +12,8 @@ export const formatDateTimeForClickHouse = (date: Date | string): string => {
|
||||
|
||||
export const formatDateForClickHouse = (date: Date): string =>
|
||||
date.toISOString().slice(0, 10);
|
||||
|
||||
// ClickHouse returns DateTime64 values as naive strings (YYYY-MM-DD HH:mm:ss.SSS) in UTC.
|
||||
// Parse them as UTC explicitly, otherwise `new Date` assumes the server's local timezone.
|
||||
export const parseClickHouseDateTime = (value: string): Date =>
|
||||
new Date(`${value.replace(' ', 'T')}Z`);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/object-record-created';
|
||||
import { OBJECT_RECORD_DELETED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/object-record-delete';
|
||||
import { OBJECT_RECORD_UPDATED_EVENT } from 'src/engine/core-modules/audit/utils/events/object-event/object-record-updated';
|
||||
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated';
|
||||
import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-deactivated';
|
||||
import { type GenericTrackEvent } from 'src/engine/core-modules/audit/utils/events/workspace-event/track';
|
||||
import { OBJECT_RECORD_CREATED_EVENT } from 'src/engine/core-modules/event-logs/emit/events/object-event/object-record-created';
|
||||
import { OBJECT_RECORD_DELETED_EVENT } from 'src/engine/core-modules/event-logs/emit/events/object-event/object-record-delete';
|
||||
import { OBJECT_RECORD_UPDATED_EVENT } from 'src/engine/core-modules/event-logs/emit/events/object-event/object-record-updated';
|
||||
import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/event-logs/emit/events/workspace-event/custom-domain/custom-domain-activated';
|
||||
import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/event-logs/emit/events/workspace-event/custom-domain/custom-domain-deactivated';
|
||||
import { type GenericTrackEvent } from 'src/engine/core-modules/event-logs/emit/events/workspace-event/track';
|
||||
import { formatDateTimeForClickHouse } from 'src/database/clickHouse/clickHouse.util';
|
||||
import { SEED_APPLE_WORKSPACE_ID } from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import { USER_WORKSPACE_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
|
||||
|
||||
@@ -144,9 +144,11 @@ export class SyncCallRecordingStandardObjectsCommand extends ActiveOrSuspendedWo
|
||||
];
|
||||
|
||||
if (!isDefined(calendarEventObjectMetadata)) {
|
||||
throw new Error(
|
||||
`calendarEvent object not found for workspace ${workspaceId}`,
|
||||
this.logger.warn(
|
||||
`calendarEvent object not found for workspace ${workspaceId}, skipping CallRecording standard metadata sync`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const { twentyStandardFlatApplication } =
|
||||
|
||||
@@ -56,14 +56,19 @@ export const typeORMCoreModuleOptions: TypeOrmModuleOptions = {
|
||||
migrationsRun: false,
|
||||
migrationsTableName: '_typeorm_migrations',
|
||||
metadataTableName: '_typeorm_generated_columns_and_materialized_views',
|
||||
// The TypeORM migration system is frozen — historical migrations live in
|
||||
// `legacy-typeorm-migrations-do-not-add/` and are loaded here only so the
|
||||
// `_typeorm_migrations` table stays consistent for older deployments.
|
||||
// Do NOT add new files there: write a fast/slow instance command instead.
|
||||
// See `packages/twenty-server/docs/UPGRADE_COMMANDS.md`.
|
||||
migrations:
|
||||
process.env.IS_BILLING_ENABLED === 'true'
|
||||
? [
|
||||
`${isJest ? 'src/' : 'dist/'}database/typeorm/core/migrations/common/*{.ts,.js}`,
|
||||
`${isJest ? 'src/' : 'dist/'}database/typeorm/core/migrations/billing/*{.ts,.js}`,
|
||||
`${isJest ? 'src/' : 'dist/'}database/typeorm/core/legacy-typeorm-migrations-do-not-add/common/*{.ts,.js}`,
|
||||
`${isJest ? 'src/' : 'dist/'}database/typeorm/core/legacy-typeorm-migrations-do-not-add/billing/*{.ts,.js}`,
|
||||
]
|
||||
: [
|
||||
`${isJest ? 'src/' : 'dist/'}database/typeorm/core/migrations/common/*{.ts,.js}`,
|
||||
`${isJest ? 'src/' : 'dist/'}database/typeorm/core/legacy-typeorm-migrations-do-not-add/common/*{.ts,.js}`,
|
||||
],
|
||||
ssl:
|
||||
process.env.PG_SSL_ALLOW_SELF_SIGNED === 'true'
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Legacy TypeORM migrations — do not add new files here
|
||||
|
||||
This directory contains historical TypeORM migrations (`common/` and `billing/`).
|
||||
They are kept so that older deployments can still replay them against the
|
||||
`_typeorm_migrations` table.
|
||||
|
||||
**The TypeORM migration system is frozen.** Do not add new files here.
|
||||
|
||||
The active upgrade system is **instance commands** (fast / slow) and
|
||||
**workspace commands**, registered with `@RegisteredInstanceCommand` and
|
||||
`@RegisteredWorkspaceCommand`. Generate one with:
|
||||
|
||||
```bash
|
||||
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow>
|
||||
```
|
||||
|
||||
See `packages/twenty-server/docs/UPGRADE_COMMANDS.md` for the full guide and
|
||||
`packages/twenty-server/src/database/commands/upgrade-version-command/` for
|
||||
existing examples.
|
||||
|
||||
Note: `../migrations/utils/` is **not** legacy — those SQL helpers are still
|
||||
imported by current instance/workspace commands and live outside this folder
|
||||
on purpose.
|
||||