Compare commits

..
Author SHA1 Message Date
neo773 a73020eb45 wip 2026-06-05 21:39:54 +05:30
neo773 0a4dd19472 feat(messaging): add integration testing framework for sync jobs
Open the BullMQ driver's return-value channel with a backward-compatible
TResult generic so jobs can return typed results, and add an MSW-based
integration harness that drives messaging sync jobs end-to-end against
mocked Gmail/Microsoft Graph APIs with real DB assertions.

Covers full pipeline (folder sync, list fetch, import), folder discovery
and import policy, token refresh and insufficient-permissions, rate-limit
throttling, sync cursor, failed-channel recovery, and stale-sync recovery
across the Gmail and Microsoft drivers.
2026-06-05 16:11:02 +05:30
748 changed files with 6169 additions and 17850 deletions
+3 -28
View File
@@ -6,43 +6,18 @@ permissions:
on:
push:
tags:
- 'twenty/v*'
- 'sdk/v*'
defaults:
run:
shell: bash --noprofile --norc -euo pipefail {0}
- 'v*'
jobs:
dispatch-tag:
deploy-tag:
timeout-minutes: 3
runs-on: ubuntu-latest
steps:
- name: Resolve dispatch event from tag family
id: target
env:
REF_NAME: ${{ github.ref_name }}
run: |
case "$REF_NAME" in
twenty/v*)
event_type=auto-deploy-twenty
;;
sdk/v*)
event_type=auto-publish-npm
;;
*)
echo "Unsupported tag '$REF_NAME', expected 'twenty/v*' or 'sdk/v*'." >&2
exit 1
;;
esac
printf 'event_type=%s\n' "$event_type" >> "$GITHUB_OUTPUT"
- name: Repository Dispatch
env:
GH_TOKEN: ${{ secrets.TWENTY_INFRA_TOKEN }}
EVENT_TYPE: ${{ steps.target.outputs.event_type }}
REF_NAME: ${{ github.ref_name }}
run: |
gh api repos/twentyhq/twenty-infra/dispatches \
-f "event_type=$EVENT_TYPE" \
-f event_type=auto-deploy-tag \
-f "client_payload[github][ref_name]=$REF_NAME"
+63
View File
@@ -0,0 +1,63 @@
name: "Release: on merge"
permissions:
contents: write
on:
pull_request:
types:
- closed
defaults:
run:
shell: bash --noprofile --norc -euo pipefail {0}
jobs:
tag_and_release:
timeout-minutes: 10
runs-on: ubuntu-latest
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')
steps:
- name: Check PR Author
id: check_author
env:
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
run: |
set -euo pipefail
if [[ "$PR_AUTHOR" != "github-actions[bot]" ]]; then
echo "PR author ($PR_AUTHOR) is not trusted. Exiting."
exit 1
fi
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: main
- name: Get version from PR title
id: extract_version
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: |
set -euo pipefail
VERSION=$(printf '%s' "$PR_TITLE" | sed -n 's/.*Release v\([0-9][0-9.]*\).*/\1/p')
if [ -z "$VERSION" ]; then
echo "No valid version found in PR title. Exiting."
exit 1
fi
printf 'VERSION=%s\n' "$VERSION" >> "$GITHUB_ENV"
- name: Push new tag
run: |
set -euo pipefail
git config --global user.name 'Github Action Deploy'
git config --global user.email 'github-action-deploy@twenty.com'
git tag "v${{ env.VERSION }}"
git push origin "v${{ env.VERSION }}"
- uses: release-drafter/release-drafter@09c613e259eb8d4e7c81c2cb00618eb5fc4575a7 # v5
if: contains(github.event.pull_request.labels.*.name, 'create_release')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag: v${{ env.VERSION }}
@@ -11,7 +11,6 @@ on:
permissions:
actions: read
contents: read
pull-requests: read
jobs:
@@ -84,33 +83,6 @@ jobs:
core.setOutput('has_pr', 'true');
core.info(`PR #${prNumber}`);
- name: Compute merge-base for Argos reference
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
id: merge-base
uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7.1.0
with:
script: |
const headSha = context.payload.workflow_run.head_sha;
try {
const { data: comparison } = await github.rest.repos.compareCommitsWithBasehead({
owner: context.repo.owner,
repo: context.repo.repo,
basehead: `main...${headSha}`,
});
if (comparison.merge_base_commit?.sha) {
core.setOutput('sha', comparison.merge_base_commit.sha);
core.info(`Merge base: ${comparison.merge_base_commit.sha}`);
} else {
core.info('Could not determine merge base — will skip reference_commit');
core.setOutput('sha', '');
}
} catch (error) {
core.warning(`Failed to compute merge base: ${error instanceof Error ? error.message : String(error)}`);
core.setOutput('sha', '');
}
- name: Dispatch to ci-privileged
if: steps.check-artifact.outputs.exists == 'true' && steps.pr-info.outputs.has_pr == 'true'
env:
@@ -120,23 +92,15 @@ jobs:
REPOSITORY: ${{ github.repository }}
BRANCH: ${{ github.event.workflow_run.head_branch }}
COMMIT: ${{ github.event.workflow_run.head_sha }}
REFERENCE_COMMIT: ${{ steps.merge-base.outputs.sha }}
run: |
ARGS=(
--method POST
-f event_type=visual-regression
-f "client_payload[pr_number]=$PR_NUMBER"
-f "client_payload[run_id]=$WORKFLOW_RUN_ID"
-f "client_payload[repo]=$REPOSITORY"
-f "client_payload[branch]=$BRANCH"
gh api repos/twentyhq/ci-privileged/dispatches \
--method POST \
-f event_type=visual-regression \
-f "client_payload[pr_number]=$PR_NUMBER" \
-f "client_payload[run_id]=$WORKFLOW_RUN_ID" \
-f "client_payload[repo]=$REPOSITORY" \
-f "client_payload[branch]=$BRANCH" \
-f "client_payload[commit]=$COMMIT"
)
if [ -n "$REFERENCE_COMMIT" ]; then
ARGS+=(-f "client_payload[reference_commit]=$REFERENCE_COMMIT")
fi
gh api repos/twentyhq/ci-privileged/dispatches "${ARGS[@]}"
dispatch-main:
if: >-
+1 -1
View File
@@ -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
View File
@@ -52,7 +52,6 @@
"packages/twenty-server",
"packages/twenty-emails",
"packages/twenty-ui",
"packages/twenty-new-ui",
"packages/twenty-utils",
"packages/twenty-zapier",
"packages/twenty-website",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "create-twenty-app",
"version": "2.11.0",
"version": "2.10.0",
"description": "Command-line interface to create Twenty application",
"main": "dist/cli.cjs",
"bin": "dist/cli.cjs",
@@ -3,7 +3,7 @@ import { NavigationMenuItemType } from 'twenty-shared/types';
import { POST_CARD_UNIVERSAL_IDENTIFIER } from '../objects/post-card.object';
export default defineNavigationMenuItem({
universalIdentifier: 'e8031eca-d6ea-4a4b-b828-38227dba896a',
universalIdentifier: 'c1a2b3c4-0001-4a7b-8c9d-0e1f2a3b4c5d',
position: 0,
type: NavigationMenuItemType.OBJECT,
targetObjectUniversalIdentifier: POST_CARD_UNIVERSAL_IDENTIFIER,
@@ -1,10 +0,0 @@
import { defineViewField } from 'twenty-sdk/define';
import { ALL_POST_CARDS_VIEW_ID } from '../views/all-post-cards.view';
export default defineViewField({
fieldMetadataUniversalIdentifier: '7b57bd63-5a4c-46ca-9d52-42c8f02d1df6',
position: 5,
universalIdentifier: 'cd582d11-ea21-4dc3-b9c1-0298ce3b6b54',
viewUniversalIdentifier: ALL_POST_CARDS_VIEW_ID,
isVisible: true,
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "twenty-client-sdk",
"version": "2.11.0",
"version": "2.10.0",
"sideEffects": false,
"license": "AGPL-3.0",
"scripts": {
@@ -1434,6 +1434,11 @@ type EnterpriseSubscriptionStatusDTO {
isCancellationScheduled: Boolean!
}
type Analytics {
"""Boolean that confirms query was dispatched"""
success: Boolean!
}
type VerificationRecord {
type: String!
key: String!
@@ -1782,7 +1787,6 @@ enum FeatureFlagKey {
IS_REST_METADATA_API_NEW_FORMAT_DIRECT
IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED
IS_SETTINGS_DISCOVERY_HERO_ENABLED
IS_CALL_RECORDING_ENABLED
IS_WORKFLOW_RUN_STEP_LOGS_ENABLED
}
@@ -2616,11 +2620,6 @@ type SendEmailOutput {
error: String
}
type Analytics {
"""Boolean that confirms query was dispatched"""
success: Boolean!
}
type EventLogRecord {
event: String!
timestamp: DateTime!
@@ -3007,6 +3006,7 @@ type Query {
): IndexConnection!
findManyAgents: [Agent!]!
findOneAgent(input: AgentIdInput!): Agent!
myConnectedAccounts: [ConnectedAccountPublicDTO!]!
getRoles: [Role!]!
getToolIndex: [ToolIndexEntry!]!
getToolInputSchema(toolName: String!): JSON
@@ -3027,7 +3027,6 @@ 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,6 +3199,8 @@ 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!
@@ -3252,6 +3253,7 @@ 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!
@@ -3280,7 +3282,6 @@ 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!
@@ -3342,8 +3343,6 @@ 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 +3359,7 @@ type Mutation {
syncMarketplaceCatalog: Boolean!
createDevelopmentApplication(universalIdentifier: String!, name: String!): DevelopmentApplication!
generateApplicationToken(applicationId: UUID!): ApplicationTokenPair!
syncApplication(manifest: JSON!, dryRun: Boolean): WorkspaceMigration!
syncApplication(manifest: JSON!): WorkspaceMigration!
uploadApplicationFile(file: Upload!, applicationUniversalIdentifier: String!, fileFolder: FileFolder!, filePath: String!): File!
upgradeApplication(appRegistrationId: String!, targetVersion: String!): Boolean!
renewApplicationToken(applicationRefreshToken: String!): ApplicationTokenPair!
@@ -3716,6 +3715,11 @@ input RevokeApiKeyInput {
id: UUID!
}
enum AnalyticsType {
PAGEVIEW
TRACK
}
input CreateApprovedAccessDomainInput {
domain: String!
email: String!
@@ -4425,11 +4429,6 @@ input EditSsoInput {
status: SSOIdentityProviderStatus!
}
enum AnalyticsType {
PAGEVIEW
TRACK
}
input SendEmailInput {
connectedAccountId: String!
to: String!
@@ -4498,7 +4497,6 @@ type Subscription {
onEventSubscription(eventStreamId: String!): EventSubscription
logicFunctionLogs(input: LogicFunctionLogsInput!): LogicFunctionLogs!
onAgentChatEvent(threadId: UUID!): AgentChatEvent!
eventLogsLive(table: EventLogTable!): [EventLogRecord!]
}
input LogicFunctionLogsInput {
@@ -1091,6 +1091,12 @@ 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']
@@ -1408,7 +1414,7 @@ export interface FeatureFlag {
__typename: 'FeatureFlag'
}
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' | 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED' | 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' | 'IS_CALL_RECORDING_ENABLED' | 'IS_WORKFLOW_RUN_STEP_LOGS_ENABLED'
export type FeatureFlagKey = 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_JSON_FILTER_ENABLED' | 'IS_MARKETPLACE_SETTING_TAB_VISIBLE' | 'IS_PUBLIC_DOMAIN_ENABLED' | 'IS_EMAIL_GROUP_ENABLED' | 'IS_JUNCTION_RELATIONS_ENABLED' | 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' | 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED' | 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' | 'IS_WORKFLOW_RUN_STEP_LOGS_ENABLED'
export interface WorkspaceUrls {
customUrl?: Scalars['String']
@@ -2308,12 +2314,6 @@ 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,6 +2613,7 @@ export interface Query {
indexMetadatas: IndexConnection
findManyAgents: Agent[]
findOneAgent: Agent
myConnectedAccounts: ConnectedAccountPublicDTO[]
getRoles: Role[]
getToolIndex: ToolIndexEntry[]
getToolInputSchema?: Scalars['JSON']
@@ -2624,7 +2625,6 @@ export interface Query {
getViewGroup?: ViewGroup
myMessageFolders: MessageFolder[]
myMessageChannels: MessageChannel[]
myConnectedAccounts: ConnectedAccountPublicDTO[]
myCalendarChannels: CalendarChannel[]
minimalMetadata: MinimalMetadata
appConnections: AppConnection[]
@@ -2724,6 +2724,8 @@ export interface Mutation {
updateApiKey?: ApiKey
revokeApiKey?: ApiKey
assignRoleToApiKey: Scalars['Boolean']
createObjectEvent: Analytics
trackAnalytics: Analytics
skipSyncEmailOnboardingStep: OnboardingStepSuccess
skipBookOnboardingStep: OnboardingStepSuccess
checkoutSession: BillingSession
@@ -2776,6 +2778,7 @@ export interface Mutation {
createOneAgent: Agent
updateOneAgent: Agent
deleteOneAgent: Agent
deleteConnectedAccount: ConnectedAccountPublicDTO
updateWorkspaceMemberRole: WorkspaceMember
createOneRole: Role
updateOneRole: Role
@@ -2804,7 +2807,6 @@ export interface Mutation {
updateMessageChannel: MessageChannel
createEmailGroupChannel: CreateEmailGroupChannelOutput
deleteEmailGroupChannel: MessageChannel
deleteConnectedAccount: ConnectedAccountPublicDTO
updateCalendarChannel: CalendarChannel
createChatThread: AgentChatThread
sendChatMessage: SendChatMessageResult
@@ -2866,8 +2868,6 @@ export interface Mutation {
createSAMLIdentityProvider: SetupSso
deleteSSOIdentityProvider: DeleteSso
editSSOIdentityProvider: EditSso
createObjectEvent: Analytics
trackAnalytics: Analytics
duplicateDashboard: DuplicatedDashboard
impersonate: Impersonate
sendEmail: SendEmailOutput
@@ -2892,17 +2892,16 @@ export interface Mutation {
__typename: 'Mutation'
}
export type WorkspaceMigrationActionType = 'delete' | 'create' | 'update'
export type AnalyticsType = 'PAGEVIEW' | 'TRACK'
export type WorkspaceMigrationActionType = 'delete' | 'create' | 'update'
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'
}
@@ -4050,6 +4049,13 @@ 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
@@ -5358,13 +5364,6 @@ 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
@@ -5672,6 +5671,7 @@ export interface QueryGenqlSelection{
filter: IndexFilter} })
findManyAgents?: AgentGenqlSelection
findOneAgent?: (AgentGenqlSelection & { __args: {input: AgentIdInput} })
myConnectedAccounts?: ConnectedAccountPublicDTOGenqlSelection
getRoles?: RoleGenqlSelection
getToolIndex?: ToolIndexEntryGenqlSelection
getToolInputSchema?: { __args: {toolName: Scalars['String']} }
@@ -5689,7 +5689,6 @@ 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)} })
@@ -5812,6 +5811,8 @@ 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)} })
@@ -5864,6 +5865,7 @@ 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} })
@@ -5892,7 +5894,6 @@ 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)} })
@@ -5954,8 +5955,6 @@ 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} })
@@ -5973,7 +5972,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'], dryRun?: (Scalars['Boolean'] | null)} })
syncApplication?: (WorkspaceMigrationGenqlSelection & { __args: {manifest: Scalars['JSON']} })
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']} })
@@ -6357,7 +6356,6 @@ 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
}
@@ -7005,6 +7003,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 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"')
@@ -8133,14 +8139,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 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"')
@@ -8814,7 +8812,6 @@ export const enumFeatureFlagKey = {
IS_REST_METADATA_API_NEW_FORMAT_DIRECT: 'IS_REST_METADATA_API_NEW_FORMAT_DIRECT' as const,
IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED: 'IS_LOGIC_FUNCTION_PREBUILT_MODE_ENABLED' as const,
IS_SETTINGS_DISCOVERY_HERO_ENABLED: 'IS_SETTINGS_DISCOVERY_HERO_ENABLED' as const,
IS_CALL_RECORDING_ENABLED: 'IS_CALL_RECORDING_ENABLED' as const,
IS_WORKFLOW_RUN_STEP_LOGS_ENABLED: 'IS_WORKFLOW_RUN_STEP_LOGS_ENABLED' as const
}
@@ -8992,17 +8989,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,
File diff suppressed because it is too large Load Diff
+39 -3
View File
@@ -6,10 +6,10 @@ services:
volumes:
- server-local-data:/app/packages/twenty-server/.local-storage
ports:
- "3010:3000"
- "3000: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}/${PG_DATABASE_NAME}
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default
SERVER_URL: ${SERVER_URL}
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
DISABLE_DB_MIGRATIONS: ${DISABLE_DB_MIGRATIONS}
@@ -46,6 +46,11 @@ 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
@@ -59,7 +64,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}/${PG_DATABASE_NAME}
PG_DATABASE_URL: postgres://${PG_DATABASE_USER:-postgres}:${PG_DATABASE_PASSWORD:-postgres}@${PG_DATABASE_HOST:-db}:${PG_DATABASE_PORT:-5432}/default
SERVER_URL: ${SERVER_URL}
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
DISABLE_DB_MIGRATIONS: "true" # it already runs on the server
@@ -96,7 +101,38 @@ 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:
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 124 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 70 KiB

