snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-17 23:28:13 +05:00
commit e7da97194a
32688 changed files with 4208681 additions and 0 deletions

View File

@@ -0,0 +1,18 @@
import { defineConfig } from '@eloqnt/cli';
export default defineConfig({
messages: {
path: 'src/locales',
sourceLocale: 'en',
format: 'po',
},
lint: {
overrides: [
{
// Lingui's pseudo-locale (`pseudoLocale` in lingui.config.ts), not a real locale
locales: ['pseudo-en'],
rules: { 'invalid-locale': 'off' },
},
],
},
});

View File

@@ -0,0 +1,11 @@
REACT_APP_SERVER_BASE_URL=http://localhost:3000
VITE_BUILD_SOURCEMAP=false
# ———————— Optional ————————
# REACT_APP_PORT=3001
# VITE_ENABLE_SSL=false
# VITE_HOST=localhost.com
# SSL_KEY_PATH="./certs/your-cert.key"
# SSL_CERT_PATH="./certs/your-cert.crt"
# IS_DEBUG_MODE=false

47
packages/twenty-front/.gitignore vendored Normal file
View File

@@ -0,0 +1,47 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# dependencies
/node_modules
/coverage*
/.pnp
.pnp.js
# testing
/coverage
storybook-static
build-storybook.log
# production
/build
dist
dist-ssr
# env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
.vite/
.nyc_output/
# project structure cache (legacy)
projectStructure.cache.json

View File

@@ -0,0 +1,158 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "typescript", "unicorn", "import"],
"jsPlugins": ["../twenty-oxlint-rules/dist/oxlint-plugin.mjs"],
"categories": {
"correctness": "off"
},
"ignorePatterns": [
"node_modules",
"src/generated",
"src/generated-metadata",
"src/generated-admin",
"src/locales/generated",
"src/modules/onboarding/components/WelcomeOverlay/welcomeHalftoneDots.ts",
"src/testing/mock-data",
"**/__mocks__/**",
"**/*.stories.ts",
"**/*.stories.tsx",
"build",
"coverage",
"storybook-static"
],
"rules": {
"func-style": ["error", "declaration", { "allowArrowFunctions": true }],
"no-console": [
"warn",
{ "allow": ["group", "groupCollapsed", "groupEnd"] }
],
"no-script-url": "error",
"no-control-regex": "off",
"no-debugger": "error",
"no-duplicate-imports": "error",
"no-undef": "off",
"no-unused-vars": "off",
"no-redeclare": "off",
"no-restricted-imports": [
"error",
{
"patterns": [
{
"group": ["../*"],
"message": "Relative parent imports are not allowed. Use @/ alias instead."
},
{
"group": ["@tabler/icons-react"],
"message": "Please import icons from `twenty-ui`"
},
{
"group": ["react-hotkeys-web-hook"],
"importNames": ["useHotkeys"],
"message": "Please use the custom wrapper: `useScopedHotkeys` from `twenty-ui`"
},
{
"group": ["lodash"],
"message": "Please use the standalone lodash package (for instance: `import groupBy from 'lodash.groupby'` instead of `import { groupBy } from 'lodash'`)"
}
]
}
],
"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-no-script-url": "error",
"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": "^_"
}
],
"typescript/strict-boolean-expressions": [
"error",
{
"allowNullableBoolean": true,
"allowNullableObject": true,
"allowString": true,
"allowNumber": true,
"allowNullableString": true,
"allowNullableNumber": true,
"allowNullableEnum": true,
"allowAny": true
}
],
"twenty/effect-components": "error",
"twenty/no-hardcoded-colors": "error",
"twenty/matching-state-variable": "error",
"twenty/sort-css-properties-alphabetically": "error",
"twenty/styled-components-prefixed-with-styled": "error",
"twenty/no-state-useref": "error",
"twenty/component-props-naming": "error",
"twenty/no-navigate-prefer-link": "error",
"twenty/no-jotai-store-in-selector": "error",
"twenty/no-direct-atom-family-in-selector": "error",
"twenty/folder-structure": "error",
"twenty/no-icu-escaping-apostrophe": "error",
"twenty/no-miscased-acronym-in-message": "error",
"twenty/enforce-module-boundaries": [
"error",
{
"depConstraints": [
{
"sourceTag": "scope:frontend",
"onlyDependOnLibsWithTags": ["scope:shared", "scope:frontend"]
}
]
}
]
},
"overrides": [
{
"files": ["src/modules/ui/input/components/NavigationButton.tsx"],
"rules": {
"react/jsx-props-no-spreading": "off"
}
},
{
"files": ["**/constants/*.ts", "**/*.constants.ts"],
"rules": {
"twenty/max-consts-per-file": ["error", { "max": 1 }]
}
}
]
}

View File

@@ -0,0 +1,5 @@
src/generated
src/generated-metadata
src/generated-admin
src/locales/generated
src/testing/mock-data/generated

View File

@@ -0,0 +1,67 @@
import type { StorybookConfig } from '@storybook/react-vite';
const computeStoriesGlob = () => {
if (process.env.STORYBOOK_SCOPE === 'pages') {
return [
'../src/pages/**/*.stories.@(js|jsx|ts|tsx)',
'../src/__stories__/*.stories.@(js|jsx|ts|tsx)',
'../src/pages/**/*.docs.mdx',
'../src/__stories__/*.docs.mdx',
];
}
if (process.env.STORYBOOK_SCOPE === 'modules') {
return [
'../src/modules/**/!(perf)/*.stories.@(js|jsx|ts|tsx)',
'../src/modules/**/*.docs.mdx',
];
}
if (process.env.STORYBOOK_SCOPE === 'performance') {
return ['../src/modules/**/perf/*.perf.stories.@(js|jsx|ts|tsx)'];
}
if (process.env.STORYBOOK_SCOPE === 'ui-docs') {
return ['../src/modules/ui/**/*.docs.mdx'];
}
return ['../src/**/*.docs.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'];
};
const config: StorybookConfig = {
stories: computeStoriesGlob(),
build: {
test: {
disabledAddons: [
'@storybook/addon-docs',
'@storybook/addon-essentials/docs',
],
},
},
addons: [
'@storybook-community/storybook-addon-cookie',
'@storybook/addon-links',
'@storybook/addon-coverage',
'storybook-addon-pseudo-states',
'storybook-addon-mock-date',
'@storybook/addon-vitest',
],
framework: '@storybook/react-vite',
viteFinal: async (viteConfig) => {
const { mergeConfig } = await import('vite');
return mergeConfig(viteConfig, {
logLevel: 'warn',
});
},
logLevel: 'error',
docs: {},
};
export default config;

