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,22 @@
---
title: Лучшие практики
---
Этот документ описывает лучшие практики, которым следует следовать при работе с бэкендом.
## Следуйте модульному подходу
Бэкенд следует модульному подходу, который является основным принципом при работе с NestJS. Убедитесь, что вы разбиваете код на повторно используемые модули, чтобы поддерживать чистую и хорошо организованную кодовую базу.
Каждый модуль должен инкапсулировать конкретную функциональность и иметь чётко определённую область ответственности. Этот модульный подход обеспечивает четкое разделение областей ответственности и устраняет лишние сложности.
## Предоставляйте сервисы для использования в модулях
Всегда создавайте сервисы с одной чёткой областью ответственности, что повышает читаемость и облегчает сопровождение кода. Называйте сервисы понятно и единообразно.
Также следует экспортировать сервисы, которые вы хотите использовать в других модулях. Экспорт сервисов в другие модули возможен благодаря мощной системе внедрения зависимостей NestJS и способствует слабой связности между компонентами.
## Избегайте использования типа `any`
Когда вы объявляете переменную с типом `any`, система проверки типов TypeScript перестаёт выполнять проверку, что позволяет присваивать переменной значения любого типа. TypeScript использует вывод типов для определения типа переменной на основе значения. Если объявить переменную с типом `any`, TypeScript больше не сможет вывести её тип. Это затрудняет выявление ошибок, связанных с типами, во время разработки, приводит к ошибкам времени выполнения и делает код менее поддерживаемым, менее надёжным и труднее понятным для других разработчиков.
Поэтому всё должно иметь тип. Поэтому, если вы создаёте новый объект с именем и фамилией, следует создать интерфейс или тип с полями имя и фамилия, определяющий структуру объекта, с которым вы работаете.

View File

@@ -0,0 +1,39 @@
---
title: Пользовательские объекты
---
Объекты - это структуры, которые позволяют хранить данные (записи, атрибуты и значения), специфичные для организации. В Twenty предусмотрены как стандартные, так и пользовательские объекты.
Стандартные объекты - это встроенные объекты с набором атрибутов, доступных для всех пользователей. В Twenty к стандартным объектам относятся Company и Person. Стандартные объекты имеют стандартные поля, которые также доступны всем пользователям Twenty, например, Company.displayName.
Пользовательские объекты - это объекты, которые вы можете создать для хранения уникальной информации для вашей организации. Они не являются встроенными; участники вашего рабочего пространства могут создавать и настраивать пользовательские объекты для хранения информации, для которой стандартные объекты не подходят.
## Схема высокого уровня
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/custom-object-schema.png" alt="Схема высокого уровня" />
</div>
<br />
## Как это работает
Пользовательские объекты происходят из таблиц метаданных, определяющих форму, имя и тип объектов. Вся эта информация присутствует в базе данных схемы метаданных, состоящей из таблиц:
* **DataSource**: Указывает, где находятся данные.
* **Объект**: Описывает объект и связывается с DataSource.
* **Поле**: Описывает поля объекта и соединяет с объектом.
Чтобы добавить пользовательский объект, участник рабочего пространства сделает запрос к /metadata API. Это обновляет метаданные соответствующим образом и создает схему GraphQL на основе метаданных, храня их в кэше GQL для дальнейшего использования.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/add-custom-objects.jpeg" alt="Запросите API /metadata для добавления пользовательских объектов" />
</div>
<br />
Для извлечения данных процесс включает выполнение запросов через конечную точку /graphql и их передачу через Query Resolver.
<div style={{textAlign: 'center'}}>
<img src="/images/docs/server/custom-object-schema.png" alt="Запросите конечную точку /graphql для получения данных" />
</div>

View File

@@ -0,0 +1,46 @@
---
title: Флаги функций
---
Флаги функций используются для скрытия экспериментальных функций. Для Twenty они устанавливаются на уровне рабочей области, а не на уровне пользователя.
## Добавление нового флага функции
В `FeatureFlagKey.ts` добавьте флаг функции:
```ts
type FeatureFlagKey =
| 'IS_FEATURENAME_ENABLED'
| ...;
```
Также добавьте его в перечисление в `feature-flag.entity.ts`:
```ts
enum FeatureFlagKeys {
IsFeatureNameEnabled = 'IS_FEATURENAME_ENABLED',
...
}
```
Чтобы применить флаг функции к **бэкенд**-функции, используйте:
```ts
@Gate({
featureFlag: 'IS_FEATURENAME_ENABLED',
})
```
Чтобы применить флаг функции к **фронтенд**-функции, используйте:
```ts
const isFeatureNameEnabled = useIsFeatureEnabled('IS_FEATURENAME_ENABLED');
```
## Настройте флаги функций для развертывания
Измените соответствующую запись в таблице `core.featureFlag`:
| идентификатор | ключ | workspaceId | значение |
| ------------- | ------------------------ | ----------- | -------- |
| Случайный | `IS_FEATURENAME_ENABLED` | WorkspaceID | `истина` |

View File