@@ -283,7 +283,6 @@ export type FeatureFlag = {
};
export enum FeatureFlagKey {
IS_CALL_RECORDING_ENABLED = 'IS_CALL_RECORDING_ENABLED',
IS_EMAIL_GROUP_ENABLED = 'IS_EMAIL_GROUP_ENABLED',
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
@@ -1648,7 +1648,6 @@ export type FeatureFlag = {
};
export enum FeatureFlagKey {
IS_CALL_RECORDING_ENABLED = 'IS_CALL_RECORDING_ENABLED',
IS_EMAIL_GROUP_ENABLED = 'IS_EMAIL_GROUP_ENABLED',
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
IS_JUNCTION_RELATIONS_ENABLED = 'IS_JUNCTION_RELATIONS_ENABLED',
@@ -3315,7 +3314,6 @@ export type MutationStopAgentChatStreamArgs = {
export type MutationSyncApplicationArgs = {
dryRun?: InputMaybe<Scalars['Boolean']>;
manifest: Scalars['JSON'];
};
@@ -4933,18 +4931,12 @@ 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;
};
@@ -7410,14 +7402,7 @@ 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 }>, 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 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 UpdateLabPublicFeatureFlagMutationVariables = Exact<{
input: UpdateLabPublicFeatureFlagInput;
@@ -8268,8 +8253,7 @@ 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":"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 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 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>;
@@ -30,7 +30,7 @@ export default meta;
export type Story = StoryObj<typeof RecordIndexPage>;
export const Default: Story = {
// oxlint-disable-next-line typescript/ban-ts-comment
// oxlint-disable-next-line @typescripttypescript/ban-ts-comment
// @ts-ignore
decorators: [LoadingDecorator, PageDecorator],
play: async ({ canvasElement }) => {
@@ -79,7 +79,7 @@ export type Story = StoryObj<typeof RecordIndexPage>;
export const Default: Story = {
parameters: userMetadataLoaderMocks,
// oxlint-disable-next-line typescript/ban-ts-comment
// oxlint-disable-next-line @typescripttypescript/ban-ts-comment
// @ts-ignore
decorators: [PageDecorator],
play: async ({ canvasElement }) => {
@@ -690,7 +690,7 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
<Route
element={
<SettingsProtectedRouteWrapper
settingsPermission={PermissionFlagType.AI_SETTINGS}
settingsPermission={PermissionFlagType.AI}
/>
}
>
@@ -8,7 +8,6 @@ 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';
@@ -83,13 +82,8 @@ export const RecordBoardDragDropContext = ({
}
const existingRecordSorts = store.get(currentRecordSorts);
const boardCardDropBehavior = getBoardCardDropBehavior({
hasRecordSorts: existingRecordSorts.length > 0,
sourceDroppableId: result.source.droppableId,
destinationDroppableId: result.destination.droppableId,
});
if (boardCardDropBehavior.shouldBlockDrop) {
if (existingRecordSorts.length > 0) {
store.set(isRecordBoardDropProcessingCallbackState, false);
endRecordDrag();
openModal(RECORD_INDEX_REMOVE_SORTING_MODAL_ID);
@@ -97,9 +91,7 @@ export const RecordBoardDragDropContext = ({
}
try {
processBoardCardDrop(result, originalDragSelection, {
shouldUpdatePosition: boardCardDropBehavior.shouldUpdatePosition,
});
processBoardCardDrop(result, originalDragSelection);
} catch (error) {
store.set(isRecordBoardDropProcessingCallbackState, false);
endRecordDrag();
@@ -1,42 +0,0 @@
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,
});
});
});
@@ -1,17 +0,0 @@
export const getBoardCardDropBehavior = ({
hasRecordSorts,
sourceDroppableId,
destinationDroppableId,
}: {
hasRecordSorts: boolean;
sourceDroppableId: string;
destinationDroppableId: string;
}) => {
const isMovingInsideSameRecordGroup =
sourceDroppableId === destinationDroppableId;
return {
shouldBlockDrop: hasRecordSorts && isMovingInsideSameRecordGroup,
shouldUpdatePosition: !hasRecordSorts,
};
};
@@ -40,15 +40,9 @@ export const useProcessBoardCardDrop = () => {
);
const processBoardCardDrop = useCallback(
(
boardCardDropResult: DropResult,
selectedRecordIds: string[],
options?: { shouldUpdatePosition?: boolean },
) => {
(boardCardDropResult: DropResult, selectedRecordIds: string[]) => {
if (!isDefined(selectFieldMetadataItem)) return;
const shouldUpdatePosition = options?.shouldUpdatePosition ?? true;
processGroupDrop({
groupDropResult: boardCardDropResult,
store,
@@ -57,10 +51,7 @@ export const useProcessBoardCardDrop = () => {
recordIndexRecordIdsByGroupCallbackFamilyState,
onUpdateRecord: ({ recordId, position }, targetRecordGroupValue) => {
updateDroppedRecordOnBoard(
{
recordId,
position: shouldUpdatePosition ? position : undefined,
},
{ recordId, position },
targetRecordGroupValue,
);
},
@@ -45,6 +45,10 @@ export const useUpdateDroppedRecordOnBoard = () => {
recordStoreFamilyState.atomFamily(recordId),
) as Record<string, unknown> | null | undefined;
if (!isDefined(newPosition)) {
return;
}
if (!isDefined(initialRecord)) {
return;
}
@@ -90,10 +94,7 @@ export const useUpdateDroppedRecordOnBoard = () => {
const isSamePosition = initialRecord.position === newPosition;
if (
movingInsideSameRecordGroup &&
(!isDefined(newPosition) || isSamePosition)
) {
if (movingInsideSameRecordGroup && isSamePosition) {
return;
}
@@ -125,32 +126,25 @@ export const useUpdateDroppedRecordOnBoard = () => {
);
}
if (isDefined(newPosition)) {
const targetGroupRecordsWithIds = extractRecordPositions(
currentRecordIdsInTargetRecordGroup,
store,
);
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),
);
} else {
store.set(
recordIndexRecordIdsByGroupCallbackFamilyState(targetRecordGroupId),
[...currentRecordIdsInTargetRecordGroup, recordId],
);
}
store.set(
recordIndexRecordIdsByGroupCallbackFamilyState(targetRecordGroupId),
newTargetRecordGroupWithIds.map((record) => record.id),
);
upsertRecordsInStore({
partialRecords: [
@@ -161,7 +155,7 @@ export const useUpdateDroppedRecordOnBoard = () => {
(initialRecord as { __typename?: string })?.__typename ??
'Record',
[selectFieldMetadataItem.name]: targetRecordGroupValue,
...(isDefined(newPosition) && { position: newPosition }),
position: newPosition,
} as ObjectRecord,
],
});
@@ -170,7 +164,7 @@ export const useUpdateDroppedRecordOnBoard = () => {
idToUpdate: recordId,
updateOneRecordInput: {
[selectFieldMetadataItem.name]: targetRecordGroupValue,
...(isDefined(newPosition) && { position: newPosition }),
position: newPosition,
},
});
},
Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 40 KiB

@@ -1 +1,4 @@
export { SettingsDatePickerInput as EventLogDatePickerInput } from '@/settings/components/SettingsDatePickerInput';
export {
SettingsDatePickerInput as EventLogDatePickerInput,
type SettingsDatePickerInputProps as EventLogDatePickerInputProps,
} from '@/settings/components/SettingsDatePickerInput';
@@ -12,16 +12,18 @@ 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,
type EventLogTable,
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[];
@@ -104,6 +106,9 @@ 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>>(() =>
@@ -112,6 +117,7 @@ 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])),
@@ -236,21 +242,85 @@ export const EventLogResultsTable = ({
</StyledResizableHeaderContainer>
))}
</TableRow>
{records.map((record) => (
{records.map((record, index) => (
<TableRow
key={`${record.timestamp}-${record.event}`}
key={`${record.timestamp}-${record.event}-${index}`}
gridTemplateColumns={gridTemplateColumns}
>
{baseColumns.map((column) => (
<TableCell
key={column.id}
overflow="hidden"
textOverflow="ellipsis"
whiteSpace="nowrap"
>
{column.renderCell(record)}
</TableCell>
))}
<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>
</>
)}
</TableRow>
))}
</Table>
@@ -1,5 +1,3 @@
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';
@@ -10,26 +8,34 @@ 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 = (
Object.entries(TABLE_LABELS) as [EventLogTable, MessageDescriptor][]
).map(([table, label]) => ({
value: table,
label: t(label),
}));
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`,
},
];
return (
<Select
@@ -1,51 +1,24 @@
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useMemo, useState } from 'react';
import { 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 { SettingsOptionCardContentButton } from '@/settings/components/SettingsOptions/SettingsOptionCardContentButton';
import { SettingsEnterpriseFeatureGateCard } from '@/settings/components/SettingsEnterpriseFeatureGateCard';
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 {
IconArrowUp,
IconLock,
IconPlayerPause,
IconPlayerPlay,
} from 'twenty-ui/display';
import { Button, IconButton } from 'twenty-ui/input';
import { IconRefresh } from 'twenty-ui/display';
import { IconButton } from 'twenty-ui/input';
import { Card } from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
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%;
`;
import { EventLogTable } from '~/generated-metadata/graphql';
const StyledCardContent = styled.div`
display: flex;
@@ -67,10 +40,8 @@ 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`
@@ -79,9 +50,10 @@ 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`
flex: 1;
min-height: 0;
height: 480px;
overflow: hidden;
`;
@@ -92,64 +64,45 @@ export const SettingsLogs = () => {
const currentWorkspace = useAtomStateValue(currentWorkspaceState);
const isClickHouseConfigured = useAtomStateValue(isClickHouseConfiguredState);
const billing = useAtomStateValue(billingState);
const navigateSettings = useNavigateSettings();
const isBillingEnabled = billing?.isBillingEnabled ?? false;
const hasAuditLogsEntitlement =
currentWorkspace?.billingEntitlements?.some(
(entitlement) =>
entitlement.key === BillingEntitlementKey.AUDIT_LOGS &&
entitlement.value,
) === true;
const hasEnterpriseAccess =
currentWorkspace?.hasValidSignedEnterpriseKey === 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 || hasAuditLogsEntitlement);
isClickHouseConfigured && (isApplicationLog || hasEnterpriseAccess);
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,
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,
},
{ 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],
first: RECORDS_PER_PAGE,
},
{ skip: !canQuery },
);
const handleTableChange = (table: EventLogTable) => {
@@ -161,63 +114,39 @@ 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 && !hasAuditLogsEntitlement) {
return renderUpgradeCard();
if (!isApplicationLog && !hasEnterpriseAccess) {
return (
<SettingsEnterpriseFeatureGateCard
title={t`Enterprise feature`}
description={t`Upgrade to Enterprise to access this log type.`}
buttonTitle={t`Activate`}
/>
);
}
if (!isClickHouseConfigured) {
return (
<SettingsEmptyPlaceholder>
{t`Logs require ClickHouse to be configured. Please contact your administrator.`}
{t`Audit 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 logs. Please try again.`}
{t`Something went wrong while loading audit logs. Please try again.`}
</SettingsEmptyPlaceholder>
);
}
return (
<StyledResults>
<StyledRecordCount>{t`${displayedRecords.length} of ${totalCount + liveRecords.length}`}</StyledRecordCount>
<StyledRecordCount>{t`${records.length} of ${totalCount}`}</StyledRecordCount>
<StyledTableWrapper>
<EventLogResultsTable
records={displayedRecords}
records={records}
loading={loading}
hasNextPage={hasNextPage}
onLoadMore={loadMore}
@@ -229,7 +158,7 @@ export const SettingsLogs = () => {
};
return (
<StyledRoot>
<>
<Card rounded fullWidth>
<StyledCardContent>
<StyledSelectorRow>
@@ -239,15 +168,17 @@ export const SettingsLogs = () => {
onChange={handleTableChange}
/>
</StyledSelectorGrow>
{canQuery && (
<IconButton
Icon={isPaused ? IconPlayerPlay : IconPlayerPause}
variant="secondary"
size="medium"
ariaLabel={isPaused ? t`Resume` : t`Pause`}
onClick={() => setIsPaused((previous) => !previous)}
/>
)}
<IconButton
Icon={IconRefresh}
variant="secondary"
size="medium"
ariaLabel={t`Refresh`}
onClick={() => {
if (canQuery) {
void refetch();
}
}}
/>
</StyledSelectorRow>
<EventLogFilters
table={selectedTable}
@@ -258,6 +189,6 @@ export const SettingsLogs = () => {
</Card>
{renderResults()}
</StyledRoot>
</>
);
};
@@ -10,6 +10,7 @@ export const GET_EVENT_LOGS = gql`
properties
recordId
objectMetadataId
isCustom
}
totalCount
pageInfo {
@@ -1,14 +0,0 @@
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
}
}
`;
@@ -1,68 +0,0 @@
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, fetchMore } = useQuery<
const { data, loading, error, refetch, fetchMore } = useQuery<
EventLogsData,
EventLogsVariables
>(GET_EVENT_LOGS, {
@@ -70,6 +70,7 @@ export const useEventLogs = (
hasNextPage,
loading,
error,
refetch,
loadMore,
};
};
@@ -0,0 +1,121 @@
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;
};
@@ -1,125 +0,0 @@
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;
};
@@ -171,7 +171,7 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
label: t`AI`,
path: SettingsPath.AI,
Icon: IconSparkles,
isHidden: !permissionMap[PermissionFlagType.AI_SETTINGS],
isHidden: !permissionMap[PermissionFlagType.AI],
},
{
label: t`Email`,
Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 111 KiB

@@ -32,7 +32,7 @@ export const StepBar = ({ activeStep, children }: StepBarProps) => {
}
// If the child is not a Step, return it as-is
// oxlint-disable-next-line typescript/ban-ts-comment
// oxlint-disable-next-line @typescripttypescript/ban-ts-comment
// @ts-expect-error
if (child.type?.displayName !== Step.displayName) {
return child;
@@ -64,7 +64,7 @@ export type Story = StoryObj<typeof RecordShowPage>;
// TEMP_DISABLED_TEST: Temporarily commented out due to test failure
// export const Default: Story = {
// // oxlint-disable-next-line typescript/ban-ts-comment
// // oxlint-disable-next-line @typescripttypescript/ban-ts-comment
// // @ts-ignore
// decorators: [PageDecorator, ContextStoreDecorator],
// play: async ({ canvasElement }) => {
@@ -0,0 +1,169 @@
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>
);
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 92 KiB

After

Width:  |  Height:  |  Size: 90 KiB

@@ -50,6 +50,10 @@ export const SettingsGeneral = () => {
);
const renderActiveTabContent = () => {
if (activeTabId === GENERAL_TAB_LOGS) {
return <SettingsLogs />;
}
if (activeTabId === GENERAL_TAB_SECURITY) {
return <SettingsSecuritySettings />;
}
@@ -93,13 +97,7 @@ export const SettingsGeneral = () => {
}
links={[{ children: t`Workspace` }, { children: t`General` }]}
>
{activeTabId === GENERAL_TAB_LOGS ? (
<SettingsLogs />
) : (
<SettingsPageContainer>
{renderActiveTabContent()}
</SettingsPageContainer>
)}
<SettingsPageContainer>{renderActiveTabContent()}</SettingsPageContainer>
</SettingsPageLayout>
);
};
Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 94 KiB

@@ -1,4 +1,4 @@
// oxlint-disable-next-line typescript/ban-ts-comment
// oxlint-disable-next-line @typescripttypescript/ban-ts-comment
// @ts-ignore
import { type WorkflowRun } from '@/workflow/types/Workflow';
import { StepStatus } from 'twenty-shared/workflow';
-77
View File
@@ -1,77 +0,0 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "import", "unicorn"],
"jsPlugins": ["../twenty-oxlint-rules/dist/oxlint-plugin.mjs"],
"categories": {
"correctness": "off"
},
"ignorePatterns": ["node_modules", "generated", "**/*.module.scss.d.ts"],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": [
"warn",
{ "allow": ["group", "groupCollapsed", "groupEnd"] }
],
"no-control-regex": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
"no-undef": "off",
"no-unused-vars": "off",
"no-redeclare": "off",
"import/no-duplicates": "error",
"react/no-unescaped-entities": "off",
"react/prop-types": "off",
"react/jsx-key": "off",
"react/display-name": "off",
"react/jsx-uses-react": "off",
"react/react-in-jsx-scope": "off",
"react/jsx-no-useless-fragment": "off",
"react/jsx-props-no-spreading": ["error", { "explicitSpread": "ignore" }],
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"typescript/no-redeclare": "error",
"typescript/ban-ts-comment": "error",
"typescript/consistent-type-imports": [
"error",
{
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}
],
"typescript/explicit-function-return-type": "off",
"typescript/explicit-module-boundary-types": "off",
"typescript/no-empty-object-type": [
"error",
{
"allowInterfaces": "with-single-extends"
}
],
"typescript/no-empty-function": "off",
"typescript/no-explicit-any": "off",
"typescript/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"twenty/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{
"sourceTag": "scope:shared",
"onlyDependOnLibsWithTags": ["scope:shared"]
}
]
}
]
}
}
-6
View File
@@ -1,6 +0,0 @@
dist
storybook-static
coverage
# Generated by vite-plugin-sass-dts (committed; regenerated in dev mode).
*.module.scss.d.ts
-78
View File
@@ -1,78 +0,0 @@
[
{
"name": ". (root barrel)",
"path": "dist/index.mjs",
"limit": "60 kB",
"import": "*"
},
{
"name": "accessibility",
"path": "dist/accessibility.mjs",
"limit": "10 kB"
},
{
"name": "assets",
"path": "dist/assets.mjs",
"limit": "10 kB"
},
{
"name": "testing",
"path": "dist/testing.mjs",
"limit": "10 kB"
},
{
"name": "components",
"path": "dist/components.mjs",
"limit": "40 kB"
},
{
"name": "display",
"path": "dist/display.mjs",
"limit": "30 kB"
},
{
"name": "feedback",
"path": "dist/feedback.mjs",
"limit": "20 kB"
},
{
"name": "input",
"path": "dist/input.mjs",
"limit": "50 kB"
},
{
"name": "layout",
"path": "dist/layout.mjs",
"limit": "20 kB"
},
{
"name": "navigation",
"path": "dist/navigation.mjs",
"limit": "20 kB"
},
{
"name": "json-visualizer",
"path": "dist/json-visualizer.mjs",
"limit": "30 kB"
},
{
"name": "theme",
"path": "dist/theme.mjs",
"limit": "55 kB"
},
{
"name": "theme-constants",
"path": "dist/theme-constants.mjs",
"limit": "20 kB"
},
{
"name": "utilities",
"path": "dist/utilities.mjs",
"limit": "10 kB"
},
{
"name": "style.css",
"path": "dist/style.css",
"limit": "40 kB"
}
]
-58
View File
@@ -1,58 +0,0 @@
import type { StorybookConfig } from '@storybook/react-vite';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import checker from 'vite-plugin-checker';
const dirname =
typeof __dirname !== 'undefined'
? __dirname
: path.dirname(fileURLToPath(import.meta.url));
const isVitest = Boolean(process.env.VITEST);
const config: StorybookConfig = {
stories: ['../src/**/*.@(mdx|stories.@(js|jsx|ts|tsx))'],
addons: [
'@storybook-community/storybook-addon-cookie',
'@storybook/addon-links',
'@storybook/addon-coverage',
'@storybook/addon-a11y',
'storybook-addon-pseudo-states',
'@storybook/addon-vitest',
],
framework: '@storybook/react-vite',
viteFinal: async (viteConfig) => {
const plugins = [...(viteConfig.plugins ?? [])];
if (!isVitest) {
plugins.push(
checker({
typescript: {
tsconfigPath: path.resolve(dirname, '../tsconfig.json'),
},
}),
);
}
return {
...viteConfig,
plugins,
resolve: {
...viteConfig.resolve,
alias: {
...(viteConfig.resolve?.alias ?? {}),
'@tabler/icons-react': '@tabler/icons-react/dist/esm/icons/index.mjs',
},
},
};
},
};
export default config;
// To customize your Vite configuration you can use the viteFinal field.
// Check https://storybook.js.org/docs/react/builders/vite#configuration
// and https://nx.dev/recipes/storybook/custom-builder-configs
@@ -1,34 +0,0 @@
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
rel="stylesheet"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.7/iframeResizer.contentWindow.min.js"></script>
<style type="text/css">
body {
margin: 0;
font-family: 'Inter', sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html {
font-size: 13px;
}
.sbdocs-wrapper {
padding: 0 !important;
}
*::-webkit-scrollbar {
height: 4px;
width: 4px;
}
*::-webkit-scrollbar-corner {
background-color: transparent;
}
*::-webkit-scrollbar-thumb {
background-color: transparent;
border-radius: 2px;
}
</style>
@@ -1,24 +0,0 @@
import { type Preview } from '@storybook/react-vite';
import '@new-ui/theme-constants/theme-light.css';
import '@new-ui/theme-constants/theme-dark.css';
import { ThemeProvider } from '@new-ui/theme-constants';
const preview: Preview = {
tags: ['autodocs'],
parameters: {
a11y: {
test: 'error',
},
},
decorators: [
(Story) => {
return (
<ThemeProvider colorScheme="light">
<Story />
</ThemeProvider>
);
},
],
};
export default preview;
@@ -1,5 +0,0 @@
import { setProjectAnnotations } from '@storybook/react-vite';
import * as projectAnnotations from './preview';
// Apply Storybook's preview configuration to Vitest runs.
setProjectAnnotations([projectAnnotations]);
+57 -13
View File
@@ -1,10 +1,6 @@
# twenty-new-ui
> **Status:** Phase 0 — Foundations in progress. The package is scaffolded and builds
> on SCSS Modules + Base UI; the theme layer is ported from `twenty-ui` with a parity
> test, and the size/Storybook/a11y harnesses are wired up. Remaining Phase 0 work: the
> CI diff-table workflow, the `twenty-ui` component inventory, and the `modules/ui` triage.
> The sections below remain the design document for the full effort.
> **Status:** Planning. This is a design document; no implementation has started.
`twenty-new-ui` is the next generation of Twenty's UI library, replacing [`twenty-ui`](../twenty-ui).
It is built on a headless component library and a zero-runtime, CSS-variable styling layer.
@@ -34,9 +30,8 @@ components are now **in scope** — they migrate into `twenty-new-ui` (see [Appl
## Decision 1 — Headless library: Base UI
Adopt **Base UI** ([`mui/base-ui`](https://github.com/mui/base-ui), published to npm as
[`@base-ui/react`](https://base-ui.com), MIT) as the behavioral foundation; build Twenty's visual
design on top of it.
Adopt **Base UI** (`@base-ui-components/react`, MIT) as the behavioral foundation; build Twenty's
visual design on top of it.
| | Base UI | shadcn/ui | Radix |
| --- | --- | --- | --- |
@@ -89,7 +84,7 @@ packages/twenty-new-ui/
├── vitest.config.ts # storybook component tests
├── .storybook/
├── .size-limit.json # per-entry bundle budgets
├── scripts/ # generateBarrels.ts
├── scripts/ # generateBarrels.ts, generateTheme.ts
└── src/
├── styles/ # global: reset, theme vars, mixins, breakpoints
├── theme/ theme-constants/
@@ -111,8 +106,8 @@ as-is.
- Keep the public API identical: `ThemeProvider`, `ThemeContext`, `useTheme`, the `themeCssVariables` shape, `ThemeType`, color helpers, and the `theme-light.css` / `theme-dark.css` exports.
- Reuse `twenty-ui`'s token values verbatim to guarantee identical design.
- Tokens live in `src/theme/` (`THEME_LIGHT` / `THEME_DARK`); the `--t-*` CSS variables and the `themeCssVariables` accessor are static files mirrored token-for-token from `twenty-ui` (matching `twenty-ui`'s own static-CSS approach).
- A theme parity test asserts the theme CSS and `themeCssVariables` stay identical to `twenty-ui`'s `--t-*` values.
- Tokens are authored in `src/theme/` and generated into CSS variables + a typed accessor.
- A generated-output diff test asserts the new theme CSS produces the same `--t-*` values as `twenty-ui`.
## Component migration map
@@ -224,7 +219,7 @@ CI surfaces a per-PR diff table (`twenty-ui` vs `twenty-new-ui`) for size, a11y,
- Vite library mode, dual ESM/CJS, `vite-plugin-dts`, `vite-plugin-svgr`; SCSS via Vite's built-in `sass`; no Babel.
- `sideEffects: ["**/*.css", "**/*.scss"]`; emit per-entry CSS plus `style.css` / `theme-light.css` / `theme-dark.css`.
- Public package (remove `private`); ships as `twenty-new-ui` until cut-over, then claims the `twenty-ui` name once the old package is removed.
- Public package (remove `private`); scoped name (e.g. `@twenty/ui`).
- **Changesets** for semver + changelog; GitHub Actions release with `npm publish --provenance`.
- Declare `react` / `react-dom` as peer dependencies; validate the `exports`/types map with `publint` + `@arethetypeswrong/cli`.
- Publish the Storybook as living documentation.
@@ -264,10 +259,59 @@ a passing visual-parity diff, and a within-budget size entry.
## Open questions
1. Published package name: `twenty-new-ui` now, renamed to `twenty-ui` at cut-over (Phase 6).
1. Published package name/scope (proposed `@twenty/ui`).
2. Styling: confirm SCSS Modules vs vanilla-extract vs plain CSS Modules.
3. Variants helper: `clsx` + `data-*` vs `cva`.
4. Visual regression tooling: Chromatic vs self-hosted image snapshots.
5. How aggressively to drop `framer-motion` in favor of CSS/Base UI transitions.
6. Scope of `assets` / `testing` / `json-visualizer`: port verbatim or modernize.
7. Where to draw the generic-vs-app-specific line for `modules/ui`, and whether hybrid components live as a headless core in `twenty-new-ui` with a thin app wrapper in `twenty-front`.
## Appendix — example component pattern
`src/input/components/Toggle.tsx`
```tsx
import { Switch } from '@base-ui-components/react/switch';
import { clsx } from 'clsx';
import styles from './Toggle.module.scss';
type ToggleProps = {
value?: boolean;
onChange?: (value: boolean) => void;
disabled?: boolean;
size?: 'small' | 'medium';
};
export const Toggle = ({ value, onChange, disabled, size = 'medium' }: ToggleProps) => (
<Switch.Root
checked={value}
onCheckedChange={onChange}
disabled={disabled}
className={clsx(styles.root, styles[size])}
>
<Switch.Thumb className={styles.thumb} />
</Switch.Root>
);
```
`src/input/components/Toggle.module.scss`
```scss
.root {
background: var(--t-background-quaternary);
border-radius: var(--t-border-radius-pill);
transition: background var(--t-animation-duration-fast) ease;
&[data-checked] { background: var(--t-color-blue); }
&[data-disabled] { opacity: 0.32; cursor: not-allowed; }
}
.thumb {
background: var(--t-background-primary);
border-radius: 50%;
transition: translate var(--t-animation-duration-fast) ease;
.root[data-checked] & { translate: 100% 0; }
}
```
@@ -1 +0,0 @@
export default 'test-file-stub';
@@ -1,7 +0,0 @@
// Proxy so `styles.anyClassName` resolves to its key when SCSS is imported in Jest.
module.exports = new Proxy(
{},
{
get: (_target, key) => (key === '__esModule' ? false : String(key)),
},
);
-41
View File
@@ -1,41 +0,0 @@
import { readFileSync } from 'fs';
import { dirname, resolve } from 'path';
import { pathsToModuleNameMapper } from 'ts-jest';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const tsConfigPath = resolve(__dirname, './tsconfig.json');
const tsConfig = JSON.parse(readFileSync(tsConfigPath, 'utf8'));
const jestConfig = {
displayName: 'twenty-new-ui',
preset: '../../jest.preset.js',
setupFilesAfterEnv: ['./setupTests.ts'],
testEnvironment: 'jsdom',
transformIgnorePatterns: ['../../node_modules/'],
transform: {
'^.+\\.[tj]sx?$': [
'@swc/jest',
{
jsc: {
parser: { syntax: 'typescript', tsx: true },
transform: { react: { runtime: 'automatic' } },
},
},
],
},
moduleNameMapper: {
'\\.(jpg|jpeg|png|gif|webp|svg|svg)$': '<rootDir>/__mocks__/imageMockUi.js',
'\\.(scss|css)$': '<rootDir>/__mocks__/styleMock.js',
...pathsToModuleNameMapper(tsConfig.compilerOptions.paths, {
prefix: '<rootDir>/',
}),
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
extensionsToTreatAsEsm: ['.ts', '.tsx'],
coverageDirectory: './coverage',
};
export default jestConfig;
-208
View File
@@ -1,208 +0,0 @@
{
"name": "twenty-new-ui",
"//name": "Named `twenty-new-ui` because the target name `twenty-ui` is still taken by the existing packages/twenty-ui workspace (Yarn forbids duplicate workspace names). Renamed to `twenty-ui` at the Phase 6 cut-over once the old package is removed.",
"version": "0.0.0",
"main": "dist/index.cjs",
"module": "dist/index.mjs",
"style": "./dist/style.css",
"type": "module",
"sideEffects": [
"**/*.css",
"**/*.scss"
],
"devDependencies": {
"@argos-ci/storybook": "^6.0.6",
"@prettier/sync": "^0.5.2",
"@size-limit/preset-small-lib": "^11.1.6",
"@storybook-community/storybook-addon-cookie": "^5.0.0",
"@storybook/addon-a11y": "^10.3.3",
"@storybook/addon-coverage": "^3.0.0",
"@storybook/addon-links": "^10.3.3",
"@storybook/addon-vitest": "^10.2.13",
"@storybook/react-vite": "^10.3.3",
"@swc/core": "^1.15.11",
"@swc/jest": "^0.2.39",
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@types/jest": "^30.0.0",
"@types/react": "^18.2.39",
"@types/react-dom": "^18.2.15",
"@vitejs/plugin-react-swc": "4.2.3",
"@vitest/browser-playwright": "4.0.18",
"jest": "29.7.0",
"jest-environment-jsdom": "30.0.0-beta.3",
"prettier": "^3.1.1",
"sass": "^1.83.0",
"sass-embedded": "^1.83.0",
"size-limit": "^11.1.6",
"slash": "^5.1.0",
"storybook-addon-pseudo-states": "^10.3.3",
"ts-jest": "^29.1.1",
"tsx": "^4.19.3",
"vite-plugin-checker": "^0.10.2",
"vite-plugin-dts": "3.8.1",
"vite-plugin-sass-dts": "^1.3.31",
"vite-plugin-svgr": "^4.3.0",
"vite-tsconfig-paths": "^4.2.1",
"vitest": "4.0.18"
},
"dependencies": {
"@base-ui/react": "^1.5.0",
"@monaco-editor/react": "^4.7.0",
"@radix-ui/colors": "^3.0.0",
"@tabler/icons-react": "^3.31.0",
"clsx": "^2.1.1",
"date-fns": "^2.30.0",
"framer-motion": "^11.18.0",
"glob": "^11.1.0",
"hex-rgb": "^5.0.0",
"jotai": "^2.17.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-responsive": "^9.0.2",
"react-router-dom": "^6.4.4",
"twenty-shared": "workspace:*",
"zod": "^4.1.11"
},
"peerDependencies": {
"monaco-editor": ">= 0.25.0 < 1"
},
"scripts": {
"build": "npx vite build"
},
"files": [
"dist",
"accessibility",
"assets",
"components",
"display",
"feedback",
"input",
"json-visualizer",
"layout",
"navigation",
"testing",
"theme",
"theme-constants",
"utilities"
],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
},
"./style.css": "./dist/style.css",
"./theme-light.css": "./dist/theme-light.css",
"./theme-dark.css": "./dist/theme-dark.css",
"./accessibility": {
"types": "./dist/accessibility/index.d.ts",
"import": "./dist/accessibility.mjs",
"require": "./dist/accessibility.cjs"
},
"./assets": {
"types": "./dist/assets/index.d.ts",
"import": "./dist/assets.mjs",
"require": "./dist/assets.cjs"
},
"./components": {
"types": "./dist/components/index.d.ts",
"import": "./dist/components.mjs",
"require": "./dist/components.cjs"
},
"./display": {
"types": "./dist/display/index.d.ts",
"import": "./dist/display.mjs",
"require": "./dist/display.cjs"
},
"./feedback": {
"types": "./dist/feedback/index.d.ts",
"import": "./dist/feedback.mjs",
"require": "./dist/feedback.cjs"
},
"./input": {
"types": "./dist/input/index.d.ts",
"import": "./dist/input.mjs",
"require": "./dist/input.cjs"
},
"./json-visualizer": {
"types": "./dist/json-visualizer/index.d.ts",
"import": "./dist/json-visualizer.mjs",
"require": "./dist/json-visualizer.cjs"
},
"./layout": {
"types": "./dist/layout/index.d.ts",
"import": "./dist/layout.mjs",
"require": "./dist/layout.cjs"
},
"./navigation": {
"types": "./dist/navigation/index.d.ts",
"import": "./dist/navigation.mjs",
"require": "./dist/navigation.cjs"
},
"./testing": {
"types": "./dist/testing/index.d.ts",
"import": "./dist/testing.mjs",
"require": "./dist/testing.cjs"
},
"./theme": {
"types": "./dist/theme/index.d.ts",
"import": "./dist/theme.mjs",
"require": "./dist/theme.cjs"
},
"./theme-constants": {
"types": "./dist/theme-constants/index.d.ts",
"import": "./dist/theme-constants.mjs",
"require": "./dist/theme-constants.cjs"
},
"./utilities": {
"types": "./dist/utilities/index.d.ts",
"import": "./dist/utilities.mjs",
"require": "./dist/utilities.cjs"
}
},
"typesVersions": {
"*": {
"accessibility": [
"dist/accessibility/index.d.ts"
],
"assets": [
"dist/assets/index.d.ts"
],
"components": [
"dist/components/index.d.ts"
],
"display": [
"dist/display/index.d.ts"
],
"feedback": [
"dist/feedback/index.d.ts"
],
"input": [
"dist/input/index.d.ts"
],
"json-visualizer": [
"dist/json-visualizer/index.d.ts"
],
"layout": [
"dist/layout/index.d.ts"
],
"navigation": [
"dist/navigation/index.d.ts"
],
"testing": [
"dist/testing/index.d.ts"
],
"theme": [
"dist/theme/index.d.ts"
],
"theme-constants": [
"dist/theme-constants/index.d.ts"
],
"utilities": [
"dist/utilities/index.d.ts"
]
}
}
}
-103
View File
@@ -1,103 +0,0 @@
{
"name": "twenty-new-ui",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"sourceRoot": "packages/twenty-new-ui/src",
"projectType": "library",
"tags": ["scope:shared"],
"targets": {
"build": {
"dependsOn": ["^build"],
"outputs": [
"{projectRoot}/dist",
"{projectRoot}/accessibility/package.json",
"{projectRoot}/accessibility/dist",
"{projectRoot}/assets/package.json",
"{projectRoot}/assets/dist",
"{projectRoot}/components/package.json",
"{projectRoot}/components/dist",
"{projectRoot}/display/package.json",
"{projectRoot}/display/dist",
"{projectRoot}/feedback/package.json",
"{projectRoot}/feedback/dist",
"{projectRoot}/input/package.json",
"{projectRoot}/input/dist",
"{projectRoot}/json-visualizer/package.json",
"{projectRoot}/json-visualizer/dist",
"{projectRoot}/layout/package.json",
"{projectRoot}/layout/dist",
"{projectRoot}/navigation/package.json",
"{projectRoot}/navigation/dist",
"{projectRoot}/testing/package.json",
"{projectRoot}/testing/dist",
"{projectRoot}/theme/package.json",
"{projectRoot}/theme/dist",
"{projectRoot}/theme-constants/package.json",
"{projectRoot}/theme-constants/dist",
"{projectRoot}/utilities/package.json",
"{projectRoot}/utilities/dist"
]
},
"generateBarrels": {
"executor": "nx:run-commands",
"cache": true,
"inputs": ["production", "{projectRoot}/scripts/generateBarrels.ts"],
"outputs": [
"{projectRoot}/src/**/*/index.ts",
"{projectRoot}/package.json"
],
"options": { "command": "tsx {projectRoot}/scripts/generateBarrels.ts" }
},
"build:individual": {
"executor": "nx:run-commands",
"cache": true,
"dependsOn": ["build"],
"inputs": ["production", "^production"],
"outputs": ["{projectRoot}/dist/individual"],
"options": {
"cwd": "{projectRoot}",
"command": "npx vite build -c vite.config.individual.ts"
}
},
"size": {
"executor": "nx:run-commands",
"cache": true,
"dependsOn": ["build"],
"inputs": ["{projectRoot}/dist", "{projectRoot}/.size-limit.json"],
"options": {
"cwd": "{projectRoot}",
"command": "npx size-limit"
},
"configurations": {
"why": { "command": "npx size-limit --why" }
}
},
"clean": {
"executor": "nx:run-commands",
"options": { "command": "rimraf {projectRoot}/dist" }
},
"lint": {},
"fmt": { "options": { "files": "src" }, "configurations": { "fix": {} } },
"test": {},
"typecheck": {},
"storybook:build": { "configurations": { "test": {} } },
"storybook:serve:dev": { "options": { "port": 6008 } },
"storybook:serve:static": {
"options": {
"buildTarget": "twenty-new-ui:storybook:build",
"port": 6008
},
"configurations": { "test": {} }
},
"storybook:test": {},
"storybook:test:no-coverage": {},
"storybook:visual-diff": {
"executor": "nx:run-commands",
"dependsOn": ["storybook:build"],
"options": {
"cwd": "{projectRoot}",
"command": "bash scripts/visual-diff.sh"
}
},
"storybook:coverage": {}
}
}
@@ -1,505 +0,0 @@
import prettier from '@prettier/sync';
import * as fs from 'fs';
import { globSync } from 'glob';
import path from 'path';
import { type Options } from 'prettier';
import slash from 'slash';
import ts from 'typescript';
// TODO prastoin refactor this file in several one into its dedicated package and make it a TypeScript CLI
const INDEX_FILENAME = 'index';
const PACKAGE_JSON_FILENAME = 'package.json';
const NX_PROJECT_CONFIGURATION_FILENAME = 'project.json';
const PACKAGE_PATH = path.resolve('packages/twenty-ui');
const SRC_PATH = path.resolve(`${PACKAGE_PATH}/src`);
const PACKAGE_JSON_PATH = path.join(PACKAGE_PATH, PACKAGE_JSON_FILENAME);
const NX_PROJECT_CONFIGURATION_PATH = path.join(
PACKAGE_PATH,
NX_PROJECT_CONFIGURATION_FILENAME,
);
const prettierConfigFile = prettier.resolveConfigFile();
if (prettierConfigFile == null) {
throw new Error('Prettier config file not found');
}
const prettierConfiguration = prettier.resolveConfig(prettierConfigFile);
const prettierFormat = (str: string, parser: Options['parser']) =>
prettier.format(str, {
...prettierConfiguration,
parser,
});
type createTypeScriptFileArgs = {
path: string;
content: string;
filename: string;
};
const createTypeScriptFile = ({
content,
path: filePath,
filename,
}: createTypeScriptFileArgs) => {
const header = `
/*
* _____ _
*|_ _|_ _____ _ __ | |_ _ _
* | | \\ \\ /\\ / / _ \\ '_ \\| __| | | | Auto-generated file
* | | \\ V V / __/ | | | |_| |_| | Any edits to this will be overridden
* |_| \\_/\\_/ \\___|_| |_|\\__|\\__, |
* |___/
*/
`;
const formattedContent = prettierFormat(
`${header}\n${content}\n`,
'typescript',
);
fs.writeFileSync(
path.join(filePath, `${filename}.ts`),
formattedContent,
'utf-8',
);
};
const getLastPathFolder = (pathStr: string) => path.basename(pathStr);
const getSubDirectoryPaths = (directoryPath: string): string[] => {
const pattern = slash(path.join(directoryPath, '*/'));
return globSync(pattern, {
ignore: [...EXCLUDED_DIRECTORIES],
cwd: SRC_PATH,
nodir: false,
maxDepth: 1,
}).sort((a, b) => a.localeCompare(b));
};
const partitionFileExportsByType = (declarations: DeclarationOccurrence[]) => {
return declarations.reduce<{
typeAndInterfaceDeclarations: DeclarationOccurrence[];
otherDeclarations: DeclarationOccurrence[];
}>(
(acc, { kind, name }) => {
if (kind === 'type' || kind === 'interface') {
return {
...acc,
typeAndInterfaceDeclarations: [
...acc.typeAndInterfaceDeclarations,
{ kind, name },
],
};
}
return {
...acc,
otherDeclarations: [...acc.otherDeclarations, { kind, name }],
};
},
{
typeAndInterfaceDeclarations: [],
otherDeclarations: [],
},
);
};
const generateModuleIndexFiles = (exportByBarrel: ExportByBarrel[]) => {
return exportByBarrel.map<createTypeScriptFileArgs>(
({ barrel: { moduleDirectory }, allFileExports }) => {
const content = allFileExports
.sort((a, b) => a.file.localeCompare(b.file))
.map(({ exports, file }) => {
const { otherDeclarations, typeAndInterfaceDeclarations } =
partitionFileExportsByType(exports);
const fileWithoutExtension = path.parse(file).name;
const pathToImport = slash(
path.relative(
moduleDirectory,
path.join(path.dirname(file), fileWithoutExtension),
),
);
const mapDeclarationNameAndJoin = (
declarations: DeclarationOccurrence[],
) => declarations.map(({ name }) => name).join(', ');
const typeExport =
typeAndInterfaceDeclarations.length > 0
? `export type { ${mapDeclarationNameAndJoin(typeAndInterfaceDeclarations)} } from "./${pathToImport}"`
: '';
const othersExport =
otherDeclarations.length > 0
? `export { ${mapDeclarationNameAndJoin(otherDeclarations)} } from "./${pathToImport}"`
: '';
return [typeExport, othersExport]
.filter((el) => el !== '')
.join('\n');
})
.join('\n');
return {
content,
path: moduleDirectory,
filename: INDEX_FILENAME,
};
},
);
};
type JsonUpdate = Record<string, any>;
type WriteInJsonFileArgs = {
content: JsonUpdate;
file: string;
};
const updateJsonFile = ({ content, file }: WriteInJsonFileArgs) => {
const updatedJsonFile = JSON.stringify(content);
const formattedContent = prettier.format(updatedJsonFile, {
...prettierConfiguration,
filepath: file,
});
fs.writeFileSync(file, formattedContent, 'utf-8');
};
const writeInPackageJson = (update: JsonUpdate) => {
const rawJsonFile = fs.readFileSync(PACKAGE_JSON_PATH, 'utf-8');
const initialJsonFile = JSON.parse(rawJsonFile);
updateJsonFile({
file: PACKAGE_JSON_PATH,
content: {
...initialJsonFile,
...update,
},
});
};
const updateNxProjectConfigurationBuildOutputs = (outputs: JsonUpdate) => {
const rawJsonFile = fs.readFileSync(NX_PROJECT_CONFIGURATION_PATH, 'utf-8');
const initialJsonFile = JSON.parse(rawJsonFile);
updateJsonFile({
file: NX_PROJECT_CONFIGURATION_PATH,
content: {
...initialJsonFile,
targets: {
...initialJsonFile.targets,
build: {
...initialJsonFile.targets.build,
outputs,
},
},
},
});
};
type ExportOccurrence = {
types: string;
import: string;
require: string;
};
type ExportsConfig = Record<string, ExportOccurrence | string>;
const generateModulePackageExports = (moduleDirectories: string[]) => {
return moduleDirectories.reduce<ExportsConfig>(
(acc, moduleDirectory) => {
const moduleName = getLastPathFolder(moduleDirectory);
if (moduleName === undefined) {
throw new Error(
`Should never occur, moduleName is undefined ${moduleDirectory}`,
);
}
return {
...acc,
[`./${moduleName}`]: {
types: `./dist/${moduleName}/index.d.ts`,
import: `./dist/${moduleName}.mjs`,
require: `./dist/${moduleName}.cjs`,
},
};
},
{
'./style.css': './dist/style.css',
'./theme-light.css': './dist/theme-light.css',
'./theme-dark.css': './dist/theme-dark.css',
},
);
};
const computePackageJsonFilesAndExportsConfig = (
moduleDirectories: string[],
) => {
const entrypoints = moduleDirectories.map(getLastPathFolder);
const exports = {
'.': {
types: './dist/index.d.ts',
import: './dist/index.mjs',
require: './dist/index.cjs',
},
...generateModulePackageExports(moduleDirectories),
} satisfies ExportsConfig;
const typesVersionsEntries = entrypoints.reduce<Record<string, string[]>>(
(acc, moduleName) => ({
...acc,
[`${moduleName}`]: [`dist/${moduleName}/index.d.ts`],
}),
{},
);
return {
exports,
typesVersions: { '*': typesVersionsEntries },
files: ['dist', ...entrypoints],
};
};
const computeProjectNxBuildOutputsPath = (moduleDirectories: string[]) => {
const dynamicOutputsPath = moduleDirectories
.map(getLastPathFolder)
.flatMap((barrelName) =>
['package.json', 'dist'].map(
(subPath) => `{projectRoot}/${barrelName}/${subPath}`,
),
);
return ['{projectRoot}/dist', ...dynamicOutputsPath];
};
const EXCLUDED_EXTENSIONS = [
'**/*.test.ts',
'**/*.test.tsx',
'**/*.spec.ts',
'**/*.spec.tsx',
'**/*.stories.ts',
'**/*.stories.tsx',
] as const;
const EXCLUDED_DIRECTORIES = [
'**/__tests__/**',
'**/__mocks__/**',
'**/__stories__/**',
'**/internal/**',
] as const;
function getTypeScriptFiles(
directoryPath: string,
includeIndex: boolean = false,
): string[] {
const pattern = slash(path.join(directoryPath, '**', '*.{ts,tsx}'));
const files = globSync(pattern, {
cwd: SRC_PATH,
nodir: true,
ignore: [...EXCLUDED_EXTENSIONS, ...EXCLUDED_DIRECTORIES],
});
return files.filter(
(file) =>
!file.endsWith('.d.ts') &&
(includeIndex ? true : !file.endsWith('index.ts')),
);
}
const getKind = (
node: ts.VariableStatement,
): Extract<ExportKind, 'const' | 'let' | 'var'> => {
const isConst = (node.declarationList.flags & ts.NodeFlags.Const) !== 0;
if (isConst) {
return 'const';
}
const isLet = (node.declarationList.flags & ts.NodeFlags.Let) !== 0;
if (isLet) {
return 'let';
}
return 'var';
};
function extractExportsFromSourceFile(sourceFile: ts.SourceFile) {
const exports: DeclarationOccurrence[] = [];
function visit(node: ts.Node) {
if (!ts.canHaveModifiers(node)) {
return ts.forEachChild(node, visit);
}
const modifiers = ts.getModifiers(node);
const isExport = modifiers?.some(
(mod) => mod.kind === ts.SyntaxKind.ExportKeyword,
);
if (!isExport && !ts.isExportDeclaration(node)) {
return ts.forEachChild(node, visit);
}
switch (true) {
case ts.isTypeAliasDeclaration(node):
exports.push({
kind: 'type',
name: node.name.text,
});
break;
case ts.isInterfaceDeclaration(node):
exports.push({
kind: 'interface',
name: node.name.text,
});
break;
case ts.isEnumDeclaration(node):
exports.push({
kind: 'enum',
name: node.name.text,
});
break;
case ts.isFunctionDeclaration(node) && node.name !== undefined:
exports.push({
kind: 'function',
name: node.name.text,
});
break;
case ts.isVariableStatement(node):
node.declarationList.declarations.forEach((decl) => {
const kind = getKind(node);
if (ts.isIdentifier(decl.name)) {
exports.push({
kind,
name: decl.name.text,
});
} else if (ts.isObjectBindingPattern(decl.name)) {
decl.name.elements.forEach((element) => {
if (
!ts.isBindingElement(element) ||
!ts.isIdentifier(element.name)
) {
return;
}
exports.push({
kind,
name: element.name.text,
});
});
}
});
break;
case ts.isClassDeclaration(node) && node.name !== undefined:
exports.push({
kind: 'class',
name: node.name.text,
});
break;
case ts.isExportDeclaration(node):
if (node.exportClause && ts.isNamedExports(node.exportClause)) {
node.exportClause.elements.forEach((element) => {
const exportName = element.name.text;
// Check both the declaration and the individual specifier for type-only exports
const isTypeExport =
node.isTypeOnly || ts.isTypeOnlyExportDeclaration(node);
if (isTypeExport) {
// should handle kind
exports.push({
kind: 'type',
name: exportName,
});
return;
}
exports.push({
kind: 'const',
name: exportName,
});
});
}
break;
}
return ts.forEachChild(node, visit);
}
visit(sourceFile);
return exports;
}
type ExportKind =
| 'type'
| 'interface'
| 'enum'
| 'function'
| 'const'
| 'let'
| 'var'
| 'class';
type DeclarationOccurrence = { kind: ExportKind; name: string };
type FileExports = Array<{
file: string;
exports: DeclarationOccurrence[];
}>;
function findAllExports(directoryPath: string): FileExports {
const results: FileExports = [];
const files = getTypeScriptFiles(directoryPath);
for (const file of files) {
const sourceFile = ts.createSourceFile(
file,
fs.readFileSync(file, 'utf8'),
ts.ScriptTarget.Latest,
true,
);
const exports = extractExportsFromSourceFile(sourceFile);
if (exports.length > 0) {
results.push({
file,
exports,
});
}
}
return results;
}
type ExportByBarrel = {
barrel: {
moduleName: string;
moduleDirectory: string;
};
allFileExports: FileExports;
};
const retrieveExportsByBarrel = (barrelDirectories: string[]) => {
return barrelDirectories.map<ExportByBarrel>((moduleDirectory) => {
const moduleExportsPerFile = findAllExports(moduleDirectory);
const moduleName = getLastPathFolder(moduleDirectory);
if (!moduleName) {
throw new Error(
`Should never occur moduleName not found ${moduleDirectory}`,
);
}
return {
barrel: {
moduleName,
moduleDirectory,
},
allFileExports: moduleExportsPerFile,
};
});
};
const main = () => {
const moduleDirectories = getSubDirectoryPaths(SRC_PATH);
const exportsByBarrel = retrieveExportsByBarrel(moduleDirectories);
const moduleIndexFiles = generateModuleIndexFiles(exportsByBarrel);
const packageJsonConfig =
computePackageJsonFilesAndExportsConfig(moduleDirectories);
const nxBuildOutputsPath =
computeProjectNxBuildOutputsPath(moduleDirectories);
updateNxProjectConfigurationBuildOutputs(nxBuildOutputsPath);
writeInPackageJson(packageJsonConfig);
moduleIndexFiles.forEach(createTypeScriptFile);
};
main();
@@ -1,49 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
if [[ -f "$SCRIPT_DIR/.env" ]]; then
set -a
source "$SCRIPT_DIR/.env"
set +a
fi
ARGOS_API_BASE_URL="${ARGOS_API_BASE_URL:-http://127.0.0.1:4002/v2/}"
ARGOS_TOKEN="${ARGOS_TOKEN:?ARGOS_TOKEN is required set it in packages/twenty-new-ui/.env}"
USERNAME=$(whoami)
GIT_BRANCH="${ARGOS_BRANCH:-$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")}"
COMMIT="${ARGOS_COMMIT:-$(git rev-parse HEAD 2>/dev/null || echo "unknown")}"
REFERENCE_COMMIT="${ARGOS_REFERENCE_COMMIT:-$(git merge-base HEAD main 2>/dev/null || echo "")}"
export ARGOS_API_BASE_URL
export ARGOS_TOKEN
export ARGOS_BUILD_NAME="${USERNAME}/twenty-new-ui"
export ARGOS_BRANCH="${USERNAME}/${GIT_BRANCH}"
export ARGOS_COMMIT="$COMMIT"
export ARGOS_REFERENCE_COMMIT="${REFERENCE_COMMIT}"
echo "Argos visual diff"
echo " API: $ARGOS_API_BASE_URL"
echo " Build name: $ARGOS_BUILD_NAME"
echo " Branch: $ARGOS_BRANCH"
echo " Commit: ${ARGOS_COMMIT:0:12}"
echo " Ref commit: ${ARGOS_REFERENCE_COMMIT:0:12}"
echo ""
npx http-server storybook-static --port 6007 --silent &
HTTP_PID=$!
trap "kill $HTTP_PID 2>/dev/null || true" EXIT
SERVER_UP=false
for i in $(seq 1 30); do
if curl -sf http://localhost:6007 > /dev/null 2>&1; then SERVER_UP=true; break; fi
sleep 1
done
if [[ "$SERVER_UP" != "true" ]]; then
echo "Storybook static server did not start on http://localhost:6007 after 30s" >&2
exit 1
fi
export STORYBOOK_URL="http://localhost:6007"
npx vitest run
-5
View File
@@ -1,5 +0,0 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';
@@ -1,2 +0,0 @@
// Auto-generated barrel placeholder — populated as components migrate into this entry point.
export {};
@@ -1,2 +0,0 @@
// Auto-generated barrel placeholder — populated as components migrate into this entry point.
export {};
@@ -1,2 +0,0 @@
// Auto-generated barrel placeholder — populated as components migrate into this entry point.
export {};
@@ -1,2 +0,0 @@
// Auto-generated barrel placeholder — populated as components migrate into this entry point.
export {};
@@ -1,2 +0,0 @@
// Auto-generated barrel placeholder — populated as components migrate into this entry point.
export {};
-7
View File
@@ -1,7 +0,0 @@
declare global {
interface Window {
_env_?: Record<string, string>;
}
}
export {};
-4
View File
@@ -1,4 +0,0 @@
// Side-effect import: the reset must ship exactly once in the aggregated style.css.
import './styles/base/reset.scss';
export {};
@@ -1,15 +0,0 @@
// Entry point for the individual/self-contained build (vite.config.individual.ts).
// Re-exports all public modules so a single bundle contains every component with
// internal deps bundled, while React stays external for the consumer's bundler.
export * from './accessibility';
export * from './components';
export * from './display';
export * from './feedback';
export * from './input';
export * from './json-visualizer';
export * from './layout';
export * from './navigation';
export * from './theme';
export * from './theme-constants';
export * from './utilities';
@@ -1,64 +0,0 @@
.root {
position: relative;
display: flex;
flex-shrink: 0;
align-items: center;
align-self: flex-start;
border-radius: 10px;
background-color: var(--t-background-transparent-medium);
cursor: pointer;
transition: background-color duration(normal) ease;
&[data-checked] {
background-color: var(--toggle-on-color, var(--t-color-blue));
}
&[data-disabled] {
opacity: 0.5;
pointer-events: none;
}
}
.centered {
align-self: center;
}
.small {
width: 24px;
height: 16px;
}
.medium {
width: 32px;
height: 20px;
}
.thumb {
position: absolute;
top: 50%;
left: 0;
display: block;
border-radius: 50%;
background-color: var(--t-background-primary);
transition: transform duration(normal) ease;
}
.small .thumb {
width: 12px;
height: 12px;
transform: translate(2px, -50%);
}
.medium .thumb {
width: 16px;
height: 16px;
transform: translate(2px, -50%);
}
.small[data-checked] .thumb {
transform: translate(10px, -50%);
}
.medium[data-checked] .thumb {
transform: translate(14px, -50%);
}
@@ -1,8 +0,0 @@
declare const classNames: {
readonly root: 'root';
readonly centered: 'centered';
readonly small: 'small';
readonly medium: 'medium';
readonly thumb: 'thumb';
};
export default classNames;
@@ -1,50 +0,0 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { useState } from 'react';
import { ThemeProvider } from '@new-ui/theme-constants';
import { Toggle } from './Toggle';
const meta: Meta<typeof Toggle> = {
title: 'Input/Toggle',
component: Toggle,
args: { toggleSize: 'medium', 'aria-label': 'Example toggle' },
};
export default meta;
type Story = StoryObj<typeof Toggle>;
export const On: Story = { args: { value: true } };
export const Off: Story = { args: { value: false } };
export const Small: Story = { args: { value: true, toggleSize: 'small' } };
export const CustomColor: Story = {
args: { value: true, color: 'color(display-p3 0.2 0.7 0.4)' },
};
export const Disabled: Story = { args: { value: true, disabled: true } };
const ControlledToggle = () => {
const [value, setValue] = useState(false);
return (
<Toggle value={value} onChange={setValue} aria-label="Example toggle" />
);
};
export const Interactive: Story = {
render: () => <ControlledToggle />,
};
export const Dark: Story = {
args: { value: true },
decorators: [
(Story) => (
<ThemeProvider colorScheme="dark">
<Story />
</ThemeProvider>
),
],
};
@@ -1,54 +0,0 @@
import { Switch } from '@base-ui/react/switch';
import { clsx } from 'clsx';
import styles from './Toggle.module.scss';
export type ToggleSize = 'small' | 'medium';
export type ToggleProps = {
id?: string;
value?: boolean;
onChange?: (value: boolean, e?: React.MouseEvent<HTMLDivElement>) => void;
color?: string;
toggleSize?: ToggleSize;
className?: string;
centered?: boolean;
disabled?: boolean;
'aria-label'?: string;
'aria-labelledby'?: string;
};
export const Toggle = ({
id,
value = false,
onChange,
color,
toggleSize = 'medium',
className,
centered,
disabled,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
}: ToggleProps) => (
<Switch.Root
id={id}
checked={value}
disabled={disabled}
onCheckedChange={(checked) => onChange?.(checked)}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
className={clsx(
styles.root,
styles[toggleSize],
centered && styles.centered,
className,
)}
style={
color
? ({ '--toggle-on-color': color } as React.CSSProperties)
: undefined
}
>
<Switch.Thumb className={styles.thumb} />
</Switch.Root>
);
@@ -1,2 +0,0 @@
export { Toggle } from './components/Toggle';
export type { ToggleProps, ToggleSize } from './components/Toggle';
@@ -1,2 +0,0 @@
// Auto-generated barrel placeholder — populated as components migrate into this entry point.
export {};
@@ -1,2 +0,0 @@
// Auto-generated barrel placeholder — populated as components migrate into this entry point.
export {};
@@ -1,2 +0,0 @@
// Auto-generated barrel placeholder — populated as components migrate into this entry point.
export {};
@@ -1,18 +0,0 @@
@use 'sass:map';
// Mirrors twenty-ui's MOBILE_VIEWPORT (768px).
$breakpoints: (
'mobile': 768px,
);
@mixin respond-to($name) {
$value: map.get($breakpoints, $name);
@if $value == null {
@error 'Unknown breakpoint: #{$name}';
}
@media (max-width: $value) {
@content;
}
}
@@ -1,5 +0,0 @@
// duration(fast) => calc(var(--t-animation-duration-fast) * 1s)
// Theme animation durations are stored unitless; this scales them to seconds.
@function duration($name) {
@return calc(var(--t-animation-duration-#{$name}) * 1s);
}
@@ -1,6 +0,0 @@
@mixin focus-ring {
&:focus-visible {
outline: 2px solid var(--t-color-blue);
outline-offset: 1px;
}
}
@@ -1,15 +0,0 @@
*,
*::before,
*::after {
box-sizing: border-box;
}
button {
margin: 0;
padding: 0;
font: inherit;
color: inherit;
background: none;
border: none;
cursor: pointer;
}
@@ -1,2 +0,0 @@
// Auto-generated barrel placeholder — populated as components migrate into this entry point.
export {};
@@ -1,121 +0,0 @@
import { createContext, useLayoutEffect, useState } from 'react';
import { themeCssVariables } from './themeCssVariables';
type StringLeaves<T> = {
[K in keyof T]: T[K] extends string ? string : StringLeaves<T[K]>;
};
type DeepMerge<T, U> = {
[K in keyof T]: K extends keyof U
? U[K] extends Record<string, unknown>
? T[K] extends Record<string, unknown>
? DeepMerge<T[K], U[K]>
: U[K]
: U[K]
: T[K];
};
// CSS variables that resolve to pure numbers at runtime
type NumericOverrides = {
icon: {
size: { sm: number; md: number; lg: number; xl: number };
stroke: { sm: number; md: number; lg: number };
};
animation: {
duration: { instant: number; fast: number; normal: number; slow: number };
};
text: {
lineHeight: { lg: number; md: number };
iconSizeMedium: number;
iconSizeSmall: number;
iconStrikeLight: number;
iconStrikeMedium: number;
iconStrikeBold: number;
};
spacingMultiplicator: number;
lastLayerZIndex: number;
};
export type ThemeType = DeepMerge<
StringLeaves<typeof themeCssVariables>,
NumericOverrides
>;
export type ThemeContextType = {
theme: ThemeType;
colorScheme: 'light' | 'dark';
};
const computeThemeFromCss = (): ThemeType => {
if (
typeof document === 'undefined' ||
typeof getComputedStyle !== 'function'
) {
return themeCssVariables as unknown as ThemeType;
}
const computedStyle = getComputedStyle(document.documentElement);
const resolve = (obj: Record<string, unknown>): Record<string, unknown> => {
const result: Record<string, unknown> = {};
for (const key of Object.keys(obj)) {
const value = obj[key];
if (typeof value === 'string' && value.startsWith('var(')) {
const varName = value.slice(4, -1);
const raw = computedStyle.getPropertyValue(varName).trim();
const num = Number(raw);
result[key] = raw !== '' && !isNaN(num) ? num : raw;
} else if (typeof value === 'object' && value !== null) {
result[key] = resolve(value as Record<string, unknown>);
} else {
result[key] = value;
}
}
return result;
};
return resolve(
themeCssVariables as unknown as Record<string, unknown>,
) as unknown as ThemeType;
};
const applyColorSchemeClass = (colorScheme: 'light' | 'dark') => {
if (typeof document === 'undefined') return;
const root = document.documentElement;
if (!root?.classList) return;
root.classList.toggle('dark', colorScheme === 'dark');
root.classList.toggle('light', colorScheme === 'light');
};
export const ThemeContext = createContext<ThemeContextType>({
theme: themeCssVariables as unknown as ThemeType,
colorScheme: 'light',
});
export const ThemeProvider = ({
children,
colorScheme,
}: {
children: React.ReactNode;
colorScheme: 'light' | 'dark';
}) => {
const [theme, setTheme] = useState<ThemeType>(() => {
applyColorSchemeClass(colorScheme);
return computeThemeFromCss();
});
useLayoutEffect(() => {
applyColorSchemeClass(colorScheme);
setTheme(computeThemeFromCss());
}, [colorScheme]);
return (
<ThemeContext.Provider value={{ theme, colorScheme }}>
{children}
</ThemeContext.Provider>
);
};
@@ -1,28 +0,0 @@
import {
MAIN_COLOR_NAMES,
type ThemeColor,
} from '@new-ui/theme/constants/MainColorNames';
import { getNextThemeColor } from '../getNextThemeColor';
describe('getNextThemeColor', () => {
it('returns the next theme color', () => {
const currentColor: ThemeColor = MAIN_COLOR_NAMES[0];
const nextColor: ThemeColor = MAIN_COLOR_NAMES[1];
expect(getNextThemeColor(MAIN_COLOR_NAMES, currentColor)).toBe(nextColor);
});
it('returns the first color when reaching the end', () => {
const currentColor: ThemeColor =
MAIN_COLOR_NAMES[MAIN_COLOR_NAMES.length - 1];
const nextColor: ThemeColor = MAIN_COLOR_NAMES[0];
expect(getNextThemeColor(MAIN_COLOR_NAMES, currentColor)).toBe(nextColor);
});
it('returns the first color when currentColorIsUndefined', () => {
const firstColor: ThemeColor = MAIN_COLOR_NAMES[0];
expect(getNextThemeColor(MAIN_COLOR_NAMES, undefined)).toBe(firstColor);
});
});
@@ -1,50 +0,0 @@
import { readFileSync } from 'fs';
import { resolve } from 'path';
import { themeCssVariables as newThemeCssVariables } from '../themeCssVariables';
import { themeCssVariables as oldThemeCssVariables } from '../../../../twenty-ui/src/theme-constants/themeCssVariables';
const NEW_DIR = resolve(__dirname, '..');
const OLD_DIR = resolve(__dirname, '../../../../twenty-ui/src/theme-constants');
// Parse a theme CSS file into ordered [name, value] pairs, normalizing
// whitespace so formatting differences (multi-line vs single-line values,
// header comment) don't affect the comparison. Stops each value at the ';'
// that terminates it, skipping ';' embedded in values (e.g. data URIs).
const parseTokens = (dir: string, file: string): [string, string][] => {
const text = readFileSync(resolve(dir, file), 'utf8');
const body = text.slice(text.indexOf('{') + 1, text.lastIndexOf('}'));
const re = /--t-([a-z0-9_-]+):\s*([\s\S]*?);(?=\s*(?:--t-|$))/g;
const tokens: [string, string][] = [];
let match: RegExpExecArray | null;
while ((match = re.exec(body)) !== null) {
const value = match[2]
.trim()
.replace(/\s+/g, ' ')
.replace(/\(\s+/g, '(')
.replace(/\s+\)/g, ')');
tokens.push([match[1], value]);
}
return tokens;
};
// twenty-new-ui must produce the exact same --t-* tokens and values (and order)
// as twenty-ui, so the swap is token-for-token with no visual change.
// Comparison ignores incidental formatting (header, whitespace) but not values.
describe('theme parity with twenty-ui', () => {
it('theme-light.css tokens match twenty-ui', () => {
expect(parseTokens(NEW_DIR, 'theme-light.css')).toEqual(
parseTokens(OLD_DIR, 'theme-light.css'),
);
});
it('theme-dark.css tokens match twenty-ui', () => {
expect(parseTokens(NEW_DIR, 'theme-dark.css')).toEqual(
parseTokens(OLD_DIR, 'theme-dark.css'),
);
});
it('themeCssVariables is deeply equal to twenty-ui', () => {
expect(newThemeCssVariables).toEqual(oldThemeCssVariables);
});
});
@@ -1,3 +0,0 @@
// CSS custom properties don't work in media queries, so MOBILE_VIEWPORT
// must be a static number rather than a var(--...) reference.
export const MOBILE_VIEWPORT = 768;
@@ -1,15 +0,0 @@
import { type ThemeColor } from '@new-ui/theme/constants/MainColorNames';
export const getNextThemeColor = (
colorNames: ThemeColor[],
currentColor?: ThemeColor,
): ThemeColor => {
if (currentColor === null || currentColor === undefined) {
return colorNames[0];
}
const currentColorIndex = colorNames.findIndex(
(color) => color === currentColor,
);
const nextColorIndex = (currentColorIndex + 1) % colorNames.length;
return colorNames[nextColorIndex];
};
@@ -1,14 +0,0 @@
/*
* _____ _
*|_ _|_ _____ _ __ | |_ _ _
* | | \ \ /\ / / _ \ '_ \| __| | | | Auto-generated file
* | | \ V V / __/ | | | |_| |_| | Any edits to this will be overridden
* |_| \_/\_/ \___|_| |_|\__|\__, |
* |___/
*/
export { MOBILE_VIEWPORT } from './constants';
export { getNextThemeColor } from './getNextThemeColor';
export { themeCssVariables } from './themeCssVariables';
export type { ThemeType, ThemeContextType } from './ThemeProvider';
export { ThemeContext, ThemeProvider } from './ThemeProvider';
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
@@ -1,23 +0,0 @@
import * as RadixColors from '@radix-ui/colors';
import { COLOR_DARK } from '@new-ui/theme/constants/ColorsDark';
export const ACCENT_DARK = {
primary: COLOR_DARK.blue5,
secondary: COLOR_DARK.blue5,
tertiary: COLOR_DARK.blue3,
quaternary: COLOR_DARK.blue2,
accent3570: COLOR_DARK.blue8,
accent4060: COLOR_DARK.blue8,
accent1: RadixColors.indigoDarkP3.indigo1,
accent2: RadixColors.indigoDarkP3.indigo2,
accent3: RadixColors.indigoDarkP3.indigo3,
accent4: RadixColors.indigoDarkP3.indigo4,
accent5: RadixColors.indigoDarkP3.indigo5,
accent6: RadixColors.indigoDarkP3.indigo6,
accent7: RadixColors.indigoDarkP3.indigo7,
accent8: RadixColors.indigoDarkP3.indigo8,
accent9: RadixColors.indigoDarkP3.indigo9,
accent10: RadixColors.indigoDarkP3.indigo10,
accent11: RadixColors.indigoDarkP3.indigo11,
accent12: RadixColors.indigoDarkP3.indigo12,
};

Some files were not shown because too many files have changed in this diff Show More