View File

@@ -0,0 +1,8 @@
<style>
[data-is-storybook='true'] {
background-color: transparent !important;
}
:has(#storybook-panel-root) {
z-index: 100;
}
</style>

View File

@@ -0,0 +1,27 @@
<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>

View File

@@ -0,0 +1,169 @@
import '@fontsource/dm-mono/400.css';
import '@fontsource/dm-mono/500.css';
import '@fontsource/inter/400.css';
import '@fontsource/inter/500.css';
import '@fontsource/inter/600.css';
import '@fontsource/inter/700.css';
import { i18n } from '@lingui/core';
import { I18nProvider } from '@lingui/react';
import { type Preview } from '@storybook/react-vite';
import { http, HttpResponse } from 'msw';
import { initialize, mswLoader } from 'msw-storybook-addon';
import { SOURCE_LOCALE } from 'twenty-shared/translations';
// oxlint-disable-next-line no-restricted-imports
import { DateFormat } from '../src/modules/localization/constants/DateFormat';
// oxlint-disable-next-line no-restricted-imports
import { TimeFormat } from '../src/modules/localization/constants/TimeFormat';
// oxlint-disable-next-line no-restricted-imports
import { FileUploadProvider } from '../src/modules/file-upload/components/FileUploadProvider';
// oxlint-disable-next-line no-restricted-imports
import { RootDecorator } from '../src/testing/decorators/RootDecorator';
// oxlint-disable-next-line no-restricted-imports
import { resetJotaiStore } from '../src/modules/ui/utilities/state/jotai/jotaiStore';
// oxlint-disable-next-line no-restricted-imports
import { UserContext } from '../src/modules/users/contexts/UserContext';
// Stories rendering CodeEditor / GraphiQL need Monaco's worker factory, which
// the app normally sets up in src/index.tsx.
// oxlint-disable-next-line no-restricted-imports
import '../src/modules/app/utils/setupMonacoEnvironment';
import 'react-loading-skeleton/dist/skeleton.css';
import 'twenty-ui/style.css';
import 'twenty-ui/theme-light.css';
import 'twenty-ui/theme-dark.css';
import { ThemeProvider } from 'twenty-ui/theme-constants';
// oxlint-disable-next-line no-restricted-imports
import { messages as enMessages } from '../src/locales/generated/en';
i18n.load({ [SOURCE_LOCALE]: enMessages });
i18n.activate(SOURCE_LOCALE);
// oxlint-disable-next-line no-restricted-imports
import { ClickOutsideListenerContext } from '../src/modules/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
const MOCK_IMAGE_SVG =
'<svg xmlns="http://www.w3.org/2000/svg" width="192" height="192"><rect width="192" height="192" fill="#d9d9d9"/></svg>';
const respondWithMockImage = () =>
new HttpResponse(MOCK_IMAGE_SVG, {
headers: { 'Content-Type': 'image/svg+xml' },
});
const remoteImageMockHandlers = [
http.get('https://picsum.photos/*', respondWithMockImage),
http.get('https://twenty-icons.com/*', respondWithMockImage),
http.get('https://twentyhq.github.io/*', respondWithMockImage),
http.get('https://via.placeholder.com/*', respondWithMockImage),
http.get(
'https://twenty-front-screenshots.s3.eu-west-3.amazonaws.com/*',
respondWithMockImage,
),
];
initialize(
{
onUnhandledRequest: async (request: Request) => {
const fileExtensionsToIgnore =
/\.(ts|tsx|js|jsx|svg|css|png|woff2)(\?v=[a-zA-Z0-9]+)?/;
if (fileExtensionsToIgnore.test(request.url)) {
return;
}
if (request.url.startsWith('http://localhost:3000/files/')) {
return;
}
try {
const requestBody = await request.json();
// oxlint-disable-next-line no-console
console.warn(`Unhandled ${request.method} request to ${request.url}
with payload ${JSON.stringify(requestBody)}\n
This request should be mocked with MSW`);
} catch (error) {
// oxlint-disable-next-line no-console
console.error(`Cannot parse msw request body : ${error}`);
}
// oxlint-disable-next-line no-console
console.warn(
`Unhandled ${request.method} request to ${request.url} \n This request should be mocked with MSW`,
);
},
quiet: true,
},
remoteImageMockHandlers,
);
// Mirrors production's MinimalMetadataGater so any story rendering a
// date-aware component (DateTimeDisplay, etc.) sees a real IANA timeZone
// instead of UserContext's default `{}`. Stories needing a specific timezone
// can still override by nesting their own UserContext.Provider.
const STORYBOOK_DEFAULT_USER_CONTEXT = {
dateFormat: DateFormat.DAY_FIRST,
timeFormat: TimeFormat.HOUR_24,
timeZone: 'UTC',
};
const INTER_FONT_WEIGHTS = ['400', '500', '600'];
const waitForInterFontLoadedBeforeScreenshot = async () => {
await Promise.all(
INTER_FONT_WEIGHTS.map((weight) =>
document.fonts.load(`${weight} 1em Inter`),
),
);
await document.fonts.ready;
};
const preview: Preview = {
decorators: [
(Story) => {
return (
<I18nProvider i18n={i18n}>
<ThemeProvider colorScheme="light">
<FileUploadProvider>
<ClickOutsideListenerContext.Provider
value={{ excludedClickOutsideId: undefined }}
>
<UserContext.Provider value={STORYBOOK_DEFAULT_USER_CONTEXT}>
<Story />
</UserContext.Provider>
</ClickOutsideListenerContext.Provider>
</FileUploadProvider>
</ThemeProvider>
</I18nProvider>
);
},
RootDecorator,
],
beforeEach: () => {
resetJotaiStore();
},
loaders: [mswLoader, waitForInterFontLoadedBeforeScreenshot],
parameters: {
argos: {
// CSS zoom combined with viewport-sized stories makes the iframe grow during capture.
fitToContent: { zoom: 1 },
},
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/,
},
},
mockingDate: new Date('2024-03-12T09:30:00.000Z'),
options: {
storySort: {
order: ['UI', 'Modules', 'Pages'],
},
},
},
};
export default preview;