@@ -0,0 +1,125 @@
---
title: Архитектура папок
info: Подробное рассмотрение нашей архитектуры папок сервера
---
Структура серверной директории следующая:
```
server
└───ability
└───constants
└───core
└───database
└───decorators
└───filters
└───guards
└───health
└───integrations
└───metadata
└───workspace
└───utils
```
## Правила доступа
Определяет разрешения и включает обработчики для каждого объекта.
## Декораторы
Определяет пользовательские декораторы в NestJS для расширенной функциональности.
Смотрите [пользовательские декораторы](https://docs.nestjs.com/custom-decorators) для более подробной информации.
## Фильтры
Включает фильтры исключений для обработки исключений, которые могут возникать в конечных точках GraphQL.
## Гарды
Смотрите [защиты](https://docs.nestjs.com/guards) для более подробной информации.
## Работоспособность
Включает общедоступный REST API (healthz), который возвращает JSON, чтобы подтвердить, что база данных работает должным образом.
## Метаданные
Определяет пользовательские объекты и предоставляет GraphQL API (graphql/metadata).
## Рабочее пространство
Генерирует и обслуживает пользовательскую GraphQL схему на основе метаданных.
### Структура каталога рабочей области
```
workspace
└───workspace-schema-builder
└───factories
└───graphql-types
└───database
└───interfaces
└───object-definitions
└───services
└───storage
└───utils
└───workspace-resolver-builder
└───factories
└───interfaces
└───workspace-query-builder
└───factories
└───interfaces
└───workspace-query-runner
└───interfaces
└───utils
└───workspace-datasource
└───workspace-manager
└───workspace-migration-runner
└───utils
└───workspace.module.ts
└───workspace.factory.spec.ts
└───workspace.factory.ts
```
Корень каталога рабочей области включает `workspace.factory.ts`, файл, содержащий функцию `createGraphQLSchema`. Эта функция генерирует схему, специфичную для рабочей области, используя метаданные для адаптации схемы для индивидуальных рабочих областей. Разделением построения схемы и резолвера, мы используем функцию `makeExecutableSchema`, которая объединяет эти отдельные элементы.
Эта стратегия касается не только организации, но и помогает в оптимизации, например в кэшировании сгенерированных определений типов для повышения производительности и масштабируемости.
### Конструктор схем рабочей области
Генерирует GraphQL схему и включает:
#### Фабрики:
Специализированные конструкторы для генерации связанных с GraphQL конструкций.
* Фабрика типов переводит метаданные поля в типы GraphQL, используя `TypeMapperService`.
* Фабрика определения типов создает объекты ввода или вывода GraphQL на основе `objectMetadata`.
#### Типы GraphQL
Включает перечисления, входы, объекты и скаляры и служит строительными блоками для построения схемы.
#### Интерфейсы и определение объектов
Содержит чертежи для GraphQL сущностей и включает как предопределенные, так и пользовательские типы, такие как `MONEY` или `URL`.
#### Сервисы
Содержит сервис, ответственный за ассоциацию FieldMetadataType с соответствующим скаляром GraphQL или модификаторами запроса.
#### Хранилище
Включает класс `TypeDefinitionsStorage`, содержащий переиспользуемые определения типов, предотвращая дублирование GraphQL типов.
### Конструктор резолверов рабочей области
Создает функции резолверов для запроса и изменения схемы GraphQL.
Каждая фабрика в этом каталоге отвечает за создание отдельного типа резолвера, как, например, `FindManyResolverFactory`, предназначенной для адаптируемого применения в различных таблицах.
### Выполнители запросов рабочей области
Выполняет сгенерированные запросы в базе данных и анализирует результат.

View File

@@ -0,0 +1,41 @@
---
title: Очередь сообщений
---
Очереди позволяют выполнять асинхронные операции. Их можно использовать для выполнения фоновых задач, таких как отправка приветственного письма при регистрации.
Каждый случай использования будет иметь собственный класс очереди, расширенный из `MessageQueueServiceBase`.
В настоящее время мы поддерживаем только `bull-mq`[bull-mq](https://bullmq.io/) в качестве драйвера очереди.
## Шаги для создания и использования новой очереди
1. Добавьте имя очереди для вашей новой очереди в перечисление `MESSAGE_QUEUES`.
2. Предоставьте фабричную реализацию очереди с именем очереди в качестве токена зависимости.
3. Инжектируйте созданную вами очередь в необходимый модуль/сервис с именем очереди в качестве токена зависимости.
4. Добавьте класс worker с внедрением зависимостей на основе токена, как у producer.
### Пример использования
```ts
class Resolver {
constructor(@Inject(MESSAGE_QUEUES.custom) private queue: MessageQueueService) {}
async onSomeAction() {
//business logic
await this.queue.add(someData);
}
}
//async worker
class CustomWorker {
constructor(@Inject(MESSAGE_QUEUES.custom) private queue: MessageQueueService) {
this.initWorker();
}
async initWorker() {
await this.queue.work(async ({ id, data }) => {
//worker logic
});
}
}
```

View File

@@ -0,0 +1,87 @@
---
title: Команды бекэнда
icon: terminal
---
## Полезные команды
Эти команды следует выполнять из папки packages/twenty-server.
Из любой другой папки можно запускать `npx nx {command} twenty-server` (или `npx nx run twenty-server:{command}`).
### Первоначальная настройка
```
npx nx database:reset twenty-server # setup the database with dev seeds
```
### Запуск сервера
```
npx nx run twenty-server:start
```
### Lint
```
npx nx run twenty-server:lint # pass --fix to fix lint errors
```
### Тест
```
npx nx run twenty-server:test:unit # run unit tests
npx nx run twenty-server:test:integration # run integration tests
```
Примечание: вы можете использовать `npx nx run twenty-server:test:integration:with-db-reset`, если необходимо сбросить базу данных перед выполнением интеграционных тестов.
### Сброс базы данных
Если вы хотите сбросить и заполнить базу данных начальными данными, выполните следующую команду:
```bash
npx nx run twenty-server:database:reset
```
### Миграции
#### Для объектов в схемах Core/Metadata (TypeORM)
```bash
npx nx run twenty-server:database:migrate:generate
```
## Технологический стек
Для работы с серверной частью Twenty в основном использует NestJS.
Prisma был первым ORM, который мы использовали. Но чтобы пользователи могли создавать собственные поля и объекты, использование более низкого уровня было более логичным, так как нам нужен был тонкий контроль. Теперь проект использует TypeORM.
Вот как теперь выглядит стек технологий.
**Ядро**
* [NestJS](https://nestjs.com/)
* [TypeORM](https://typeorm.io/)
* [GraphQL Yoga](https://the-guild.dev/graphql/yoga-server)
**База данных**
* [Postgres](https://www.postgresql.org/)
**Интеграции сторонних решений**
* [Sentry](https://sentry.io/welcome/) для отслеживания ошибок
**Тестирование**
* [Jest](https://jestjs.io/)
**Инструменты**
* [Yarn](https://yarnpkg.com/)
* [Oxlint](https://oxc.rs/docs/guide/usage/linter.html)
**Разработка**
* [AWS EKS](https://aws.amazon.com/eks/)

View File

@@ -0,0 +1,85 @@
---
title: Приложение Zapier
---
Легко синхронизируйте Twenty с более чем 3000 приложениями с помощью [Zapier](https://zapier.com/). Автоматизируйте задачи, повышайте продуктивность и улучшайте ваши отношения с клиентами!
## О Zapier
Zapier — это инструмент, который позволяет автоматизировать рабочие процессы, соединяя приложения, которые ваша команда использует каждый день. Основная концепция Zapier — это рабочие процессы автоматизации, называемые Zaps, которые включают триггеры и действия.
Вы можете узнать больше о том, как работает Zapier [здесь](https://zapier.com/how-it-works).
## Настройка
### Шаг 1: Установите пакеты Zapier
```bash
cd packages/twenty-zapier
yarn
```
### Шаг 2: Войдите через CLI
Используйте свои учетные данные Zapier для входа с помощью CLI:
```bash
zapier login
```
### Шаг 3: Настройка переменных окружения
В каталоге `packages/twenty-zapier` выполните:
```bash
cp .env.example .env
```
Запустите приложение локально, перейдите на [http://localhost:3000/settings/api-webhooks](http://localhost:3000/settings/api-webhooks) и сгенерируйте API ключ.
Замените значение **YOUR_API_KEY** в файле `.env` сгенерированным вами API ключом.
## Разработка
<Warning>
Перед выполнением любой команды `zapier` обязательно запустите `yarn build`.
</Warning>
### Тест
```bash
yarn test
```
### Lint
```bash
yarn format
```
### Отслеживайте и компилируйте при редактировании кода
```bash
yarn watch
```
### Проверьте ваше приложение Zapier
```bash
yarn validate
```
### Разверните ваше приложение Zapier
```bash
yarn deploy
```
### Список всех команд CLI Zapier
```bash
zapier
```

View File

@@ -0,0 +1,79 @@
---
title: Ошибки, запросы и PR
icon: bug
info: Сообщайте о проблемах, предлагайте новые функции и вносите вклад в код
---
## Сообщение об ошибках
Чтобы сообщить об ошибке, пожалуйста, [создайте задачу на GitHub](https://github.com/twentyhq/twenty/issues/new).
Вы также можете попросить помощи на [Discord](https://discord.gg/cx5n4Jzs57).
## Запросы на Новые Функции
Если вы не уверены, что это ошибка, и вам кажется, что это ближе к запросу на новую функцию, то вам, вероятно, стоит [начать обсуждение вместо этого](https://github.com/twentyhq/twenty/discussions/new).
## Отправьте PR
Вклад в код Twenty начинается с PR.
### Перед началом
1. Проверьте [существующие обращения](https://github.com/twentyhq/twenty/issues) на предмет связанной работы
2. Для новых функций сначала откройте обращение для обсуждения
3. Ознакомьтесь с нашим [Кодексом поведения](https://github.com/twentyhq/twenty/blob/main/.github/CODE_OF_CONDUCT.md)
### Сделайте форк и клонируйте
1. Сделайте форк репозитория на GitHub
2. Клонируйте свой форк:
```bash
git clone https://github.com/YOUR_USERNAME/twenty.git
cd twenty
```
3. Добавьте удалённый репозиторий upstream:
```bash
git remote add upstream https://github.com/twentyhq/twenty.git
```
### Создайте ветку
```bash
git checkout -b feature/your-feature-name
```
Используйте понятные названия веток:
* `feature/add-export-button`
* `fix/login-redirect-issue`
* `docs/update-api-guide`
### Внесите изменения
1. Пишите чистый, хорошо документированный код
2. Соблюдайте существующий стиль кода
3. Добавьте тесты для новой функциональности
4. Обновите документацию при необходимости
### Отправьте свой PR
1. Отправьте свою ветку:
```bash
git push origin feature/your-feature-name
```
2. Откройте PR на GitHub
3. Заполните шаблон PR
4. Привяжите связанные обращения
### Чек-лист PR
* [ ] Код соответствует руководству по стилю проекта
* [ ] Тесты проходят локально
* [ ] Документация обновлена
* [ ] Описание PR объясняет изменения

View File

@@ -0,0 +1,329 @@
---
title: Лучшие практики
icon: star
---
Этот документ описывает лучшие практики, которых следует придерживаться при работе с фронтендом.
## Управление состоянием
React и Jotai отвечают за управление состоянием в коде.
### Используйте атомы Jotai для хранения состояния
Полезно создавать столько атомов, сколько вам нужно для хранения состояния.
<Warning>
Лучше использовать дополнительные атомы, чем пытаться быть слишком кратким с передачей свойств.
</Warning>
```tsx
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
defaultValue: 'default value',
});
export const MyComponent = () => {
const [myAtom, setMyAtom] = useAtomState(myAtomState);
return (
<div>
<input
value={myAtom}
onChange={(e) => setMyAtom(e.target.value)}
/>
</div>
);
}
```
### Не используйте `useRef` для хранения состояния
Избегайте использования `useRef` для хранения состояния.
Если вы хотите сохранять состояние, используйте `useState` или атомы Jotai с `useAtomState`.
Смотрите [как управлять повторными рендерами](#managing-re-renders), если вы считаете, что вам нужен `useRef`, чтобы предотвратить их.
## Управление повторными рендерами
Управлять повторными рендерами в React может быть сложно.
Вот некоторые правила, которые стоит соблюдать, чтобы избегать ненужных повторных рендеров.
Помните, что вы всегда можете избежать повторных рендеров, если поймете причину их возникновения.
### Работа на корневом уровне
Избежать повторных рендеров в новых функциях стало проще, устранив их на корневом уровне.
Сайдкар-компонент `PageChangeEffect` содержит всего один `useEffect`, который реализует всю логику при изменении страницы.
Таким образом, вы знаете, что есть только одно место, которое может вызвать повторный рендер.
### Всегда думайте дважды, прежде чем добавлять `useEffect` в ваш код
Повторные рендеры часто вызываются ненужными `useEffect`.
Подумайте, нужно ли вам использовать `useEffect`, или же вы можете перенести логику в функцию обработчика событий.
Как правило, несложно перенести логику в функции `handleClick` или `handleChange`.
Вы также можете найти их в библиотеках, таких как Apollo: `onCompleted`, `onError` и т. д.
### Используйте дополнительный компонент для извлечения `useEffect` или логики получения данных
Если вы считаете, что нужно добавить `useEffect` в корневой компонент, стоит рассмотреть возможность его извлечения в сайдкар-компонент.
Вы можете применять то же самое для логики получения данных, с хуками Apollo.
```tsx
// ❌ Bad, will cause re-renders even if data is not changing,
// because useEffect needs to be re-evaluated
export const PageComponent = () => {
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
setData(someDependency);
}
}, [someDependency]);
return <div>{data}</div>;
};
export const App = () => (
<PageComponent />
);
```
```tsx
// ✅ Good, will not cause re-renders if data is not changing,
// because useEffect is re-evaluated in another sibling component
export const PageComponent = () => {
const [data, setData] = useAtomState(dataState);
return <div>{data}</div>;
};
export const PageData = () => {
const [data, setData] = useAtomState(dataState);
const [someDependency] = useAtomState(someDependencyState);
useEffect(() => {
if(someDependency !== data) {
setData(someDependency);
}
}, [someDependency]);
return <></>;
};
export const App = () => (
<>
<PageData />
<PageComponent />
</>
);
```
### Используйте состояния семейства атомов и селекторы
Состояния семейства атомов и селекторы — отличный способ избежать повторных рендеров.
Они полезны, когда нужно хранить список элементов.
### Не следует использовать `React.memo(MyComponent)`
Избегайте использования `React.memo()`, так как это не решает причину повторного рендера, но разрывает цепочку рендера, что может привести к неожиданному поведению и усложнить рефакторинг кода.
### Ограничьте использование `useCallback` или `useMemo`
Часто это не нужно и делает код труднее читаемым и поддерживаемым для улучшения производительности, которую сложно заметить.
## Console.logs
`console.log` полезны во время разработки, предоставляя информацию в реальном времени о значениях переменных и потоке кода. Однако оставление их в коде для продакшена может привести к нескольким проблемам:
1. **Производительность**: Избыточное логирование может повлиять на производительность, особенно в клиентских приложениях.
2. **Безопасность**: Логирование конфиденциальных данных может раскрыть критическую информацию тем, кто может просматривать консоль браузера.
3. **Чистота**: Заполнение консоли логами может скрыть важные предупреждения или ошибки, которые нужно увидеть разработчикам или инструментам.
4. **Профессионализм**: Конечные пользователи или клиенты, проверяющие консоль и видящие множество логов, могут усомниться в качестве и обработке кода.
Убедитесь, что все `console.logs` удалены перед загрузкой кода в продакшен.
## Называние
### Наименование переменных
Названия переменных должны точно описывать их цель или функцию.
#### Проблемы с универсальными именами
Универсальные имена в программировании не идеальны, так как они не хватает определенности, что ведет к неясности и ухудшению читаемости кода. Такие имена не передают цель переменной или функции, делая трудным понимать намерение кода без более глубокого исследования. Это может привести к увеличению времени отладки, повышению вероятности ошибок и трудностям в поддержке и сотрудничестве. Между тем, описательные имена делают код самодокументирующимся и более простым для навигации, улучшая качество кода и продуктивность разработчиков.
```tsx
// ❌ Bad, uses a generic name that doesn't communicate its
// purpose or content clearly
const [value, setValue] = useState('');
```
```tsx
// ✅ Good, uses a descriptive name
const [email, setEmail] = useState('');
```
#### Некоторые слова, которых стоит избегать в названиях переменных
* dummy
### Обработчики событий
Названия обработчиков событий должны начинаться с `handle`, в то время как `on` используется как префикс для наименования событий в пропсах компонентов.
```tsx
// ❌ Bad
const onEmailChange = (val: string) => {
// ...
};
```
```tsx
// ✅ Good
const handleEmailChange = (val: string) => {
// ...
};
```
## Необязательные пропсы
Избегайте передачи значения по умолчанию для необязательного пропса.
**ПРИМЕР**
Возьмите компонент `EmailField`, определенный ниже:
```tsx
type EmailFieldProps = {
value: string;
disabled?: boolean;
};
const EmailField = ({ value, disabled = false }: EmailFieldProps) => (
<TextInput value={value} disabled={disabled} fullWidth />
);
```
**Использование**
```tsx
// ❌ Bad, passing in the same value as the default value adds no value
const Form = () => <EmailField value="username@email.com" disabled={false} />;
```
```tsx
// ✅ Good, assumes the default value
const Form = () => <EmailField value="username@email.com" />;
```
## Компонент как пропсы
Постарайтесь максимально передавать неинстанцированные компоненты как пропсы, чтобы дети могли самостоятельно определять, какие пропсы им нужно передать.
Наиболее распространенный пример этого — иконки компонентов:
```tsx
const SomeParentComponent = () => <MyComponent Icon={MyIcon} />;
// In MyComponent
const MyComponent = ({ MyIcon }: { MyIcon: IconComponent }) => {
const theme = useTheme();
return (
<div>
<MyIcon size={theme.icon.size.md}>
</div>
)
};
```
Чтобы React понял, что компонент является компонентом, нужно использовать PascalCase, чтобы потом инстанцировать его с `<MyIcon>`
## Сверление пропсов: минимизируйте его
Сверление пропсов в контексте React — это практика передачи переменных состояния и их сеттеров через многие уровни компонентов, даже если промежуточные компоненты их не используют. Хотя иногда это необходимо, чрезмерное сверление пропсов может привести к:
1. **Уменьшение читаемости**: Отслеживание происхождения пропса или мест, где он используется, может стать запутанным в глубоко вложенной структуре компонентов.
2. **Трудности в обслуживании**: Изменения в структуре пропсов одного компонента могут потребовать изменений в нескольких компонентах, даже если они их не используют напрямую.
3. **Снижение повторного использования компонентов**: Компонент, получающий много пропсов только для передачи их дальше, становится менее универсальным и сложным для повторного использования в разных контекстах.
Если вы считаете, что чрезмерно используете сверление пропсов, обратите внимание на [лучшие практики управления состоянием](#state-management).
## Импорт
При импорте предпочтение стоит отдать указанным псевдонимам, а не полным или относительным путям.
**Псевдонимы**
```js
{
alias: {
"~": path.resolve(__dirname, "src"),
"@": path.resolve(__dirname, "src/modules"),
"@testing": path.resolve(__dirname, "src/testing"),
},
}
```
**Использование**
```tsx
// ❌ Bad, specifies the entire relative path
import {
CatalogDecorator
} from '../../../../../testing/decorators/CatalogDecorator';
import {
ComponentDecorator
} from '../../../../../testing/decorators/ComponentDecorator';
```
```tsx
// ✅ Good, utilises the designated aliases
import { CatalogDecorator } from '~/testing/decorators/CatalogDecorator';
import { ComponentDecorator } from 'twenty-ui/testing';
```
## Проверка схемы
[Zod](https://github.com/colinhacks/zod) — это проверка схемы для нетипизированных объектов:
```js
const validationSchema = z
.object({
exist: z.boolean(),
email: z
.string()
.email('Email must be a valid email'),
password: z
.string()
.regex(PASSWORD_REGEX, 'Password must contain at least 8 characters'),
})
.required();
type Form = z.infer<typeof validationSchema>;
```
## Изменения с нарушением совместимости
Всегда проводите тщательное ручное тестирование перед тем, как продолжить, чтобы гарантировать, что изменения не вызвали перебоев в других местах, поскольку тесты еще не были широко интегрированы.

View File

@@ -0,0 +1,110 @@
---
title: Архитектура папок
icon: folder-tree
info: Подробное рассмотрение нашей архитектуры папок
---
В этом руководстве вы изучите детали структуры каталога проекта и то, как она способствует организации и сопровождаемости Twenty.
Следуя этому соглашению об архитектуре папок, легче находить файлы, связанные с конкретными функциями, и обеспечивать масштабируемость и поддерживаемость приложения.
```
front
└───modules
│ └───module1
│ │ └───submodule1
│ └───module2
│ └───ui
│ │ └───display
│ │ └───inputs
│ │ │ └───buttons
│ │ └───...
└───pages
└───...
```
## Страницы
Включает компоненты высокого уровня, определенные маршрутами приложения. Они импортируют более низкоуровневые компоненты из папки модулей (подробности ниже).
## Модули
Каждый модуль представляет собой функционал или группу функций, содержащий свои специфические компоненты, состояния и операционную логику.
Все они должны следовать структуре ниже. Вы можете вложить модули в модули (называются подмодулями), и те же правила будут применяться.
```
module1
└───components
│ └───component1
│ └───component2
└───constants
└───contexts
└───graphql
│ └───fragments
│ └───queries
│ └───mutations
└───hooks
│ └───internal
└───states
│ └───selectors
└───types
└───utils
```
### Контексты
Контекст — это способ передачи данных через дерево компонентов без необходимости передавать props вручную на каждом уровне.
См. [React Context](https://react.dev/reference/react#context-hooks) для более подробной информации.
### GraphQL
Включает фрагменты, запросы и мутации.
См. [GraphQL](https://graphql.org/learn/) для более подробной информации.
* Фрагменты
Фрагмент — это переиспользуемая часть запроса, которую можно использовать в разных местах. Используя фрагменты, легче избежать дублирования кода.
См. [GraphQL Fragments](https://graphql.org/learn/queries/#fragments) для более подробной информации.
* Запросы
См. [GraphQL Queries](https://graphql.org/learn/queries/) для более подробной информации.
* Мутации
См. [GraphQL Mutations](https://graphql.org/learn/queries/#mutations) для более подробной информации.
### Хуки
См. [Hooks](https://react.dev/learn/reusing-logic-with-custom-hooks) для более подробной информации.
### Состояния
Содержит логику управления состоянием. [Jotai](https://jotai.org) этим управляет.
* Селекторы: Производные атомы (с использованием `createAtomSelector`) вычисляют значения из других атомов и автоматически мемоизируются.
Встроенное управление состоянием в React все еще управляет состоянием внутри компонента.
### Утилиты
Должны содержать только переиспользуемые чистые функции. В противном случае создайте пользовательские хуки в папке `hooks`.
## Пользовательский интерфейс
Содержит все переиспользуемые компоненты пользовательского интерфейса, используемые в приложении.
Эта папка может содержать подкаталоги, такие как `data`, `display`, `feedback` и `input` для конкретных типов компонентов. Каждый компонент должен быть автономным и переиспользуемым, чтобы вы могли использовать его в разных частях приложения.
Разделяя компоненты пользовательского интерфейса от других компонентов в папке `modules`, легче поддерживать согласованность дизайна и вносить изменения в пользовательский интерфейс, не затрагивая другие части (бизнес-логику) кодовой базы.
## Интерфейс и зависимости
Вы можете импортировать код других модулей из любого модуля, кроме папки `ui`. Это позволит упростить тестирование его кода.
### Внутренний
Каждая часть (хуки, состояния, ...) модуля может иметь внутреннюю папку, содержащую части, которые используются только внутри модуля.

View File

@@ -0,0 +1,91 @@
---
title: Команды фронтенда
icon: terminal
---
## Полезные команды
### Запуск приложения
```bash
npx nx start twenty-front
```
### Перегенерировать схему graphql на основе схемы API graphql
```bash
npx nx run twenty-front:graphql:generate --configuration=metadata
```
ИЛИ
```bash
npx nx run twenty-front:graphql:generate
```
### Линт
```bash
npx nx run twenty-front:lint # pass --fix to fix lint errors
```
## Переводы
```bash
npx nx run twenty-front:lingui:extract
npx nx run twenty-front:lingui:compile
```
### Тест
```bash
npx nx run twenty-front:test # run jest tests
npx nx run twenty-front:storybook:serve:dev # run storybook
npx nx run twenty-front:storybook:test # run tests # (needs yarn storybook:serve:dev to be running)
npx nx run twenty-front:storybook:coverage # (needs yarn storybook:serve:dev to be running)
```
## Технологический стек
Проект имеет чистый и простой стек с минимальным кодом.
**Приложение**
* [React](https://react.dev/)
* [Apollo](https://www.apollographql.com/docs/)
* [GraphQL Codegen](https://the-guild.dev/graphql/codegen)
* [Jotai](https://jotai.org/)
* [TypeScript](https://www.typescriptlang.org/)
**Тестирование**
* [Jest](https://jestjs.io/)
* [Storybook](https://storybook.js.org/)
**Инструменты**
* [Yarn](https://yarnpkg.com/)
* [Craco](https://craco.js.org/docs/)
* [Oxlint](https://oxc.rs/docs/guide/usage/linter.html)
## Архитектура
### Маршрутизация
[React Router](https://reactrouter.com/) обрабатывает маршрутизацию.
Чтобы избежать ненужных [перерисовок](/l/ru/developers/contribute/capabilities/frontend-development/best-practices-front#managing-re-renders), вся логика маршрутизации находится в `useEffect` в `PageChangeEffect`.
### Управление состоянием
[Jotai](https://jotai.org/) отвечает за управление состоянием.
[лучшие практики](/l/ru/developers/contribute/capabilities/frontend-development/best-practices-front#state-management) для получения дополнительной информации об управлении состоянием.
## Тестирование
[Jest](https://jestjs.io/) служит инструментом для модульного тестирования, а [Storybook](https://storybook.js.org/) — для тестирования компонентов.
Jest в основном используется для тестирования утилит, а не самих компонентов.
Storybook предназначен для тестирования поведения изолированных компонентов, а также для отображения системы дизайна.

View File

@@ -0,0 +1,178 @@
---
title: Горячие клавиши
---
## Введение
Когда вам нужно прослушивать горячую клавишу, вы обычно используете обработчик событий `onKeyDown`.
Однако в `twenty-front` могут возникнуть конфликты между одинаковыми горячими клавишами, которые используются в разных компонентах, смонтированных одновременно.
Например, если у вас есть страница, которая реагирует на клавишу Enter, и модальное окно, реагирующее на ту же клавишу Enter, с компонентом Select внутри этого окна, то может возникнуть конфликт, когда все они смонтированы одновременно.
## Хук `useScopedHotkeys`
Для решения этой проблемы у нас есть индивидуальный хук, который позволяет мониторить горячие клавиши без конфликтов.
Вы размещаете его в компоненте, и он будет реагировать на горячие клавиши только тогда, когда компонент смонтирован И заданные **области горячих клавиш** активны.
## Как прослушивать горячие клавиши на практике?
Настройка мониторинга горячих клавиш включает в себя два шага :
1. Установить [область горячих клавиш](#what-is-a-hotkey-scope-), которая будет реагировать на горячие клавиши
2. Использовать хук `useScopedHotkeys` для мониторинга горячих клавиш
Настройка областей горячих клавиш необходима даже на простых страницах, так как другие элементы пользовательского интерфейса, такие как меню слева или меню команд, также могут реагировать на горячие клавиши.
## Кейсы использования горячих клавиш
В общем, у вас будет два случая использования, требующих горячих клавиш :
1. На странице или в компоненте, смонтированном на странице
2. В компоненте типа модального окна, который фокусируется из-за действия пользователя
Второй случай использования может происходить рекурсивно : например, раскрывающееся меню в модальном окне.
### Прослушивание горячих клавиш на странице
Пример :
```tsx
const PageListeningEnter = () => {
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
// 1. Set the hotkey scope in a useEffect
useEffect(() => {
setHotkeyScopeAndMemorizePreviousScope(
ExampleHotkeyScopes.ExampleEnterPage,
);
// Revert to the previous hotkey scope when the component is unmounted
return () => {
goBackToPreviousHotkeyScope();
};
}, [goBackToPreviousHotkeyScope, setHotkeyScopeAndMemorizePreviousScope]);
// 2. Use the useScopedHotkeys hook
useScopedHotkeys(
Key.Enter,
() => {
// Some logic executed on this page when the user presses Enter
// ...
},
ExampleHotkeyScopes.ExampleEnterPage,
);
return <div>My page that listens for Enter</div>;
};
```
### Прослушивание горячих клавиш в компоненте типа модального окна
Для этого примера мы используем модальный компонент, который прослушивает ключ Escape, чтобы сообщить своему родителю закрыть его.
В этом случае взаимодействие пользователя меняет область операций.
```tsx
const ExamplePageWithModal = () => {
const [showModal, setShowModal] = useState(false);
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
const handleOpenModalClick = () => {
// 1. Set the hotkey scope when user opens the modal
setShowModal(true);
setHotkeyScopeAndMemorizePreviousScope(
ExampleHotkeyScopes.ExampleModal,
);
};
const handleModalClose = () => {
// 1. Revert to the previous hotkey scope when the modal is closed
setShowModal(false);
goBackToPreviousHotkeyScope();
};
return <div>
<h1>My page with a modal</h1>
<button onClick={handleOpenModalClick}>Open modal</button>
{showModal && <MyModalComponent onClose={handleModalClose} />}
</div>;
};
```
Затем в компоненте модального окна :
```tsx
const MyDropdownComponent = ({ onClose }: { onClose: () => void }) => {
// 2. Use the useScopedHotkeys hook to listen for Escape.
// Note that escape is a common hotkey that could be used by many other components
// So it's important to use a hotkey scope to avoid conflicts
useScopedHotkeys(
Key.Escape,
() => {
onClose()
},
ExampleHotkeyScopes.ExampleModal,
);
return <div>My modal component</div>;
};
```
Важно использовать этот шаблон, когда вы не уверены, что просто использование useEffect при монтировании/размонтировании будет достаточным для избегания конфликтов.
Эти конфликты могут быть сложными для отладки, и они могут возникать чаще, чем ожидалось, с использованием useEffects.
## Что такое область горячих клавиш?
Область горячих клавиш — это строка, представляющая собой контекст, в котором активны горячие клавиши. Она обычно кодируется в виде перечисления (enum).
При изменении области горячих клавиш, горячие клавиши, слушающие эту область, будут включены, а слушающие другие области — отключены.
Вы можете установить только одну область за раз.
Например, области горячих клавиш для каждой страницы определены в enum `PageHotkeyScope`:
```tsx
export enum PageHotkeyScope {
Settings = 'settings',
CreateWorkspace = 'create-workspace',
SignInUp = 'sign-in-up',
CreateProfile = 'create-profile',
PlanRequired = 'plan-required',
ShowPage = 'show-page',
PersonShowPage = 'person-show-page',
CompanyShowPage = 'company-show-page',
CompaniesPage = 'companies-page',
PeoplePage = 'people-page',
OpportunitiesPage = 'opportunities-page',
ProfilePage = 'profile-page',
WorkspaceMemberPage = 'workspace-member-page',
TaskPage = 'task-page',
}
```
Внутренне текущая выбранная область действия хранится в атоме Jotai, который используется по всему приложению:
```tsx
export const currentHotkeyScopeState = createState<HotkeyScope>({
key: 'currentHotkeyScopeState',
defaultValue: INITIAL_HOTKEYS_SCOPE,
});
```
Но этот атом никогда не следует обрабатывать вручную! Мы увидим, как использовать это в следующем разделе.
## Как это работает внутренне?
Мы сделали тонкую обертку поверх [react-hotkeys-hook](https://react-hotkeys-hook.vercel.app/docs/intro), которая делает его более производительным и избегает ненужных повторных рендеров.
Мы также создаем атом Jotai, чтобы управлять состоянием области действия горячих клавиш и сделать его доступным везде в приложении.

View File

@@ -0,0 +1,8 @@
---
title: Сторибук
description: Просмотр библиотеки компонентов UI Twenty
---
Посмотрите нашу полную библиотеку компонентов и документацию в Сторибуке.
[Открыть Сторибук →](https://storybook.twenty.com)

View File

@@ -0,0 +1,291 @@
---
title: Руководство по стилю
icon: paintbrush
---
Этот документ включает правила, которые нужно соблюдать при написании кода.
Цель заключается в обеспечении однородности кода, который будет легко читать и поддерживать.
Для этого лучше быть немного более многословными, чем слишком краткими.
Всегда помните, что код читают чаще, чем пишут, особенно в проекте с открытым исходным кодом, в который может внести вклад кто угодно.
Существует много правил, которые здесь не описаны, но автоматически проверяются линтерами.
## React
### Используйте функциональные компоненты.
Всегда используйте функциональные компоненты TSX.
Не используйте стандартный `import` с `const`, так как это сложнее для чтения и импорта с автозаполнением кода.
```tsx
// ❌ Bad, harder to read, harder to import with code completion
const MyComponent = () => {
return <div>Hello World</div>;
};
export default MyComponent;
// ✅ Good, easy to read, easy to import with code completion
export function MyComponent() {
return <div>Hello World</div>;
};
```
### Свойства
Создайте тип пропсов и назовите его `(ComponentName)Props`, если нет необходимости экспортировать его.
Используйте деструктуризацию пропсов.
```tsx
// ❌ Bad, no type
export const MyComponent = (props) => <div>Hello {props.name}</div>;
// ✅ Good, type
type MyComponentProps = {
name: string;
};
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
```
#### Воздержитесь от использования `React.FC` или `React.FunctionComponent` для определения типов пропсов.
```tsx
/* ❌ - Bad, defines the component type annotations with `FC`
* - With `React.FC`, the component implicitly accepts a `children` prop
* even if it's not defined in the prop type. This might not always be
* desirable, especially if the component doesn't intend to render
* children.
*/
const EmailField: React.FC<{
value: string;
}> = ({ value }) => <TextInput value={value} disabled fullWidth />;
```
```tsx
/* ✅ - Good, a separate type (OwnProps) is explicitly defined for the
* component's props
* - This method doesn't automatically include the children prop. If
* you want to include it, you have to specify it in OwnProps.
*/
type EmailFieldProps = {
value: string;
};
const EmailField = ({ value }: EmailFieldProps) => (
<TextInput value={value} disabled fullWidth />
);
```
#### Нет разворачивания пропсов одиночной переменной в JSX-элементах.
Избегайте спреда пропсов из одной переменной в JSX-элементах, например `{...props}`. Подобная практика часто приводит к менее читаемому и сложному в поддержке коду, так как непонятно, какие пропсы принимает компонент.
```tsx
/* ❌ - Bad, spreads a single variable prop into the underlying component
*/
const MyComponent = (props: OwnProps) => {
return <OtherComponent {...props} />;
}
```
```tsx
/* ✅ - Good, Explicitly lists all props
* - Enhances readability and maintainability
*/
const MyComponent = ({ prop1, prop2, prop3 }: MyComponentProps) => {
return <OtherComponent {...{ prop1, prop2, prop3 }} />;
};
```
Обоснование:
* С первого взгляда становится яснее, какие пропсы передаются в коде, что делает его легче понятным и поддерживаемым.
* Это помогает предотвратить жесткое связывание между компонентами через их пропсы.
* Инструменты для анализа кода облегчают обнаружение опечаток или неиспользуемых пропсов при явном перечислении пропсов.
## JavaScript
### Используйте оператор нулевого слияния `??`
```tsx
// ❌ Bad, can return 'default' even if value is 0 or ''
const value = process.env.MY_VALUE || 'default';
// ✅ Good, will return 'default' only if value is null or undefined
const value = process.env.MY_VALUE ?? 'default';
```
### Используйте опциональную цепочку `?.`
```tsx
// ❌ Bad
onClick && onClick();
// ✅ Good
onClick?.();
```
## TypeScript
### Используйте `type` вместо `interface`
Всегда используйте `type` вместо `interface`, так как они почти всегда пересекаются, а `type` более гибок.
```tsx
// ❌ Bad
interface MyInterface {
name: string;
}
// ✅ Good
type MyType = {
name: string;
};
```
### Используйте строковые литералы вместо перечислений.
[Строковые литералы](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types) - это основной способ обработки значений, напоминающих перечисление, в TypeScript. Их проще расширять с помощью Pick и Omit, и они обеспечивают более удобную работу разработчика, особенно благодаря автодополнению кода.
Вы можете увидеть, почему TypeScript рекомендует избегать перечислений [здесь](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#enums).
```tsx
// ❌ Bad, utilizes an enum
enum Color {
Red = "red",
Green = "green",
Blue = "blue",
}
let color = Color.Red;
```
```tsx
// ✅ Good, utilizes a string literal
let color: "red" | "green" | "blue" = "red";
```
#### GraphQL и внутренние библиотеки
Вы должны использовать перечисления, которые генерирует кодогенератор GraphQL.
Также лучше использовать перечисление при использовании внутренней библиотеки, чтобы она не требовала явного указания типа строкового литерала, не связанного с внутренним API.
Пример:
```TSX
const {
setHotkeyScopeAndMemorizePreviousScope,
goBackToPreviousHotkeyScope,
} = usePreviousHotkeyScope();
setHotkeyScopeAndMemorizePreviousScope(
RelationPickerHotkeyScope.RelationPicker,
);
```
## Стилизация
### Использование StyledComponents
Стилизуйте компоненты с помощью [Linaria styled](https://github.com/callstack/linaria).
```tsx
// ❌ Bad
<div className="my-class">Hello World</div>
```
```tsx
// ✅ Good
const StyledTitle = styled.div`
color: red;
`;
```
Добавляйте префикс "Styled" к стилизованным компонентам, чтобы отличать их от "реальных" компонентов.
```tsx
// ❌ Bad
const Title = styled.div`
color: red;
`;
```
```tsx
// ✅ Good
const StyledTitle = styled.div`
color: red;
`;
```
### Темизация
Использование темы для большинства стилевых компонентов является предпочтительным подходом.
#### Единицы измерения
Избегайте использования значений `px` или `rem` напрямую в стилизованных компонентах. Необходимые значения обычно уже определены в теме, поэтому рекомендуется использовать тему для этих целей.
#### Цвета
Избегайте введения новых цветов; используйте существующую палитру из темы. Если палитра не соответствует, оставьте комментарий, чтобы команда могла это исправить.
```tsx
// ❌ Bad, directly specifies style values without utilizing the theme
const StyledButton = styled.button`
color: #333333;
font-size: 1rem;
font-weight: 400;
margin-left: 4px;
border-radius: 50px;
`;
```
```tsx
// ✅ Good, utilizes the theme
const StyledButton = styled.button`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
font-weight: ${({ theme }) => theme.font.weight.regular};
margin-left: ${({ theme }) => theme.spacing(1)};
border-radius: ${({ theme }) => theme.border.rounded};
`;
```
## Запрещение импорта типов
Избегайте импорта типов. Чтобы обеспечить соблюдение этого стандарта, правило Oxlint проверяет и сообщает о любых импортах с ключевым словом type. Это помогает сохранить согласованность и читаемость кода TypeScript.
```tsx
// ❌ Bad
import { type Meta, type StoryObj } from '@storybook/react';
// ❌ Bad
import type { Meta, StoryObj } from '@storybook/react';
// ✅ Good
import { Meta, StoryObj } from '@storybook/react';
```
### Почему избегать импорта типов
* **Согласованность**: Избегая импорта типов и используя единый подход для импорта как типов, так и значений, кодовая база остается согласованной в стиле импорта модулей.
* **Читаемость**: Избегающий импорт типов улучшает читаемость кода, делая ясным, когда вы импортируете значения или типы. Это снижает двусмысленность и облегчает понимание назначения импортируемых символов.
* **Поддерживаемость**: Это повышает поддерживаемость кодовой базы, поскольку разработчики могут идентифицировать и находить импорты только типов при просмотре или изменении кода.
### Правило Oxlint
Правило Oxlint `typescript/consistent-type-imports` обеспечивает соблюдение стандарта импортов без ключевого слова type. Это правило создаст ошибки или предупреждения для всех нарушений импорта типов.
Обратите внимание, что это правило касается редких крайних случаев, когда случаются непреднамеренные импорты типов. Сам TypeScript не рекомендует эту практику, как указано в [примечаниях к выпуску TypeScript 3.8](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html). В большинстве случаев вам не нужно использовать импорты только типов.
Чтобы гарантировать соответствие вашего кода этому правилу, убедитесь, что вы запускаете Oxlint как часть вашего рабочего процесса разработки.

View File

@@ -0,0 +1,61 @@
---
title: Работа с Figma
info: Узнайте, как можно сотрудничать с Figma компании Twenty
---
Figma - это совместный инструмент для проектирования пользовательских интерфейсов, который помогает преодолеть барьер в коммуникации между дизайнерами и разработчиками.
Это руководство объясняет, как вы можете работать с Figma.
## Доступ
1. **Получите доступ к общей ссылке:** Вы можете открыть файл проекта Figma [здесь](https://www.figma.com/file/xt8O9mFeLl46C5InWwoMrN/Twenty).
2. **Войдите в систему:** Если вы ещё не вошли, Figma предложит вам это сделать.
Ключевые функции доступны только для авторизованных пользователей, такие как режим разработчика и возможность выбора конкретного фрейма.
<Warning>
Вы не сможете эффективно сотрудничать без учетной записи.
</Warning>
## Структура Figma
На левой боковой панели вы можете получить доступ к разным страницам Figma компании Twenty. Вот как они организованы:
* **Страница компонентов:** Это первая страница. Дизайнер использует её для создания и организации элементов дизайна, которые могут быть переиспользованы в течение всего файла дизайна. Например, кнопки, иконки, символы или любые другие переиспользуемые компоненты. Она служит для поддержания консистентности дизайна.
* **Главная страница:** Вторая страница — это главная страница, которая показывает полный пользовательский интерфейс проекта. Вы можете нажать ***Play***, чтобы использовать полный прототип приложения.
* **Страницы с функциями:** Другие страницы обычно посвящены функциям в процессе разработки. Они содержат дизайн определённых функций или модулей приложения или веб-сайта. Они, как правило, все еще в процессе разработки.
## Полезные советы
С доступом только для чтения, вы не можете редактировать дизайн, но вы можете получить доступ ко всем функциям, которые будут полезны для преобразования дизайнов в код.
### Используйте режим разработчика
Dev Mode Figma повышает производительность разработчиков, обеспечивая легкую навигацию по дизайну, эффективное управление ресурсами, инструменты связи, интеграции с инструментами для разработки, быстрые фрагменты кода и информацию о ключевых слоях, устраняя разрыв между дизайном и разработкой. Подробнее о режиме Dev можно узнать [здесь](https://www.figma.com/dev-mode/).
Переключитесь в режим "Разработчик" в правой части панели инструментов, чтобы посмотреть спецификации дизайна, скопировать CSS и получить доступ к ресурсам.
### Использование прототипа
Кликните на любой элемент на холсте и нажмите кнопку “Play” в верхней правой части интерфейса, чтобы открыть просмотр прототипа. Режим прототипа позволяет взаимодействовать с дизайном, как если бы это был финальный продукт. Он демонстрирует переходы между экранами и то, как элементы интерфейса, такие как кнопки, ссылки или меню, ведут себя при взаимодействии.
1. **Понимание переходов и анимаций:** В режиме Прототипа вы можете увидеть любые переходы или анимации, добавленные дизайнером между экранами или элементами интерфейса, предоставляя четкие визуальные инструкции разработчикам о предположительном поведении и стиле.
2. **Уточнение реализации:** Прототип также может помочь уменьшить неясности. Разработчики могут взаимодействовать с ним, чтобы лучше понять функциональность или внешний вид определенных элементов.
Для получения более подробной информации и руководств о платформе Figma, вы можете посетить официальную [Документацию Figma](https://help.figma.com/hc/en-us).
### Измерение расстояний
Выберите элемент, удерживая клавишу `Option` (Mac) или `Alt` (Windows), затем наведите курсор на другой элемент, чтобы увидеть расстояние между ними.
### Расширение Figma для VSCode (Рекомендуется)
[Figma для VS Code](https://marketplace.visualstudio.com/items?itemName=figma.figma-vscode-extension)
позволяет вам просматривать и инспектировать файлы дизайна, сотрудничать с дизайнерами, отслеживать изменения и ускорять внедрение - все это, не покидая текстового редактора.
Это часть наших рекомендуемых расширений.
## Сотрудничество
1. **Использование комментариев:** Вы можете воспользоваться функцией комментариев, кликнув по значку пузыря в левой части панели инструментов.
2. **Чат курсора:** Приятная особенность Figma - это чат курсора. Просто нажмите `;` на Mac и `/` на Windows, чтобы отправить сообщение, если вы видите, что кто-то еще использует Figma одновременно с вами.

View File

@@ -0,0 +1,318 @@
---
title: Локальная настройка
icon: laptop-code
description: Руководство для участников (или любопытных разработчиков), которые хотят запускать Twenty локально.
---
## Требования
<Tabs>
<Tab title="Linux и macOS">
Прежде чем установить и использовать Twenty, убедитесь, что у вас установлено следующее:
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [Node v24.5.0](https://nodejs.org/en/download)
* [yarn v4](https://yarnpkg.com/getting-started/install)
* [nvm](https://github.com/nvm-sh/nvm/blob/master/README.md)
<Warning>
`npm` не будет работать, используйте `yarn`. Yarn теперь поставляется в комплекте с Node.js, так что устанавливать его отдельно не нужно.
Нужно лишь выполнить `corepack enable`, чтобы активировать Yarn, если вы еще этого не сделали.
</Warning>
</Tab>
<Tab title="Windows (WSL)">
1. Установите WSL
Откройте PowerShell от имени администратора и выполните:
```powershell
wsl --install
```
Теперь должно появиться приглашение на перезагрузку компьютера. Если нет, перезагрузите его вручную.
После перезагрузки откроется окно PowerShell и начнётся установка Ubuntu. Это может занять некоторое время.
Появится запрос на создание имени пользователя и пароля для вашей установки Ubuntu.
2. Установите и настройте git
```bash
sudo apt-get install git
git config --global user.name "Your Name"
git config --global user.email "youremail@domain.com"
```
3. Установите nvm, node.js и yarn
<Warning>
Используйте `nvm`, чтобы установить правильную версию `node`. `.nvmrc` гарантирует, что все участники используют одну и ту же версию.
</Warning>
```bash
sudo apt-get install curl
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
```
Закройте и откройте снова терминал, чтобы использовать nvm. Затем выполните следующие команды.
```bash
nvm install # installs recommended node version
nvm use # use recommended node version
corepack enable
```
</Tab>
</Tabs>
---
## Шаг 1: Клонирование с помощью Git
Выполните следующую команду в терминале.
<Tabs>
<Tab title="SSH (Рекомендовано)">
Если вы еще не настроили SSH ключи, вы можете узнать, как это сделать [здесь](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/about-ssh).
```bash
git clone git@github.com:twentyhq/twenty.git
```
</Tab>
<Tab title="HTTPS">
```bash
git clone https://github.com/twentyhq/twenty.git
```
</Tab>
</Tabs>
## Шаг 2: Перейдите в корень
```bash
cd twenty
```
Все команды в следующих шагах следует выполнять из корня проекта.
## Шаг 3: Настройка базы данных PostgreSQL
<Tabs>
<Tab title="Linux">
**Опция 1 (предпочтительно):** Чтобы настроить базу данных локально:
Используйте следующую ссылку для установки PostgreSQL на компьютер с Linux: [Установка PostgreSQL](https://www.postgresql.org/download/linux/)
```bash
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
Примечание: Возможно, вам потребуется добавить `sudo -u postgres` к команде перед `psql`, чтобы избежать ошибок с правами.
**Опция 2:** Если у вас установлен docker:
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
<Tab title="Mac OS">
**Опция 1 (предпочтительно):** Чтобы настроить вашу базу данных локально с помощью `brew`:
```bash
brew install postgresql@16
export PATH="/opt/homebrew/opt/postgresql@16/bin:$PATH"
brew services start postgresql@16
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
Вы можете проверить, запущен ли сервер PostgreSQL, выполнив:
```bash
brew services list
```
Установщик может не создать пользователя `postgres` по умолчанию при установке
через Homebrew на macOS. Вместо этого он создает роль PostgreSQL, которая совпадает с вашим именем пользователя в MacOS
например, "john".
Чтобы проверить и создать пользователя `postgres`, при необходимости выполните следующие шаги:
```bash
# Connect to PostgreSQL
psql postgres
or
psql -U $(whoami) -d postgres
```
Оказавшись в приглашении psql (postgres=#), выполните:
```bash
# List existing PostgreSQL roles
\du
```
Вы увидите вывод, похожий на следующий:
```bash
Role name | Attributes | Member of
-----------+-------------+-----------
john | Superuser | {}
```
Если вы не видите роль `postgres` в списке, перейдите к следующему шагу.
Создайте роль `postgres` вручную:
```bash
CREATE ROLE postgres WITH SUPERUSER LOGIN;
```
Это создает роль суперпользователя с именем `postgres` с доступом для входа.
```bash
Role name | Attributes | Member of
-----------+-------------+-----------
postgres | Superuser | {}
john | Superuser | {}
```
**Опция 2:** Если у вас установлен docker:
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
<Tab title="Windows (WSL)">
Все последующие шаги следует выполнять в терминале WSL (внутри вашей виртуальной машины)
**Опция 1:** Чтобы настроить PostgreSQL локально:
Используйте следующую ссылку для установки PostgreSQL на виртуальную машину с Linux: [Установка PostgreSQL](https://www.postgresql.org/download/linux/)
```bash
psql postgres -c "CREATE DATABASE \"default\";" -c "CREATE DATABASE test;"
```
Примечание: Возможно, вам потребуется добавить `sudo -u postgres` к команде перед `psql`, чтобы избежать ошибок с правами.
**Опция 2:** Если у вас установлен docker:
Запуск Docker на WSL добавляет дополнительный уровень сложности.
Используйте эту опцию только если вы комфортно себя чувствуете с дополнительными шагами, включая включение [Docker Desktop WSL2](https://docs.docker.com/desktop/wsl).
```bash
make -C packages/twenty-docker postgres-on-docker
```
</Tab>
</Tabs>
Теперь вы можете получить доступ к базе данных по адресу `localhost:5432`.
Если вы использовали вариант с Docker выше, учетные данные по умолчанию: пользователь `postgres` и пароль `postgres`. Для нативных установок PostgreSQL используйте учетные данные и роли, настроенные на вашей машине.
## Шаг 4: Настройка базы данных Redis (кэш)
Twenty требует кэша Redis для обеспечения наилучшей производительности.
<Tabs>
<Tab title="Linux">
**Опция 1:** Чтобы настроить Redis локально:
Используйте следующую ссылку для установки Redis на вашу Linux машину: [Установка Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/)
**Опция 2:** Если у вас установлен docker:
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
<Tab title="Mac OS">
**Опция 1 (предпочтительно):** Чтобы настроить Redis локально с помощью `brew`:
```bash
brew install redis
```
Запустите сервер Redis:
```bash
brew services start redis
```
**Опция 2:** Если у вас установлен docker:
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
<Tab title="Windows (WSL)">
**Опция 1:** Чтобы настроить Redis локально:
Используйте следующую ссылку для установки Redis на вашу Linux виртуальную машину: [Установка Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/)
**Опция 2:** Если у вас установлен docker:
```bash
make -C packages/twenty-docker redis-on-docker
```
</Tab>
</Tabs>
Если вам нужен графический интерфейс клиента, мы рекомендуем [Redis Insight](https://redis.io/insight/) (доступна бесплатная версия).
## Шаг 5: Настройка переменных окружения
Используйте переменные окружения или файлы `.env` для настройки вашего проекта. Подробнее [здесь](/l/ru/developers/self-host/capabilities/setup).
Скопируйте `.env.example` файлы в `/front` и `/server`:
```bash
cp ./packages/twenty-front/.env.example ./packages/twenty-front/.env
cp ./packages/twenty-server/.env.example ./packages/twenty-server/.env
```
<Info>
**Режим нескольких рабочих пространств:** По умолчанию Twenty работает в режиме одного рабочего пространства, где можно создать только одно рабочее пространство. Чтобы включить поддержку нескольких рабочих пространств (полезно для тестирования функций, основанных на поддоменах), установите `IS_MULTIWORKSPACE_ENABLED=true` в файле `.env` вашего сервера. См. [Режим нескольких рабочих пространств](/l/ru/developers/self-host/capabilities/setup#multi-workspace-mode) для подробностей.
</Info>
## Шаг 6: Установка зависимостей
Чтобы собрать сервер Twenty и добавить данные в вашу базу данных, выполните следующую команду:
```bash
yarn
```
Обратите внимание, что `npm` или `pnpm` не будут работать
## Шаг 7: Запуск проекта
<Tabs>
<Tab title="Linux">
В зависимости от вашего дистрибутива Linux, сервер Redis может быть запущен автоматически.
Если нет, проверьте [Руководство по установке Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) для вашего дистрибутива.
</Tab>
<Tab title="Mac OS">
Redis должен уже работать. Если нет, выполните:
```bash
brew services start redis
```
</Tab>
<Tab title="Windows (WSL)">
В зависимости от вашей дистрибуции Linux, сервер Redis может быть запущен автоматически.
Если нет, проверьте [руководство по установке Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) для вашего дистрибутива.
</Tab>
</Tabs>
Настройте вашу базу данных с помощью следующей команды:
```bash
npx nx database:reset twenty-server
```
Запустите сервер, рабочую программу и сервисы фронтенда:
```bash
npx nx start twenty-server
npx nx worker twenty-server
npx nx start twenty-front
```
В качестве альтернативы, вы можете запустить все сервисы сразу:
```bash
npx nx start
```
## Шаг 8: Использовать Twenty
**Frontend**
Фронтенд Twenty будет работать на [http://localhost:3001](http://localhost:3001).
Вы можете войти, используя учетную запись демо по умолчанию: `tim@apple.dev` (пароль: `tim@apple.dev`)
**Backend**
* Сервер Twenty будет работать на [http://localhost:3000](http://localhost:3000).
* К GraphQL API можно получить доступ по адресу [http://localhost:3000/graphql](http://localhost:3000/graphql).
* К REST API можно обратиться по адресу [http://localhost:3000/rest](http://localhost:3000/rest).
## Устранение неполадок
Если у вас возникли проблемы, проверьте [Устранение неполадок](/l/ru/developers/self-host/capabilities/troubleshooting) для получения решений.

View File

@@ -0,0 +1,77 @@
---
title: Команды
icon: terminal
description: Полезные команды для разработки Twenty.
---
Команды можно запускать из корня репозитория с помощью `npx nx`. Используйте `npx nx run {project}:{command}` для явного указания цели.
## Запуск приложения
```bash
npx nx start twenty-front # Frontend dev server (http://localhost:3001)
npx nx start twenty-server # Backend server (http://localhost:3000)
npx nx run twenty-server:worker # Background worker
```
## База данных
```bash
npx nx database:reset twenty-server # Reset and seed database
npx nx run twenty-server:database:migrate:prod # Run migrations
npx nx run twenty-server:database:migrate:generate --name <name> --type <fast|slow> # Generate a migration
```
## Линтинг
```bash
npx nx lint:diff-with-main twenty-front # Lint changed files (fastest)
npx nx lint:diff-with-main twenty-server
npx nx lint twenty-front --configuration=fix # Auto-fix
```
## Проверка типов
```bash
npx nx typecheck twenty-front
npx nx typecheck twenty-server
```
## Тестирование
```bash
# Frontend
npx nx test twenty-front # Jest unit tests
npx nx storybook:build twenty-front # Build Storybook
npx nx storybook:test twenty-front # Storybook tests
# Backend
npx nx run twenty-server:test:unit # Unit tests
npx nx run twenty-server:test:integration # Integration tests
npx nx run twenty-server:test:integration:with-db-reset # Integration with DB reset
# Single file (fastest)
npx jest path/to/test.test.ts --config=packages/{project}/jest.config.mjs
```
## GraphQL
```bash
npx nx run twenty-front:graphql:generate # Regenerate types
npx nx run twenty-front:graphql:generate --configuration=metadata # Metadata schema
```
## Переводы
```bash
npx nx run twenty-front:lingui:extract # Extract strings
npx nx run twenty-front:lingui:compile # Compile translations
```
## Сборка
```bash
npx nx build twenty-shared # Must be built first
npx nx build twenty-front
npx nx build twenty-server
```

View File

@@ -0,0 +1,31 @@
---
title: Внести вклад
description: Внесите вклад в разработку Twenty с открытым исходным кодом.
---
<Frame>
<img src="/images/user-guide/github/github-header.png" alt="ИИ" />
</Frame>
## Обзор
Twenty — проект с открытым исходным кодом, который приветствует вклад сообщества. Независимо от того, исправляете ли вы ошибки, добавляете функции или улучшаете документацию, ваш вклад помогает сделать Twenty лучше для всех.
## Способы внести вклад
* **Сообщайте об ошибках**: помогайте выявлять и документировать проблемы
* **Предлагайте функции**: предлагайте и реализуйте новую функциональность
* **Улучшайте документацию**: сделайте нашу документацию понятнее и полезнее
* **Фронтенд-разработка**: работайте над интерфейсом на базе React
* **Бэкенд-разработка**: вносите вклад в сервер NestJS
## Начало работы
<CardGroup cols={2}>
<Card title="Отчеты об ошибках и запросы" icon="bug" href="/l/ru/developers/contribute/capabilities/bug-and-requests">
Сообщайте о проблемах или запрашивайте новые функции
</Card>
<Card title="Разработка интерфейса" icon="browser" href="/l/ru/developers/contribute/capabilities/frontend-development/folder-architecture-front">
Внесите вклад в пользовательский интерфейс
</Card>
</CardGroup>

View File

@@ -0,0 +1,176 @@
---
title: Руководство по стилю
icon: paintbrush
description: Соглашения по коду и лучшие практики для внесения вклада в Twenty.
---
## React
### Только функциональные компоненты
Всегда используйте функциональные компоненты TSX с именованными экспортами.
```tsx
// ❌ Bad
const MyComponent = () => {
return <div>Hello World</div>;
};
export default MyComponent;
// ✅ Good
export function MyComponent() {
return <div>Hello World</div>;
};
```
### Свойства
Создайте тип с именем `{ComponentName}Props`. Используйте деструктуризацию. Не используйте `React.FC`.
```tsx
type MyComponentProps = {
name: string;
};
export const MyComponent = ({ name }: MyComponentProps) => <div>Hello {name}</div>;
```
### Не используйте спред одного объекта пропсов
```tsx
// ❌ Bad
const MyComponent = (props: MyComponentProps) => <Other {...props} />;
// ✅ Good
const MyComponent = ({ prop1, prop2 }: MyComponentProps) => <Other {...{ prop1, prop2 }} />;
```
## Управление состоянием
### Атомы Jotai для глобального состояния
```tsx
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
export const myAtomState = createAtomState<string>({
key: 'myAtomState',
defaultValue: 'default value',
});
```
* Предпочитайте атомы вместо проброса пропсов
* Не используйте `useRef` для состояния — используйте `useState` или атомы
* Используйте семейства атомов и селекторы для списков
### Избегайте лишних повторных рендеров
* Выносите `useEffect` и загрузку данных в соседние сайдкар-компоненты
* Предпочитайте обработчики событий (`handleClick`, `handleChange`) вместо `useEffect`
* Не используйте `React.memo()` — вместо этого исправьте первопричину
* Ограничьте использование `useCallback` или `useMemo`
```tsx
// ❌ Bad — useEffect in the same component causes re-renders
export const Page = () => {
const [data, setData] = useAtomState(dataState);
const [dep] = useAtomState(depState);
useEffect(() => { setData(dep); }, [dep]);
return <div>{data}</div>;
};
// ✅ Good — extract into sibling
export const PageData = () => {
const [data, setData] = useAtomState(dataState);
const [dep] = useAtomState(depState);
useEffect(() => { setData(dep); }, [dep]);
return <></>;
};
export const Page = () => {
const [data] = useAtomState(dataState);
return <div>{data}</div>;
};
```
## TypeScript
* **`type` вместо `interface`** — более гибкий, проще компоновать
* **Строковые литералы вместо перечислений** — за исключением перечислений из GraphQL codegen и внутренних API библиотеки
* **Без `any`** — строгий режим TypeScript обязателен
* **Без type-импортов** — используйте обычные импорты (принудительно через Oxlint `typescript/consistent-type-imports`)
* **Используйте [Zod](https://github.com/colinhacks/zod)** для проверки во время выполнения нетипизированных объектов
## JavaScript
```tsx
// Use nullish-coalescing (??) instead of ||
const value = process.env.MY_VALUE ?? 'default';
// Use optional chaining
onClick?.();
```
## Именование
* **Переменные**: camelCase, информативные (`email`, а не `value`, `fieldMetadata`, а не `fm`)
* **Константы**: SCREAMING_SNAKE_CASE
* **Типы/Классы**: PascalCase
* **Файлы/директории**: kebab-case (`.component.tsx`, `.service.ts`, `.entity.ts`)
* **Обработчики событий**: `handleClick` (не `onClick` для функции-обработчика)
* **Пропсы компонента**: используйте префикс имени компонента (`ButtonProps`)
* **Стилизованные компоненты**: префикс `Styled` (`StyledTitle`)
## Стилизация
Используйте стилизованные компоненты [Linaria](https://github.com/callstack/linaria). Используйте значения темы — избегайте жестко заданных `px`, `rem` или цветов.
```tsx
// ❌ Bad
const StyledButton = styled.button`
color: #333333;
font-size: 1rem;
margin-left: 4px;
`;
// ✅ Good
const StyledButton = styled.button`
color: ${({ theme }) => theme.font.color.primary};
font-size: ${({ theme }) => theme.font.size.md};
margin-left: ${({ theme }) => theme.spacing(1)};
`;
```
## Импорт
Используйте алиасы вместо относительных путей:
```tsx
// ❌ Bad
import { Foo } from '../../../../../testing/decorators/Foo';
// ✅ Good
import { Foo } from '~/testing/decorators/Foo';
import { Bar } from '@/modules/bar/components/Bar';
```
## Структура папок
```
front
└── modules/ # Feature modules
│ └── module1/
│ ├── components/
│ ├── constants/
│ ├── contexts/
│ ├── graphql/ (fragments, queries, mutations)
│ ├── hooks/
│ ├── states/ (atoms, selectors)
│ ├── types/
│ └── utils/
└── pages/ # Route-level components
└── ui/ # Reusable UI components (display, input, feedback, ...)
```
* Модули могут импортировать из других модулей, но `ui/` должен оставаться без зависимостей
* Используйте подпапки `internal/` для приватного кода модуля
* Компоненты — до 300 строк, сервисы — до 500 строк