View File

@@ -0,0 +1,40 @@
import { Globals } from '@react-spring/web';
import { setProjectAnnotations } from '@storybook/react-vite';
import { MotionGlobalConfig } from 'framer-motion';
import * as projectAnnotations from './preview';
import { waitForServiceWorkerToControlPage } from './waitForServiceWorkerToControlPage';
MotionGlobalConfig.skipAnimations = true;
Globals.assign({ skipAnimation: true });
const disableCssAnimationsStyle = document.createElement('style');
disableCssAnimationsStyle.innerHTML = `*, *::before, *::after {
animation-duration: 0s !important;
animation-delay: 0s !important;
transition-duration: 0s !important;
transition-delay: 0s !important;
}`;
document.head.appendChild(disableCssAnimationsStyle);
// msw registers its service worker while ./preview evaluates, and it reloads
// any page that has a registration without being controlled by it, which
// vitest reports as "the iframe was reloaded during a test" and which fails
// the whole shard. Holding every file here until the worker controls its page
// (including files that run zero tests, which otherwise complete instantly)
// means no test iframe is ever created while the worker is still activating.
await waitForServiceWorkerToControlPage();
// Pre-warm the dynamic imports used by lazy() story components so the
// modules are cached before any test runs (avoids flaky timeouts and Argos
// screenshots that capture a Suspense skeleton instead of the loaded content).
import('~/testing/utils/getTestEnrichedObjectMetadataItemsMock');
import('react-markdown');
import('remark-gfm');
import('@/activities/components/ActivityRichTextEditor');
import('@/object-record/record-field/ui/meta-types/input/components/RichTextFieldEditor');
import('@/navigation-menu-item/display/sections/favorites/components/FavoritesSectionDispatcher');
import('@/navigation-menu-item/display/sections/workspace/components/WorkspaceSectionDispatcher');
// This is an important step to apply the right configuration when testing your stories.
// More info at: https://storybook.js.org/docs/api/portable-stories/portable-stories-vitest#setprojectannotations
setProjectAnnotations([projectAnnotations]);

View File

@@ -0,0 +1,38 @@
import { isDefined } from 'twenty-shared/utils';
// The worker takes tens of seconds to activate on CI, because its script
// request queues behind the story module transforms. serviceWorker.ready is
// used rather than getRegistration() because msw registers the worker
// asynchronously while ./preview evaluates, so the registration may not exist
// yet when this runs. The backstop covers a worker that never activates.
const SERVICE_WORKER_CONTROL_BACKSTOP_MS = 60_000;
export const waitForServiceWorkerToControlPage = async () => {
if (isDefined(navigator.serviceWorker.controller)) {
return;
}
const backstop = new Promise<void>((resolve) => {
setTimeout(resolve, SERVICE_WORKER_CONTROL_BACKSTOP_MS);
});
await Promise.race([
navigator.serviceWorker.ready.then(() => undefined),
backstop,
]);
if (isDefined(navigator.serviceWorker.controller)) {
return;
}
await Promise.race([
new Promise<void>((resolve) => {
navigator.serviceWorker.addEventListener(
'controllerchange',
() => resolve(),
{ once: true },
);
}),
backstop,
]);
};

View File

@@ -0,0 +1,44 @@
{
"jsc": {
"target": "es2017",
"parser": {
"syntax": "typescript",
"tsx": true,
"decorators": false,
"dynamicImport": false
},
"transform": {
"react": {
"pragma": "React.createElement",
"pragmaFrag": "React.Fragment",
"throwIfNamespace": true,
"development": false,
"useBuiltins": false,
"runtime": "automatic"
},
"hidden": {
"jest": true
}
},
"experimental": {
"plugins": [
[
"@lingui/swc-plugin",
{
"runtimeModules": {
"i18n": ["@lingui/core", "i18n"],
"trans": ["@lingui/react", "Trans"]
}
}
]
]
}
},
"module": {
"type": "commonjs",
"strict": false,
"strictMode": true,
"lazy": false,
"noInterop": false
}
}

View File

@@ -0,0 +1 @@
Run `yarn dev` while server running on port `3000`

View File

@@ -0,0 +1 @@
export default jest.fn((..._params) => [65, 131, 196, 1]);

View File

@@ -0,0 +1 @@
export default 'test-file-stub';

View File

@@ -0,0 +1 @@
export default 'test-file-stub';

View File

@@ -0,0 +1,6 @@
export default class WorkerMock {
postMessage() {}
terminate() {}
addEventListener() {}
removeEventListener() {}
}

View File

@@ -0,0 +1,28 @@
module.exports = {
schema:
(process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000') +
'/admin-panel',
documents: [
'./src/modules/settings/admin-panel/**/graphql/**/*.{ts,tsx}',
'./src/modules/settings/application-registrations/graphql/fragments/*.{ts,tsx}',
'!./src/**/*.test.{ts,tsx}',
'!./src/**/*.stories.{ts,tsx}',
'!./src/**/__mocks__/*.ts',
],
overwrite: true,
generates: {
'./src/generated-admin/graphql.ts': {
plugins: ['typescript', 'typescript-operations', 'typed-document-node'],
config: {
skipTypename: false,
defaultScalarType: 'any',
scalars: {
DateTime: 'string',
UUID: 'string',
},
namingConvention: { enumValues: 'keep' },
},
},
},
};

View File

@@ -0,0 +1,69 @@
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
module.exports = {
schema:
(process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000') +
'/metadata',
documents: [
'./src/modules/auth/graphql/**/*.{ts,tsx}',
'./src/modules/users/graphql/**/*.{ts,tsx}',
'./src/modules/views/graphql/**/*.{ts,tsx}',
'./src/modules/ai/graphql/**/*.{ts,tsx}',
'./src/modules/applications/graphql/**/*.{ts,tsx}',
'./src/modules/workspace/graphql/**/*.{ts,tsx}',
'./src/modules/workspace-member/graphql/**/*.{ts,tsx}',
'./src/modules/workspace-invitation/graphql/**/*.{ts,tsx}',
'./src/modules/settings/**/graphql/**/*.{ts,tsx}',
'!./src/modules/settings/admin-panel/**/graphql/**/*.{ts,tsx}',
// DPA operations are served by the core (/graphql) schema via @CoreResolver,
// not the metadata schema, so they must not be validated against /metadata.
'!./src/modules/settings/legal/**/graphql/**/*.{ts,tsx}',
'./src/modules/logic-functions/graphql/**/*.{ts,tsx}',
'./src/modules/databases/graphql/**/*.{ts,tsx}',
'./src/modules/analytics/graphql/**/*.{ts,tsx}',
'./src/modules/object-metadata/graphql/**/*.{ts,tsx}',
'./src/modules/navigation-menu-item/**/graphql/**/*.{ts,tsx}',
'./src/modules/command-menu-item/graphql/**/*.{ts,tsx}',
'./src/modules/attachments/graphql/**/*.{ts,tsx}',
'./src/modules/file/graphql/**/*.{ts,tsx}',
'./src/modules/onboarding/graphql/**/*.{ts,tsx}',
'./src/modules/front-components/graphql/**/*.{ts,tsx}',
'./src/modules/page-layout/widgets/**/graphql/**/*.{ts,tsx}',
'./src/modules/activities/calendar/graphql/mutations/**/*.{ts,tsx}',
'./src/modules/activities/emails/graphql/mutations/**/*.{ts,tsx}',
'./src/modules/activities/timeline-activities/graphql/**/*.{ts,tsx}',
'./src/modules/activities/emails/graphql/metadata-queries/**/*.{ts,tsx}',
'./src/modules/dashboards/graphql/**/*.{ts,tsx}',
'./src/modules/page-layout/graphql/**/*.{ts,tsx}',
'./src/modules/marketplace/graphql/**/*.{ts,tsx}',
'./src/modules/metadata-store/graphql/**/*.{ts,tsx}',
'./src/modules/sse-db-event/graphql/**/*.{ts,tsx}',
'./src/modules/geo-map/graphql-query/*.{ts,tsx}',
'./src/pages/**/graphql/**/*.{ts,tsx}',
'!./src/**/*.test.{ts,tsx}',
'!./src/**/*.stories.{ts,tsx}',
'!./src/**/__mocks__/*.ts',
],
overwrite: true,
generates: {
'./src/generated-metadata/graphql.ts': {
plugins: ['typescript', 'typescript-operations', 'typed-document-node'],
config: {
skipTypename: false,
defaultScalarType: 'any',
scalars: {
DateTime: 'string',
UUID: 'string',
},
namingConvention: { enumValues: 'keep' },
},
},
},
};

View File

@@ -0,0 +1,34 @@
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
module.exports = {
schema:
(process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000') +
'/graphql',
documents: [
'./src/modules/workflow/**/graphql/**/*.{ts,tsx}',
'./src/modules/object-core/**/graphql/**/*.{ts,tsx}',
'./src/modules/activities/emails/graphql/queries/**/*.{ts,tsx}',
'./src/modules/activities/emails/graphql/operation-signatures/**/*.{ts,tsx}',
'./src/modules/activities/calendar/graphql/queries/**/*.{ts,tsx}',
'./src/modules/search/graphql/**/*.{ts,tsx}',
'./src/modules/command-menu/graphql/**/*.{ts,tsx}',
'!./src/**/*.test.{ts,tsx}',
'!./src/**/*.stories.{ts,tsx}',
'!./src/**/__mocks__/*.ts',
],
overwrite: true,
generates: {
'./src/generated/graphql.ts': {
plugins: ['typescript', 'typescript-operations', 'typed-document-node'],
config: {
skipTypename: false,
defaultScalarType: 'any',
scalars: {
DateTime: 'string',
},
namingConvention: { enumValues: 'keep' },
},
},
},
};

View File

@@ -0,0 +1,64 @@
{
"projectRoot": "packages/twenty-front",
"structureRoot": "src",
"regexParameters": {
"camelCase": "^[a-z]+([A-Za-z0-9]+)+",
"kebab-case": "[a-z][a-z0-9]*(?:-[a-z0-9]+)*"
},
"structure": [
{ "name": "*" },
{ "name": "*", "children": [] },
{ "name": "modules", "ruleId": "modulesFolderRule" }
],
"rules": {
"modulesFolderRule": {
"children": [
{ "ruleId": "moduleFolderRule" },
{ "name": "types", "children": [] }
]
},
"moduleFolderRule": {
"name": "{kebab-case}",
"folderRecursionLimit": 4,
"children": [
{ "ruleId": "moduleFolderRule" },
{ "name": "hooks", "ruleId": "hooksLeafFolderRule" },
{ "name": "utils", "ruleId": "utilsLeafFolderRule" },
{ "name": "states", "children": [] },
{ "name": "types", "children": [] },
{ "name": "graphql", "children": [] },
{ "name": "components", "children": [] },
{ "name": "effect-components", "children": [] },
{ "name": "constants", "children": [] },
{ "name": "validation-schemas", "children": [] },
{ "name": "contexts", "children": [] },
{ "name": "scopes", "children": [] },
{ "name": "services", "children": [] },
{ "name": "errors", "children": [] }
]
},
"hooksLeafFolderRule": {
"folderRecursionLimit": 2,
"children": [
{ "name": "use{PascalCase}.(ts|tsx)" },
{
"name": "__tests__",
"children": [{ "name": "use{PascalCase}.test.(ts|tsx)" }]
},
{ "name": "internal", "ruleId": "hooksLeafFolderRule" }
]
},
"utilsLeafFolderRule": {
"children": [
{ "name": "{camelCase}.ts" },
{
"name": "__tests__",
"children": [{ "name": "{camelCase}.test.ts" }]
}
]
}
}
}

View File

@@ -0,0 +1,89 @@
<!doctype html>
<html lang="en" translate="no" class="light">
<head>
<meta charset="UTF-8" />
<link
rel="icon"
type="image/x-icon"
href="/images/icons/android/android-launchericon-48-48.png"
data-rh="true"
/>
<link rel="apple-touch-icon" href="/images/icons/ios/192.png" />
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="A modern open-source CRM" />
<meta
property="og:image"
content="https://raw.githubusercontent.com/twentyhq/twenty/main/docs/static/img/social-card.png"
/>
<meta property="og:description" content="A modern open-source CRM" />
<meta property="og:title" content="Twenty" />
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:image"
content="https://raw.githubusercontent.com/twentyhq/twenty/main/docs/static/img/social-card.png"
/>
<meta name="twitter:description" content="A modern open-source CRM" />
<meta name="twitter:title" content="Twenty" />
<title>Twenty</title>
<!-- BEGIN: Twenty Config -->
<script id="twenty-env-config">
window._env_ = {
// This will be overwritten
};
</script>
<!-- END: Twenty Config -->
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, interactive-widget=resizes-content"
/>
<script type="module">
const disableInputAutoZoom = () => {
const viewportMetadata = document.querySelector('meta[name=viewport]');
if (viewportMetadata !== null) {
viewportMetadata.setAttribute(
'content',
'width=device-width, initial-scale=1.0, maximum-scale=1.0, interactive-widget=resizes-content',
);
}
};
const isIOS = /iPad|iPhone/.test(navigator.userAgent);
if (isIOS) {
disableInputAutoZoom();
}
</script>
<style>
:root {
/* THEME_DARK.background.secondary */
--theme-dark-background-secondary: #1b1b1b;
/* THEME_LIGHT.background.secondary */
--theme-light-background-secondary: #fcfcfc;
}
@media (prefers-color-scheme: dark) {
html,
body {
background-color: var(--theme-dark-background-secondary);
}
}
@media (prefers-color-scheme: light) {
html,
body {
background-color: var(--theme-light-background-secondary);
}
}
</style>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,88 @@
class JestFailuresOnlyReporter {
constructor() {
this._failures = [];
}
onTestResult(_test, testResult) {
if (testResult.numFailingTests === 0 && !testResult.testExecError) {
return;
}
const relativePath = testResult.testFilePath.replace(
process.cwd() + '/',
'',
);
process.stderr.write(`\n\x1b[31mFAIL\x1b[0m ${relativePath}\n`);
if (testResult.failureMessage) {
process.stderr.write(testResult.failureMessage + '\n');
}
if (testResult.testExecError) {
process.stderr.write(
`\n Runtime error: ${testResult.testExecError.message}\n`,
);
}
this._failures.push({ path: relativePath, testResult });
}
onRunComplete(_testContexts, aggregatedResults) {
const {
numPassedTestSuites,
numFailedTestSuites,
numTotalTestSuites,
numPassedTests,
numFailedTests,
numTotalTests,
} = aggregatedResults;
process.stderr.write('\n');
if (this._failures.length > 0) {
process.stderr.write(
`\x1b[31m${'='.repeat(60)}\x1b[0m\n` +
`\x1b[31m FAILED TEST SUITES SUMMARY\x1b[0m\n` +
`\x1b[31m${'='.repeat(60)}\x1b[0m\n\n`,
);
for (const failure of this._failures) {
process.stderr.write(` \x1b[31m✕\x1b[0m ${failure.path}\n`);
const failedTests = failure.testResult.testResults.filter(
(result) => result.status === 'failed',
);
for (const failedTest of failedTests) {
process.stderr.write(` \x1b[31m→\x1b[0m ${failedTest.fullName}\n`);
}
process.stderr.write('\n');
}
process.stderr.write(`\x1b[31m${'='.repeat(60)}\x1b[0m\n\n`);
}
const suiteSummary =
`Test Suites: ` +
(numFailedTestSuites > 0
? `\x1b[31m${numFailedTestSuites} failed\x1b[0m, `
: '') +
(numPassedTestSuites > 0
? `\x1b[32m${numPassedTestSuites} passed\x1b[0m, `
: '') +
`${numTotalTestSuites} total`;
const testSummary =
`Tests: ` +
(numFailedTests > 0 ? `\x1b[31m${numFailedTests} failed\x1b[0m, ` : '') +
(numPassedTests > 0 ? `\x1b[32m${numPassedTests} passed\x1b[0m, ` : '') +
`${numTotalTests} total`;
process.stderr.write(suiteSummary + '\n');
process.stderr.write(testSummary + '\n');
}
}
module.exports = JestFailuresOnlyReporter;

View File

@@ -0,0 +1,99 @@
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 isCI = process.env.CI === 'true';
// oxlint-disable-next-line no-undef
process.env.TZ = 'GMT';
// oxlint-disable-next-line no-undef
process.env.LC_ALL = 'en_US.UTF-8';
const jestConfig = {
// For more information please have a look to official docs https://jestjs.io/docs/configuration/#prettierpath-string
// Prettier v3 will should be supported in jest v30 https://github.com/jestjs/jest/releases/tag/v30.0.0-alpha.1
prettierPath: null,
...(isCI && { reporters: ['./jest-failures-only-reporter.cjs'] }),
displayName: 'twenty-front',
preset: '../../jest.preset.js',
setupFilesAfterEnv: ['./setupTests.ts'],
testEnvironment: 'jsdom',
testEnvironmentOptions: {},
transformIgnorePatterns: [
'/node_modules/(?!(twenty-ui|apollo-upload-client|extract-files|is-plain-obj|@preact/signals-core|marked|ai|@ai-sdk|@workflow)/.*)',
'../../node_modules/(?!(twenty-ui|apollo-upload-client|extract-files|is-plain-obj|@preact/signals-core|marked|ai|@ai-sdk|@workflow)/.*)',
'../../twenty-ui/',
],
transform: {
'^.+\\.(ts|js|tsx|jsx|mjs)$': [
'@swc/jest',
{
jsc: {
parser: {
syntax: 'typescript',
tsx: true,
},
transform: {
react: {
runtime: 'automatic',
},
},
experimental: {
plugins: [['@lingui/swc-plugin', {}]],
},
},
},
],
},
moduleNameMapper: {
'\\.(jpg|jpeg|png|gif|webp|svg|svg\\?react)$':
'<rootDir>/__mocks__/imageMockFront.js',
'\\.css$': '<rootDir>/__mocks__/styleMock.js',
'\\?worker$': '<rootDir>/__mocks__/workerMock.js',
...pathsToModuleNameMapper(tsConfig.compilerOptions.paths, {
prefix: '<rootDir>/',
}),
},
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
extensionsToTreatAsEsm: ['.ts', '.tsx'],
coverageThreshold: {
global: {
statements: 47.3,
lines: 45.9,
functions: 39.5,
},
},
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
coveragePathIgnorePatterns: [
'states/.+State.ts$',
'states/selectors/*',
'contexts/.+Context.ts',
'testing/*',
'tests/*',
'config/*',
'graphql/queries/*',
'graphql/mutations/*',
'graphql/subscriptions/*',
'graphql/fragments/*',
'types/*',
'constants/*',
'generated-metadata/*',
'generated/*',
'__stories__/*',
'display/icon/index.ts',
],
coverageDirectory: './coverage',
maxWorkers: 3,
workerIdleMemoryLimit: '512MB',
errorOnDeprecated: true,
testTimeout: 30000,
};
export default jestConfig;

View File

@@ -0,0 +1,22 @@
import { defineConfig } from '@lingui/conf';
import { formatter } from '@lingui/format-po';
import { APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
export default defineConfig({
sourceLocale: SOURCE_LOCALE,
locales: Object.values(APP_LOCALES),
pseudoLocale: 'pseudo-en',
fallbackLocales: {
'pseudo-en': 'en',
default: SOURCE_LOCALE,
},
catalogs: [
{
path: '<rootDir>/src/locales/{locale}',
include: ['src'],
},
],
catalogsMergePath: '<rootDir>/src/locales/generated/{locale}',
compileNamespace: 'ts',
format: formatter({ lineNumbers: false, printLinguiId: true }),
});

View File

@@ -0,0 +1,49 @@
// @ts-check
const globalCoverage = {
branches: 23,
statements: 39,
lines: 39,
functions: 28,
exclude: ['src/generated/**/*'],
};
const modulesCoverage = {
branches: 25,
statements: 43,
lines: 44,
functions: 30,
include: ['src/modules/**/*'],
exclude: ['src/**/*.ts'],
};
const pagesCoverage = {
branches: 35,
statements: 60,
lines: 60,
functions: 45,
exclude: ['src/generated/**/*', 'src/modules/**/*', 'src/**/*.ts'],
};
const performanceCoverage = {
branches: 33,
statements: 60,
lines: 60,
functions: 45,
exclude: ['src/generated/**/*', 'src/modules/**/*', 'src/**/*.ts'],
};
const getCoverageConfig = () => {
const storybookStoriesFolders = process.env.STORYBOOK_SCOPE;
switch (storybookStoriesFolders) {
case 'pages':
return pagesCoverage;
case 'modules':
return modulesCoverage;
case 'performance':
return performanceCoverage;
default:
return globalCoverage;
}
};
module.exports = getCoverageConfig();

View File

@@ -0,0 +1,209 @@
{
"name": "twenty-front",
"private": true,
"type": "module",
"scripts": {
"build": "NODE_ENV=production NODE_OPTIONS=--max-old-space-size=8192 npx vite build",
"build:sourcemaps": "NODE_ENV=production VITE_BUILD_SOURCEMAP=true NODE_OPTIONS=--max-old-space-size=8192 npx vite build",
"start:prod": "NODE_ENV=production npx serve -s build",
"tsup": "npx tsup"
},
"engines": {
"node": "^24.5.0",
"npm": "please-use-yarn",
"yarn": "^4.0.2"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"msw": {
"workerDirectory": "public"
},
"dependencies": {
"@apollo/client": "^4.0.0",
"@blocknote/mantine": "^0.51.4",
"@blocknote/react": "^0.51.4",
"@blocknote/xl-docx-exporter": "^0.51.4",
"@blocknote/xl-pdf-exporter": "^0.51.4",
"@calcom/embed-react": "^1.5.3",
"@cyntler/react-doc-viewer": "^1.17.0",
"@dagrejs/dagre": "^1.1.8",
"@dnd-kit/react": "^0.3.2",
"@dr.pogodin/react-helmet": "^3.2.2",
"@floating-ui/react": "^0.24.3",
"@fontsource/dm-mono": "^5.2.7",
"@fontsource/inter": "^5.2.8",
"@graphiql/plugin-explorer": "^5.1.2",
"@graphiql/react": "^0.37.6",
"@hookform/resolvers": "^5.2.2",
"@linaria/core": "^7.0.0",
"@linaria/react": "^7.0.1",
"@lingui/core": "^5.9.5",
"@lingui/detect-locale": "^5.9.5",
"@lingui/react": "^5.9.5",
"@mantine/core": "^8.3.11",
"@mantine/hooks": "^8.3.11",
"@mantine/utils": "^6.0.22",
"@monaco-editor/react": "^4.7.0",
"@nivo/core": "^0.99.0",
"@nivo/line": "^0.99.0",
"@nivo/pie": "^0.99.0",
"@nivo/radial-bar": "^0.99.0",
"@react-pdf/renderer": "^4.1.6",
"@scalar/api-reference-react": "^0.9.42",
"@sentry/react": "^10.51.0",
"@sniptt/guards": "^0.2.0",
"@stripe/react-stripe-js": "^6.6.0",
"@stripe/stripe-js": "^9.8.0",
"@tiptap/core": "3.30.5",
"@tiptap/extension-bold": "3.30.5",
"@tiptap/extension-document": "3.30.5",
"@tiptap/extension-hard-break": "3.30.5",
"@tiptap/extension-heading": "3.30.5",
"@tiptap/extension-image": "3.30.5",
"@tiptap/extension-italic": "3.30.5",
"@tiptap/extension-link": "3.30.5",
"@tiptap/extension-list": "3.30.5",
"@tiptap/extension-paragraph": "3.30.5",
"@tiptap/extension-strike": "3.30.5",
"@tiptap/extension-text": "3.30.5",
"@tiptap/extension-underline": "3.30.5",
"@tiptap/extensions": "3.30.5",
"@tiptap/react": "3.30.5",
"@types/marked": "^6.0.0",
"@xyflow/react": "^12.4.2",
"ai": "7.0.93",
"apollo-link-rest": "^0.10.0-rc.2",
"apollo-upload-client": "^19.0.0",
"buffer": "^6.0.3",
"country-flag-icons": "^1.5.11",
"cron-parser": "5.1.1",
"d3-shape": "^3.2.0",
"date-fns": "^4.4.0",
"date-fns-tz": "^3.2.0",
"deep-equal": "^2.2.2",
"file-saver": "^2.0.5",
"framer-motion": "^11.18.0",
"fuse.js": "^7.1.0",
"graphiql": "^5.2.3",
"graphql": "16.8.1",
"graphql-sse": "^2.5.4",
"graphql-tag": "^2.12.6",
"idb-keyval": "^6.2.5",
"immer": "^10.1.1",
"input-otp": "^1.4.2",
"jotai": "^2.17.1",
"js-cookie": "^3.0.5",
"json-2-csv": "^5.4.0",
"json-logic-js": "^2.0.5",
"jwt-decode": "^4.0.0",
"libphonenumber-js": "^1.10.26",
"linkify-react": "^4.1.3",
"linkifyjs": "^4.1.3",
"lodash.camelcase": "^4.3.0",
"lodash.groupby": "^4.6.0",
"lodash.isempty": "^4.4.0",
"lodash.omit": "^4.5.0",
"lodash.uniqby": "^4.7.0",
"marked": "^17.0.1",
"microdiff": "^1.3.2",
"papaparse": "^5.4.1",
"pluralize": "^8.0.0",
"qs": "^6.15.2",
"react": "^19.2.0",
"react-data-grid": "7.0.0-beta.59",
"react-datepicker": "^9.1.0",
"react-dom": "^19.2.0",
"react-dropzone": "^14.2.3",
"react-error-boundary": "^4.0.11",
"react-grid-layout": "^2.2.4",
"react-hook-form": "^7.45.1",
"react-hotkeys-hook": "^4.4.4",
"react-imask": "^7.6.0",
"react-intersection-observer": "^9.15.1",
"react-loading-skeleton": "^3.3.1",
"react-markdown": "^10.1.0",
"react-phone-number-input": "patch:react-phone-number-input@npm%3A3.4.5#../../.yarn/patches/react-phone-number-input-npm-3.4.5-dc2895c306.patch",
"react-qr-code": "^2.0.18",
"react-responsive": "^9.0.2",
"react-router-dom": "^6.30.6",
"react-textarea-autosize": "^8.4.1",
"remark-gfm": "^4.0.1",
"rxjs": "^7.2.0",
"temporal-polyfill": "^0.3.0",
"transliteration": "^2.3.5",
"ts-key-enum": "^2.0.12",
"twenty-front-component-renderer": "workspace:*",
"twenty-shared": "workspace:*",
"twenty-ui": "workspace:*",
"type-fest": "4.10.1",
"use-debounce": "^10.0.0",
"uuid": "^11.1.1",
"xlsx-ugnis": "^0.19.3",
"zod": "^4.1.11"
},
"devDependencies": {
"@argos-ci/storybook": "6.2.3",
"@babel/core": "^7.14.5",
"@babel/preset-typescript": "^7.24.6",
"@eloqnt/cli": "^0.6.29",
"@graphql-codegen/cli": "^6.3.1",
"@graphql-codegen/typed-document-node": "^6.1.8",
"@graphql-codegen/typescript": "^5.0.10",
"@graphql-codegen/typescript-operations": "^5.1.0",
"@lingui/cli": "^5.9.5",
"@lingui/swc-plugin": "^5.11.0",
"@lingui/vite-plugin": "^5.9.5",
"@playwright/test": "^1.60.0",
"@storybook-community/storybook-addon-cookie": "^5.0.0",
"@storybook/addon-coverage": "^3.0.0",
"@storybook/addon-docs": "^10.4.6",
"@storybook/addon-links": "^10.4.6",
"@storybook/react-vite": "^10.4.6",
"@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",
"@tiptap/suggestion": "3.30.5",
"@types/deep-equal": "^1.0.1",
"@types/file-saver": "^2.0.7",
"@types/jest": "^30.0.0",
"@types/js-cookie": "^3.0.3",
"@types/json-logic-js": "^2",
"@types/uuid": "^9.0.2",
"@vitejs/plugin-react-swc": "^4.3.1",
"@vitest/coverage-istanbul": "^4.1.0",
"@wyw-in-js/vite": "^1.1.0",
"dotenv-cli": "^7.4.4",
"esbuild": "^0.28.1",
"jest": "29.7.0",
"jest-environment-jsdom": "30.0.0-beta.3",
"jest-fetch-mock": "^3.0.3",
"monaco-editor": "0.52.2",
"monaco-editor-auto-typings": "^0.4.5",
"monaco-graphql": "1.8.0",
"msw": "2.12.14",
"msw-storybook-addon": "^2.0.6",
"optionator": "^0.9.1",
"oxlint": "^1.51.0",
"oxlint-tsgolint": "^0.16.0",
"playwright": "^1.60.0",
"prettier": "^3.1.1",
"rollup-plugin-node-polyfills": "^0.2.1",
"rollup-plugin-visualizer": "^5.14.0",
"storybook-addon-mock-date": "2.0.0",
"storybook-addon-pseudo-states": "^10.3.3",
"ts-jest": "^29.1.1",
"vite-plugin-svgr": "^4.3.0"
}
}

View File

@@ -0,0 +1,308 @@
{
"name": "twenty-front",
"$schema": "../../node_modules/nx/schemas/project-schema.json",
"projectType": "application",
"tags": ["scope:frontend"],
"targets": {
"build": {
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "{projectRoot}/build"
},
"dependsOn": ["^build"]
},
"build:sourcemaps": {
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "{projectRoot}/build"
},
"dependsOn": ["^build"]
},
"serve": {
"executor": "nx:run-commands",
"options": {
"command": "npx serve -s {projectRoot}/build"
}
},
"start": {
"executor": "@nx/vite:dev-server",
"options": {
"buildTarget": "twenty-front:build",
"hmr": true
}
},
"preview": {
"executor": "@nx/vite:preview-server",
"options": {
"buildTarget": "twenty-front:build",
"port": 3001,
"open": true
}
},
"reset:env": {
"executor": "nx:run-commands",
"inputs": ["{projectRoot}/.env.example"],
"outputs": ["{projectRoot}/.env"],
"cache": true,
"options": {
"cwd": "{projectRoot}",
"command": "cp .env.example .env"
}
},
"typecheck": {},
"lint": {
"executor": "nx:run-commands",
"cache": true,
"dependsOn": ["twenty-oxlint-rules:build", "twenty-shared:build"],
"inputs": [
"{projectRoot}/src/**/*.{ts,tsx}",
"{projectRoot}/tsconfig*.json",
"{workspaceRoot}/tsconfig.base.json",
"{projectRoot}/.oxlintrc.json",
"{workspaceRoot}/packages/twenty-oxlint-rules/dist/oxlint-plugin.mjs"
],
"options": {
"cwd": "{projectRoot}",
"command": "npx oxlint --type-aware -c .oxlintrc.json src/ && (npx oxfmt --check src/ || (echo 'ERROR: oxfmt formatting check failed! Fix with: npx nx lint twenty-front --configuration=fix' && false))"
},
"configurations": {
"fix": {
"command": "npx oxlint --type-aware --fix -c .oxlintrc.json src/ && npx oxfmt src/"
}
}
},
"lint:i18n": {
"executor": "nx:run-commands",
"cache": true,
"inputs": [
"{projectRoot}/src/locales/*.po",
"{projectRoot}/.eloqnt/config.ts",
"{workspaceRoot}/yarn.lock"
],
"options": {
"cwd": "{projectRoot}",
"command": "npx eloqnt lint"
}
},
"lint:diff-with-main": {
"executor": "nx:run-commands",
"dependsOn": ["twenty-oxlint-rules:build"],
"options": {
"cwd": "{projectRoot}",
"command": "FILES=$(git diff --name-only --relative --diff-filter=d main...HEAD -- src/ | grep -E '\\.(ts|tsx)$'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --type-aware -c .oxlintrc.json $FILES && (npx oxfmt --check $FILES || (echo 'ERROR: oxfmt formatting check failed! Fix with: npx nx lint:diff-with-main twenty-front --configuration=fix' && false)))"
},
"configurations": {
"fix": {
"command": "FILES=$(git diff --name-only --relative --diff-filter=d main...HEAD -- src/ | grep -E '\\.(ts|tsx)$'); [ -z \"$FILES\" ] && echo 'No changed files.' || (npx oxlint --type-aware --fix -c .oxlintrc.json $FILES && npx oxfmt $FILES)"
}
}
},
"fmt": {
"options": {
"files": "src"
},
"configurations": {
"fix": {}
}
},
"test": {},
"storybook:build": {
"options": {
"env": {
"NODE_OPTIONS": "--max_old_space_size=8000"
}
},
"configurations": {
"docs": {
"env": {
"STORYBOOK_SCOPE": "ui-docs"
}
},
"modules": {
"env": {
"STORYBOOK_SCOPE": "modules"
}
},
"pages": {
"env": {
"STORYBOOK_SCOPE": "pages"
}
},
"performance": {
"env": {
"STORYBOOK_SCOPE": "performance"
}
}
}
},
"storybook:serve:dev": {
"options": {
"port": 6006
},
"configurations": {
"docs": {
"env": {
"STORYBOOK_SCOPE": "ui-docs"
}
},
"modules": {
"env": {
"STORYBOOK_SCOPE": "modules"
}
},
"pages": {
"env": {
"STORYBOOK_SCOPE": "pages"
}
},
"performance": {
"env": {
"STORYBOOK_SCOPE": "performance"
}
}
}
},
"storybook:serve:static": {
"options": {
"port": 6006
},
"configurations": {
"docs": {
"env": {
"STORYBOOK_SCOPE": "ui-docs"
}
},
"modules": {
"env": {
"STORYBOOK_SCOPE": "modules"
}
},
"pages": {
"env": {
"STORYBOOK_SCOPE": "pages"
}
},
"performance": {
"env": {
"STORYBOOK_SCOPE": "performance"
}
}
}
},
"storybook:coverage": {
"configurations": {
"text": {},
"docs": {
"env": {
"STORYBOOK_SCOPE": "ui-docs"
}
},
"modules": {
"env": {
"STORYBOOK_SCOPE": "modules"
}
},
"pages": {
"env": {
"STORYBOOK_SCOPE": "pages"
}
},
"performance": {
"env": {
"STORYBOOK_SCOPE": "performance"
}
}
}
},
"storybook:test": {
"configurations": {
"docs": {
"env": {
"STORYBOOK_SCOPE": "ui-docs"
}
},
"modules": {
"env": {
"STORYBOOK_SCOPE": "modules"
}
},
"pages": {
"env": {
"STORYBOOK_SCOPE": "pages"
}
},
"performance": {
"env": {
"STORYBOOK_SCOPE": "performance"
}
}
}
},
"storybook:test:no-coverage": {
"configurations": {
"docs": {
"env": {
"STORYBOOK_SCOPE": "ui-docs"
}
},
"modules": {
"env": {
"STORYBOOK_SCOPE": "modules"
}
},
"pages": {
"env": {
"STORYBOOK_SCOPE": "pages"
}
},
"performance": {
"env": {
"STORYBOOK_SCOPE": "performance"
}
}
}
},
"graphql:generate": {
"executor": "nx:run-commands",
"defaultConfiguration": "data",
"options": {
"cwd": "{projectRoot}",
"command": "dotenv graphql-codegen -- --config={args.config}"
},
"configurations": {
"data": {
"config": "codegen.cjs"
},
"metadata": {
"config": "codegen-metadata.cjs"
},
"admin": {
"config": "codegen-admin.cjs"
}
}
},
"mock:generate": {
"executor": "nx:run-commands",
"options": {
"cwd": "{projectRoot}",
"command": "dotenv npx vite-node scripts/generate-mock-data.ts"
}
},
"lingui:extract": {
"executor": "nx:run-commands",
"dependsOn": ["^build"],
"options": {
"cwd": "{projectRoot}",
"command": "lingui extract --overwrite --clean"
}
},
"lingui:compile": {
"executor": "nx:run-commands",
"dependsOn": ["^build"],
"options": {
"cwd": "{projectRoot}",
"command": "lingui compile --typescript"
}
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 618 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 660 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 701 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 859 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 B

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