Compare commits
240
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba5d1c2ed1 | ||
|
|
4929b9e954 | ||
|
|
45b7e3d6aa | ||
|
|
50f068d640 | ||
|
|
4ae8381ffc | ||
|
|
856859f9f3 | ||
|
|
98d67e0f6b | ||
|
|
1fa55bfd02 | ||
|
|
001c2097a3 | ||
|
|
9107f5bbc7 | ||
|
|
435e21d23f | ||
|
|
448241540d | ||
|
|
e7f958c9cf | ||
|
|
98e73791a4 | ||
|
|
f3857c41b8 | ||
|
|
2c02f2c499 | ||
|
|
121788c42f | ||
|
|
ecb7270a7b | ||
|
|
e365b546d9 | ||
|
|
f080b952eb | ||
|
|
b84a8588be | ||
|
|
e6a415b438 | ||
|
|
d81f006979 | ||
|
|
eaf9fe27b2 | ||
|
|
b339c93699 | ||
|
|
887371054a | ||
|
|
b56f85f36a | ||
|
|
1ed13182d0 | ||
|
|
20d997bc5a | ||
|
|
0f2af6a6cb | ||
|
|
c6ec764b23 | ||
|
|
959cbfb725 | ||
|
|
458a61ed30 | ||
|
|
9a3852bf04 | ||
|
|
c369baf63a | ||
|
|
9e8024a07a | ||
|
|
df196b4a03 | ||
|
|
af0af0a237 | ||
|
|
0d4fe4575b | ||
|
|
ccddd105d8 | ||
|
|
e21a7e11b2 | ||
|
|
1ac87d106e | ||
|
|
9a0295fd3d | ||
|
|
a0c0e7de25 | ||
|
|
7cbbd082e3 | ||
|
|
7944ca29fa | ||
|
|
b51eb6471f | ||
|
|
22bc3004b9 | ||
|
|
129d1ede86 | ||
|
|
a2e3af5625 | ||
|
|
41f09c5a4c | ||
|
|
a900b4a4b4 | ||
|
|
020cf7f3e0 | ||
|
|
0bccdedb4f | ||
|
|
088be4c47b | ||
|
|
ad88108b28 | ||
|
|
58e8b466f3 | ||
|
|
ba59ab8094 | ||
|
|
cd1a2712d5 | ||
|
|
932adf8e4b | ||
|
|
26fabdc3d0 | ||
|
|
82617727a2 | ||
|
|
87922253f3 | ||
|
|
7da8450075 | ||
|
|
d444648cc0 | ||
|
|
ec57d3fe0f | ||
|
|
00209f7e2c | ||
|
|
b107020ad3 | ||
|
|
9f3f2e21d8 | ||
|
|
21472da38f | ||
|
|
bf1e0d0811 | ||
|
|
917a36821a | ||
|
|
66da296799 | ||
|
|
3bc887e12e | ||
|
|
d060c843d1 | ||
|
|
175df59c21 | ||
|
|
6ad581d178 | ||
|
|
712b1553bd | ||
|
|
cc1145c701 | ||
|
|
726b1373d9 | ||
|
|
783e57f5d8 | ||
|
|
163254baef | ||
|
|
aaa6511007 | ||
|
|
2a670520b9 | ||
|
|
10bd005021 | ||
|
|
0e25aeb5be | ||
|
|
e051cce24f | ||
|
|
bd03073b6d | ||
|
|
b3d8f8813f | ||
|
|
37d9828458 | ||
|
|
e16b6d6485 | ||
|
|
9626c7ce02 | ||
|
|
754de411fe | ||
|
|
530f74e28c | ||
|
|
5a46cf7bd3 | ||
|
|
7b10202c69 | ||
|
|
9e54a8082c | ||
|
|
1a13258302 | ||
|
|
c0ea049ad7 | ||
|
|
b33f86fbf5 | ||
|
|
88424611ec | ||
|
|
082400f751 | ||
|
|
a14b0ab6ca | ||
|
|
6e29f66ce8 | ||
|
|
9bcc2fc5d9 | ||
|
|
01e203a70b | ||
|
|
db9636bd9e | ||
|
|
36c2b0e23b | ||
|
|
66deb8be63 | ||
|
|
01d2269bd0 | ||
|
|
04502e5abb | ||
|
|
549c7a613b | ||
|
|
743b733e70 | ||
|
|
612f7c37a5 | ||
|
|
42108e0611 | ||
|
|
9a2dc45eb7 | ||
|
|
7de565f70c | ||
|
|
67074a7581 | ||
|
|
330737aa2e | ||
|
|
4cb64c6aa5 | ||
|
|
6b3ef404b0 | ||
|
|
6aaafb76b6 | ||
|
|
7ec4508d5f | ||
|
|
a05a9c8f79 | ||
|
|
ce1ffa8550 | ||
|
|
2cc3c75c7e | ||
|
|
9733ff1b8e | ||
|
|
e4075caa65 | ||
|
|
c3781e87cc | ||
|
|
e9b5cb830c | ||
|
|
e40c758aa6 | ||
|
|
53c314d0fa | ||
|
|
618df704e6 | ||
|
|
058489b5cc | ||
|
|
3bd431e95d | ||
|
|
f4a61f26c0 | ||
|
|
8e6b267ff3 | ||
|
|
88146c2170 | ||
|
|
3706da9bcb | ||
|
|
9bac8f15d4 | ||
|
|
7332379d26 | ||
|
|
2455c859b4 | ||
|
|
e3753bf822 | ||
|
|
f3faa11dd2 | ||
|
|
477fbc0865 | ||
|
|
08a3d983cb | ||
|
|
ee15e034b5 | ||
|
|
b7274da8fa | ||
|
|
4a485aecb0 | ||
|
|
5ae1d94f23 | ||
|
|
015ccbf0a7 | ||
|
|
3d362e6e01 | ||
|
|
d7f025157b | ||
|
|
98482f3a01 | ||
|
|
171efe2a19 | ||
|
|
7512b9f9bb | ||
|
|
c0cc0689d6 | ||
|
|
0891886aa0 | ||
|
|
f768bbe512 | ||
|
|
610c0ebc9d | ||
|
|
e4aad7751f | ||
|
|
7c5a13852b | ||
|
|
163c1175cb | ||
|
|
963f2de864 | ||
|
|
e3fcff00b0 | ||
|
|
b4e924b671 | ||
|
|
63a3f93a78 | ||
|
|
8938dd637f | ||
|
|
20977428a1 | ||
|
|
347298902d | ||
|
|
5750c9be0c | ||
|
|
08feb6f651 | ||
|
|
c9c3b2b691 | ||
|
|
09e1684300 | ||
|
|
e80e9a6a25 | ||
|
|
c2fe18af53 | ||
|
|
2b702b2b45 | ||
|
|
dc167b2d3d | ||
|
|
1e6c5b57b2 | ||
|
|
3516be2cf4 | ||
|
|
9477bb3677 | ||
|
|
aac032e517 | ||
|
|
5544b5dcfe | ||
|
|
8244610bdc | ||
|
|
e3db73ef46 | ||
|
|
7523143f12 | ||
|
|
cfc4e0e343 | ||
|
|
c3d565f266 | ||
|
|
2b7b05de2e | ||
|
|
4b3c58e013 | ||
|
|
c775d65952 | ||
|
|
b80146c890 | ||
|
|
5604cdbb4b | ||
|
|
da064d5e88 | ||
|
|
f694bb99b3 | ||
|
|
8190cd5fbf | ||
|
|
b901bdec40 | ||
|
|
b1d3ec665a | ||
|
|
ac3ac5cd4d | ||
|
|
6f251a6f8e | ||
|
|
0876197c8d | ||
|
|
4f903fa0ba | ||
|
|
2072d5f720 | ||
|
|
d54b713264 | ||
|
|
84afbb4d2c | ||
|
|
ebfaff0a53 | ||
|
|
c3d8404112 | ||
|
|
a95a286c59 | ||
|
|
c173f01601 | ||
|
|
cda70a70ca | ||
|
|
a48b69d1e5 | ||
|
|
4817c86bf0 | ||
|
|
0befb021d0 | ||
|
|
e7b3f65c0c | ||
|
|
5184491c59 | ||
|
|
f17cc4d190 | ||
|
|
463ce43442 | ||
|
|
5cc0c03262 | ||
|
|
e2b9bc935f | ||
|
|
b7c1d47273 | ||
|
|
befbcef824 | ||
|
|
83b800a077 | ||
|
|
d08c098065 | ||
|
|
d88fa0cb2b | ||
|
|
216a7331f8 | ||
|
|
975c64bf3d | ||
|
|
b34f7bcb33 | ||
|
|
14e8c869be | ||
|
|
21c51ec251 | ||
|
|
5c3c2e08a6 | ||
|
|
fdb5d2fbcd | ||
|
|
5c2c588885 | ||
|
|
90a30263ac | ||
|
|
13c2234856 | ||
|
|
f6b7ab2251 | ||
|
|
8ffc554c9a | ||
|
|
d5a8cc2085 | ||
|
|
3075707bf8 | ||
|
|
e6d3dc07e2 | ||
|
|
310d13fd17 |
@@ -0,0 +1,34 @@
|
||||
FROM ubuntu:22.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
git \
|
||||
make \
|
||||
build-essential \
|
||||
postgresql-client \
|
||||
docker.io \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install nvm (project recommends nvm + .nvmrc for consistent Node versions)
|
||||
ENV NVM_DIR=/usr/local/nvm
|
||||
RUN mkdir -p $NVM_DIR \
|
||||
&& curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
|
||||
|
||||
SHELL ["/bin/bash", "-c"]
|
||||
|
||||
# Copy .nvmrc so nvm install picks up the right version
|
||||
COPY .nvmrc /tmp/.nvmrc
|
||||
|
||||
# Install Node.js from .nvmrc, enable Corepack, and symlink binaries
|
||||
# so they're available on PATH without hardcoding a version
|
||||
RUN . $NVM_DIR/nvm.sh \
|
||||
&& nvm install $(cat /tmp/.nvmrc) \
|
||||
&& nvm alias default $(cat /tmp/.nvmrc) \
|
||||
&& corepack enable \
|
||||
&& BIN_DIR=$(dirname $(nvm which default)) \
|
||||
&& ln -sf $BIN_DIR/node /usr/local/bin/node \
|
||||
&& ln -sf $BIN_DIR/npm /usr/local/bin/npm \
|
||||
&& ln -sf $BIN_DIR/npx /usr/local/bin/npx \
|
||||
&& ln -sf $BIN_DIR/corepack /usr/local/bin/corepack
|
||||
@@ -1,18 +1,10 @@
|
||||
{
|
||||
"install": "curl -fsSL https://deb.nodesource.com/setup_24.x | sudo -E bash - && sudo apt-get install -y nodejs && node --version && yarn install && echo 'Installing dependencies complete'",
|
||||
"start": "sudo service docker start && echo 'Docker service started' && sleep 3 && echo 'Starting PostgreSQL and Redis containers...' && make postgres-on-docker && make redis-on-docker && echo 'Waiting for containers to initialize...' && sleep 20 && echo 'Checking container status...' && docker ps --filter name=twenty_ && echo 'Waiting for PostgreSQL to be ready...' && until docker exec twenty_pg pg_isready -U postgres -h localhost; do echo 'PostgreSQL not ready yet, waiting...'; sleep 3; done && echo 'PostgreSQL is ready!' && echo 'Setting up database...' && cd packages/twenty-server && npx nx database:reset twenty-server || echo 'Database already initialized' && echo 'Environment setup complete!'",
|
||||
"install": "yarn install",
|
||||
"start": "sudo service docker start && sleep 2 && (docker start twenty_pg 2>/dev/null || make -C packages/twenty-docker postgres-on-docker) && (docker start twenty_redis 2>/dev/null || make -C packages/twenty-docker redis-on-docker) && until docker exec twenty_pg pg_isready -U postgres -h localhost 2>/dev/null; do sleep 1; done && echo 'PostgreSQL ready' && until docker exec twenty_redis redis-cli ping 2>/dev/null | grep -q PONG; do sleep 1; done && echo 'Redis ready' && bash packages/twenty-utils/setup-dev-env.sh && npx nx database:reset twenty-server",
|
||||
"terminals": [
|
||||
{
|
||||
"name": "Development Server",
|
||||
"command": "echo 'Waiting for database to be fully ready...' && sleep 30 && until docker exec twenty_pg pg_isready -U postgres -h localhost; do echo 'Waiting for PostgreSQL...'; sleep 2; done && echo 'Starting Twenty development server...' && export SERVER_URL=http://localhost:3000 && export PG_DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres && yarn start"
|
||||
},
|
||||
{
|
||||
"name": "Database Management",
|
||||
"command": "sleep 25 && echo 'Database management terminal ready' && echo 'Waiting for PostgreSQL to be available...' && until docker exec twenty_pg pg_isready -U postgres -h localhost; do echo 'Waiting for PostgreSQL...'; sleep 2; done && echo 'PostgreSQL is ready for database operations!' && echo 'You can now run database commands like:' && echo ' npx nx database:reset twenty-server' && echo ' npx nx database:migrate twenty-server' && bash"
|
||||
},
|
||||
{
|
||||
"name": "Container Logs & Status",
|
||||
"command": "sleep 10 && echo '=== Container Status Monitor ===' && while true; do echo '\\n=== Container Status at $(date) ===' && docker ps --filter name=twenty_ --format 'table {{.Names}}\\t{{.Status}}\\t{{.Ports}}' && echo '\\n=== PostgreSQL Status ===' && (docker exec twenty_pg pg_isready -U postgres -h localhost && echo 'PostgreSQL: ✅ Ready') || echo 'PostgreSQL: ❌ Not Ready' && echo '\\n=== Redis Status ===' && (docker exec twenty_redis redis-cli ping && echo 'Redis: ✅ Ready') || echo 'Redis: ❌ Not Ready' && sleep 30; done"
|
||||
"command": "yarn start"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ This directory contains Twenty's development guidelines and best practices in th
|
||||
|
||||
### React Development
|
||||
- **react-general-guidelines.mdc** - Core React development principles (Auto-attached to React files)
|
||||
- **react-state-management.mdc** - State management approaches with Recoil (Auto-attached to state files)
|
||||
- **react-state-management.mdc** - State management approaches with Jotai (Auto-attached to state files)
|
||||
|
||||
### Testing & Quality
|
||||
- **testing-guidelines.mdc** - Testing strategies and best practices (Auto-attached to test files)
|
||||
|
||||
@@ -7,7 +7,7 @@ alwaysApply: true
|
||||
# Twenty Architecture
|
||||
|
||||
## Tech Stack
|
||||
- **Frontend**: React 18, TypeScript, Recoil, Styled Components, Vite
|
||||
- **Frontend**: React 18, TypeScript, Jotai, Styled Components, Vite
|
||||
- **Backend**: NestJS, TypeORM, PostgreSQL, Redis, GraphQL
|
||||
- **Monorepo**: Nx workspace with yarn
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,16 +4,20 @@ alwaysApply: false
|
||||
---
|
||||
# React State Management
|
||||
|
||||
## Recoil Patterns
|
||||
## Jotai Patterns
|
||||
```typescript
|
||||
// ✅ Atoms for primitive state
|
||||
export const currentUserState = atom<User | null>({
|
||||
// ✅ Atoms for primitive state (use createAtomState for keyed state with optional persistence)
|
||||
import { createAtomState } from '@/ui/utilities/state/jotai/utils/createAtomState';
|
||||
|
||||
export const currentUserState = createAtomState<User | null>({
|
||||
key: 'currentUserState',
|
||||
default: null,
|
||||
defaultValue: null,
|
||||
});
|
||||
|
||||
// ✅ Selectors for derived state
|
||||
export const userDisplayNameSelector = selector({
|
||||
// ✅ Derived atoms for computed state (use createAtomSelector)
|
||||
import { createAtomSelector } from '@/ui/utilities/state/jotai/utils/createAtomSelector';
|
||||
|
||||
export const userDisplayNameSelector = createAtomSelector({
|
||||
key: 'userDisplayNameSelector',
|
||||
get: ({ get }) => {
|
||||
const user = get(currentUserState);
|
||||
@@ -21,13 +25,30 @@ export const userDisplayNameSelector = selector({
|
||||
},
|
||||
});
|
||||
|
||||
// ✅ Atom families for dynamic atoms
|
||||
export const userByIdState = atomFamily<User | null, string>({
|
||||
// ✅ Atom factory pattern for dynamic atoms (use createAtomFamilyState)
|
||||
import { createAtomFamilyState } from '@/ui/utilities/state/jotai/utils/createAtomFamilyState';
|
||||
|
||||
export const userByIdState = createAtomFamilyState<User | null, string>({
|
||||
key: 'userByIdState',
|
||||
default: null,
|
||||
defaultValue: null,
|
||||
});
|
||||
```
|
||||
|
||||
## Jotai Hooks
|
||||
```typescript
|
||||
// useAtomState - read and write (like useRecoilState)
|
||||
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
|
||||
|
||||
// useAtomStateValue - read only (like useRecoilValue)
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
|
||||
// useSetAtomState - write only (like useSetRecoilState)
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
```
|
||||
|
||||
## Provider
|
||||
Jotai works without a Provider by default (unlike Recoil's RecoilRoot). For scoped stores or testing, use `Provider` from `jotai`.
|
||||
|
||||
## Local State Guidelines
|
||||
```typescript
|
||||
// ✅ Multiple useState for unrelated state
|
||||
@@ -74,7 +95,7 @@ const increment = useCallback(() => {
|
||||
```
|
||||
|
||||
## Performance Tips
|
||||
- Use atom families for dynamic data collections
|
||||
- Implement proper selector caching
|
||||
- Avoid heavy computations in selectors
|
||||
- Use atom factory pattern (createAtomFamilyState) for dynamic data collections
|
||||
- Derived atoms (createAtomSelector) are automatically memoized by Jotai
|
||||
- Avoid heavy computations in derived atoms
|
||||
- Batch state updates when possible
|
||||
|
||||
@@ -0,0 +1,393 @@
|
||||
---
|
||||
name: syncable-entity-builder-and-validation
|
||||
description: Create validation logic and migration action builders for syncable entities in Twenty. Use when implementing business rule validation, uniqueness checks, foreign key validation, or building workspace migration actions for syncable entities. Validators never throw and never mutate.
|
||||
---
|
||||
|
||||
# Syncable Entity: Builder & Validation (Step 3/6)
|
||||
|
||||
**Purpose**: Implement business rule validation and create migration action builders.
|
||||
|
||||
**When to use**: After completing Steps 1-2 (Types, Cache, Transform). Required before implementing action handlers.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
This step creates:
|
||||
1. Validator service (business logic validation)
|
||||
2. Builder service (action creation)
|
||||
3. Orchestrator wiring (**CRITICAL** - often forgotten!)
|
||||
|
||||
**Key principles**:
|
||||
- Validators **never throw** - return error arrays
|
||||
- Validators **never mutate** - pass optimistic entity maps
|
||||
- Use indexed lookups (O(1)) not `Object.values().find()` (O(n))
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create Validator Service
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-my-entity-validator.service.ts`
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { t, msg } from '@lingui/macro';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
|
||||
import { type FlatMyEntityMaps } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity-maps.type';
|
||||
import { WorkspaceMigrationValidationError } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/types/workspace-migration-validation-error.type';
|
||||
import { MyEntityExceptionCode } from 'src/engine/metadata-modules/my-entity/exceptions/my-entity-exception-code.enum';
|
||||
|
||||
@Injectable()
|
||||
export class FlatMyEntityValidatorService {
|
||||
validateMyEntityForCreate(
|
||||
flatMyEntity: FlatMyEntity,
|
||||
optimisticFlatMyEntityMaps: FlatMyEntityMaps,
|
||||
): WorkspaceMigrationValidationError[] {
|
||||
const errors: WorkspaceMigrationValidationError[] = [];
|
||||
|
||||
// Pattern 1: Required field validation
|
||||
if (!isDefined(flatMyEntity.name) || flatMyEntity.name.trim() === '') {
|
||||
errors.push({
|
||||
code: MyEntityExceptionCode.NAME_REQUIRED,
|
||||
message: t`Name is required`,
|
||||
userFriendlyMessage: msg`Please provide a name for this entity`,
|
||||
});
|
||||
}
|
||||
|
||||
// Pattern 2: Uniqueness check - use indexed map (O(1))
|
||||
const existingEntityWithName = optimisticFlatMyEntityMaps.byName[flatMyEntity.name];
|
||||
|
||||
if (isDefined(existingEntityWithName) && existingEntityWithName.id !== flatMyEntity.id) {
|
||||
errors.push({
|
||||
code: MyEntityExceptionCode.MY_ENTITY_ALREADY_EXISTS,
|
||||
message: t`Entity with name ${flatMyEntity.name} already exists`,
|
||||
userFriendlyMessage: msg`An entity with this name already exists`,
|
||||
});
|
||||
}
|
||||
|
||||
// Pattern 3: Foreign key validation
|
||||
if (isDefined(flatMyEntity.parentEntityId)) {
|
||||
const parentEntity = optimisticFlatParentEntityMaps.byId[flatMyEntity.parentEntityId];
|
||||
|
||||
if (!isDefined(parentEntity)) {
|
||||
errors.push({
|
||||
code: MyEntityExceptionCode.PARENT_ENTITY_NOT_FOUND,
|
||||
message: t`Parent entity with ID ${flatMyEntity.parentEntityId} not found`,
|
||||
userFriendlyMessage: msg`The specified parent entity does not exist`,
|
||||
});
|
||||
} else if (isDefined(parentEntity.deletedAt)) {
|
||||
errors.push({
|
||||
code: MyEntityExceptionCode.PARENT_ENTITY_DELETED,
|
||||
message: t`Parent entity is deleted`,
|
||||
userFriendlyMessage: msg`Cannot reference a deleted parent entity`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 4: Standard entity protection
|
||||
if (flatMyEntity.isCustom === false) {
|
||||
errors.push({
|
||||
code: MyEntityExceptionCode.STANDARD_ENTITY_CANNOT_BE_CREATED,
|
||||
message: t`Cannot create standard entity`,
|
||||
userFriendlyMessage: msg`Standard entities can only be created by the system`,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
validateMyEntityForUpdate(
|
||||
flatMyEntity: FlatMyEntity,
|
||||
updates: Partial<FlatMyEntity>,
|
||||
optimisticFlatMyEntityMaps: FlatMyEntityMaps,
|
||||
): WorkspaceMigrationValidationError[] {
|
||||
const errors: WorkspaceMigrationValidationError[] = [];
|
||||
|
||||
// Standard entity protection
|
||||
if (flatMyEntity.isCustom === false) {
|
||||
errors.push({
|
||||
code: MyEntityExceptionCode.STANDARD_ENTITY_CANNOT_BE_UPDATED,
|
||||
message: t`Cannot update standard entity`,
|
||||
userFriendlyMessage: msg`Standard entities cannot be modified`,
|
||||
});
|
||||
return errors; // Early return if standard
|
||||
}
|
||||
|
||||
// Uniqueness check for name changes
|
||||
if (isDefined(updates.name) && updates.name !== flatMyEntity.name) {
|
||||
const existingEntityWithName = optimisticFlatMyEntityMaps.byName[updates.name];
|
||||
|
||||
if (isDefined(existingEntityWithName) && existingEntityWithName.id !== flatMyEntity.id) {
|
||||
errors.push({
|
||||
code: MyEntityExceptionCode.MY_ENTITY_ALREADY_EXISTS,
|
||||
message: t`Entity with name ${updates.name} already exists`,
|
||||
userFriendlyMessage: msg`An entity with this name already exists`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
|
||||
validateMyEntityForDelete(
|
||||
flatMyEntity: FlatMyEntity,
|
||||
): WorkspaceMigrationValidationError[] {
|
||||
const errors: WorkspaceMigrationValidationError[] = [];
|
||||
|
||||
// Standard entity protection
|
||||
if (flatMyEntity.isCustom === false) {
|
||||
errors.push({
|
||||
code: MyEntityExceptionCode.STANDARD_ENTITY_CANNOT_BE_DELETED,
|
||||
message: t`Cannot delete standard entity`,
|
||||
userFriendlyMessage: msg`Standard entities cannot be deleted`,
|
||||
});
|
||||
}
|
||||
|
||||
return errors;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Performance warning**: Avoid `Object.values().find()` - use indexed maps instead!
|
||||
|
||||
```typescript
|
||||
// ❌ BAD: O(n) - slow for large datasets
|
||||
const duplicate = Object.values(optimisticFlatMyEntityMaps.byId).find(
|
||||
(entity) => entity.name === flatMyEntity.name && entity.id !== flatMyEntity.id
|
||||
);
|
||||
|
||||
// ✅ GOOD: O(1) - use indexed map
|
||||
const existingEntityWithName = optimisticFlatMyEntityMaps.byName[flatMyEntity.name];
|
||||
if (isDefined(existingEntityWithName) && existingEntityWithName.id !== flatMyEntity.id) {
|
||||
// Handle duplicate
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Create Builder Service
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/workspace-migration-my-entity-actions-builder.service.ts`
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { WorkspaceEntityMigrationBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-entity-migration-builder.service';
|
||||
import { FlatMyEntityValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-my-entity-validator.service';
|
||||
import { type UniversalFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-my-entity.type';
|
||||
import {
|
||||
type UniversalCreateMyEntityAction,
|
||||
type UniversalUpdateMyEntityAction,
|
||||
type UniversalDeleteMyEntityAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/types/workspace-migration-my-entity-action.type';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceMigrationMyEntityActionsBuilderService extends WorkspaceEntityMigrationBuilderService<
|
||||
'myEntity',
|
||||
UniversalFlatMyEntity,
|
||||
UniversalCreateMyEntityAction,
|
||||
UniversalUpdateMyEntityAction,
|
||||
UniversalDeleteMyEntityAction
|
||||
> {
|
||||
constructor(
|
||||
private readonly flatMyEntityValidatorService: FlatMyEntityValidatorService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected buildCreateAction(
|
||||
universalFlatMyEntity: UniversalFlatMyEntity,
|
||||
flatEntityMaps: AllFlatEntityMapsByMetadataName,
|
||||
): BuildWorkspaceMigrationActionReturnType<UniversalCreateMyEntityAction> {
|
||||
const validationResult = this.flatMyEntityValidatorService.validateMyEntityForCreate(
|
||||
universalFlatMyEntity,
|
||||
flatEntityMaps.flatMyEntityMaps,
|
||||
);
|
||||
|
||||
if (validationResult.length > 0) {
|
||||
return {
|
||||
status: 'failed',
|
||||
errors: validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: {
|
||||
type: 'create',
|
||||
metadataName: 'myEntity',
|
||||
universalFlatEntity: universalFlatMyEntity,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected buildUpdateAction(
|
||||
universalFlatMyEntity: UniversalFlatMyEntity,
|
||||
universalUpdates: Partial<UniversalFlatMyEntity>,
|
||||
flatEntityMaps: AllFlatEntityMapsByMetadataName,
|
||||
): BuildWorkspaceMigrationActionReturnType<UniversalUpdateMyEntityAction> {
|
||||
const validationResult = this.flatMyEntityValidatorService.validateMyEntityForUpdate(
|
||||
universalFlatMyEntity,
|
||||
universalUpdates,
|
||||
flatEntityMaps.flatMyEntityMaps,
|
||||
);
|
||||
|
||||
if (validationResult.length > 0) {
|
||||
return {
|
||||
status: 'failed',
|
||||
errors: validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: {
|
||||
type: 'update',
|
||||
metadataName: 'myEntity',
|
||||
universalFlatEntity: universalFlatMyEntity,
|
||||
universalUpdates,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
protected buildDeleteAction(
|
||||
universalFlatMyEntity: UniversalFlatMyEntity,
|
||||
): BuildWorkspaceMigrationActionReturnType<UniversalDeleteMyEntityAction> {
|
||||
const validationResult = this.flatMyEntityValidatorService.validateMyEntityForDelete(
|
||||
universalFlatMyEntity,
|
||||
);
|
||||
|
||||
if (validationResult.length > 0) {
|
||||
return {
|
||||
status: 'failed',
|
||||
errors: validationResult,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: 'success',
|
||||
action: {
|
||||
type: 'delete',
|
||||
metadataName: 'myEntity',
|
||||
universalFlatEntity: universalFlatMyEntity,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Wire into Orchestrator (**CRITICAL**)
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-build-orchestrator.service.ts`
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class WorkspaceMigrationBuildOrchestratorService {
|
||||
constructor(
|
||||
// ... existing builders
|
||||
private readonly workspaceMigrationMyEntityActionsBuilderService: WorkspaceMigrationMyEntityActionsBuilderService,
|
||||
) {}
|
||||
|
||||
async buildWorkspaceMigration({
|
||||
allFlatEntityOperationByMetadataName,
|
||||
flatEntityMaps,
|
||||
isSystemBuild,
|
||||
}: BuildWorkspaceMigrationInput): Promise<BuildWorkspaceMigrationOutput> {
|
||||
// ... existing code
|
||||
|
||||
// Add your entity builder
|
||||
const myEntityResult = await this.workspaceMigrationMyEntityActionsBuilderService.build({
|
||||
flatEntitiesToCreate: allFlatEntityOperationByMetadataName.myEntity?.flatEntityToCreate ?? [],
|
||||
flatEntitiesToUpdate: allFlatEntityOperationByMetadataName.myEntity?.flatEntityToUpdate ?? [],
|
||||
flatEntitiesToDelete: allFlatEntityOperationByMetadataName.myEntity?.flatEntityToDelete ?? [],
|
||||
flatEntityMaps,
|
||||
isSystemBuild,
|
||||
});
|
||||
|
||||
// ... aggregate errors
|
||||
|
||||
return {
|
||||
status: aggregatedErrors.length > 0 ? 'failed' : 'success',
|
||||
errors: aggregatedErrors,
|
||||
actions: [
|
||||
...existingActions,
|
||||
...myEntityResult.actions,
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**⚠️ This step is the most commonly forgotten!** Your entity won't sync without orchestrator wiring.
|
||||
|
||||
---
|
||||
|
||||
## Validation Patterns
|
||||
|
||||
### Pattern 1: Required Field
|
||||
```typescript
|
||||
if (!isDefined(field) || field.trim() === '') {
|
||||
errors.push({ code: ..., message: ..., userFriendlyMessage: ... });
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Uniqueness (O(1) lookup)
|
||||
```typescript
|
||||
const existing = optimisticMaps.byName[entity.name];
|
||||
if (isDefined(existing) && existing.id !== entity.id) {
|
||||
errors.push({ ... });
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: Foreign Key Validation
|
||||
```typescript
|
||||
if (isDefined(entity.parentId)) {
|
||||
const parent = parentMaps.byId[entity.parentId];
|
||||
if (!isDefined(parent)) {
|
||||
errors.push({ code: NOT_FOUND, ... });
|
||||
} else if (isDefined(parent.deletedAt)) {
|
||||
errors.push({ code: DELETED, ... });
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4: Standard Entity Protection
|
||||
```typescript
|
||||
if (entity.isCustom === false) {
|
||||
errors.push({ code: STANDARD_ENTITY_PROTECTED, ... });
|
||||
return errors; // Early return
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
Before moving to Step 4:
|
||||
|
||||
- [ ] Validator service created
|
||||
- [ ] Validator **never throws** (returns error arrays)
|
||||
- [ ] Validator **never mutates** (uses optimistic maps)
|
||||
- [ ] All uniqueness checks use indexed maps (O(1))
|
||||
- [ ] Required field validation implemented
|
||||
- [ ] Foreign key validation implemented
|
||||
- [ ] Standard entity protection implemented
|
||||
- [ ] Builder service extends `WorkspaceEntityMigrationBuilderService`
|
||||
- [ ] Builder creates actions with universal entities
|
||||
- [ ] **Builder wired into orchestrator** (**CRITICAL**)
|
||||
- [ ] **Builder injected in orchestrator constructor**
|
||||
- [ ] **Builder called in `buildWorkspaceMigration`**
|
||||
- [ ] **Actions added to orchestrator return statement**
|
||||
|
||||
---
|
||||
|
||||
## Next Step
|
||||
|
||||
Once builder and validation are complete, proceed to:
|
||||
**[Syncable Entity: Runner & Actions (Step 4/6)](../syncable-entity-runner-and-actions/SKILL.md)**
|
||||
|
||||
For complete workflow, see `@creating-syncable-entity` rule.
|
||||
@@ -0,0 +1,303 @@
|
||||
---
|
||||
name: syncable-entity-cache-and-transform
|
||||
description: Create cache services and transformation utilities for syncable entities in Twenty. Use when implementing entity-to-flat conversions, input DTO transpilation to universal flat entities, or cache recomputation for syncable entities.
|
||||
---
|
||||
|
||||
# Syncable Entity: Cache & Transform (Step 2/6)
|
||||
|
||||
**Purpose**: Create cache layer and transformation utilities to convert between different entity representations.
|
||||
|
||||
**When to use**: After completing Step 1 (Types & Constants). Required before building validators and action handlers.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
This step creates:
|
||||
1. Cache service for flat entity maps
|
||||
2. Entity-to-flat conversion utility
|
||||
3. Input transform utils (DTO → Universal Flat Entity)
|
||||
|
||||
**Key principle**: Input transform utils must output **universal flat entities** (with `universalIdentifier` and foreign keys mapped to universal identifiers).
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create Cache Service
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-my-entity/services/flat-my-entity-cache.service.ts`
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { WorkspaceCache } from 'src/engine/twenty-orm/decorators/workspace-cache.decorator';
|
||||
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
|
||||
import { type FlatMyEntityMaps } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity-maps.type';
|
||||
import { fromMyEntityEntityToFlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/utils/from-my-entity-entity-to-flat-my-entity.util';
|
||||
|
||||
@Injectable()
|
||||
export class FlatMyEntityCacheService {
|
||||
constructor(
|
||||
@InjectRepository(MyEntityEntity, 'metadata')
|
||||
private readonly myEntityRepository: Repository<MyEntityEntity>,
|
||||
) {}
|
||||
|
||||
@WorkspaceCache({ flatMapsKey: 'flatMyEntityMaps' })
|
||||
async getFlatMyEntityMaps(): Promise<FlatMyEntityMaps> {
|
||||
const myEntities = await this.myEntityRepository.find({
|
||||
withDeleted: true, // CRITICAL: Include soft-deleted entities
|
||||
});
|
||||
|
||||
const flatMyEntities = myEntities.map((entity) =>
|
||||
fromMyEntityEntityToFlatMyEntity(entity),
|
||||
);
|
||||
|
||||
return {
|
||||
byId: Object.fromEntries(flatMyEntities.map((e) => [e.id, e])),
|
||||
byName: Object.fromEntries(flatMyEntities.map((e) => [e.name, e])),
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Critical rules**:
|
||||
- Use `@WorkspaceCache` decorator with unique `flatMapsKey`
|
||||
- **Always** use `withDeleted: true` to include soft-deleted entities
|
||||
- Cache key pattern: `flat{EntityName}Maps` (camelCase)
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Entity-to-Flat Conversion
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-my-entity/utils/from-my-entity-entity-to-flat-my-entity.util.ts`
|
||||
|
||||
```typescript
|
||||
import { v4 } from 'uuid';
|
||||
import { type MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
|
||||
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
|
||||
|
||||
export const fromMyEntityEntityToFlatMyEntity = (
|
||||
entity: MyEntityEntity,
|
||||
): FlatMyEntity => {
|
||||
return {
|
||||
id: entity.id,
|
||||
// Critical: generate a new UUID for universalIdentifier
|
||||
universalIdentifier: v4(),
|
||||
workspaceId: entity.workspaceId,
|
||||
applicationId: entity.applicationId,
|
||||
name: entity.name,
|
||||
label: entity.label,
|
||||
description: entity.description,
|
||||
isCustom: entity.isCustom,
|
||||
parentEntityId: entity.parentEntityId,
|
||||
settings: entity.settings,
|
||||
createdAt: entity.createdAt.toISOString(),
|
||||
updatedAt: entity.updatedAt.toISOString(),
|
||||
deletedAt: entity.deletedAt?.toISOString() ?? null,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
**Critical**: `universalIdentifier` must be a new UUID generated with `v4()` (not `entity.id`)
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Input Transform Utils (DTO → Universal Flat Entity)
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-my-entity/utils/from-create-my-entity-input-to-universal-flat-my-entity.util.ts`
|
||||
|
||||
```typescript
|
||||
import { v4 } from 'uuid';
|
||||
import { sanitizeString } from 'twenty-shared/string';
|
||||
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
|
||||
import { type UniversalFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-my-entity.type';
|
||||
import { resolveEntityRelationUniversalIdentifiers } from 'src/engine/metadata-modules/flat-entity/utils/resolve-entity-relation-universal-identifiers.util';
|
||||
import { type AllFlatEntityMapsByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps-by-metadata-name.type';
|
||||
|
||||
export const fromCreateMyEntityInputToUniversalFlatMyEntity = ({
|
||||
input,
|
||||
workspaceId,
|
||||
flatEntityMaps,
|
||||
}: {
|
||||
input: CreateMyEntityInput;
|
||||
workspaceId: string;
|
||||
flatEntityMaps?: AllFlatEntityMapsByMetadataName;
|
||||
}): UniversalFlatMyEntity => {
|
||||
const id = v4();
|
||||
const universalIdentifier = v4();
|
||||
|
||||
// 1. Extract foreign key IDs BEFORE sanitization
|
||||
const parentEntityId = input.parentEntityId ?? null;
|
||||
|
||||
// 2. Sanitize string properties
|
||||
const name = sanitizeString(input.name);
|
||||
const label = sanitizeString(input.label);
|
||||
const description = input.description ? sanitizeString(input.description) : null;
|
||||
|
||||
// 3. Build base flat entity
|
||||
const baseFlatEntity = {
|
||||
id,
|
||||
universalIdentifier,
|
||||
workspaceId,
|
||||
applicationId: null,
|
||||
name,
|
||||
label,
|
||||
description,
|
||||
isCustom: true,
|
||||
parentEntityId,
|
||||
settings: input.settings ?? null,
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
deletedAt: null,
|
||||
};
|
||||
|
||||
// 4. Resolve foreign keys to universal identifiers (if flatEntityMaps provided)
|
||||
if (flatEntityMaps) {
|
||||
return resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'myEntity',
|
||||
flatEntity: baseFlatEntity,
|
||||
flatEntityMaps,
|
||||
});
|
||||
}
|
||||
|
||||
// 5. Return with null universal foreign keys if no maps
|
||||
return {
|
||||
...baseFlatEntity,
|
||||
parentEntityUniversalIdentifier: null,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
**Key steps**:
|
||||
1. Generate IDs (`id` and `universalIdentifier` with `v4()`)
|
||||
2. Extract foreign keys **before** sanitization
|
||||
3. Sanitize all string properties
|
||||
4. Build base flat entity
|
||||
5. Resolve foreign keys → universal identifiers
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Create Flat Entity Module
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-my-entity/flat-my-entity.module.ts`
|
||||
|
||||
```typescript
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
|
||||
import { FlatMyEntityCacheService } from 'src/engine/metadata-modules/flat-my-entity/services/flat-my-entity-cache.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([MyEntityEntity], 'metadata')],
|
||||
providers: [FlatMyEntityCacheService],
|
||||
exports: [FlatMyEntityCacheService],
|
||||
})
|
||||
export class FlatMyEntityModule {}
|
||||
```
|
||||
|
||||
**Rules**:
|
||||
- Import entity with `'metadata'` datasource
|
||||
- Export cache service for use in other modules
|
||||
|
||||
---
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Pattern: Foreign Key Resolution
|
||||
|
||||
```typescript
|
||||
// Extract foreign keys BEFORE sanitization
|
||||
const parentEntityId = input.parentEntityId ?? null;
|
||||
|
||||
// After building base entity, resolve to universal identifiers
|
||||
const universalFlatEntity = resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'myEntity',
|
||||
flatEntity: baseFlatEntity,
|
||||
flatEntityMaps,
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern: JSONB with SerializedRelation
|
||||
|
||||
```typescript
|
||||
// For JSONB properties containing foreign keys
|
||||
const settings = input.settings
|
||||
? {
|
||||
...input.settings,
|
||||
fieldMetadataId: input.settings.fieldMetadataId,
|
||||
}
|
||||
: null;
|
||||
|
||||
// After resolution, JSONB foreign keys become universal identifiers
|
||||
return resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'myEntity',
|
||||
flatEntity: { ...baseFlatEntity, settings },
|
||||
flatEntityMaps,
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern: Update Transform
|
||||
|
||||
```typescript
|
||||
// from-update-my-entity-input-to-universal-flat-my-entity-updates.util.ts
|
||||
export const fromUpdateMyEntityInputToUniversalFlatMyEntityUpdates = ({
|
||||
input,
|
||||
flatEntityMaps,
|
||||
}: {
|
||||
input: UpdateMyEntityInput;
|
||||
flatEntityMaps?: AllFlatEntityMapsByMetadataName;
|
||||
}): Partial<UniversalFlatMyEntity> => {
|
||||
const updates: Partial<UniversalFlatMyEntity> = {};
|
||||
|
||||
if (input.name !== undefined) {
|
||||
updates.name = sanitizeString(input.name);
|
||||
}
|
||||
|
||||
if (input.parentEntityId !== undefined) {
|
||||
updates.parentEntityId = input.parentEntityId;
|
||||
}
|
||||
|
||||
updates.updatedAt = new Date().toISOString();
|
||||
|
||||
// Resolve foreign keys if maps provided
|
||||
if (flatEntityMaps) {
|
||||
return resolveEntityRelationUniversalIdentifiers({
|
||||
metadataName: 'myEntity',
|
||||
flatEntity: updates as any,
|
||||
flatEntityMaps,
|
||||
});
|
||||
}
|
||||
|
||||
return updates;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
Before moving to Step 3:
|
||||
|
||||
- [ ] Cache service created with `@WorkspaceCache` decorator
|
||||
- [ ] Cache uses `withDeleted: true`
|
||||
- [ ] Cache key follows `flat{EntityName}Maps` pattern
|
||||
- [ ] Entity-to-flat conversion implemented
|
||||
- [ ] `universalIdentifier` set correctly (generated with `v4()`)
|
||||
- [ ] Create input transform implemented
|
||||
- [ ] Update input transform implemented (if needed)
|
||||
- [ ] Foreign keys extracted before sanitization
|
||||
- [ ] String properties sanitized
|
||||
- [ ] Foreign keys resolved to universal identifiers
|
||||
- [ ] Flat entity module created and exports cache service
|
||||
|
||||
---
|
||||
|
||||
## Next Step
|
||||
|
||||
Once cache and transform utilities are complete, proceed to:
|
||||
**[Syncable Entity: Builder & Validation (Step 3/6)](../syncable-entity-builder-and-validation/SKILL.md)**
|
||||
|
||||
For complete workflow, see `@creating-syncable-entity` rule.
|
||||
@@ -0,0 +1,326 @@
|
||||
---
|
||||
name: syncable-entity-integration
|
||||
description: Wire syncable entity services into NestJS modules, create service layer and resolvers for Twenty entities. Use when registering builders, validators, and action handlers in modules, creating business services, or exposing entities via GraphQL API with proper exception handling.
|
||||
---
|
||||
|
||||
# Syncable Entity: Integration (Step 5/6)
|
||||
|
||||
**Purpose**: Wire everything together, register in modules, create services and resolvers.
|
||||
|
||||
**When to use**: After completing Steps 1-4 (all previous steps). Required before testing.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
This step:
|
||||
1. Registers services in 3 NestJS modules
|
||||
2. Creates service layer (returns flat entities)
|
||||
3. Creates resolver layer (converts flat → DTO)
|
||||
4. Uses exception interceptor for GraphQL
|
||||
|
||||
**Key principle**: Services return flat entities, resolvers transpile flat → DTO.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Register in Builder Module
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-builder/workspace-migration-builder.module.ts`
|
||||
|
||||
```typescript
|
||||
import { WorkspaceMigrationMyEntityActionsBuilderService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/workspace-migration-my-entity-actions-builder.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// ... existing imports
|
||||
],
|
||||
providers: [
|
||||
// ... existing providers
|
||||
WorkspaceMigrationMyEntityActionsBuilderService,
|
||||
],
|
||||
exports: [
|
||||
// ... existing exports
|
||||
WorkspaceMigrationMyEntityActionsBuilderService,
|
||||
],
|
||||
})
|
||||
export class WorkspaceMigrationBuilderModule {}
|
||||
```
|
||||
|
||||
**Important**: Add to both `providers` AND `exports` (builder needs to be exported for orchestrator).
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Register in Validators Module
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/workspace-migration-builder-validators.module.ts`
|
||||
|
||||
```typescript
|
||||
import { FlatMyEntityValidatorService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/validators/services/flat-my-entity-validator.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// ... existing imports
|
||||
],
|
||||
providers: [
|
||||
// ... existing providers
|
||||
FlatMyEntityValidatorService,
|
||||
],
|
||||
exports: [
|
||||
// ... existing exports
|
||||
FlatMyEntityValidatorService,
|
||||
],
|
||||
})
|
||||
export class WorkspaceMigrationBuilderValidatorsModule {}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Register Action Handlers
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-schema-migration-runner-action-handlers.module.ts`
|
||||
|
||||
```typescript
|
||||
import { CreateMyEntityActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/create-my-entity-action-handler.service';
|
||||
import { UpdateMyEntityActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/update-my-entity-action-handler.service';
|
||||
import { DeleteMyEntityActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/delete-my-entity-action-handler.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// ... existing imports
|
||||
],
|
||||
providers: [
|
||||
// ... existing providers
|
||||
CreateMyEntityActionHandlerService,
|
||||
UpdateMyEntityActionHandlerService,
|
||||
DeleteMyEntityActionHandlerService,
|
||||
],
|
||||
exports: [
|
||||
// ... existing exports (action handlers typically not exported)
|
||||
],
|
||||
})
|
||||
export class WorkspaceSchemaMigrationRunnerActionHandlersModule {}
|
||||
```
|
||||
|
||||
**Note**: Action handlers are typically only in `providers`, not `exports`.
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Create Service Layer
|
||||
|
||||
**File**: `src/engine/metadata-modules/my-entity/my-entity.service.ts`
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { findFlatEntityByIdInFlatEntityMapsOrThrow } from 'src/engine/metadata-modules/flat-entity/utils/find-flat-entity-by-id-in-flat-entity-maps-or-throw.util';
|
||||
import { fromCreateMyEntityInputToUniversalFlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/utils/from-create-my-entity-input-to-universal-flat-my-entity.util';
|
||||
import { WorkspaceMigrationBuilderException } from 'src/engine/workspace-manager/workspace-migration/exceptions/workspace-migration-builder-exception';
|
||||
import { WorkspaceMigrationValidateBuildAndRunService } from 'src/engine/workspace-manager/workspace-migration/services/workspace-migration-validate-build-and-run-service';
|
||||
|
||||
@Injectable()
|
||||
export class MyEntityService {
|
||||
constructor(
|
||||
private readonly workspaceMigrationValidateBuildAndRunService: WorkspaceMigrationValidateBuildAndRunService,
|
||||
private readonly workspaceManyOrAllFlatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
) {}
|
||||
|
||||
async create(input: CreateMyEntityInput, workspaceId: string): Promise<FlatMyEntity> {
|
||||
// 1. Transform input to universal flat entity
|
||||
const universalFlatMyEntityToCreate = fromCreateMyEntityInputToUniversalFlatMyEntity({
|
||||
input,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
// 2. Validate, build, and run
|
||||
const result =
|
||||
await this.workspaceMigrationValidateBuildAndRunService.validateBuildAndRunWorkspaceMigration(
|
||||
{
|
||||
allFlatEntityOperationByMetadataName: {
|
||||
myEntity: {
|
||||
flatEntityToCreate: [universalFlatMyEntityToCreate],
|
||||
flatEntityToDelete: [],
|
||||
flatEntityToUpdate: [],
|
||||
},
|
||||
},
|
||||
workspaceId,
|
||||
isSystemBuild: false,
|
||||
},
|
||||
);
|
||||
|
||||
// 3. Throw if validation failed
|
||||
if (isDefined(result)) {
|
||||
throw new WorkspaceMigrationBuilderException(
|
||||
result,
|
||||
'Validation errors occurred while creating entity',
|
||||
);
|
||||
}
|
||||
|
||||
// 4. Return freshly cached flat entity
|
||||
const { flatMyEntityMaps } =
|
||||
await this.workspaceManyOrAllFlatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
|
||||
{
|
||||
workspaceId,
|
||||
flatMapsKeys: ['flatMyEntityMaps'],
|
||||
},
|
||||
);
|
||||
|
||||
return findFlatEntityByIdInFlatEntityMapsOrThrow({
|
||||
flatEntityId: universalFlatMyEntityToCreate.id,
|
||||
flatEntityMaps: flatMyEntityMaps,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Service pattern**:
|
||||
1. Transform input → universal flat entity
|
||||
2. Call `validateBuildAndRunWorkspaceMigration`
|
||||
3. Throw if validation errors
|
||||
4. **Return flat entity** (not DTO)
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Create Resolver Layer
|
||||
|
||||
**File**: `src/engine/metadata-modules/my-entity/my-entity.resolver.ts`
|
||||
|
||||
```typescript
|
||||
import { UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { WorkspaceMigrationGraphqlApiExceptionInterceptor } from 'src/engine/workspace-manager/workspace-migration/interceptors/workspace-migration-graphql-api-exception.interceptor';
|
||||
import { MyEntityService } from 'src/engine/metadata-modules/my-entity/my-entity.service';
|
||||
import { fromFlatMyEntityToMyEntityDto } from 'src/engine/metadata-modules/my-entity/utils/from-flat-my-entity-to-my-entity-dto.util';
|
||||
|
||||
@Resolver(() => MyEntityDto)
|
||||
@UseInterceptors(WorkspaceMigrationGraphqlApiExceptionInterceptor)
|
||||
export class MyEntityResolver {
|
||||
constructor(private readonly myEntityService: MyEntityService) {}
|
||||
|
||||
@Mutation(() => MyEntityDto)
|
||||
async createMyEntity(
|
||||
@Args('input') input: CreateMyEntityInput,
|
||||
@Workspace() { id: workspaceId }: Workspace,
|
||||
): Promise<MyEntityDto> {
|
||||
// Service returns flat entity
|
||||
const flatMyEntity = await this.myEntityService.create(input, workspaceId);
|
||||
|
||||
// Resolver converts flat entity to DTO
|
||||
return fromFlatMyEntityToMyEntityDto(flatMyEntity);
|
||||
}
|
||||
|
||||
@Mutation(() => MyEntityDto)
|
||||
async updateMyEntity(
|
||||
@Args('id') id: string,
|
||||
@Args('input') input: UpdateMyEntityInput,
|
||||
@Workspace() { id: workspaceId }: Workspace,
|
||||
): Promise<MyEntityDto> {
|
||||
const flatMyEntity = await this.myEntityService.update(id, input, workspaceId);
|
||||
return fromFlatMyEntityToMyEntityDto(flatMyEntity);
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async deleteMyEntity(
|
||||
@Args('id') id: string,
|
||||
@Workspace() { id: workspaceId }: Workspace,
|
||||
) {
|
||||
await this.myEntityService.delete(id, workspaceId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Resolver responsibilities**:
|
||||
- Receives flat entities from service
|
||||
- **Converts flat → DTO** using conversion utility
|
||||
- Returns DTOs to GraphQL API
|
||||
- Uses exception interceptor for error formatting
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Flat-to-DTO Conversion
|
||||
|
||||
**File**: `src/engine/metadata-modules/my-entity/utils/from-flat-my-entity-to-my-entity-dto.util.ts`
|
||||
|
||||
```typescript
|
||||
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
|
||||
import { type MyEntityDto } from 'src/engine/metadata-modules/my-entity/dtos/my-entity.dto';
|
||||
|
||||
export const fromFlatMyEntityToMyEntityDto = (
|
||||
flatMyEntity: FlatMyEntity,
|
||||
): MyEntityDto => {
|
||||
return {
|
||||
id: flatMyEntity.id,
|
||||
name: flatMyEntity.name,
|
||||
label: flatMyEntity.label,
|
||||
description: flatMyEntity.description,
|
||||
isCustom: flatMyEntity.isCustom,
|
||||
createdAt: flatMyEntity.createdAt,
|
||||
updatedAt: flatMyEntity.updatedAt,
|
||||
// Convert foreign key IDs to relation objects if needed
|
||||
// parentEntity: flatMyEntity.parentEntityId ? { id: flatMyEntity.parentEntityId } : null,
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Layer Responsibilities
|
||||
|
||||
| Layer | Input | Output | Responsibility |
|
||||
|-------|-------|--------|----------------|
|
||||
| **Service** | Input DTO | Flat Entity | Business logic, validation orchestration |
|
||||
| **Resolver** | Service result | DTO | Flat → DTO conversion, GraphQL exposure |
|
||||
|
||||
**Service Layer**:
|
||||
- Works with flat entities internally
|
||||
- Returns `FlatMyEntity` type
|
||||
- No knowledge of DTOs or GraphQL types
|
||||
|
||||
**Resolver Layer**:
|
||||
- Receives flat entities from service
|
||||
- Converts flat entities to DTOs
|
||||
- Returns DTOs to GraphQL API
|
||||
|
||||
---
|
||||
|
||||
## Exception Interceptor
|
||||
|
||||
The `WorkspaceMigrationGraphqlApiExceptionInterceptor` automatically handles:
|
||||
|
||||
1. `FlatEntityMapsException` → Converts to GraphQL errors (NotFoundError, etc.)
|
||||
2. `WorkspaceMigrationBuilderException` → Formats validation errors with i18n
|
||||
3. `WorkspaceMigrationRunnerException` → Formats runner errors
|
||||
|
||||
**What it does**:
|
||||
- Catches exceptions and formats for API responses
|
||||
- Translates error messages based on user locale
|
||||
- Ensures consistent error structure for frontend
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
Before moving to Step 6 (Testing):
|
||||
|
||||
- [ ] Builder registered in builder module (providers + exports)
|
||||
- [ ] Validator registered in validators module (providers + exports)
|
||||
- [ ] All 3 action handlers registered in action handlers module (providers)
|
||||
- [ ] Service layer created
|
||||
- [ ] Service returns flat entities (not DTOs)
|
||||
- [ ] Resolver layer created
|
||||
- [ ] Resolver uses exception interceptor
|
||||
- [ ] Resolver converts flat → DTO
|
||||
- [ ] Flat-to-DTO conversion utility created
|
||||
|
||||
---
|
||||
|
||||
## Next Step
|
||||
|
||||
Once integration is complete, proceed to (**MANDATORY**):
|
||||
**[Syncable Entity: Integration Testing (Step 6/6)](../syncable-entity-testing/SKILL.md)**
|
||||
|
||||
For complete workflow, see `@creating-syncable-entity` rule.
|
||||
@@ -0,0 +1,355 @@
|
||||
---
|
||||
name: syncable-entity-runner-and-actions
|
||||
description: Implement action handlers for executing workspace migrations in Twenty. Use when creating database operations for syncable entities, implementing universal-to-flat entity transpilation, or handling create/update/delete actions in the runner layer.
|
||||
---
|
||||
|
||||
# Syncable Entity: Runner & Actions (Step 4/6)
|
||||
|
||||
**Purpose**: Execute migration actions against the database with proper transpilation from universal to flat entities.
|
||||
|
||||
**When to use**: After completing Steps 1-3 (Types, Cache, Builder). Required before integration.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
This step creates:
|
||||
1. Create action handler
|
||||
2. Update action handler
|
||||
3. Delete action handler
|
||||
4. Universal-to-flat conversion utilities
|
||||
|
||||
**Key pattern**: Each handler has two phases:
|
||||
1. **Transpilation**: Universal action → Flat action
|
||||
2. **Execution**: Flat action → Database operation
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create Action Handler
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/create-my-entity-action-handler.service.ts`
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceCreateActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-create-action-handler.service';
|
||||
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
|
||||
import { fromUniversalFlatMyEntityToFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util';
|
||||
import {
|
||||
type UniversalCreateMyEntityAction,
|
||||
type FlatCreateMyEntityAction,
|
||||
} from 'src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/types/workspace-migration-my-entity-action.type';
|
||||
|
||||
@Injectable()
|
||||
export class CreateMyEntityActionHandlerService extends WorkspaceCreateActionHandlerService<
|
||||
'myEntity',
|
||||
UniversalCreateMyEntityAction,
|
||||
FlatCreateMyEntityAction
|
||||
> {
|
||||
constructor(
|
||||
@InjectRepository(MyEntityEntity, 'metadata')
|
||||
private readonly myEntityRepository: Repository<MyEntityEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
// Phase 1: Transpile universal action to flat action
|
||||
protected transpileUniversalActionToFlatAction(
|
||||
universalAction: UniversalCreateMyEntityAction,
|
||||
flatEntityMaps: AllFlatEntityMapsByMetadataName,
|
||||
): FlatCreateMyEntityAction {
|
||||
return {
|
||||
type: 'create',
|
||||
metadataName: 'myEntity',
|
||||
flatEntity: fromUniversalFlatMyEntityToFlatMyEntity(
|
||||
universalAction.universalFlatEntity,
|
||||
flatEntityMaps,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// Phase 2: Execute flat action against database
|
||||
protected async executeForMetadata(
|
||||
flatActions: FlatCreateMyEntityAction[],
|
||||
): Promise<void> {
|
||||
const flatEntities = flatActions.map((action) => action.flatEntity);
|
||||
|
||||
await this.insertFlatEntitiesInRepository({
|
||||
repository: this.myEntityRepository,
|
||||
flatEntities,
|
||||
});
|
||||
}
|
||||
|
||||
protected async executeForWorkspaceSchema(): Promise<void> {
|
||||
// No workspace schema changes needed for metadata-only entity
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Key helper methods**:
|
||||
- `transpileUniversalActionToFlatAction`: Converts universal → flat
|
||||
- `insertFlatEntitiesInRepository`: Base class helper for inserts
|
||||
- `executeForMetadata`: Metadata database operations
|
||||
- `executeForWorkspaceSchema`: Workspace schema changes (if needed)
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Update Action Handler
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/update-my-entity-action-handler.service.ts`
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceUpdateActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-update-action-handler.service';
|
||||
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
|
||||
import { fromUniversalFlatMyEntityToFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util';
|
||||
import { resolveUniversalUpdateRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
|
||||
|
||||
@Injectable()
|
||||
export class UpdateMyEntityActionHandlerService extends WorkspaceUpdateActionHandlerService<
|
||||
'myEntity',
|
||||
UniversalUpdateMyEntityAction,
|
||||
FlatUpdateMyEntityAction
|
||||
> {
|
||||
constructor(
|
||||
@InjectRepository(MyEntityEntity, 'metadata')
|
||||
private readonly myEntityRepository: Repository<MyEntityEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected transpileUniversalActionToFlatAction(
|
||||
universalAction: UniversalUpdateMyEntityAction,
|
||||
flatEntityMaps: AllFlatEntityMapsByMetadataName,
|
||||
): FlatUpdateMyEntityAction {
|
||||
const flatEntity = fromUniversalFlatMyEntityToFlatMyEntity(
|
||||
universalAction.universalFlatEntity,
|
||||
flatEntityMaps,
|
||||
);
|
||||
|
||||
// Resolve universal foreign keys in updates to regular IDs
|
||||
const flatUpdates = resolveUniversalUpdateRelationIdentifiersToIds({
|
||||
metadataName: 'myEntity',
|
||||
universalUpdates: universalAction.universalUpdates,
|
||||
flatEntityMaps,
|
||||
});
|
||||
|
||||
return {
|
||||
type: 'update',
|
||||
metadataName: 'myEntity',
|
||||
flatEntity,
|
||||
updates: flatUpdates,
|
||||
};
|
||||
}
|
||||
|
||||
protected async executeForMetadata(
|
||||
flatActions: FlatUpdateMyEntityAction[],
|
||||
): Promise<void> {
|
||||
for (const action of flatActions) {
|
||||
await this.myEntityRepository.update(
|
||||
{ id: action.flatEntity.id },
|
||||
action.updates,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected async executeForWorkspaceSchema(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Update-specific helper**:
|
||||
- `resolveUniversalUpdateRelationIdentifiersToIds`: Maps universal identifiers back to regular IDs in the updates object
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Delete Action Handler
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/services/delete-my-entity-action-handler.service.ts`
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { WorkspaceDeleteActionHandlerService } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/workspace-delete-action-handler.service';
|
||||
import { MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
|
||||
import { fromUniversalFlatMyEntityToFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util';
|
||||
|
||||
@Injectable()
|
||||
export class DeleteMyEntityActionHandlerService extends WorkspaceDeleteActionHandlerService<
|
||||
'myEntity',
|
||||
UniversalDeleteMyEntityAction,
|
||||
FlatDeleteMyEntityAction
|
||||
> {
|
||||
constructor(
|
||||
@InjectRepository(MyEntityEntity, 'metadata')
|
||||
private readonly myEntityRepository: Repository<MyEntityEntity>,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
protected transpileUniversalActionToFlatAction(
|
||||
universalAction: UniversalDeleteMyEntityAction,
|
||||
flatEntityMaps: AllFlatEntityMapsByMetadataName,
|
||||
): FlatDeleteMyEntityAction {
|
||||
// Use base class helper for delete transpilation
|
||||
return this.transpileUniversalDeleteActionToFlatDeleteAction({
|
||||
universalAction,
|
||||
flatEntityMaps,
|
||||
fromUniversalFlatEntityToFlatEntity: fromUniversalFlatMyEntityToFlatMyEntity,
|
||||
});
|
||||
}
|
||||
|
||||
protected async executeForMetadata(
|
||||
flatActions: FlatDeleteMyEntityAction[],
|
||||
): Promise<void> {
|
||||
const ids = flatActions.map((action) => action.flatEntity.id);
|
||||
|
||||
await this.myEntityRepository.delete(ids);
|
||||
}
|
||||
|
||||
protected async executeForWorkspaceSchema(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Delete-specific helper**:
|
||||
- `transpileUniversalDeleteActionToFlatDeleteAction`: Base class helper that handles standard delete transpilation
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Universal-to-Flat Conversion
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-runner/action-handlers/my-entity/utils/from-universal-flat-my-entity-to-flat-my-entity.util.ts`
|
||||
|
||||
```typescript
|
||||
import { resolveUniversalRelationIdentifiersToIds } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/utils/resolve-universal-relation-identifiers-to-ids.util';
|
||||
import { type UniversalFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-my-entity.type';
|
||||
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
|
||||
import { type AllFlatEntityMapsByMetadataName } from 'src/engine/metadata-modules/flat-entity/types/all-flat-entity-maps-by-metadata-name.type';
|
||||
|
||||
export const fromUniversalFlatMyEntityToFlatMyEntity = (
|
||||
universalFlatMyEntity: UniversalFlatMyEntity,
|
||||
flatEntityMaps: AllFlatEntityMapsByMetadataName,
|
||||
): FlatMyEntity => {
|
||||
// Resolve universal foreign keys back to regular IDs
|
||||
return resolveUniversalRelationIdentifiersToIds({
|
||||
metadataName: 'myEntity',
|
||||
universalFlatEntity: universalFlatMyEntity,
|
||||
flatEntityMaps,
|
||||
}) as FlatMyEntity;
|
||||
};
|
||||
```
|
||||
|
||||
**Key utility**:
|
||||
- `resolveUniversalRelationIdentifiersToIds`: Maps universal identifiers → regular IDs (reverse of `resolveEntityRelationUniversalIdentifiers`)
|
||||
|
||||
---
|
||||
|
||||
## Action Handler Patterns
|
||||
|
||||
### Pattern: Create Handler
|
||||
```typescript
|
||||
// 1. Transpile: Universal → Flat
|
||||
protected transpileUniversalActionToFlatAction(
|
||||
universalAction,
|
||||
flatEntityMaps,
|
||||
) {
|
||||
return {
|
||||
type: 'create',
|
||||
metadataName: 'myEntity',
|
||||
flatEntity: fromUniversalFlatMyEntityToFlatMyEntity(
|
||||
universalAction.universalFlatEntity,
|
||||
flatEntityMaps,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Execute: Flat → Database
|
||||
protected async executeForMetadata(flatActions) {
|
||||
await this.insertFlatEntitiesInRepository({
|
||||
repository: this.myEntityRepository,
|
||||
flatEntities: flatActions.map(a => a.flatEntity),
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: Update Handler
|
||||
```typescript
|
||||
// Transpile with update-specific resolution
|
||||
protected transpileUniversalActionToFlatAction(
|
||||
universalAction,
|
||||
flatEntityMaps,
|
||||
) {
|
||||
const flatEntity = fromUniversalFlatMyEntityToFlatMyEntity(
|
||||
universalAction.universalFlatEntity,
|
||||
flatEntityMaps,
|
||||
);
|
||||
|
||||
const flatUpdates = resolveUniversalUpdateRelationIdentifiersToIds({
|
||||
metadataName: 'myEntity',
|
||||
universalUpdates: universalAction.universalUpdates,
|
||||
flatEntityMaps,
|
||||
});
|
||||
|
||||
return { type: 'update', metadataName: 'myEntity', flatEntity, updates: flatUpdates };
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern: Delete Handler
|
||||
```typescript
|
||||
// Use base class helper
|
||||
protected transpileUniversalActionToFlatAction(
|
||||
universalAction,
|
||||
flatEntityMaps,
|
||||
) {
|
||||
return this.transpileUniversalDeleteActionToFlatDeleteAction({
|
||||
universalAction,
|
||||
flatEntityMaps,
|
||||
fromUniversalFlatEntityToFlatEntity: fromUniversalFlatMyEntityToFlatMyEntity,
|
||||
});
|
||||
}
|
||||
|
||||
// Delete
|
||||
protected async executeForMetadata(flatActions) {
|
||||
const ids = flatActions.map(a => a.flatEntity.id);
|
||||
await this.myEntityRepository.delete(ids);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
Before moving to Step 5:
|
||||
|
||||
- [ ] Create action handler implemented
|
||||
- [ ] Update action handler implemented
|
||||
- [ ] Delete action handler implemented
|
||||
- [ ] All handlers extend appropriate base class
|
||||
- [ ] `transpileUniversalActionToFlatAction` implemented in all handlers
|
||||
- [ ] `executeForMetadata` implemented in all handlers
|
||||
- [ ] `executeForWorkspaceSchema` implemented (or returns empty)
|
||||
- [ ] Universal-to-flat conversion utility created
|
||||
- [ ] Create handler uses `insertFlatEntitiesInRepository`
|
||||
- [ ] Update handler uses `resolveUniversalUpdateRelationIdentifiersToIds`
|
||||
- [ ] Delete handler uses `transpileUniversalDeleteActionToFlatDeleteAction`
|
||||
- [ ] Delete handler uses hard delete (`delete()`)
|
||||
|
||||
---
|
||||
|
||||
## Next Step
|
||||
|
||||
Once action handlers are complete, proceed to:
|
||||
**[Syncable Entity: Integration (Step 5/6)](../syncable-entity-integration/SKILL.md)**
|
||||
|
||||
For complete workflow, see `@creating-syncable-entity` rule.
|
||||
@@ -0,0 +1,494 @@
|
||||
---
|
||||
name: syncable-entity-testing
|
||||
description: Create comprehensive integration tests for syncable entities in Twenty. Use when writing integration tests for metadata entities, covering validator exceptions, input transpilation errors, and CRUD operations. Tests are MANDATORY for all syncable entities.
|
||||
---
|
||||
|
||||
# Syncable Entity: Integration Testing (Step 6/6 - MANDATORY)
|
||||
|
||||
**Purpose**: Create comprehensive test suite covering all validation scenarios, input transpilation exceptions, and successful use cases.
|
||||
|
||||
**When to use**: After completing Steps 1-5. Integration tests are **REQUIRED** for all syncable entities.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
Tests must cover:
|
||||
1. **Failing scenarios** - All validator exceptions and input transpilation errors
|
||||
2. **Successful scenarios** - All CRUD operations and edge cases
|
||||
3. **Test utilities** - Reusable query factories and helper functions
|
||||
|
||||
**Test pattern**: Two-file pattern (query factory + wrapper) for each operation.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Create Test Utilities
|
||||
|
||||
### Pattern: Query Factory
|
||||
|
||||
**File**: `test/integration/metadata/suites/my-entity/utils/create-my-entity-query-factory.util.ts`
|
||||
|
||||
```typescript
|
||||
import gql from 'graphql-tag';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
|
||||
|
||||
export type CreateMyEntityFactoryInput = CreateMyEntityInput;
|
||||
|
||||
const DEFAULT_MY_ENTITY_GQL_FIELDS = `
|
||||
id
|
||||
name
|
||||
label
|
||||
description
|
||||
isCustom
|
||||
createdAt
|
||||
updatedAt
|
||||
`;
|
||||
|
||||
export const createMyEntityQueryFactory = ({
|
||||
input,
|
||||
gqlFields = DEFAULT_MY_ENTITY_GQL_FIELDS,
|
||||
}: PerformMetadataQueryParams<CreateMyEntityFactoryInput>) => ({
|
||||
query: gql`
|
||||
mutation CreateMyEntity($input: CreateMyEntityInput!) {
|
||||
createMyEntity(input: $input) {
|
||||
${gqlFields}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern: Wrapper Utility
|
||||
|
||||
**File**: `test/integration/metadata/suites/my-entity/utils/create-my-entity.util.ts`
|
||||
|
||||
```typescript
|
||||
import {
|
||||
type CreateMyEntityFactoryInput,
|
||||
createMyEntityQueryFactory,
|
||||
} from 'test/integration/metadata/suites/my-entity/utils/create-my-entity-query-factory.util';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { type CommonResponseBody } from 'test/integration/metadata/types/common-response-body.type';
|
||||
import { type PerformMetadataQueryParams } from 'test/integration/metadata/types/perform-metadata-query.type';
|
||||
import { warnIfErrorButNotExpectedToFail } from 'test/integration/metadata/utils/warn-if-error-but-not-expected-to-fail.util';
|
||||
import { warnIfNoErrorButExpectedToFail } from 'test/integration/metadata/utils/warn-if-no-error-but-expected-to-fail.util';
|
||||
import { type MyEntityDto } from 'src/engine/metadata-modules/my-entity/dtos/my-entity.dto';
|
||||
|
||||
export const createMyEntity = async ({
|
||||
input,
|
||||
gqlFields,
|
||||
expectToFail = false,
|
||||
token,
|
||||
}: PerformMetadataQueryParams<CreateMyEntityFactoryInput>): CommonResponseBody<{
|
||||
createMyEntity: MyEntityDto;
|
||||
}> => {
|
||||
const graphqlOperation = createMyEntityQueryFactory({
|
||||
input,
|
||||
gqlFields,
|
||||
});
|
||||
|
||||
const response = await makeMetadataAPIRequest(graphqlOperation, token);
|
||||
|
||||
if (expectToFail === true) {
|
||||
warnIfNoErrorButExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'My entity creation should have failed but did not',
|
||||
});
|
||||
}
|
||||
|
||||
if (expectToFail === false) {
|
||||
warnIfErrorButNotExpectedToFail({
|
||||
response,
|
||||
errorMessage: 'My entity creation has failed but should not',
|
||||
});
|
||||
}
|
||||
|
||||
return { data: response.body.data, errors: response.body.errors };
|
||||
};
|
||||
```
|
||||
|
||||
**Required utilities** (follow same pattern):
|
||||
- `update-my-entity-query-factory.util.ts` + `update-my-entity.util.ts`
|
||||
- `delete-my-entity-query-factory.util.ts` + `delete-my-entity.util.ts`
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Failing Creation Tests
|
||||
|
||||
**File**: `test/integration/metadata/suites/my-entity/failing-my-entity-creation.integration-spec.ts`
|
||||
|
||||
```typescript
|
||||
import { expectOneNotInternalServerErrorSnapshot } from 'test/integration/graphql/utils/expect-one-not-internal-server-error-snapshot.util';
|
||||
import { createMyEntity } from 'test/integration/metadata/suites/my-entity/utils/create-my-entity.util';
|
||||
import { deleteMyEntity } from 'test/integration/metadata/suites/my-entity/utils/delete-my-entity.util';
|
||||
import {
|
||||
eachTestingContextFilter,
|
||||
type EachTestingContext,
|
||||
} from 'twenty-shared/testing';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
|
||||
|
||||
type TestContext = {
|
||||
input: CreateMyEntityInput;
|
||||
};
|
||||
|
||||
type GlobalTestContext = {
|
||||
existingEntityLabel: string;
|
||||
existingEntityName: string;
|
||||
};
|
||||
|
||||
const globalTestContext: GlobalTestContext = {
|
||||
existingEntityLabel: 'Existing Test Entity',
|
||||
existingEntityName: 'existingTestEntity',
|
||||
};
|
||||
|
||||
type CreateMyEntityTestingContext = EachTestingContext<TestContext>[];
|
||||
|
||||
describe('My entity creation should fail', () => {
|
||||
let existingEntityId: string | undefined;
|
||||
|
||||
beforeAll(async () => {
|
||||
// Setup: Create entity for uniqueness tests
|
||||
const { data } = await createMyEntity({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: globalTestContext.existingEntityName,
|
||||
label: globalTestContext.existingEntityLabel,
|
||||
},
|
||||
});
|
||||
|
||||
existingEntityId = data.createMyEntity.id;
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup
|
||||
if (isDefined(existingEntityId)) {
|
||||
await deleteMyEntity({
|
||||
expectToFail: false,
|
||||
input: { id: existingEntityId },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const failingMyEntityCreationTestCases: CreateMyEntityTestingContext = [
|
||||
// Input transpilation validation
|
||||
{
|
||||
title: 'when name is missing',
|
||||
context: {
|
||||
input: {
|
||||
label: 'Entity Missing Name',
|
||||
} as CreateMyEntityInput,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when label is missing',
|
||||
context: {
|
||||
input: {
|
||||
name: 'entityMissingLabel',
|
||||
} as CreateMyEntityInput,
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when name is empty string',
|
||||
context: {
|
||||
input: {
|
||||
name: '',
|
||||
label: 'Empty Name Entity',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Validator business logic
|
||||
{
|
||||
title: 'when name already exists (uniqueness)',
|
||||
context: {
|
||||
input: {
|
||||
name: globalTestContext.existingEntityName,
|
||||
label: 'Duplicate Name Entity',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'when trying to create standard entity',
|
||||
context: {
|
||||
input: {
|
||||
name: 'myEntity',
|
||||
label: 'Standard Entity',
|
||||
isCustom: false,
|
||||
} as CreateMyEntityInput,
|
||||
},
|
||||
},
|
||||
|
||||
// Foreign key validation
|
||||
{
|
||||
title: 'when parentEntityId does not exist',
|
||||
context: {
|
||||
input: {
|
||||
name: 'invalidParentEntity',
|
||||
label: 'Invalid Parent Entity',
|
||||
parentEntityId: '00000000-0000-0000-0000-000000000000',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
it.each(eachTestingContextFilter(failingMyEntityCreationTestCases))(
|
||||
'$title',
|
||||
async ({ context }) => {
|
||||
const { errors } = await createMyEntity({
|
||||
expectToFail: true,
|
||||
input: context.input,
|
||||
});
|
||||
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
**Test coverage requirements**:
|
||||
- ✅ Missing required fields
|
||||
- ✅ Empty strings
|
||||
- ✅ Invalid format
|
||||
- ✅ Uniqueness violations
|
||||
- ✅ Standard entity protection
|
||||
- ✅ Foreign key validation
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Successful Creation Tests
|
||||
|
||||
**File**: `test/integration/metadata/suites/my-entity/successful-my-entity-creation.integration-spec.ts`
|
||||
|
||||
```typescript
|
||||
import { createMyEntity } from 'test/integration/metadata/suites/my-entity/utils/create-my-entity.util';
|
||||
import { deleteMyEntity } from 'test/integration/metadata/suites/my-entity/utils/delete-my-entity.util';
|
||||
import { type CreateMyEntityInput } from 'src/engine/metadata-modules/my-entity/dtos/create-my-entity.input';
|
||||
|
||||
describe('My entity creation should succeed', () => {
|
||||
let createdEntityId: string;
|
||||
|
||||
afterEach(async () => {
|
||||
if (createdEntityId) {
|
||||
await deleteMyEntity({
|
||||
expectToFail: false,
|
||||
input: { id: createdEntityId },
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should create entity with minimal required input', async () => {
|
||||
const { data } = await createMyEntity({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: 'minimalEntity',
|
||||
label: 'Minimal Entity',
|
||||
},
|
||||
});
|
||||
|
||||
createdEntityId = data?.createMyEntity?.id;
|
||||
|
||||
expect(data.createMyEntity).toMatchObject({
|
||||
id: expect.any(String),
|
||||
name: 'minimalEntity',
|
||||
label: 'Minimal Entity',
|
||||
description: null,
|
||||
isCustom: true,
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
});
|
||||
});
|
||||
|
||||
it('should create entity with all optional fields', async () => {
|
||||
const input = {
|
||||
name: 'fullEntity',
|
||||
label: 'Full Entity',
|
||||
description: 'Entity with all fields specified',
|
||||
} as const satisfies CreateMyEntityInput;
|
||||
|
||||
const { data } = await createMyEntity({
|
||||
expectToFail: false,
|
||||
input,
|
||||
});
|
||||
|
||||
createdEntityId = data?.createMyEntity?.id;
|
||||
|
||||
expect(data.createMyEntity).toMatchObject({
|
||||
id: expect.any(String),
|
||||
name: 'fullEntity',
|
||||
label: 'Full Entity',
|
||||
description: 'Entity with all fields specified',
|
||||
isCustom: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should sanitize input by trimming whitespace', async () => {
|
||||
const { data } = await createMyEntity({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: ' entityWithSpaces ',
|
||||
label: ' Entity With Spaces ',
|
||||
description: ' Description with spaces ',
|
||||
},
|
||||
});
|
||||
|
||||
createdEntityId = data?.createMyEntity?.id;
|
||||
|
||||
expect(data.createMyEntity).toMatchObject({
|
||||
id: expect.any(String),
|
||||
name: 'entityWithSpaces',
|
||||
label: 'Entity With Spaces',
|
||||
description: 'Description with spaces',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle long text content', async () => {
|
||||
const longDescription = 'A'.repeat(1000);
|
||||
|
||||
const { data } = await createMyEntity({
|
||||
expectToFail: false,
|
||||
input: {
|
||||
name: 'longDescEntity',
|
||||
label: 'Long Description Entity',
|
||||
description: longDescription,
|
||||
},
|
||||
});
|
||||
|
||||
createdEntityId = data?.createMyEntity?.id;
|
||||
|
||||
expect(data.createMyEntity).toMatchObject({
|
||||
id: expect.any(String),
|
||||
description: longDescription,
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Test coverage requirements**:
|
||||
- ✅ Minimal required input
|
||||
- ✅ All optional fields
|
||||
- ✅ Input sanitization
|
||||
- ✅ Long text content
|
||||
- ✅ Special characters
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Update and Delete Tests
|
||||
|
||||
Create similar test files for update and delete operations:
|
||||
|
||||
**Required files**:
|
||||
- `failing-my-entity-update.integration-spec.ts`
|
||||
- `successful-my-entity-update.integration-spec.ts`
|
||||
- `failing-my-entity-deletion.integration-spec.ts`
|
||||
- `successful-my-entity-deletion.integration-spec.ts`
|
||||
|
||||
---
|
||||
|
||||
## Testing Best Practices
|
||||
|
||||
### Pattern: Cleanup
|
||||
```typescript
|
||||
afterEach(async () => {
|
||||
if (createdEntityId) {
|
||||
await deleteMyEntity({
|
||||
expectToFail: false,
|
||||
input: { id: createdEntityId },
|
||||
});
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### Pattern: Type-Safe Inputs
|
||||
```typescript
|
||||
const input = {
|
||||
name: 'myEntity',
|
||||
label: 'My Entity',
|
||||
} as const satisfies CreateMyEntityInput;
|
||||
```
|
||||
|
||||
### Pattern: Snapshot Testing
|
||||
```typescript
|
||||
expectOneNotInternalServerErrorSnapshot({
|
||||
errors,
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all entity tests
|
||||
npx jest test/integration/metadata/suites/my-entity --config=packages/twenty-server/jest.config.mjs
|
||||
|
||||
# Run specific test file
|
||||
npx jest test/integration/metadata/suites/my-entity/failing-my-entity-creation.integration-spec.ts --config=packages/twenty-server/jest.config.mjs
|
||||
|
||||
# Update snapshots
|
||||
npx jest test/integration/metadata/suites/my-entity --updateSnapshot --config=packages/twenty-server/jest.config.mjs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Test Checklist
|
||||
|
||||
### Test Utilities
|
||||
- [ ] `create-my-entity-query-factory.util.ts` created
|
||||
- [ ] `create-my-entity.util.ts` created
|
||||
- [ ] `update-my-entity-query-factory.util.ts` created
|
||||
- [ ] `update-my-entity.util.ts` created
|
||||
- [ ] `delete-my-entity-query-factory.util.ts` created
|
||||
- [ ] `delete-my-entity.util.ts` created
|
||||
|
||||
### Failing Tests Coverage
|
||||
- [ ] Missing required fields
|
||||
- [ ] Empty string validation
|
||||
- [ ] Uniqueness violations
|
||||
- [ ] Standard entity protection
|
||||
- [ ] Foreign key validation
|
||||
- [ ] JSONB property validation (if applicable)
|
||||
|
||||
### Successful Tests Coverage
|
||||
- [ ] Create with minimal input
|
||||
- [ ] Create with all optional fields
|
||||
- [ ] Input sanitization (whitespace)
|
||||
- [ ] Long text content
|
||||
- [ ] Update single field
|
||||
- [ ] Update multiple fields
|
||||
- [ ] Successful deletion
|
||||
|
||||
### Snapshot Tests
|
||||
- [ ] All failing tests use `expectOneNotInternalServerErrorSnapshot`
|
||||
- [ ] Snapshots committed to `__snapshots__/` directory
|
||||
|
||||
---
|
||||
|
||||
## Success Criteria
|
||||
|
||||
Your integration tests are complete when:
|
||||
|
||||
✅ All test utilities created (minimum 6 files)
|
||||
✅ Failing creation tests cover all validators
|
||||
✅ Failing update tests cover business rules
|
||||
✅ Failing deletion tests cover protection rules
|
||||
✅ Successful tests cover all use cases
|
||||
✅ All snapshots generated and committed
|
||||
✅ All tests pass consistently
|
||||
✅ Test coverage meets requirements (>80%)
|
||||
|
||||
---
|
||||
|
||||
## Final Step
|
||||
|
||||
✅ **Step 6 Complete!** → Your syncable entity is fully tested and production-ready!
|
||||
|
||||
**Congratulations!** You've successfully created a new syncable entity in Twenty's workspace migration system.
|
||||
|
||||
For complete workflow, see `@creating-syncable-entity` rule.
|
||||
@@ -0,0 +1,340 @@
|
||||
---
|
||||
name: syncable-entity-types-and-constants
|
||||
description: Define types, entities, and central constant registrations for syncable entities in Twenty's workspace migration system. Use when creating new syncable entities, defining TypeORM entities, flat entity types, or registering in central constants (ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME, ALL_ONE_TO_MANY_METADATA_RELATIONS, ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY, ALL_MANY_TO_ONE_METADATA_RELATIONS).
|
||||
---
|
||||
|
||||
# Syncable Entity: Types & Constants (Step 1/6)
|
||||
|
||||
**Purpose**: Define all types, entities, and register in central constants. This is the foundation - everything else depends on these types being correct.
|
||||
|
||||
**When to use**: First step when creating any new syncable entity. Must be completed before other steps.
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
This step creates:
|
||||
1. Metadata name constant (twenty-shared)
|
||||
2. TypeORM entity (extends `SyncableEntity`)
|
||||
3. Flat entity types
|
||||
4. Action types (universal + flat)
|
||||
5. Central constant registrations (5 constants)
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Add Metadata Name
|
||||
|
||||
**File**: `packages/twenty-shared/src/metadata/all-metadata-name.constant.ts`
|
||||
|
||||
```typescript
|
||||
export const ALL_METADATA_NAME = {
|
||||
// ... existing entries
|
||||
myEntity: 'myEntity',
|
||||
} as const;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Create TypeORM Entity
|
||||
|
||||
**File**: `src/engine/metadata-modules/my-entity/entities/my-entity.entity.ts`
|
||||
|
||||
```typescript
|
||||
import { Entity, Column, ManyToOne, JoinColumn } from 'typeorm';
|
||||
import { SyncableEntity } from 'src/engine/workspace-manager/types/syncable-entity.interface';
|
||||
|
||||
@Entity({ name: 'myEntity' })
|
||||
export class MyEntityEntity extends SyncableEntity {
|
||||
@Column({ type: 'varchar' })
|
||||
name: string;
|
||||
|
||||
@Column({ type: 'varchar' })
|
||||
label: string;
|
||||
|
||||
@Column({ type: 'boolean', default: true })
|
||||
isCustom: boolean;
|
||||
|
||||
// Foreign key example (optional)
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
parentEntityId: string | null;
|
||||
|
||||
@ManyToOne(() => ParentEntityEntity, { nullable: true })
|
||||
@JoinColumn({ name: 'parentEntityId' })
|
||||
parentEntity: ParentEntityEntity | null;
|
||||
|
||||
// JSONB column example (optional)
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
settings: Record<string, any> | null;
|
||||
}
|
||||
```
|
||||
|
||||
**Key rules**:
|
||||
- Must extend `SyncableEntity` (provides `id`, `universalIdentifier`, `applicationId`, etc.)
|
||||
- Must have `isCustom` boolean column
|
||||
- Use `@Column({ type: 'jsonb' })` for JSON data
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Define Flat Entity Types
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type.ts`
|
||||
|
||||
```typescript
|
||||
import { type FlatEntityFrom } from 'src/engine/metadata-modules/flat-entity/types/flat-entity-from.type';
|
||||
import { type MyEntityEntity } from 'src/engine/metadata-modules/my-entity/entities/my-entity.entity';
|
||||
|
||||
export type FlatMyEntity = FlatEntityFrom<MyEntityEntity>;
|
||||
```
|
||||
|
||||
**Maps file** (if entity has indexed lookups):
|
||||
|
||||
```typescript
|
||||
// flat-my-entity-maps.type.ts
|
||||
export type FlatMyEntityMaps = {
|
||||
byId: Record<string, FlatMyEntity>;
|
||||
byName: Record<string, FlatMyEntity>;
|
||||
// Add other indexes as needed
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Define Editable Properties
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-my-entity/constants/editable-flat-my-entity-properties.constant.ts`
|
||||
|
||||
```typescript
|
||||
export const EDITABLE_FLAT_MY_ENTITY_PROPERTIES = [
|
||||
'name',
|
||||
'label',
|
||||
'description',
|
||||
'parentEntityId',
|
||||
'settings',
|
||||
] as const satisfies ReadonlyArray<keyof FlatMyEntity>;
|
||||
```
|
||||
|
||||
**Rule**: Only include properties that can be updated (exclude `id`, `createdAt`, `universalIdentifier`, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Define Action Types
|
||||
|
||||
**File**: `src/engine/workspace-manager/workspace-migration/workspace-migration-builder/builders/my-entity/types/workspace-migration-my-entity-action.type.ts`
|
||||
|
||||
```typescript
|
||||
import { type FlatMyEntity } from 'src/engine/metadata-modules/flat-my-entity/types/flat-my-entity.type';
|
||||
import { type UniversalFlatMyEntity } from 'src/engine/workspace-manager/workspace-migration/universal-flat-entity/types/universal-flat-my-entity.type';
|
||||
|
||||
// Universal actions (used by builder/runner)
|
||||
export type UniversalCreateMyEntityAction = {
|
||||
type: 'create';
|
||||
metadataName: 'myEntity';
|
||||
universalFlatEntity: UniversalFlatMyEntity;
|
||||
};
|
||||
|
||||
export type UniversalUpdateMyEntityAction = {
|
||||
type: 'update';
|
||||
metadataName: 'myEntity';
|
||||
universalFlatEntity: UniversalFlatMyEntity;
|
||||
universalUpdates: Partial<UniversalFlatMyEntity>;
|
||||
};
|
||||
|
||||
export type UniversalDeleteMyEntityAction = {
|
||||
type: 'delete';
|
||||
metadataName: 'myEntity';
|
||||
universalFlatEntity: UniversalFlatMyEntity;
|
||||
};
|
||||
|
||||
// Flat actions (internal to runner)
|
||||
export type FlatCreateMyEntityAction = {
|
||||
type: 'create';
|
||||
metadataName: 'myEntity';
|
||||
flatEntity: FlatMyEntity;
|
||||
};
|
||||
|
||||
export type FlatUpdateMyEntityAction = {
|
||||
type: 'update';
|
||||
metadataName: 'myEntity';
|
||||
flatEntity: FlatMyEntity;
|
||||
updates: Partial<FlatMyEntity>;
|
||||
};
|
||||
|
||||
export type FlatDeleteMyEntityAction = {
|
||||
type: 'delete';
|
||||
metadataName: 'myEntity';
|
||||
flatEntity: FlatMyEntity;
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Register in Central Constants
|
||||
|
||||
### 6a. AllFlatEntityTypesByMetadataName
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-entity/types/all-flat-entity-types-by-metadata-name.ts`
|
||||
|
||||
```typescript
|
||||
export type AllFlatEntityTypesByMetadataName = {
|
||||
// ... existing entries
|
||||
myEntity: {
|
||||
flatEntityMaps: FlatMyEntityMaps;
|
||||
universalActions: {
|
||||
create: UniversalCreateMyEntityAction;
|
||||
update: UniversalUpdateMyEntityAction;
|
||||
delete: UniversalDeleteMyEntityAction;
|
||||
};
|
||||
flatActions: {
|
||||
create: FlatCreateMyEntityAction;
|
||||
update: FlatUpdateMyEntityAction;
|
||||
delete: FlatDeleteMyEntityAction;
|
||||
};
|
||||
flatEntity: FlatMyEntity;
|
||||
universalFlatEntity: UniversalFlatMyEntity;
|
||||
entity: MyEntityEntity;
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
### 6b. ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-entity/constant/all-entity-properties-configuration-by-metadata-name.constant.ts`
|
||||
|
||||
```typescript
|
||||
export const ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME = {
|
||||
// ... existing entries
|
||||
myEntity: {
|
||||
name: { toCompare: true },
|
||||
label: { toCompare: true },
|
||||
description: { toCompare: true },
|
||||
parentEntityId: {
|
||||
toCompare: true,
|
||||
universalProperty: 'parentEntityUniversalIdentifier',
|
||||
},
|
||||
settings: {
|
||||
toCompare: true,
|
||||
toStringify: true,
|
||||
universalProperty: 'universalSettings',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
```
|
||||
|
||||
**Rules**:
|
||||
- `toCompare: true` → Editable property (checked for changes)
|
||||
- `toStringify: true` → JSONB/object property (needs JSON serialization)
|
||||
- `universalProperty` → Maps to universal version (for foreign keys & JSONB with `SerializedRelation`)
|
||||
|
||||
### 6c. ALL_ONE_TO_MANY_METADATA_RELATIONS
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-entity/constant/all-one-to-many-metadata-relations.constant.ts`
|
||||
|
||||
This constant is **type-checked** — values for `metadataName`, `flatEntityForeignKeyAggregator`, and `universalFlatEntityForeignKeyAggregator` are derived from entity type definitions. The aggregator names follow the pattern: remove trailing `'s'` from the relation property name, then append `Ids` or `UniversalIdentifiers`.
|
||||
|
||||
```typescript
|
||||
export const ALL_ONE_TO_MANY_METADATA_RELATIONS = {
|
||||
// ... existing entries
|
||||
myEntity: {
|
||||
// If myEntity has a `childEntities: ChildEntityEntity[]` property:
|
||||
childEntities: {
|
||||
metadataName: 'childEntity',
|
||||
flatEntityForeignKeyAggregator: 'childEntityIds',
|
||||
universalFlatEntityForeignKeyAggregator: 'childEntityUniversalIdentifiers',
|
||||
},
|
||||
// null for relations to non-syncable entities
|
||||
someNonSyncableRelation: null,
|
||||
},
|
||||
} as const;
|
||||
```
|
||||
|
||||
### 6d. ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-foreign-key.constant.ts`
|
||||
|
||||
Low-level primitive constant. Only contains `foreignKey` — the column name ending in `Id` that stores the foreign key. Type-checked against entity properties.
|
||||
|
||||
```typescript
|
||||
export const ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY = {
|
||||
// ... existing entries
|
||||
myEntity: {
|
||||
workspace: null,
|
||||
application: null,
|
||||
parentEntity: {
|
||||
foreignKey: 'parentEntityId',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
```
|
||||
|
||||
### 6e. ALL_MANY_TO_ONE_METADATA_RELATIONS
|
||||
|
||||
**File**: `src/engine/metadata-modules/flat-entity/constant/all-many-to-one-metadata-relations.constant.ts`
|
||||
|
||||
Derived from both `ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY` (for `foreignKey` type and `universalForeignKey` derivation) and `ALL_ONE_TO_MANY_METADATA_RELATIONS` (for `inverseOneToManyProperty` key constraint). This is the main constant consumed by utils and optimistic tooling.
|
||||
|
||||
```typescript
|
||||
export const ALL_MANY_TO_ONE_METADATA_RELATIONS = {
|
||||
// ... existing entries
|
||||
myEntity: {
|
||||
workspace: null,
|
||||
application: null,
|
||||
parentEntity: {
|
||||
metadataName: 'parentEntity',
|
||||
foreignKey: 'parentEntityId',
|
||||
inverseOneToManyProperty: 'myEntities', // key in ALL_ONE_TO_MANY_METADATA_RELATIONS['parentEntity'], or null if no inverse
|
||||
isNullable: false,
|
||||
universalForeignKey: 'parentEntityUniversalIdentifier',
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
```
|
||||
|
||||
**Derivation dependency graph**:
|
||||
|
||||
```
|
||||
ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY ALL_ONE_TO_MANY_METADATA_RELATIONS
|
||||
(foreignKey only) (metadataName, aggregators)
|
||||
│ │
|
||||
│ FK type + universalFK derivation │ inverseOneToManyProperty keys
|
||||
│ │
|
||||
└────────────────┬───────────────────────┘
|
||||
▼
|
||||
ALL_MANY_TO_ONE_METADATA_RELATIONS
|
||||
(metadataName, foreignKey, inverseOneToManyProperty,
|
||||
isNullable, universalForeignKey)
|
||||
```
|
||||
|
||||
**Rules**:
|
||||
- `workspace: null`, `application: null` — always present, always null (non-syncable relations)
|
||||
- `inverseOneToManyProperty` — must be a key in `ALL_ONE_TO_MANY_METADATA_RELATIONS[targetMetadataName]`, or `null` if the target entity doesn't expose an inverse one-to-many relation
|
||||
- `universalForeignKey` — derived from `foreignKey` by replacing the `Id` suffix with `UniversalIdentifier`
|
||||
- Optimistic utils resolve `flatEntityForeignKeyAggregator` / `universalFlatEntityForeignKeyAggregator` at runtime by looking up `inverseOneToManyProperty` in `ALL_ONE_TO_MANY_METADATA_RELATIONS`
|
||||
|
||||
---
|
||||
|
||||
## Checklist
|
||||
|
||||
Before moving to Step 2:
|
||||
|
||||
- [ ] Metadata name added to `ALL_METADATA_NAME`
|
||||
- [ ] TypeORM entity created (extends `SyncableEntity`)
|
||||
- [ ] `isCustom` column added
|
||||
- [ ] Flat entity type defined
|
||||
- [ ] Flat entity maps type defined (if needed)
|
||||
- [ ] Editable properties constant defined
|
||||
- [ ] Universal and flat action types defined
|
||||
- [ ] Registered in `AllFlatEntityTypesByMetadataName`
|
||||
- [ ] Registered in `ALL_ENTITY_PROPERTIES_CONFIGURATION_BY_METADATA_NAME`
|
||||
- [ ] Registered in `ALL_ONE_TO_MANY_METADATA_RELATIONS` (if entity has one-to-many relations)
|
||||
- [ ] Registered in `ALL_MANY_TO_ONE_METADATA_FOREIGN_KEY`
|
||||
- [ ] Registered in `ALL_MANY_TO_ONE_METADATA_RELATIONS`
|
||||
- [ ] TypeScript compiles without errors
|
||||
|
||||
---
|
||||
|
||||
## Next Step
|
||||
|
||||
Once all types and constants are defined, proceed to:
|
||||
**[Syncable Entity: Cache & Transform (Step 2/6)](../syncable-entity-cache-and-transform/SKILL.md)**
|
||||
|
||||
For complete workflow, see `@creating-syncable-entity` rule.
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
jobs:
|
||||
deploy-main:
|
||||
timeout-minutes: 3
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
steps:
|
||||
- name: Repository Dispatch
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
jobs:
|
||||
deploy-tag:
|
||||
timeout-minutes: 3
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
steps:
|
||||
- name: Repository Dispatch
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
|
||||
@@ -16,7 +16,7 @@ permissions:
|
||||
jobs:
|
||||
changed-files:
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
outputs:
|
||||
any_changed: ${{ steps.changed-files.outputs.any_changed }}
|
||||
steps:
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 45
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
@@ -771,16 +771,16 @@ jobs:
|
||||
kill $(cat /tmp/main-server.pid) || true
|
||||
fi
|
||||
|
||||
- name: Upload API specifications and diffs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: api-specifications-and-diffs
|
||||
path: |
|
||||
/tmp/main-server.log
|
||||
/tmp/current-server.log
|
||||
*-api.json
|
||||
*-schema-introspection.json
|
||||
*-diff.md
|
||||
*-diff.json
|
||||
# - name: Upload API specifications and diffs
|
||||
# if: always()
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: api-specifications-and-diffs
|
||||
# path: |
|
||||
# /tmp/main-server.log
|
||||
# /tmp/current-server.log
|
||||
# *-api.json
|
||||
# *-schema-introspection.json
|
||||
# *-diff.md
|
||||
# *-diff.json
|
||||
|
||||
|
||||
@@ -20,11 +20,12 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
packages/create-twenty-app/**
|
||||
!packages/create-twenty-app/package.json
|
||||
create-app-test:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test]
|
||||
@@ -49,7 +50,7 @@ jobs:
|
||||
ci-create-app-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
needs: [changed-files-check, create-app-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
|
||||
@@ -27,7 +27,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.11.0
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
ci-emails-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
needs: [changed-files-check, emails-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
|
||||
@@ -14,8 +14,8 @@ concurrency:
|
||||
|
||||
env:
|
||||
# restore-cache action adds 'v4-' prefix and '-<branch>-<sha>' suffix to the key
|
||||
STORYBOOK_BUILD_CACHE_KEY_FOR_RESTORE_ACTION: storybook-build-ubuntu-latest-8-cores-runner
|
||||
STORYBOOK_BUILD_CACHE_KEY_FOR_SAVE_ACTION: v4-storybook-build-ubuntu-latest-8-cores-runner-${{ github.ref_name }}-${{ github.sha }}
|
||||
STORYBOOK_BUILD_CACHE_KEY_FOR_RESTORE_ACTION: storybook-build-depot-ubuntu-24.04-8-runner
|
||||
STORYBOOK_BUILD_CACHE_KEY_FOR_SAVE_ACTION: v4-storybook-build-depot-ubuntu-24.04-8-runner-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
@@ -28,18 +28,21 @@ jobs:
|
||||
packages/twenty-ui/**
|
||||
packages/twenty-shared/**
|
||||
packages/twenty-sdk/**
|
||||
!packages/twenty-sdk/package.json
|
||||
changed-files-check-e2e:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
packages/**
|
||||
!packages/create-twenty-app/package.json
|
||||
!packages/twenty-sdk/package.json
|
||||
playwright.config.ts
|
||||
.github/workflows/ci-front.yaml
|
||||
front-sb-build:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
env:
|
||||
REACT_APP_SERVER_BASE_URL: http://localhost:3000
|
||||
steps:
|
||||
@@ -65,7 +68,7 @@ jobs:
|
||||
key: ${{ env.STORYBOOK_BUILD_CACHE_KEY_FOR_SAVE_ACTION }}
|
||||
front-sb-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: front-sb-build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -95,52 +98,52 @@ jobs:
|
||||
run: npx nx reset:env twenty-front
|
||||
- name: Run storybook tests
|
||||
run: npx nx storybook:test twenty-front --configuration=${{ matrix.storybook_scope }} --shard=${{ matrix.shard }}/${{ env.SHARD_COUNTER }}
|
||||
- name: Rename coverage file
|
||||
run: |
|
||||
if [ -f "packages/twenty-front/coverage/storybook/coverage-final.json" ]; then
|
||||
mv packages/twenty-front/coverage/storybook/coverage-final.json packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
|
||||
else
|
||||
echo "Error: coverage-final.json not found"
|
||||
ls -la packages/twenty-front/coverage/storybook/ || echo "Coverage directory does not exist"
|
||||
exit 1
|
||||
fi
|
||||
- name: Upload coverage artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
retention-days: 1
|
||||
name: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-${{ matrix.shard }}
|
||||
path: packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
|
||||
merge-reports-and-check-coverage:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
needs: front-sb-test
|
||||
env:
|
||||
PATH_TO_COVERAGE: packages/twenty-front/coverage/storybook
|
||||
strategy:
|
||||
matrix:
|
||||
storybook_scope: [modules, pages, performance]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- uses: actions/download-artifact@v4
|
||||
with:
|
||||
pattern: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-*
|
||||
merge-multiple: true
|
||||
path: coverage-artifacts
|
||||
- name: Merge coverage reports
|
||||
run: |
|
||||
mkdir -p ${{ env.PATH_TO_COVERAGE }}
|
||||
npx nyc merge coverage-artifacts ${{ env.PATH_TO_COVERAGE }}/coverage-storybook.json
|
||||
- name: Checking coverage
|
||||
run: npx nx storybook:coverage twenty-front --checkCoverage=true --configuration=${{ matrix.storybook_scope }}
|
||||
# - name: Rename coverage file
|
||||
# run: |
|
||||
# if [ -f "packages/twenty-front/coverage/storybook/coverage-final.json" ]; then
|
||||
# mv packages/twenty-front/coverage/storybook/coverage-final.json packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
|
||||
# else
|
||||
# echo "Error: coverage-final.json not found"
|
||||
# ls -la packages/twenty-front/coverage/storybook/ || echo "Coverage directory does not exist"
|
||||
# exit 1
|
||||
# fi
|
||||
# - name: Upload coverage artifact
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# retention-days: 1
|
||||
# name: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-${{ matrix.shard }}
|
||||
# path: packages/twenty-front/coverage/storybook/coverage-shard-${{matrix.shard}}.json
|
||||
# merge-reports-and-check-coverage:
|
||||
# timeout-minutes: 30
|
||||
# runs-on: depot-ubuntu-24.04
|
||||
# needs: front-sb-test
|
||||
# env:
|
||||
# PATH_TO_COVERAGE: packages/twenty-front/coverage/storybook
|
||||
# strategy:
|
||||
# matrix:
|
||||
# storybook_scope: [modules, pages, performance]
|
||||
# steps:
|
||||
# - uses: actions/checkout@v4
|
||||
# with:
|
||||
# fetch-depth: 0
|
||||
# - name: Install dependencies
|
||||
# uses: ./.github/actions/yarn-install
|
||||
# - uses: actions/download-artifact@v4
|
||||
# with:
|
||||
# pattern: coverage-artifacts-${{ matrix.storybook_scope }}-${{ github.run_id }}-*
|
||||
# merge-multiple: true
|
||||
# path: coverage-artifacts
|
||||
# - name: Merge coverage reports
|
||||
# run: |
|
||||
# mkdir -p ${{ env.PATH_TO_COVERAGE }}
|
||||
# npx nyc merge coverage-artifacts ${{ env.PATH_TO_COVERAGE }}/coverage-storybook.json
|
||||
# - name: Checking coverage
|
||||
# run: npx nx storybook:coverage twenty-front --checkCoverage=true --configuration=${{ matrix.storybook_scope }}
|
||||
front-chromatic-deployment:
|
||||
timeout-minutes: 30
|
||||
if: false
|
||||
needs: front-sb-build
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
env:
|
||||
REACT_APP_SERVER_BASE_URL: http://127.0.0.1:3000
|
||||
CHROMATIC_PROJECT_TOKEN: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
|
||||
@@ -166,8 +169,9 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
TASK_CACHE_KEY: front-task-${{ matrix.task }}
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -194,6 +198,7 @@ jobs:
|
||||
tag: scope:frontend
|
||||
tasks: reset:env
|
||||
- name: Run ${{ matrix.task }} task
|
||||
id: run-task
|
||||
uses: ./.github/actions/nx-affected
|
||||
with:
|
||||
tag: scope:frontend
|
||||
@@ -206,7 +211,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
env:
|
||||
NODE_OPTIONS: "--max-old-space-size=10240"
|
||||
steps:
|
||||
@@ -224,14 +229,14 @@ jobs:
|
||||
run: npx nx reset:env twenty-front
|
||||
- name: Build frontend
|
||||
run: npx nx build twenty-front
|
||||
- name: Upload frontend build artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: frontend-build
|
||||
path: packages/twenty-front/build
|
||||
retention-days: 1
|
||||
# - name: Upload frontend build artifact
|
||||
# uses: actions/upload-artifact@v4
|
||||
# with:
|
||||
# name: frontend-build
|
||||
# path: packages/twenty-front/build
|
||||
# retention-days: 1
|
||||
e2e-test:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
needs: [changed-files-check-e2e, front-build]
|
||||
if: |
|
||||
always() &&
|
||||
@@ -291,15 +296,18 @@ jobs:
|
||||
cp packages/twenty-front/.env.example packages/twenty-front/.env
|
||||
npx nx reset:env:e2e-testing-server twenty-server
|
||||
|
||||
- name: Download frontend build artifact
|
||||
if: needs.front-build.result == 'success'
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: frontend-build
|
||||
path: packages/twenty-front/build
|
||||
# - name: Download frontend build artifact
|
||||
# if: needs.front-build.result == 'success'
|
||||
# uses: actions/download-artifact@v4
|
||||
# with:
|
||||
# name: frontend-build
|
||||
# path: packages/twenty-front/build
|
||||
|
||||
- name: Build frontend (if not available from front-build)
|
||||
if: needs.front-build.result == 'skipped'
|
||||
# - name: Build frontend (if not available from front-build)
|
||||
# if: needs.front-build.result == 'skipped'
|
||||
# run: NODE_ENV=production NODE_OPTIONS="--max-old-space-size=10240" npx nx build twenty-front
|
||||
|
||||
- name: Build frontend
|
||||
run: NODE_ENV=production NODE_OPTIONS="--max-old-space-size=10240" npx nx build twenty-front
|
||||
|
||||
- name: Build server
|
||||
@@ -331,23 +339,23 @@ jobs:
|
||||
- name: Run Playwright tests
|
||||
run: npx nx test twenty-e2e-testing
|
||||
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report
|
||||
path: packages/twenty-e2e-testing/run_results/
|
||||
retention-days: 30
|
||||
# - uses: actions/upload-artifact@v4
|
||||
# if: always()
|
||||
# with:
|
||||
# name: playwright-report
|
||||
# path: packages/twenty-e2e-testing/run_results/
|
||||
# retention-days: 30
|
||||
|
||||
ci-front-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
needs:
|
||||
[
|
||||
changed-files-check,
|
||||
front-task,
|
||||
front-build,
|
||||
merge-reports-and-check-coverage,
|
||||
# merge-reports-and-check-coverage,
|
||||
front-sb-test,
|
||||
front-sb-build,
|
||||
]
|
||||
@@ -358,7 +366,7 @@ jobs:
|
||||
ci-e2e-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
needs: [changed-files-check-e2e, e2e-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
|
||||
@@ -25,7 +25,7 @@ defaults:
|
||||
jobs:
|
||||
create_pr:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -15,7 +15,7 @@ defaults:
|
||||
jobs:
|
||||
tag_and_release:
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
if: github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'release')
|
||||
steps:
|
||||
- name: Check PR Author
|
||||
|
||||
@@ -18,11 +18,12 @@ jobs:
|
||||
with:
|
||||
files: |
|
||||
packages/twenty-sdk/**
|
||||
!packages/twenty-sdk/package.json
|
||||
sdk-test:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test:unit, storybook:build, storybook:test, test:integration]
|
||||
@@ -49,7 +50,7 @@ jobs:
|
||||
tasks: ${{ matrix.task }}
|
||||
sdk-e2e-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: [changed-files-check, sdk-test]
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
services:
|
||||
@@ -93,7 +94,7 @@ jobs:
|
||||
ci-sdk-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
needs: [changed-files-check, sdk-test, sdk-e2e-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
@@ -143,7 +143,7 @@ jobs:
|
||||
key: ${{ steps.restore-server-setup-cache.outputs.cache-primary-key }}
|
||||
server-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: server-setup
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
@@ -164,7 +164,7 @@ jobs:
|
||||
|
||||
server-integration-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
runs-on: depot-ubuntu-24.04-8
|
||||
needs: server-setup
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -250,7 +250,7 @@ jobs:
|
||||
ci-server-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
needs: [changed-files-check, server-setup, server-test, server-integration-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
|
||||
@@ -22,7 +22,9 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
env:
|
||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test]
|
||||
@@ -45,7 +47,7 @@ jobs:
|
||||
ci-shared-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
needs: [changed-files-check, shared-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
ci-test-docker-compose-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
needs: [changed-files-check, test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
|
||||
@@ -25,7 +25,7 @@ concurrency:
|
||||
jobs:
|
||||
danger-js:
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
if: github.event.action != 'closed'
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -38,7 +38,7 @@ jobs:
|
||||
|
||||
congratulate:
|
||||
timeout-minutes: 3
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
if: github.event.action == 'closed' && github.event.pull_request.merged == true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 10
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
@@ -65,7 +65,7 @@ jobs:
|
||||
ci-website-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
needs: [changed-files-check, website-build]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
name: CI Zapier
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}
|
||||
|
||||
jobs:
|
||||
changed-files-check:
|
||||
uses: ./.github/workflows/changed-files.yaml
|
||||
with:
|
||||
files: |
|
||||
packages/twenty-zapier/**
|
||||
packages/twenty-server/**
|
||||
!packages/twenty-zapier/package.json
|
||||
!packages/twenty-zapier/CHANGELOG.md
|
||||
zapier-test:
|
||||
needs: changed-files-check
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test, validate]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/cancel-workflow-action@0.11.0
|
||||
with:
|
||||
access_token: ${{ github.token }}
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build
|
||||
run: npx nx build twenty-zapier
|
||||
- name: Run ${{ matrix.task }} task
|
||||
uses: ./.github/actions/nx-affected
|
||||
with:
|
||||
tag: scope:zapier
|
||||
tasks: ${{ matrix.task }}
|
||||
ci-zapier-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, zapier-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
run: exit 1
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
(github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@claude') && github.event.comment.user.type != 'Bot') ||
|
||||
(github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && github.event.review.user.type != 'Bot') ||
|
||||
(github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')))
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
|
||||
claude-cross-repo:
|
||||
if: github.event_name == 'repository_dispatch'
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
timeout-minutes: 60
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -34,7 +34,7 @@ concurrency:
|
||||
jobs:
|
||||
pull_docs_translations:
|
||||
name: Pull docs translations
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -21,7 +21,7 @@ concurrency:
|
||||
jobs:
|
||||
push_docs:
|
||||
name: Push documentation to Crowdin
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -32,7 +32,7 @@ concurrency:
|
||||
jobs:
|
||||
pull_translations:
|
||||
name: Pull translations
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -17,7 +17,7 @@ concurrency:
|
||||
jobs:
|
||||
extract_translations:
|
||||
name: Extract and upload translations
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -18,7 +18,7 @@ concurrency:
|
||||
jobs:
|
||||
qa_report:
|
||||
name: Generate QA Report
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
trigger-preview:
|
||||
if: github.event.action == 'opened' || github.event.action == 'synchronize' || github.event.action == 'reopened' || (github.event.action == 'labeled' && github.event.label.name == 'preview-app')
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: depot-ubuntu-24.04
|
||||
steps:
|
||||
- name: Trigger preview environment workflow
|
||||
uses: peter-evans/repository-dispatch@v2
|
||||
|
||||
@@ -17,7 +17,7 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.client_payload.pr_head_sha }}
|
||||
|
||||
|
||||
- name: Run compose setup
|
||||
run: |
|
||||
echo "Patching docker-compose.yml..."
|
||||
@@ -25,17 +25,17 @@ jobs:
|
||||
yq eval 'del(.services.server.image)' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.server.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.server.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
|
||||
|
||||
|
||||
yq eval 'del(.services.worker.image)' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.worker.build.context = "../../"' -i packages/twenty-docker/docker-compose.yml
|
||||
yq eval '.services.worker.build.dockerfile = "./packages/twenty-docker/twenty/Dockerfile"' -i packages/twenty-docker/docker-compose.yml
|
||||
|
||||
|
||||
echo "Adding SIGN_IN_PREFILLED environment variable to server service..."
|
||||
yq eval '.services.server.environment.SIGN_IN_PREFILLED = "${SIGN_IN_PREFILLED}"' -i packages/twenty-docker/docker-compose.yml
|
||||
|
||||
|
||||
echo "Setting up .env file..."
|
||||
cp packages/twenty-docker/.env.example packages/twenty-docker/.env
|
||||
|
||||
|
||||
echo "Generating secrets..."
|
||||
echo "" >> packages/twenty-docker/.env
|
||||
echo "# === Randomly generated secrets ===" >> packages/twenty-docker/.env
|
||||
@@ -46,24 +46,24 @@ jobs:
|
||||
cd packages/twenty-docker/
|
||||
docker compose build
|
||||
working-directory: ./
|
||||
|
||||
|
||||
- name: Create Tunnel
|
||||
id: expose-tunnel
|
||||
uses: codetalkio/expose-tunnel@v1.5.0
|
||||
with:
|
||||
service: bore.pub
|
||||
port: 3000
|
||||
|
||||
|
||||
- name: Start services with correct SERVER_URL
|
||||
run: |
|
||||
cd packages/twenty-docker/
|
||||
|
||||
|
||||
# Update the SERVER_URL with the tunnel URL
|
||||
echo "Setting SERVER_URL to ${{ steps.expose-tunnel.outputs.tunnel-url }}"
|
||||
sed -i '/SERVER_URL=/d' .env
|
||||
echo "" >> .env
|
||||
echo "SERVER_URL=${{ steps.expose-tunnel.outputs.tunnel-url }}" >> .env
|
||||
|
||||
|
||||
# Start the services
|
||||
echo "Docker compose up..."
|
||||
docker compose up -d || {
|
||||
@@ -71,7 +71,7 @@ jobs:
|
||||
docker compose logs
|
||||
exit 1
|
||||
}
|
||||
|
||||
|
||||
echo "Waiting for services to be ready..."
|
||||
count=0
|
||||
while [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-db-1) = "healthy" ] || [ ! $(docker inspect --format='{{.State.Health.Status}}' twenty-server-1) = "healthy" ]; do
|
||||
@@ -84,7 +84,7 @@ jobs:
|
||||
fi
|
||||
echo "Still waiting for services... ($count/60)"
|
||||
done
|
||||
|
||||
|
||||
echo "All services are up and running!"
|
||||
working-directory: ./
|
||||
|
||||
@@ -104,7 +104,7 @@ jobs:
|
||||
echo "✅ Preview Environment Ready!"
|
||||
echo "🔗 Preview URL: ${{ steps.expose-tunnel.outputs.tunnel-url }}"
|
||||
echo "⏱️ This environment will be available for 5 hours"
|
||||
|
||||
|
||||
- name: Post comment on PR
|
||||
uses: actions/github-script@v6
|
||||
with:
|
||||
@@ -113,21 +113,21 @@ jobs:
|
||||
const COMMENT_MARKER = '<!-- PR_PREVIEW_ENV -->';
|
||||
const commentBody = `${COMMENT_MARKER}
|
||||
🚀 **Preview Environment Ready!**
|
||||
|
||||
|
||||
Your preview environment is available at: ${{ steps.expose-tunnel.outputs.tunnel-url }}
|
||||
|
||||
|
||||
This environment will automatically shut down when the PR is closed or after 5 hours.`;
|
||||
|
||||
|
||||
// Get all comments
|
||||
const {data: comments} = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: ${{ github.event.client_payload.pr_number }},
|
||||
});
|
||||
|
||||
|
||||
// Find our comment
|
||||
const botComment = comments.find(comment => comment.body.includes(COMMENT_MARKER));
|
||||
|
||||
|
||||
if (botComment) {
|
||||
// Update existing comment
|
||||
await github.rest.issues.updateComment({
|
||||
@@ -147,13 +147,13 @@ jobs:
|
||||
});
|
||||
console.log('Created new comment');
|
||||
}
|
||||
|
||||
|
||||
- name: Keep tunnel alive for 5 hours
|
||||
run: timeout 300m sleep 18000 # Stop on whichever we reach first (300m or 5hour sleep)
|
||||
|
||||
|
||||
- name: Cleanup
|
||||
if: always()
|
||||
run: |
|
||||
cd packages/twenty-docker/
|
||||
docker compose down -v
|
||||
working-directory: ./
|
||||
working-directory: ./
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
.nx/installation
|
||||
.nx/cache
|
||||
.nx/workspace-data
|
||||
.nx/nxw.js
|
||||
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
@@ -49,3 +50,4 @@ dump.rdb
|
||||
mcp.json
|
||||
/.junie/
|
||||
TRANSLATION_QA_REPORT.md
|
||||
.playwright-mcp/
|
||||
|
||||
-115
@@ -1,115 +0,0 @@
|
||||
"use strict";
|
||||
// This file should be committed to your repository! It wraps Nx and ensures
|
||||
// that your local installation matches nx.json.
|
||||
// See: https://nx.dev/recipes/installation/install-non-javascript for more info.
|
||||
|
||||
|
||||
|
||||
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const cp = require('child_process');
|
||||
const installationPath = path.join(__dirname, 'installation', 'package.json');
|
||||
function matchesCurrentNxInstall(currentInstallation, nxJsonInstallation) {
|
||||
if (!currentInstallation.devDependencies ||
|
||||
!Object.keys(currentInstallation.devDependencies).length) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
if (currentInstallation.devDependencies['nx'] !==
|
||||
nxJsonInstallation.version ||
|
||||
require(path.join(path.dirname(installationPath), 'node_modules', 'nx', 'package.json')).version !== nxJsonInstallation.version) {
|
||||
return false;
|
||||
}
|
||||
for (const [plugin, desiredVersion] of Object.entries(nxJsonInstallation.plugins || {})) {
|
||||
if (currentInstallation.devDependencies[plugin] !== desiredVersion) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
function ensureDir(p) {
|
||||
if (!fs.existsSync(p)) {
|
||||
fs.mkdirSync(p, { recursive: true });
|
||||
}
|
||||
}
|
||||
function getCurrentInstallation() {
|
||||
try {
|
||||
return require(installationPath);
|
||||
}
|
||||
catch {
|
||||
return {
|
||||
name: 'nx-installation',
|
||||
version: '0.0.0',
|
||||
devDependencies: {},
|
||||
};
|
||||
}
|
||||
}
|
||||
function performInstallation(currentInstallation, nxJson) {
|
||||
fs.writeFileSync(installationPath, JSON.stringify({
|
||||
name: 'nx-installation',
|
||||
devDependencies: {
|
||||
nx: nxJson.installation.version,
|
||||
...nxJson.installation.plugins,
|
||||
},
|
||||
}));
|
||||
try {
|
||||
cp.execSync('npm i', {
|
||||
cwd: path.dirname(installationPath),
|
||||
stdio: 'inherit',
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
// revert possible changes to the current installation
|
||||
fs.writeFileSync(installationPath, JSON.stringify(currentInstallation));
|
||||
// rethrow
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
function ensureUpToDateInstallation() {
|
||||
const nxJsonPath = path.join(__dirname, '..', 'nx.json');
|
||||
let nxJson;
|
||||
try {
|
||||
nxJson = require(nxJsonPath);
|
||||
if (!nxJson.installation) {
|
||||
console.error('[NX]: The "installation" entry in the "nx.json" file is required when running the nx wrapper. See https://nx.dev/recipes/installation/install-non-javascript');
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
catch {
|
||||
console.error('[NX]: The "nx.json" file is required when running the nx wrapper. See https://nx.dev/recipes/installation/install-non-javascript');
|
||||
process.exit(1);
|
||||
}
|
||||
try {
|
||||
ensureDir(path.join(__dirname, 'installation'));
|
||||
const currentInstallation = getCurrentInstallation();
|
||||
if (!matchesCurrentNxInstall(currentInstallation, nxJson.installation)) {
|
||||
performInstallation(currentInstallation, nxJson);
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
const messageLines = [
|
||||
'[NX]: Nx wrapper failed to synchronize installation.',
|
||||
];
|
||||
if (e instanceof Error) {
|
||||
messageLines.push('');
|
||||
messageLines.push(e.message);
|
||||
messageLines.push(e.stack);
|
||||
}
|
||||
else {
|
||||
messageLines.push(e.toString());
|
||||
}
|
||||
console.error(messageLines.join('\n'));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
if (!process.env.NX_WRAPPER_SKIP_INSTALL) {
|
||||
ensureUpToDateInstallation();
|
||||
}
|
||||
|
||||
require('./installation/node_modules/nx/bin/nx');
|
||||
@@ -28,6 +28,8 @@ npx jest path/to/test.test.ts --config=packages/PROJECT/jest.config.mjs
|
||||
npx nx test twenty-front # Frontend unit tests
|
||||
npx nx test twenty-server # Backend unit tests
|
||||
npx nx run twenty-server:test:integration:with-db-reset # Integration tests with DB reset
|
||||
# To run an indivual test or a pattern of tests, use the following command:
|
||||
cd packages/{workspace} && npx jest "pattern or filename"
|
||||
|
||||
# Storybook
|
||||
npx nx storybook:build twenty-front
|
||||
@@ -88,7 +90,7 @@ npx nx run twenty-front:graphql:generate --configuration=metadata
|
||||
## Architecture Overview
|
||||
|
||||
### Tech Stack
|
||||
- **Frontend**: React 18, TypeScript, Recoil (state management), Emotion (styling), Vite
|
||||
- **Frontend**: React 18, TypeScript, Jotai (state management), Emotion (styling), Vite
|
||||
- **Backend**: NestJS, TypeORM, PostgreSQL, Redis, GraphQL (with GraphQL Yoga)
|
||||
- **Monorepo**: Nx workspace managed with Yarn 4
|
||||
|
||||
@@ -136,7 +138,7 @@ packages/
|
||||
- Multi-line comments use multiple `//` lines, not `/** */`
|
||||
|
||||
### State Management
|
||||
- **Recoil** for global state: atoms for primitive state, selectors for derived state, atom families for dynamic collections
|
||||
- **Jotai** for global state: atoms for primitive state, selectors for derived state, atom families for dynamic collections
|
||||
- Component-specific state with React hooks (`useState`, `useReducer` for complex logic)
|
||||
- GraphQL cache managed by Apollo Client
|
||||
- Use functional state updates: `setState(prev => prev + 1)`
|
||||
|
||||
@@ -28,7 +28,7 @@ See:
|
||||
🚀 [Self-hosting](https://docs.twenty.com/developers/self-hosting/docker-compose)
|
||||
🖥️ [Local Setup](https://docs.twenty.com/developers/local-setup)
|
||||
|
||||
# Does the world need another CRM?
|
||||
# Why Twenty
|
||||
|
||||
We built Twenty for three reasons:
|
||||
|
||||
@@ -120,6 +120,7 @@ Below are a few features we have implemented to date:
|
||||
<a href="https://greptile.com"><img src="./packages/twenty-website/public/images/readme/greptile.png" height="30" alt="Greptile" /></a>
|
||||
<a href="https://sentry.io/"><img src="./packages/twenty-website/public/images/readme/sentry.png" height="30" alt="Sentry" /></a>
|
||||
<a href="https://crowdin.com/"><img src="./packages/twenty-website/public/images/readme/crowdin.png" height="30" alt="Crowdin" /></a>
|
||||
<a href="https://e2b.dev/"><img src="./packages/twenty-website/public/images/readme/e2b.svg" height="30" alt="E2B" /></a>
|
||||
</p>
|
||||
|
||||
Thanks to these amazing services that we use and recommend for UI testing (Chromatic), code review (Greptile), catching bugs (Sentry) and translating (Crowdin).
|
||||
|
||||
@@ -83,6 +83,10 @@ export default [
|
||||
sourceTag: 'scope:frontend',
|
||||
onlyDependOnLibsWithTags: ['scope:shared', 'scope:frontend'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:zapier',
|
||||
onlyDependOnLibsWithTags: ['scope:shared', 'scope:zapier'],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -118,6 +118,7 @@
|
||||
"outputs": ["{projectRoot}/coverage"],
|
||||
"options": {
|
||||
"jestConfig": "{projectRoot}/jest.config.mjs",
|
||||
"silent": true,
|
||||
"coverage": true,
|
||||
"coverageReporters": ["text-summary"],
|
||||
"cacheDirectory": "../../.cache/jest/{projectRoot}"
|
||||
@@ -272,9 +273,6 @@
|
||||
"inputs": ["default", "^default"]
|
||||
}
|
||||
},
|
||||
"installation": {
|
||||
"version": "22.3.3"
|
||||
},
|
||||
"generators": {
|
||||
"@nx/react": {
|
||||
"application": {
|
||||
|
||||
+5
-5
@@ -48,7 +48,6 @@
|
||||
"react-responsive": "^9.0.2",
|
||||
"react-router-dom": "^6.4.4",
|
||||
"react-tooltip": "^5.13.1",
|
||||
"recoil": "^0.7.7",
|
||||
"remark-gfm": "^3.0.1",
|
||||
"rxjs": "^7.2.0",
|
||||
"semver": "^7.5.4",
|
||||
@@ -91,10 +90,10 @@
|
||||
"@storybook/react-vite": "^10.1.11",
|
||||
"@storybook/test-runner": "^0.24.2",
|
||||
"@stylistic/eslint-plugin": "^1.5.0",
|
||||
"@swc-node/register": "1.8.0",
|
||||
"@swc-node/register": "1.11.1",
|
||||
"@swc/cli": "^0.3.12",
|
||||
"@swc/core": "1.13.3",
|
||||
"@swc/helpers": "~0.5.2",
|
||||
"@swc/core": "1.15.11",
|
||||
"@swc/helpers": "~0.5.18",
|
||||
"@swc/jest": "^0.2.39",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
@@ -137,7 +136,7 @@
|
||||
"@typescript-eslint/parser": "^8.39.0",
|
||||
"@typescript-eslint/utils": "^8.39.0",
|
||||
"@typescript/native-preview": "^7.0.0-dev.20260116.1",
|
||||
"@vitejs/plugin-react-swc": "3.11.0",
|
||||
"@vitejs/plugin-react-swc": "4.2.3",
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"@vitest/coverage-istanbul": "^4.0.18",
|
||||
"@vitest/coverage-v8": "^4.0.18",
|
||||
@@ -183,6 +182,7 @@
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-loader": "^9.2.3",
|
||||
"ts-node": "10.9.1",
|
||||
"tsc-alias": "^1.8.16",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.17.0",
|
||||
"vite": "^7.0.0",
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
Create Twenty App is the official scaffolding CLI for building apps on top of [Twenty CRM](https://twenty.com). It sets up a ready‑to‑run project that works seamlessly with the [twenty-sdk](https://www.npmjs.com/package/twenty-sdk).
|
||||
|
||||
- Zero‑config project bootstrap
|
||||
- Preconfigured scripts for auth, dev mode (watch & sync), generate, uninstall, and function management
|
||||
- Preconfigured scripts for auth, dev mode (watch & sync), uninstall, and function management
|
||||
- Strong TypeScript support and typed client generation
|
||||
|
||||
## Documentation
|
||||
@@ -31,49 +31,86 @@ See Twenty application documentation https://docs.twenty.com/developers/extend/c
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# If you don't use yarn@4
|
||||
corepack enable
|
||||
yarn install
|
||||
|
||||
# Get help
|
||||
yarn run help
|
||||
# Get help and list all available commands
|
||||
yarn twenty help
|
||||
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
yarn auth:login
|
||||
yarn twenty auth:login
|
||||
|
||||
# Add a new entity to your application (guided)
|
||||
yarn entity:add
|
||||
|
||||
# Generate a typed Twenty client and workspace entity types
|
||||
yarn app:generate
|
||||
yarn twenty entity:add
|
||||
|
||||
# Start dev mode: watches, builds, and syncs local changes to your workspace
|
||||
yarn app:dev
|
||||
# (also auto-generates a typed API client in node_modules/twenty-sdk/generated)
|
||||
yarn twenty app:dev
|
||||
|
||||
# Watch your application's function logs
|
||||
yarn function:logs
|
||||
yarn twenty function:logs
|
||||
|
||||
# Execute a function with a JSON payload
|
||||
yarn function:execute -n my-function -p '{"key": "value"}'
|
||||
yarn twenty function:execute -n my-function -p '{"key": "value"}'
|
||||
|
||||
# Execute the post-install function
|
||||
yarn twenty function:execute --postInstall
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
yarn app:uninstall
|
||||
yarn twenty app:uninstall
|
||||
```
|
||||
|
||||
## Scaffolding modes
|
||||
|
||||
Control which example files are included when creating a new app:
|
||||
|
||||
| Flag | Behavior |
|
||||
|------|----------|
|
||||
| `-e, --exhaustive` | **(default)** Creates all example files without prompting |
|
||||
| `-m, --minimal` | Creates only core files (`application-config.ts` and `default-role.ts`) |
|
||||
| `-i, --interactive` | Prompts you to select which examples to include |
|
||||
|
||||
```bash
|
||||
# Default: all examples included
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files
|
||||
npx create-twenty-app@latest my-app -m
|
||||
|
||||
# Interactive: choose which examples to include
|
||||
npx create-twenty-app@latest my-app -i
|
||||
```
|
||||
|
||||
In interactive mode, you can pick from:
|
||||
- **Example object** — a custom CRM object definition (`objects/example-object.ts`)
|
||||
- **Example field** — a custom field on the example object (`fields/example-field.ts`)
|
||||
- **Example logic function** — a server-side handler with HTTP trigger (`logic-functions/hello-world.ts`)
|
||||
- **Example front component** — a React UI component (`front-components/hello-world.tsx`)
|
||||
- **Example view** — a saved view for the example object (`views/example-view.ts`)
|
||||
- **Example navigation menu item** — a sidebar link (`navigation-menu-items/example-navigation-menu-item.ts`)
|
||||
- **Example skill** — an AI agent skill definition (`skills/example-skill.ts`)
|
||||
|
||||
## What gets scaffolded
|
||||
- A minimal app structure ready for Twenty with example files:
|
||||
- `application-config.ts` - Application metadata configuration
|
||||
- `roles/default-role.ts` - Default role for logic functions
|
||||
- `logic-functions/hello-world.ts` - Example logic function with HTTP trigger
|
||||
- `front-components/hello-world.tsx` - Example front component
|
||||
- TypeScript configuration
|
||||
- Prewired scripts that wrap the `twenty` CLI from twenty-sdk
|
||||
|
||||
**Core files (always created):**
|
||||
- `application-config.ts` — Application metadata configuration
|
||||
- `roles/default-role.ts` — Default role for logic functions
|
||||
- `logic-functions/post-install.ts` — Post-install logic function (runs after app installation)
|
||||
- TypeScript configuration, ESLint, package.json, .gitignore
|
||||
- A prewired `twenty` script that delegates to the `twenty` CLI from twenty-sdk
|
||||
|
||||
**Example files (controlled by scaffolding mode):**
|
||||
- `objects/example-object.ts` — Example custom object with a text field
|
||||
- `fields/example-field.ts` — Example standalone field extending the example object
|
||||
- `logic-functions/hello-world.ts` — Example logic function with HTTP trigger
|
||||
- `front-components/hello-world.tsx` — Example front component
|
||||
- `views/example-view.ts` — Example saved view for the example object
|
||||
- `navigation-menu-items/example-navigation-menu-item.ts` — Example sidebar navigation link
|
||||
- `skills/example-skill.ts` — Example AI agent skill definition
|
||||
|
||||
## Next steps
|
||||
- Use `yarn auth:login` to authenticate with your Twenty workspace.
|
||||
- Explore the generated project and add your first entity with `yarn entity:add` (logic functions, front components, objects, roles).
|
||||
- Use `yarn app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
|
||||
- Keep your types up‑to‑date using `yarn app:generate`.
|
||||
- Run `yarn twenty help` to see all available commands.
|
||||
- Use `yarn twenty auth:login` to authenticate with your Twenty workspace.
|
||||
- Explore the generated project and add your first entity with `yarn twenty entity:add` (logic functions, front components, objects, roles, views, navigation menu items, skills).
|
||||
- Use `yarn twenty app:dev` while you iterate — it watches, builds, and syncs changes to your workspace in real time.
|
||||
- Types are auto‑generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated`.
|
||||
|
||||
|
||||
## Publish your application
|
||||
@@ -101,8 +138,8 @@ git push
|
||||
Our team reviews contributions for quality, security, and reusability before merging.
|
||||
|
||||
## Troubleshooting
|
||||
- Auth prompts not appearing: run `yarn auth:login` again and verify the API key permissions.
|
||||
- Types not generated: ensure `yarn app:generate` runs without errors, then re‑start `yarn app:dev`.
|
||||
- Auth prompts not appearing: run `yarn twenty auth:login` again and verify the API key permissions.
|
||||
- Types not generated: ensure `yarn twenty app:dev` is running — it auto‑generates the typed client.
|
||||
|
||||
## Contributing
|
||||
- See our [GitHub](https://github.com/twentyhq/twenty)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const jestConfig = {
|
||||
displayName: 'twenty-cli',
|
||||
displayName: 'create-twenty-app',
|
||||
preset: '../../jest.preset.js',
|
||||
testEnvironment: 'node',
|
||||
transformIgnorePatterns: ['../../node_modules/'],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "0.5.1",
|
||||
"version": "0.6.1",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
@@ -10,9 +10,7 @@
|
||||
"package.json"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npx rimraf dist && npx vite build",
|
||||
"prepublishOnly": "tsx ../twenty-utils/pack-scripts/pre-publish-only.ts",
|
||||
"postpublish": "tsx ../twenty-utils/pack-scripts/post-publish.ts"
|
||||
"build": "npx rimraf dist && npx vite build"
|
||||
},
|
||||
"keywords": [
|
||||
"twenty",
|
||||
@@ -38,7 +36,6 @@
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.kebabcase": "^4.1.1",
|
||||
"lodash.startcase": "^4.4.0",
|
||||
"twenty-shared": "workspace:*",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -48,6 +45,8 @@
|
||||
"@types/lodash.kebabcase": "^4.1.7",
|
||||
"@types/lodash.startcase": "^4",
|
||||
"@types/node": "^20.0.0",
|
||||
"twenty-sdk": "workspace:*",
|
||||
"twenty-shared": "workspace:*",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import chalk from 'chalk';
|
||||
import { Command, CommanderError } from 'commander';
|
||||
import { CreateAppCommand } from '@/create-app.command';
|
||||
import { type ScaffoldingMode } from '@/types/scaffolding-options';
|
||||
import packageJson from '../package.json';
|
||||
|
||||
const program = new Command(packageJson.name)
|
||||
@@ -12,18 +13,58 @@ const program = new Command(packageJson.name)
|
||||
'Output the current version of create-twenty-app.',
|
||||
)
|
||||
.argument('[directory]')
|
||||
.option('-e, --exhaustive', 'Create all example entities (default)')
|
||||
.option(
|
||||
'-m, --minimal',
|
||||
'Create only core entities (application-config and default-role)',
|
||||
)
|
||||
.option(
|
||||
'-i, --interactive',
|
||||
'Interactively choose which entity examples to include',
|
||||
)
|
||||
.helpOption('-h, --help', 'Display this help message.')
|
||||
.action(async (directory?: string) => {
|
||||
if (directory && !/^[a-z0-9-]+$/.test(directory)) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Invalid directory "${directory}". Must contain only lowercase letters, numbers, and hyphens`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
await new CreateAppCommand().execute(directory);
|
||||
});
|
||||
.action(
|
||||
async (
|
||||
directory?: string,
|
||||
options?: {
|
||||
exhaustive?: boolean;
|
||||
minimal?: boolean;
|
||||
interactive?: boolean;
|
||||
},
|
||||
) => {
|
||||
const modeFlags = [
|
||||
options?.exhaustive,
|
||||
options?.minimal,
|
||||
options?.interactive,
|
||||
].filter(Boolean);
|
||||
|
||||
if (modeFlags.length > 1) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
'Error: --exhaustive, --minimal, and --interactive are mutually exclusive.',
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (directory && !/^[a-z0-9-]+$/.test(directory)) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
`Invalid directory "${directory}". Must contain only lowercase letters, numbers, and hyphens`,
|
||||
),
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const mode: ScaffoldingMode = options?.minimal
|
||||
? 'minimal'
|
||||
: options?.interactive
|
||||
? 'interactive'
|
||||
: 'exhaustive';
|
||||
|
||||
await new CreateAppCommand().execute(directory, mode);
|
||||
},
|
||||
);
|
||||
|
||||
program.exitOverride();
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
## Base documentation
|
||||
|
||||
- Documentation: https://docs.twenty.com/developers/extend/capabilities/apps
|
||||
- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-sdk/src/cli/__tests__/apps/rich-app
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it.
|
||||
- Creating a view without a navigationMenuItem associated. This will make the view available on the left sidebar.
|
||||
@@ -5,36 +5,41 @@ This is a [Twenty](https://twenty.com) application project bootstrapped with [`c
|
||||
First, authenticate to your workspace:
|
||||
|
||||
```bash
|
||||
yarn auth:login
|
||||
yarn twenty auth:login
|
||||
```
|
||||
|
||||
Then, start development mode to sync your app and watch for changes:
|
||||
|
||||
```bash
|
||||
yarn app:dev
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
Open your Twenty instance and go to `/settings/applications` section to see the result.
|
||||
|
||||
## Available Commands
|
||||
|
||||
Run `yarn twenty help` to list all available commands. Common commands:
|
||||
|
||||
```bash
|
||||
# Authentication
|
||||
yarn auth:login # Authenticate with Twenty
|
||||
yarn auth:logout # Remove credentials
|
||||
yarn auth:status # Check auth status
|
||||
yarn auth:switch # Switch default workspace
|
||||
yarn auth:list # List all configured workspaces
|
||||
yarn twenty auth:login # Authenticate with Twenty
|
||||
yarn twenty auth:logout # Remove credentials
|
||||
yarn twenty auth:status # Check auth status
|
||||
yarn twenty auth:switch # Switch default workspace
|
||||
yarn twenty auth:list # List all configured workspaces
|
||||
|
||||
# Application
|
||||
yarn app:dev # Start dev mode (watch, build, and sync)
|
||||
yarn entity:add # Add a new entity (function, front-component, object, role)
|
||||
yarn app:generate # Generate typed Twenty client
|
||||
yarn function:logs # Stream function logs
|
||||
yarn function:execute # Execute a function with JSON payload
|
||||
yarn app:uninstall # Uninstall app from workspace
|
||||
yarn twenty app:dev # Start dev mode (watch, build, sync, and auto-generate typed client)
|
||||
yarn twenty entity:add # Add a new entity (object, field, function, front-component, role, view, navigation-menu-item)
|
||||
yarn twenty function:logs # Stream function logs
|
||||
yarn twenty function:execute # Execute a function with JSON payload
|
||||
yarn twenty app:uninstall # Uninstall app from workspace
|
||||
```
|
||||
|
||||
## LLMs instructions
|
||||
|
||||
Main docs and pitfalls are available in LLMS.md file.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Twenty applications, take a look at the following resources:
|
||||
|
||||
@@ -8,14 +8,24 @@ import inquirer from 'inquirer';
|
||||
import kebabCase from 'lodash.kebabcase';
|
||||
import * as path from 'path';
|
||||
|
||||
import {
|
||||
type ExampleOptions,
|
||||
type ScaffoldingMode,
|
||||
} from '@/types/scaffolding-options';
|
||||
|
||||
const CURRENT_EXECUTION_DIRECTORY = process.env.INIT_CWD || process.cwd();
|
||||
|
||||
export class CreateAppCommand {
|
||||
async execute(directory?: string): Promise<void> {
|
||||
async execute(
|
||||
directory?: string,
|
||||
mode: ScaffoldingMode = 'exhaustive',
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { appName, appDisplayName, appDirectory, appDescription } =
|
||||
await this.getAppInfos(directory);
|
||||
|
||||
const exampleOptions = await this.resolveExampleOptions(mode);
|
||||
|
||||
await this.validateDirectory(appDirectory);
|
||||
|
||||
this.logCreationInfo({ appDirectory, appName });
|
||||
@@ -27,6 +37,7 @@ export class CreateAppCommand {
|
||||
appDisplayName,
|
||||
appDescription,
|
||||
appDirectory,
|
||||
exampleOptions,
|
||||
});
|
||||
|
||||
await install(appDirectory);
|
||||
@@ -92,6 +103,103 @@ export class CreateAppCommand {
|
||||
return { appName, appDisplayName, appDirectory, appDescription };
|
||||
}
|
||||
|
||||
private async resolveExampleOptions(
|
||||
mode: ScaffoldingMode,
|
||||
): Promise<ExampleOptions> {
|
||||
if (mode === 'minimal') {
|
||||
return {
|
||||
includeExampleObject: false,
|
||||
includeExampleField: false,
|
||||
includeExampleLogicFunction: false,
|
||||
includeExampleFrontComponent: false,
|
||||
includeExampleView: false,
|
||||
includeExampleNavigationMenuItem: false,
|
||||
includeExampleSkill: false,
|
||||
};
|
||||
}
|
||||
|
||||
if (mode === 'exhaustive') {
|
||||
return {
|
||||
includeExampleObject: true,
|
||||
includeExampleField: true,
|
||||
includeExampleLogicFunction: true,
|
||||
includeExampleFrontComponent: true,
|
||||
includeExampleView: true,
|
||||
includeExampleNavigationMenuItem: true,
|
||||
includeExampleSkill: true,
|
||||
};
|
||||
}
|
||||
|
||||
const { selectedExamples } = await inquirer.prompt([
|
||||
{
|
||||
type: 'checkbox',
|
||||
name: 'selectedExamples',
|
||||
message: 'Select which example files to include:',
|
||||
choices: [
|
||||
{
|
||||
name: 'Example object (custom object definition)',
|
||||
value: 'object',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
name: 'Example field (custom field on the example object)',
|
||||
value: 'field',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
name: 'Example logic function (server-side handler)',
|
||||
value: 'logicFunction',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
name: 'Example front component (React UI component)',
|
||||
value: 'frontComponent',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
name: 'Example view (saved view for the example object)',
|
||||
value: 'view',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
name: 'Example navigation menu item (sidebar link)',
|
||||
value: 'navigationMenuItem',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
name: 'Example skill (AI agent skill definition)',
|
||||
value: 'skill',
|
||||
checked: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const includeField = selectedExamples.includes('field');
|
||||
const includeView = selectedExamples.includes('view');
|
||||
const includeObject =
|
||||
selectedExamples.includes('object') || includeField || includeView;
|
||||
|
||||
if ((includeField || includeView) && !selectedExamples.includes('object')) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
'Note: Example object auto-included because example field/view depends on it.',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
includeExampleObject: includeObject,
|
||||
includeExampleField: includeField,
|
||||
includeExampleLogicFunction: selectedExamples.includes('logicFunction'),
|
||||
includeExampleFrontComponent: selectedExamples.includes('frontComponent'),
|
||||
includeExampleView: includeView,
|
||||
includeExampleNavigationMenuItem:
|
||||
selectedExamples.includes('navigationMenuItem'),
|
||||
includeExampleSkill: selectedExamples.includes('skill'),
|
||||
};
|
||||
}
|
||||
|
||||
private async validateDirectory(appDirectory: string): Promise<void> {
|
||||
if (!(await fs.pathExists(appDirectory))) {
|
||||
return;
|
||||
@@ -125,9 +233,9 @@ export class CreateAppCommand {
|
||||
console.log('');
|
||||
console.log(chalk.blue('Next steps:'));
|
||||
console.log(chalk.gray(` cd ${dirName}`));
|
||||
console.log(chalk.gray(` corepack enable # if you don't use yarn@4`));
|
||||
console.log(chalk.gray(` yarn install # if you don't use yarn@4`));
|
||||
console.log(chalk.gray(' yarn auth:login # Authenticate with Twenty'));
|
||||
console.log(chalk.gray(' yarn app:dev # Start dev mode'));
|
||||
console.log(
|
||||
chalk.gray(' yarn twenty auth:login # Authenticate with Twenty'),
|
||||
);
|
||||
console.log(chalk.gray(' yarn twenty app:dev # Start dev mode'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
export type ScaffoldingMode = 'exhaustive' | 'minimal' | 'interactive';
|
||||
|
||||
export type ExampleOptions = {
|
||||
includeExampleObject: boolean;
|
||||
includeExampleField: boolean;
|
||||
includeExampleLogicFunction: boolean;
|
||||
includeExampleFrontComponent: boolean;
|
||||
includeExampleView: boolean;
|
||||
includeExampleNavigationMenuItem: boolean;
|
||||
includeExampleSkill: boolean;
|
||||
};
|
||||
@@ -1,7 +1,9 @@
|
||||
import * as fs from 'fs-extra';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
import { type ExampleOptions } from '@/types/scaffolding-options';
|
||||
import { GENERATED_DIR } from 'twenty-shared/application';
|
||||
import { copyBaseApplicationProject } from '@/utils/app-template';
|
||||
import * as fs from 'fs-extra';
|
||||
import { tmpdir } from 'os';
|
||||
import { join } from 'path';
|
||||
|
||||
// Mock fs-extra's copy function to skip copying base template (not available during tests)
|
||||
jest.mock('fs-extra', () => {
|
||||
@@ -15,6 +17,26 @@ jest.mock('fs-extra', () => {
|
||||
const APPLICATION_FILE_NAME = 'application-config.ts';
|
||||
const DEFAULT_ROLE_FILE_NAME = 'default-role.ts';
|
||||
|
||||
const ALL_EXAMPLES: ExampleOptions = {
|
||||
includeExampleObject: true,
|
||||
includeExampleField: true,
|
||||
includeExampleLogicFunction: true,
|
||||
includeExampleFrontComponent: true,
|
||||
includeExampleView: true,
|
||||
includeExampleNavigationMenuItem: true,
|
||||
includeExampleSkill: true,
|
||||
};
|
||||
|
||||
const NO_EXAMPLES: ExampleOptions = {
|
||||
includeExampleObject: false,
|
||||
includeExampleField: false,
|
||||
includeExampleSkill: false,
|
||||
includeExampleLogicFunction: false,
|
||||
includeExampleFrontComponent: false,
|
||||
includeExampleView: false,
|
||||
includeExampleNavigationMenuItem: false,
|
||||
};
|
||||
|
||||
describe('copyBaseApplicationProject', () => {
|
||||
let testAppDirectory: string;
|
||||
|
||||
@@ -41,6 +63,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
// Verify src/ folder exists
|
||||
@@ -62,6 +85,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const packageJsonPath = join(testAppDirectory, 'package.json');
|
||||
@@ -70,8 +94,8 @@ describe('copyBaseApplicationProject', () => {
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
expect(packageJson.name).toBe('my-test-app');
|
||||
expect(packageJson.version).toBe('0.1.0');
|
||||
expect(packageJson.dependencies['twenty-sdk']).toBe('0.5.1');
|
||||
expect(packageJson.scripts['app:dev']).toBe('twenty app:dev');
|
||||
expect(packageJson.dependencies['twenty-sdk']).toBe('latest');
|
||||
expect(packageJson.scripts['twenty']).toBe('twenty');
|
||||
});
|
||||
|
||||
it('should create .gitignore file', async () => {
|
||||
@@ -80,6 +104,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const gitignorePath = join(testAppDirectory, '.gitignore');
|
||||
@@ -87,7 +112,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
|
||||
const gitignoreContent = await fs.readFile(gitignorePath, 'utf8');
|
||||
expect(gitignoreContent).toContain('/node_modules');
|
||||
expect(gitignoreContent).toContain('generated');
|
||||
expect(gitignoreContent).toContain(GENERATED_DIR);
|
||||
});
|
||||
|
||||
it('should create yarn.lock file', async () => {
|
||||
@@ -96,6 +121,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const yarnLockPath = join(testAppDirectory, 'yarn.lock');
|
||||
@@ -111,6 +137,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const appConfigPath = join(testAppDirectory, 'src', APPLICATION_FILE_NAME);
|
||||
@@ -148,6 +175,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const roleConfigPath = join(
|
||||
@@ -192,6 +220,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
// Verify fs.copy was called with correct destination
|
||||
@@ -208,6 +237,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: '',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const appConfigPath = join(testAppDirectory, 'src', APPLICATION_FILE_NAME);
|
||||
@@ -225,6 +255,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDisplayName: 'App One',
|
||||
appDescription: 'First app',
|
||||
appDirectory: firstAppDir,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
// Create second app
|
||||
@@ -235,6 +266,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDisplayName: 'App Two',
|
||||
appDescription: 'Second app',
|
||||
appDirectory: secondAppDir,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
// Read both app configs
|
||||
@@ -267,6 +299,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDisplayName: 'App One',
|
||||
appDescription: 'First app',
|
||||
appDirectory: firstAppDir,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
// Create second app
|
||||
@@ -277,6 +310,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
appDisplayName: 'App Two',
|
||||
appDescription: 'Second app',
|
||||
appDirectory: secondAppDir,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const firstRoleConfig = await fs.readFile(
|
||||
@@ -299,4 +333,347 @@ describe('copyBaseApplicationProject', () => {
|
||||
expect(secondUuid).toBeDefined();
|
||||
expect(firstUuid).not.toBe(secondUuid);
|
||||
});
|
||||
|
||||
describe('scaffolding modes', () => {
|
||||
describe('exhaustive mode (all examples)', () => {
|
||||
it('should create all example files when all options are enabled', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const srcPath = join(testAppDirectory, 'src');
|
||||
|
||||
expect(
|
||||
await fs.pathExists(join(srcPath, 'objects', 'example-object.ts')),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await fs.pathExists(join(srcPath, 'fields', 'example-field.ts')),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, 'logic-functions', 'hello-world.ts'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, 'front-components', 'hello-world.tsx'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await fs.pathExists(join(srcPath, 'views', 'example-view.ts')),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(
|
||||
srcPath,
|
||||
'navigation-menu-items',
|
||||
'example-navigation-menu-item.ts',
|
||||
),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('minimal mode (no examples)', () => {
|
||||
it('should create only core files when no examples are enabled', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: NO_EXAMPLES,
|
||||
});
|
||||
|
||||
const srcPath = join(testAppDirectory, 'src');
|
||||
|
||||
// Core files should exist
|
||||
expect(await fs.pathExists(join(srcPath, APPLICATION_FILE_NAME))).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
await fs.pathExists(join(srcPath, 'roles', DEFAULT_ROLE_FILE_NAME)),
|
||||
).toBe(true);
|
||||
|
||||
// Example files should not exist
|
||||
expect(
|
||||
await fs.pathExists(join(srcPath, 'objects', 'example-object.ts')),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await fs.pathExists(join(srcPath, 'fields', 'example-field.ts')),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, 'logic-functions', 'hello-world.ts'),
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, 'front-components', 'hello-world.tsx'),
|
||||
),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await fs.pathExists(join(srcPath, 'views', 'example-view.ts')),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(
|
||||
srcPath,
|
||||
'navigation-menu-items',
|
||||
'example-navigation-menu-item.ts',
|
||||
),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('selective examples', () => {
|
||||
it('should create only front component when only that option is enabled', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: {
|
||||
includeExampleObject: false,
|
||||
includeExampleField: false,
|
||||
includeExampleSkill: false,
|
||||
includeExampleLogicFunction: false,
|
||||
includeExampleFrontComponent: true,
|
||||
includeExampleView: false,
|
||||
includeExampleNavigationMenuItem: false,
|
||||
},
|
||||
});
|
||||
|
||||
const srcPath = join(testAppDirectory, 'src');
|
||||
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, 'front-components', 'hello-world.tsx'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await fs.pathExists(join(srcPath, 'objects', 'example-object.ts')),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await fs.pathExists(join(srcPath, 'fields', 'example-field.ts')),
|
||||
).toBe(false);
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, 'logic-functions', 'hello-world.ts'),
|
||||
),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('should create only logic function when only that option is enabled', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: {
|
||||
includeExampleObject: false,
|
||||
includeExampleSkill: false,
|
||||
includeExampleField: false,
|
||||
includeExampleLogicFunction: true,
|
||||
includeExampleFrontComponent: false,
|
||||
includeExampleView: false,
|
||||
includeExampleNavigationMenuItem: false,
|
||||
},
|
||||
});
|
||||
|
||||
const srcPath = join(testAppDirectory, 'src');
|
||||
|
||||
expect(
|
||||
await fs.pathExists(
|
||||
join(srcPath, 'logic-functions', 'hello-world.ts'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(
|
||||
await fs.pathExists(join(srcPath, 'objects', 'example-object.ts')),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('example object', () => {
|
||||
it('should create example-object.ts with defineObject and correct structure', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const objectPath = join(
|
||||
testAppDirectory,
|
||||
'src',
|
||||
'objects',
|
||||
'example-object.ts',
|
||||
);
|
||||
|
||||
expect(await fs.pathExists(objectPath)).toBe(true);
|
||||
|
||||
const content = await fs.readFile(objectPath, 'utf8');
|
||||
|
||||
expect(content).toContain(
|
||||
"import { defineObject, FieldType } from 'twenty-sdk'",
|
||||
);
|
||||
expect(content).toContain('export default defineObject({');
|
||||
expect(content).toContain(
|
||||
'export const EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER',
|
||||
);
|
||||
expect(content).toContain('export const NAME_FIELD_UNIVERSAL_IDENTIFIER');
|
||||
expect(content).toContain("nameSingular: 'exampleItem'");
|
||||
expect(content).toContain("namePlural: 'exampleItems'");
|
||||
expect(content).toContain('FieldType.TEXT');
|
||||
expect(content).toContain(
|
||||
'labelIdentifierFieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER',
|
||||
);
|
||||
});
|
||||
|
||||
it('should generate unique UUIDs for example objects across apps', async () => {
|
||||
const firstAppDir = join(testAppDirectory, 'app1');
|
||||
await fs.ensureDir(firstAppDir);
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'app-one',
|
||||
appDisplayName: 'App One',
|
||||
appDescription: 'First app',
|
||||
appDirectory: firstAppDir,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const secondAppDir = join(testAppDirectory, 'app2');
|
||||
await fs.ensureDir(secondAppDir);
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'app-two',
|
||||
appDisplayName: 'App Two',
|
||||
appDescription: 'Second app',
|
||||
appDirectory: secondAppDir,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const firstContent = await fs.readFile(
|
||||
join(firstAppDir, 'src', 'objects', 'example-object.ts'),
|
||||
'utf8',
|
||||
);
|
||||
const secondContent = await fs.readFile(
|
||||
join(secondAppDir, 'src', 'objects', 'example-object.ts'),
|
||||
'utf8',
|
||||
);
|
||||
|
||||
const uuidRegex =
|
||||
/EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER =\s*'([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})'/;
|
||||
const firstUuid = firstContent.match(uuidRegex)?.[1];
|
||||
const secondUuid = secondContent.match(uuidRegex)?.[1];
|
||||
|
||||
expect(firstUuid).toBeDefined();
|
||||
expect(secondUuid).toBeDefined();
|
||||
expect(firstUuid).not.toBe(secondUuid);
|
||||
});
|
||||
});
|
||||
|
||||
describe('example field', () => {
|
||||
it('should create example-field.ts with defineField referencing the object', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const fieldPath = join(
|
||||
testAppDirectory,
|
||||
'src',
|
||||
'fields',
|
||||
'example-field.ts',
|
||||
);
|
||||
|
||||
expect(await fs.pathExists(fieldPath)).toBe(true);
|
||||
|
||||
const content = await fs.readFile(fieldPath, 'utf8');
|
||||
|
||||
expect(content).toContain(
|
||||
"import { defineField, FieldType } from 'twenty-sdk'",
|
||||
);
|
||||
expect(content).toContain(
|
||||
"import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object'",
|
||||
);
|
||||
expect(content).toContain('export default defineField({');
|
||||
expect(content).toContain(
|
||||
'objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER',
|
||||
);
|
||||
expect(content).toContain('FieldType.NUMBER');
|
||||
expect(content).toContain("name: 'priority'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('example view', () => {
|
||||
it('should create example-view.ts with defineView referencing the object', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const viewPath = join(
|
||||
testAppDirectory,
|
||||
'src',
|
||||
'views',
|
||||
'example-view.ts',
|
||||
);
|
||||
|
||||
expect(await fs.pathExists(viewPath)).toBe(true);
|
||||
|
||||
const content = await fs.readFile(viewPath, 'utf8');
|
||||
|
||||
expect(content).toContain("import { defineView } from 'twenty-sdk'");
|
||||
expect(content).toContain(
|
||||
"import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object'",
|
||||
);
|
||||
expect(content).toContain('export default defineView({');
|
||||
expect(content).toContain(
|
||||
'objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER',
|
||||
);
|
||||
expect(content).toContain("name: 'example-view'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('example navigation menu item', () => {
|
||||
it('should create example-navigation-menu-item.ts with defineNavigationMenuItem', async () => {
|
||||
await copyBaseApplicationProject({
|
||||
appName: 'my-test-app',
|
||||
appDisplayName: 'My Test App',
|
||||
appDescription: 'A test application',
|
||||
appDirectory: testAppDirectory,
|
||||
exampleOptions: ALL_EXAMPLES,
|
||||
});
|
||||
|
||||
const navPath = join(
|
||||
testAppDirectory,
|
||||
'src',
|
||||
'navigation-menu-items',
|
||||
'example-navigation-menu-item.ts',
|
||||
);
|
||||
|
||||
expect(await fs.pathExists(navPath)).toBe(true);
|
||||
|
||||
const content = await fs.readFile(navPath, 'utf8');
|
||||
|
||||
expect(content).toContain(
|
||||
"import { defineNavigationMenuItem } from 'twenty-sdk'",
|
||||
);
|
||||
expect(content).toContain('export default defineNavigationMenuItem({');
|
||||
expect(content).toContain("name: 'example-navigation-menu-item'");
|
||||
expect(content).toContain("icon: 'IconList'");
|
||||
expect(content).toContain('position: 0');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,6 +3,8 @@ import { join } from 'path';
|
||||
import { v4 } from 'uuid';
|
||||
import { ASSETS_DIR } from 'twenty-shared/application';
|
||||
|
||||
import { type ExampleOptions } from '@/types/scaffolding-options';
|
||||
|
||||
const SRC_FOLDER = 'src';
|
||||
|
||||
export const copyBaseApplicationProject = async ({
|
||||
@@ -10,11 +12,13 @@ export const copyBaseApplicationProject = async ({
|
||||
appDisplayName,
|
||||
appDescription,
|
||||
appDirectory,
|
||||
exampleOptions,
|
||||
}: {
|
||||
appName: string;
|
||||
appDisplayName: string;
|
||||
appDescription: string;
|
||||
appDirectory: string;
|
||||
exampleOptions: ExampleOptions;
|
||||
}) => {
|
||||
await fs.copy(join(__dirname, './constants/base-application'), appDirectory);
|
||||
|
||||
@@ -37,16 +41,66 @@ export const copyBaseApplicationProject = async ({
|
||||
fileName: 'default-role.ts',
|
||||
});
|
||||
|
||||
await createDefaultFrontComponent({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'front-components',
|
||||
fileName: 'hello-world.tsx',
|
||||
});
|
||||
if (exampleOptions.includeExampleObject) {
|
||||
await createExampleObject({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'objects',
|
||||
fileName: 'example-object.ts',
|
||||
});
|
||||
}
|
||||
|
||||
await createDefaultFunction({
|
||||
if (exampleOptions.includeExampleField) {
|
||||
await createExampleField({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'fields',
|
||||
fileName: 'example-field.ts',
|
||||
});
|
||||
}
|
||||
|
||||
if (exampleOptions.includeExampleLogicFunction) {
|
||||
await createDefaultFunction({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'logic-functions',
|
||||
fileName: 'hello-world.ts',
|
||||
});
|
||||
}
|
||||
|
||||
if (exampleOptions.includeExampleFrontComponent) {
|
||||
await createDefaultFrontComponent({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'front-components',
|
||||
fileName: 'hello-world.tsx',
|
||||
});
|
||||
}
|
||||
|
||||
if (exampleOptions.includeExampleView) {
|
||||
await createExampleView({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'views',
|
||||
fileName: 'example-view.ts',
|
||||
});
|
||||
}
|
||||
|
||||
if (exampleOptions.includeExampleNavigationMenuItem) {
|
||||
await createExampleNavigationMenuItem({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'navigation-menu-items',
|
||||
fileName: 'example-navigation-menu-item.ts',
|
||||
});
|
||||
}
|
||||
|
||||
if (exampleOptions.includeExampleSkill) {
|
||||
await createExampleSkill({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'skills',
|
||||
fileName: 'example-skill.ts',
|
||||
});
|
||||
}
|
||||
|
||||
await createDefaultPostInstallFunction({
|
||||
appDirectory: sourceFolderPath,
|
||||
fileFolder: 'logic-functions',
|
||||
fileName: 'hello-world.ts',
|
||||
fileName: 'post-install.ts',
|
||||
});
|
||||
|
||||
await createApplicationConfig({
|
||||
@@ -196,7 +250,6 @@ const handler = async (): Promise<{ message: string }> => {
|
||||
return { message: 'Hello, World!' };
|
||||
};
|
||||
|
||||
// Logic function handler - rename and implement your logic
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: '${universalIdentifier}',
|
||||
name: 'hello-world-logic-function',
|
||||
@@ -215,6 +268,200 @@ export default defineLogicFunction({
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createDefaultPostInstallFunction = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
const content = `import { defineLogicFunction } from 'twenty-sdk';
|
||||
|
||||
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '${universalIdentifier}';
|
||||
|
||||
const handler = async (): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!');
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
handler,
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createExampleObject = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const objectUniversalIdentifier = v4();
|
||||
const nameFieldUniversalIdentifier = v4();
|
||||
|
||||
const content = `import { defineObject, FieldType } from 'twenty-sdk';
|
||||
|
||||
export const EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER =
|
||||
'${objectUniversalIdentifier}';
|
||||
|
||||
export const NAME_FIELD_UNIVERSAL_IDENTIFIER =
|
||||
'${nameFieldUniversalIdentifier}';
|
||||
|
||||
export default defineObject({
|
||||
universalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
nameSingular: 'exampleItem',
|
||||
namePlural: 'exampleItems',
|
||||
labelSingular: 'Example item',
|
||||
labelPlural: 'Example items',
|
||||
description: 'A sample custom object',
|
||||
icon: 'IconBox',
|
||||
labelIdentifierFieldMetadataUniversalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
fields: [
|
||||
{
|
||||
universalIdentifier: NAME_FIELD_UNIVERSAL_IDENTIFIER,
|
||||
type: FieldType.TEXT,
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
description: 'Name of the example item',
|
||||
icon: 'IconAbc',
|
||||
},
|
||||
],
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createExampleField = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
const content = `import { defineField, FieldType } from 'twenty-sdk';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
|
||||
|
||||
export default defineField({
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
universalIdentifier: '${universalIdentifier}',
|
||||
type: FieldType.NUMBER,
|
||||
name: 'priority',
|
||||
label: 'Priority',
|
||||
description: 'Priority level for the example item (1-10)',
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createExampleView = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
const content = `import { defineView } from 'twenty-sdk';
|
||||
import { EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER } from 'src/objects/example-object';
|
||||
|
||||
export default defineView({
|
||||
universalIdentifier: '${universalIdentifier}',
|
||||
name: 'example-view',
|
||||
objectUniversalIdentifier: EXAMPLE_OBJECT_UNIVERSAL_IDENTIFIER,
|
||||
icon: 'IconList',
|
||||
position: 0,
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createExampleNavigationMenuItem = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
const content = `import { defineNavigationMenuItem } from 'twenty-sdk';
|
||||
|
||||
export default defineNavigationMenuItem({
|
||||
universalIdentifier: '${universalIdentifier}',
|
||||
name: 'example-navigation-menu-item',
|
||||
icon: 'IconList',
|
||||
position: 0,
|
||||
// Link to a view:
|
||||
// viewUniversalIdentifier: '...',
|
||||
// Or link to an object:
|
||||
// targetObjectUniversalIdentifier: '...',
|
||||
// Or link to an external URL:
|
||||
// link: 'https://example.com',
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createExampleSkill = async ({
|
||||
appDirectory,
|
||||
fileFolder,
|
||||
fileName,
|
||||
}: {
|
||||
appDirectory: string;
|
||||
fileFolder?: string;
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
|
||||
const content = `import { defineSkill } from 'twenty-sdk';
|
||||
|
||||
export const EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER =
|
||||
'${universalIdentifier}';
|
||||
|
||||
export default defineSkill({
|
||||
universalIdentifier: EXAMPLE_SKILL_UNIVERSAL_IDENTIFIER,
|
||||
name: 'example-skill',
|
||||
label: 'Example Skill',
|
||||
description: 'A sample skill for your application',
|
||||
icon: 'IconBrain',
|
||||
content: 'Add your skill instructions here. Skills provide context and capabilities to AI agents.',
|
||||
});
|
||||
`;
|
||||
|
||||
await fs.ensureDir(join(appDirectory, fileFolder ?? ''));
|
||||
await fs.writeFile(join(appDirectory, fileFolder ?? '', fileName), content);
|
||||
};
|
||||
|
||||
const createApplicationConfig = async ({
|
||||
displayName,
|
||||
description,
|
||||
@@ -230,12 +477,14 @@ const createApplicationConfig = async ({
|
||||
}) => {
|
||||
const content = `import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '${v4()}',
|
||||
displayName: '${displayName}',
|
||||
description: '${description ?? ''}',
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
`;
|
||||
|
||||
@@ -261,23 +510,12 @@ const createPackageJson = async ({
|
||||
},
|
||||
packageManager: 'yarn@4.9.2',
|
||||
scripts: {
|
||||
'auth:login': 'twenty auth:login',
|
||||
'auth:logout': 'twenty auth:logout',
|
||||
'auth:status': 'twenty auth:status',
|
||||
'auth:switch': 'twenty auth:switch',
|
||||
'auth:list': 'twenty auth:list',
|
||||
'app:dev': 'twenty app:dev',
|
||||
'entity:add': 'twenty entity:add',
|
||||
'app:generate': 'twenty app:generate',
|
||||
'function:logs': 'twenty function:logs',
|
||||
'function:execute': 'twenty function:execute',
|
||||
'app:uninstall': 'twenty app:uninstall',
|
||||
help: 'twenty help',
|
||||
twenty: 'twenty',
|
||||
lint: 'eslint',
|
||||
'lint:fix': 'eslint --fix',
|
||||
},
|
||||
dependencies: {
|
||||
'twenty-sdk': '0.5.1',
|
||||
'twenty-sdk': 'latest',
|
||||
},
|
||||
devDependencies: {
|
||||
typescript: '^5.9.3',
|
||||
|
||||
@@ -6,7 +6,13 @@ const execPromise = promisify(exec);
|
||||
|
||||
export const install = async (root: string) => {
|
||||
try {
|
||||
await execPromise('yarn', { cwd: root });
|
||||
await execPromise('corepack enable', { cwd: root });
|
||||
} catch (error: any) {
|
||||
console.warn(chalk.yellow('corepack enabled failed:'), error.stderr);
|
||||
}
|
||||
|
||||
try {
|
||||
await execPromise('yarn install', { cwd: root });
|
||||
} catch (error: any) {
|
||||
console.error(chalk.red('yarn install failed:'), error.stdout);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { defineConfig } from 'vite';
|
||||
import dts from 'vite-plugin-dts';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import packageJson from './package.json';
|
||||
import type { PackageJson } from 'type-fest';
|
||||
|
||||
const moduleEntries = Object.keys((packageJson as any).exports || {})
|
||||
.filter(
|
||||
@@ -70,13 +71,23 @@ export default defineConfig(() => {
|
||||
outDir: 'dist',
|
||||
lib: { entry: entries, name: 'create-twenty-app' },
|
||||
rollupOptions: {
|
||||
external: [
|
||||
...Object.keys((packageJson as any).dependencies || {}),
|
||||
'path',
|
||||
'fs',
|
||||
'child_process',
|
||||
'util',
|
||||
],
|
||||
external: (id: string) => {
|
||||
if (/^node:/.test(id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const builtins = ['path', 'fs', 'child_process', 'util'];
|
||||
|
||||
if (builtins.includes(id)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const deps = Object.keys(
|
||||
(packageJson as PackageJson).dependencies || {},
|
||||
);
|
||||
|
||||
return deps.some((dep) => id === dep || id.startsWith(dep + '/'));
|
||||
},
|
||||
output: [
|
||||
{
|
||||
format: 'es',
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"auth": "twenty auth login",
|
||||
"generate": "twenty app generate",
|
||||
"dev": "twenty app dev",
|
||||
"sync": "twenty app sync",
|
||||
"uninstall": "twenty app uninstall",
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"auth": "twenty auth login",
|
||||
"generate": "twenty app generate",
|
||||
"dev": "twenty app dev",
|
||||
"sync": "twenty app sync",
|
||||
"uninstall": "twenty app uninstall",
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
},
|
||||
"scripts": {
|
||||
"auth": "twenty auth login",
|
||||
"generate": "twenty app generate",
|
||||
"dev": "twenty app dev",
|
||||
"sync": "twenty app sync",
|
||||
"uninstall": "twenty app uninstall",
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"scripts": {
|
||||
"create-entity": "twenty app add",
|
||||
"dev": "twenty app dev",
|
||||
"generate": "twenty app generate",
|
||||
"sync": "twenty app sync",
|
||||
"uninstall": "twenty app uninstall",
|
||||
"auth": "twenty auth login"
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
"app:dev": "twenty app dev",
|
||||
"app:sync": "twenty app sync",
|
||||
"entity:add": "twenty entity add",
|
||||
"app:generate": "twenty app generate",
|
||||
"function:logs": "twenty function logs",
|
||||
"function:execute": "twenty function execute",
|
||||
"app:uninstall": "twenty app uninstall",
|
||||
|
||||
@@ -14,6 +14,7 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
|
||||
**What you can do today:**
|
||||
- Define custom objects and fields as code (managed data model)
|
||||
- Build logic functions with custom triggers
|
||||
- Define skills for AI agents
|
||||
- Deploy the same app across multiple workspaces
|
||||
|
||||
## Prerequisites
|
||||
@@ -26,41 +27,47 @@ Apps let you build and manage Twenty customizations **as code**. Instead of conf
|
||||
Create a new app using the official scaffolder, then authenticate and start developing:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Scaffold a new app
|
||||
# Scaffold a new app (includes all examples by default)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# If you don't use yarn@4
|
||||
corepack enable
|
||||
yarn install
|
||||
|
||||
# Authenticate using your API key (you'll be prompted)
|
||||
yarn auth:login
|
||||
|
||||
# Start dev mode: automatically syncs local changes to your workspace
|
||||
yarn app:dev
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
The scaffolder supports three modes for controlling which example files are included:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Default (exhaustive): all examples (object, field, logic function, front component, view, navigation menu item, skill)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# Minimal: only core files (application-config.ts and default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
|
||||
# Interactive: select which examples to include
|
||||
npx create-twenty-app@latest my-app --interactive
|
||||
```
|
||||
|
||||
From here you can:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new entity to your application (guided)
|
||||
yarn entity:add
|
||||
|
||||
# Generate a typed Twenty client and workspace entity types
|
||||
yarn app:generate
|
||||
yarn twenty entity:add
|
||||
|
||||
# Watch your application's function logs
|
||||
yarn function:logs
|
||||
yarn twenty function:logs
|
||||
|
||||
# Execute a function by name
|
||||
yarn function:execute -n my-function -p '{"name": "test"}'
|
||||
yarn twenty function:execute -n my-function -p '{"name": "test"}'
|
||||
|
||||
# Execute the post-install function
|
||||
yarn twenty function:execute --postInstall
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
yarn app:uninstall
|
||||
yarn twenty app:uninstall
|
||||
|
||||
# Display commands' help
|
||||
yarn help
|
||||
yarn twenty help
|
||||
```
|
||||
|
||||
See also: the CLI reference pages for [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) and [twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
|
||||
@@ -72,9 +79,9 @@ When you run `npx create-twenty-app@latest my-twenty-app`, the scaffolder:
|
||||
- Copies a minimal base application into `my-twenty-app/`
|
||||
- Adds a local `twenty-sdk` dependency and Yarn 4 configuration
|
||||
- Creates config files and scripts wired to the `twenty` CLI
|
||||
- Generates a default application config and a default function role
|
||||
- Generates core files (application config, default function role, post-install function) plus example files based on the scaffolding mode
|
||||
|
||||
A freshly scaffolded app looks like this:
|
||||
A freshly scaffolded app with the default `--exhaustive` mode looks like this:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
@@ -93,15 +100,28 @@ my-twenty-app/
|
||||
├── application-config.ts # Required - main application configuration
|
||||
├── roles/
|
||||
│ └── default-role.ts # Default role for logic functions
|
||||
├── objects/
|
||||
│ └── example-object.ts # Example custom object definition
|
||||
├── fields/
|
||||
│ └── example-field.ts # Example standalone field definition
|
||||
├── logic-functions/
|
||||
│ └── hello-world.ts # Example logic function
|
||||
└── front-components/
|
||||
└── hello-world.tsx # Example front component
|
||||
│ ├── hello-world.ts # Example logic function
|
||||
│ └── post-install.ts # Post-install logic function
|
||||
├── front-components/
|
||||
│ └── hello-world.tsx # Example front component
|
||||
├── views/
|
||||
│ └── example-view.ts # Example saved view definition
|
||||
├── navigation-menu-items/
|
||||
│ └── example-navigation-menu-item.ts # Example sidebar navigation link
|
||||
└── skills/
|
||||
└── example-skill.ts # Example AI agent skill definition
|
||||
```
|
||||
|
||||
With `--minimal`, only the core files are created (`application-config.ts`, `roles/default-role.ts`, and `logic-functions/post-install.ts`). With `--interactive`, you choose which example files to include.
|
||||
|
||||
At a high level:
|
||||
|
||||
- **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus scripts like `app:dev`, `app:generate`, `entity:add`, `function:logs`, `function:execute`, `app:uninstall`, and authentication commands that delegate to the local `twenty` CLI.
|
||||
- **package.json**: Declares the app name, version, engines (Node 24+, Yarn 4), and adds `twenty-sdk` plus a `twenty` script that delegates to the local `twenty` CLI. Run `yarn twenty help` to list all available commands.
|
||||
- **.gitignore**: Ignores common artifacts such as `node_modules`, `.yarn`, `generated/` (typed client), `dist/`, `build/`, coverage folders, log files, and `.env*` files.
|
||||
- **yarn.lock**, **.yarnrc.yml**, **.yarn/**: Lock and configure the Yarn 4 toolchain used by the project.
|
||||
- **.nvmrc**: Pins the Node.js version expected by the project.
|
||||
@@ -121,6 +141,9 @@ The SDK detects entities by parsing your TypeScript files for **`export default
|
||||
| `defineFrontComponent()` | Front component definitions |
|
||||
| `defineRole()` | Role definitions |
|
||||
| `defineField()` | Field extensions for existing objects |
|
||||
| `defineView()` | Saved view definitions |
|
||||
| `defineNavigationMenuItem()` | Navigation menu item definitions |
|
||||
| `defineSkill()` | AI agent skill definitions |
|
||||
|
||||
<Note>
|
||||
**File naming is flexible.** Entity detection is AST-based — the SDK scans your source files for the `export default define<Entity>({...})` pattern. You can organize your files and folders however you like. Grouping by entity type (e.g., `logic-functions/`, `roles/`) is just a convention for code organization, not a requirement.
|
||||
@@ -140,12 +163,12 @@ export default defineObject({
|
||||
|
||||
Later commands will add more files and folders:
|
||||
|
||||
- `yarn app:generate` will create a `generated/` folder (typed Twenty client + workspace types).
|
||||
- `yarn entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, or roles.
|
||||
- `yarn twenty app:dev` will auto-generate a typed API client in `node_modules/twenty-sdk/generated` (typed Twenty client + workspace types).
|
||||
- `yarn twenty entity:add` will add entity definition files under `src/` for your custom objects, functions, front components, roles, skills, and more.
|
||||
|
||||
## Authentication
|
||||
|
||||
The first time you run `yarn auth:login`, you'll be prompted for:
|
||||
The first time you run `yarn twenty auth:login`, you'll be prompted for:
|
||||
|
||||
- API URL (defaults to http://localhost:3000 or your current workspace profile)
|
||||
- API key
|
||||
@@ -156,25 +179,25 @@ Your credentials are stored per-user in `~/.twenty/config.json`. You can maintai
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Login interactively (recommended)
|
||||
yarn auth:login
|
||||
yarn twenty auth:login
|
||||
|
||||
# Login to a specific workspace profile
|
||||
yarn auth:login --workspace my-custom-workspace
|
||||
yarn twenty auth:login --workspace my-custom-workspace
|
||||
|
||||
# List all configured workspaces
|
||||
yarn auth:list
|
||||
yarn twenty auth:list
|
||||
|
||||
# Switch the default workspace (interactive)
|
||||
yarn auth:switch
|
||||
yarn twenty auth:switch
|
||||
|
||||
# Switch to a specific workspace
|
||||
yarn auth:switch production
|
||||
yarn twenty auth:switch production
|
||||
|
||||
# Check current authentication status
|
||||
yarn auth:status
|
||||
yarn twenty auth:status
|
||||
```
|
||||
|
||||
Once you've switched workspaces with `auth:switch`, all subsequent commands will use that workspace by default. You can still override it temporarily with `--workspace <name>`.
|
||||
Once you've switched workspaces with `yarn twenty auth:switch`, all subsequent commands will use that workspace by default. You can still override it temporarily with `--workspace <name>`.
|
||||
|
||||
## Use the SDK resources (types & config)
|
||||
|
||||
@@ -192,6 +215,9 @@ The SDK provides helper functions for defining your app entities. As described i
|
||||
| `defineFrontComponent()` | Define front components for custom UI |
|
||||
| `defineRole()` | Configure role permissions and object access |
|
||||
| `defineField()` | Extend existing objects with additional fields |
|
||||
| `defineView()` | Define saved views for objects |
|
||||
| `defineNavigationMenuItem()` | Define sidebar navigation links |
|
||||
| `defineSkill()` | Define AI agent skills |
|
||||
|
||||
These functions validate your configuration at build time and provide IDE autocompletion and type safety.
|
||||
|
||||
@@ -274,10 +300,14 @@ Key points:
|
||||
- The `universalIdentifier` must be unique and stable across deployments.
|
||||
- Each field requires a `name`, `type`, `label`, and its own stable `universalIdentifier`.
|
||||
- The `fields` array is optional — you can define objects without custom fields.
|
||||
- You can scaffold new objects using `yarn entity:add`, which guides you through naming, fields, and relationships.
|
||||
- You can scaffold new objects using `yarn twenty entity:add`, which guides you through naming, fields, and relationships.
|
||||
|
||||
<Note>
|
||||
**Base fields are created automatically.** When you define a custom object, Twenty automatically adds standard fields such as `name`, `createdAt`, `updatedAt`, `createdBy`, `position`, and `deletedAt`. You don't need to define these in your `fields` array — only add your custom fields.
|
||||
**Base fields are created automatically.** When you define a custom object, Twenty automatically adds standard fields
|
||||
such as `id`, `name`, `createdAt`, `updatedAt`, `createdBy`, `updatedBy` and `deletedAt`.
|
||||
You don't need to define these in your `fields` array — only add your custom fields.
|
||||
You can override default fields by defining a field with the same name in your `fields` array,
|
||||
but this is not recommended.
|
||||
</Note>
|
||||
|
||||
|
||||
@@ -288,6 +318,7 @@ Every app has a single `application-config.ts` file that describes:
|
||||
- **Who the app is**: identifiers, display name, and description.
|
||||
- **How its functions run**: which role they use for permissions.
|
||||
- **(Optional) variables**: key–value pairs exposed to your functions as environment variables.
|
||||
- **(Optional) post-install function**: a logic function that runs after the app is installed.
|
||||
|
||||
Use `defineApplication()` to define your application configuration:
|
||||
|
||||
@@ -295,6 +326,7 @@ Use `defineApplication()` to define your application configuration:
|
||||
// src/application-config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
@@ -310,6 +342,7 @@ export default defineApplication({
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -317,6 +350,7 @@ Notes:
|
||||
- `universalIdentifier` fields are deterministic IDs you own; generate them once and keep them stable across syncs.
|
||||
- `applicationVariables` become environment variables for your functions (for example, `DEFAULT_RECIPIENT_NAME` is available as `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
- `defaultRoleUniversalIdentifier` must match the role file (see below).
|
||||
- `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
|
||||
|
||||
#### Roles and permissions
|
||||
|
||||
@@ -449,6 +483,54 @@ Notes:
|
||||
- The `triggers` array is optional. Functions without triggers can be used as utility functions called by other functions.
|
||||
- You can mix multiple trigger types in a single function.
|
||||
|
||||
### Post-install functions
|
||||
|
||||
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
|
||||
|
||||
When you scaffold a new app with `create-twenty-app`, a post-install function is generated for you at `src/logic-functions/post-install.ts`:
|
||||
|
||||
```typescript
|
||||
// src/logic-functions/post-install.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
|
||||
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
|
||||
|
||||
const handler = async (): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!');
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
The function is wired into your app by referencing its universal identifier in `application-config.ts`:
|
||||
|
||||
```typescript
|
||||
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
|
||||
|
||||
export default defineApplication({
|
||||
// ...
|
||||
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
You can also manually execute the post-install function at any time using the CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty function:execute --postInstall
|
||||
```
|
||||
|
||||
Key points:
|
||||
- Post-install functions are standard logic functions — they use `defineLogicFunction()` like any other function.
|
||||
- The `postInstallLogicFunctionUniversalIdentifier` field in `defineApplication()` is optional. If omitted, no function runs after installation.
|
||||
- The default timeout is set to 300 seconds (5 minutes) to allow for longer setup tasks like data seeding.
|
||||
- Post-install functions do not need triggers — they are invoked by the platform during installation or manually via `function:execute --postInstall`.
|
||||
|
||||
### Route trigger payload
|
||||
|
||||
<Warning>
|
||||
@@ -541,9 +623,74 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
You can create new functions in two ways:
|
||||
|
||||
- **Scaffolded**: Run `yarn entity:add` and choose the option to add a new logic function. This generates a starter file with a handler and config.
|
||||
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new logic function. This generates a starter file with a handler and config.
|
||||
- **Manual**: Create a new `*.logic-function.ts` file and use `defineLogicFunction()`, following the same pattern.
|
||||
|
||||
### Marking a logic function as a tool
|
||||
|
||||
Logic functions can be exposed as **tools** for AI agents and workflows. When a function is marked as a tool, it becomes discoverable by Twenty's AI features and can be selected as a step in workflow automations.
|
||||
|
||||
To mark a logic function as a tool, set `isTool: true` and provide a `toolInputSchema` describing the expected input parameters using [JSON Schema](https://json-schema.org/):
|
||||
|
||||
```typescript
|
||||
// src/logic-functions/enrich-company.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
import Twenty from '~/generated';
|
||||
|
||||
const handler = async (params: { companyName: string; domain?: string }) => {
|
||||
const client = new Twenty();
|
||||
|
||||
const result = await client.mutation({
|
||||
createTask: {
|
||||
__args: {
|
||||
data: {
|
||||
title: `Enrich data for ${params.companyName}`,
|
||||
body: `Domain: ${params.domain ?? 'unknown'}`,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { taskId: result.createTask.id };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
||||
name: 'enrich-company',
|
||||
description: 'Enrich a company record with external data',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
isTool: true,
|
||||
toolInputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
},
|
||||
},
|
||||
required: ['companyName'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
|
||||
- **`isTool`** (`boolean`, default: `false`): When set to `true`, the function is registered as a tool and becomes available to AI agents and workflow automations.
|
||||
- **`toolInputSchema`** (`object`, optional): A JSON Schema object that describes the parameters your function accepts. AI agents use this schema to understand what inputs the tool expects and to validate calls. If omitted, the schema defaults to `{ type: 'object', properties: {} }` (no parameters).
|
||||
- Functions with `isTool: false` (or unset) are **not** exposed as tools. They can still be executed directly or called by other functions, but will not appear in tool discovery.
|
||||
- **Tool naming**: When exposed as a tool, the function name is automatically normalized to `logic_function_<name>` (lowercased, non-alphanumeric characters replaced with underscores). For example, `enrich-company` becomes `logic_function_enrich_company`.
|
||||
- You can combine `isTool` with triggers — a function can be both a tool (callable by AI agents) and triggered by events (cron, database events, routes) at the same time.
|
||||
|
||||
<Note>
|
||||
**Write a good `description`.** AI agents rely on the function's `description` field to decide when to use the tool. Be specific about what the tool does and when it should be called.
|
||||
</Note>
|
||||
|
||||
### Front components
|
||||
|
||||
Front components let you build custom React components that render within Twenty's UI. Use `defineFrontComponent()` to define components with built-in validation:
|
||||
@@ -573,16 +720,50 @@ Key points:
|
||||
- Front components are React components that render in isolated contexts within Twenty.
|
||||
- Use the `*.front-component.tsx` file suffix for automatic detection.
|
||||
- The `component` field references your React component.
|
||||
- Components are built and synced automatically during `yarn app:dev`.
|
||||
- Components are built and synced automatically during `yarn twenty app:dev`.
|
||||
|
||||
You can create new front components in two ways:
|
||||
|
||||
- **Scaffolded**: Run `yarn entity:add` and choose the option to add a new front component.
|
||||
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new front component.
|
||||
- **Manual**: Create a new `*.front-component.tsx` file and use `defineFrontComponent()`.
|
||||
|
||||
### Skills
|
||||
|
||||
Skills define reusable instructions and capabilities that AI agents can use within your workspace. Use `defineSkill()` to define skills with built-in validation:
|
||||
|
||||
```typescript
|
||||
// src/skills/example-skill.ts
|
||||
import { defineSkill } from 'twenty-sdk';
|
||||
|
||||
export default defineSkill({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'sales-outreach',
|
||||
label: 'Sales Outreach',
|
||||
description: 'Guides the AI agent through a structured sales outreach process',
|
||||
icon: 'IconBrain',
|
||||
content: `You are a sales outreach assistant. When reaching out to a prospect:
|
||||
1. Research the company and recent news
|
||||
2. Identify the prospect's role and likely pain points
|
||||
3. Draft a personalized message referencing specific details
|
||||
4. Keep the tone professional but conversational`,
|
||||
});
|
||||
```
|
||||
|
||||
Key points:
|
||||
- `name` is a unique identifier string for the skill (kebab-case recommended).
|
||||
- `label` is the human-readable display name shown in the UI.
|
||||
- `content` contains the skill instructions — this is the text the AI agent uses.
|
||||
- `icon` (optional) sets the icon displayed in the UI.
|
||||
- `description` (optional) provides additional context about the skill's purpose.
|
||||
|
||||
You can create new skills in two ways:
|
||||
|
||||
- **Scaffolded**: Run `yarn twenty entity:add` and choose the option to add a new skill.
|
||||
- **Manual**: Create a new file and use `defineSkill()`, following the same pattern.
|
||||
|
||||
### Generated typed client
|
||||
|
||||
Run yarn app:generate to create a local typed client in generated/ based on your workspace schema. Use it in your functions:
|
||||
The typed client is auto-generated by `yarn twenty app:dev` and stored in `node_modules/twenty-sdk/generated` based on your workspace schema. Use it in your functions:
|
||||
|
||||
```typescript
|
||||
import Twenty from '~/generated';
|
||||
@@ -591,7 +772,7 @@ const client = new Twenty();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
```
|
||||
|
||||
The client is re-generated by `yarn app:generate`. Re-run after changing your objects or when onboarding to a new workspace.
|
||||
The client is re-generated automatically by `yarn twenty app:dev` whenever your objects or fields change.
|
||||
|
||||
#### Runtime credentials in logic functions
|
||||
|
||||
@@ -605,6 +786,51 @@ Notes:
|
||||
- The API key's permissions are determined by the role referenced in your `application-config.ts` via `defaultRoleUniversalIdentifier`. This is the default role used by logic functions of your application.
|
||||
- Applications can define roles to follow least‑privilege. Grant only the permissions your functions need, then point `defaultRoleUniversalIdentifier` to that role's universal identifier.
|
||||
|
||||
#### Uploading files
|
||||
|
||||
The generated `Twenty` client includes an `uploadFile` method for attaching files to file-type fields on your workspace objects. Because standard GraphQL clients do not support multipart file uploads natively, the client provides this dedicated method that implements the [GraphQL multipart request specification](https://github.com/jaydenseric/graphql-multipart-request-spec) under the hood.
|
||||
|
||||
```typescript
|
||||
import Twenty from '~/generated';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const client = new Twenty();
|
||||
|
||||
const fileBuffer = fs.readFileSync('./invoice.pdf');
|
||||
|
||||
const uploadedFile = await client.uploadFile(
|
||||
fileBuffer, // file contents as a Buffer
|
||||
'invoice.pdf', // filename
|
||||
'application/pdf', // MIME type (defaults to 'application/octet-stream')
|
||||
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
|
||||
);
|
||||
|
||||
console.log(uploadedFile);
|
||||
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
|
||||
```
|
||||
|
||||
The method signature:
|
||||
|
||||
```typescript
|
||||
uploadFile(
|
||||
fileBuffer: Buffer,
|
||||
filename: string,
|
||||
contentType: string,
|
||||
fieldMetadataUniversalIdentifier: string,
|
||||
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
|
||||
```
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `fileBuffer` | `Buffer` | The raw file contents |
|
||||
| `filename` | `string` | The name of the file (used for storage and display) |
|
||||
| `contentType` | `string` | MIME type of the file (defaults to `application/octet-stream` if omitted) |
|
||||
| `fieldMetadataUniversalIdentifier` | `string` | The `universalIdentifier` of the file-type field on your object |
|
||||
|
||||
Key points:
|
||||
- The method sends the file to the **metadata endpoint** (not the main GraphQL endpoint), where the upload mutation is resolved.
|
||||
- It uses the field's `universalIdentifier` (not its workspace-specific ID), so your upload code works across any workspace where your app is installed — consistent with how apps reference fields everywhere else.
|
||||
- The returned `url` is a signed URL you can use to access the uploaded file.
|
||||
|
||||
### Hello World example
|
||||
|
||||
@@ -612,40 +838,29 @@ Explore a minimal, end-to-end example that demonstrates objects, logic functions
|
||||
|
||||
## Manual setup (without the scaffolder)
|
||||
|
||||
While we recommend using `create-twenty-app` for the best getting-started experience, you can also set up a project manually. Do not install the CLI globally. Instead, add `twenty-sdk` as a local dependency and wire scripts in your package.json:
|
||||
While we recommend using `create-twenty-app` for the best getting-started experience, you can also set up a project manually. Do not install the CLI globally. Instead, add `twenty-sdk` as a local dependency and wire a single script in your package.json:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D twenty-sdk
|
||||
```
|
||||
|
||||
Then add scripts like these:
|
||||
Then add a `twenty` script:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"app:generate": "twenty app:generate",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"entity:add": "twenty entity:add",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"help": "twenty help"
|
||||
"twenty": "twenty"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now you can run the same commands via Yarn, e.g. `yarn app:dev`, `yarn app:generate`, etc.
|
||||
Now you can run all commands via `yarn twenty <command>`, e.g. `yarn twenty app:dev`, `yarn twenty help`, etc.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Authentication errors: run `yarn auth:login` and ensure your API key has the required permissions.
|
||||
- Authentication errors: run `yarn twenty auth:login` and ensure your API key has the required permissions.
|
||||
- Cannot connect to server: verify the API URL and that the Twenty server is reachable.
|
||||
- Types or client missing/outdated: run `yarn app:generate`.
|
||||
- Dev mode not syncing: ensure `yarn app:dev` is running and that changes are not ignored by your environment.
|
||||
- Types or client missing/outdated: restart `yarn twenty app:dev` — it auto-generates the typed client.
|
||||
- Dev mode not syncing: ensure `yarn twenty app:dev` is running and that changes are not ignored by your environment.
|
||||
|
||||
Discord Help Channel: https://discord.com/channels/1130383047699738754/1130386664812982322
|
||||
|
||||
+3
-3
@@ -11,7 +11,7 @@ title: كائنات مخصصة
|
||||
## مخطط على مستوى عالي
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/server/custom-object-schema.png" alt="مخطط على مستوى عالي" />
|
||||
<img src="/images/docs/server/custom-object-schema.png" alt="مخطط على مستوى عالي" />
|
||||
</div>
|
||||
|
||||
<br />
|
||||
@@ -27,7 +27,7 @@ title: كائنات مخصصة
|
||||
لإضافة كائن مخصص، سيقوم عضو مساحة العمل بالاستعلام عن واجهة برمجة التطبيقات /metadata. يقوم هذا بتحديث البيانات الوصفية وفقًا لذلك ويحسب مخطط GraphQL استنادًا إلى البيانات الوصفية، ويخزنها في ذاكرة GQL للاستخدام لاحقًا.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/server/add-custom-objects.jpeg" alt="استعلام واجهة برمجة التطبيقات /metadata لإضافة الكائنات المخصصة" />
|
||||
<img src="/images/docs/server/add-custom-objects.jpeg" alt="استعلام واجهة برمجة التطبيقات /metadata لإضافة الكائنات المخصصة" />
|
||||
</div>
|
||||
|
||||
<br />
|
||||
@@ -35,5 +35,5 @@ title: كائنات مخصصة
|
||||
لجلب البيانات، تتضمن العملية إجراء استعلامات من خلال نقطة النهاية /graphql وتمريرها من خلال محلل الاستعلام.
|
||||
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="/images/docs/server/custom-object-schema.png" alt="استعلام نقطة النهاية /graphql لجلب البيانات" />
|
||||
<img src="/images/docs/server/custom-object-schema.png" alt="استعلام نقطة النهاية /graphql لجلب البيانات" />
|
||||
</div>
|
||||
|
||||
+4
-2
@@ -60,9 +60,11 @@ npx nx run twenty-server:command workspace:sync-metadata -f
|
||||
```
|
||||
|
||||
<Warning>
|
||||
سيؤدي هذا إلى إسقاط قاعدة البيانات وإعادة تشغيل الهجرات والبذور.
|
||||
|
||||
تأكد من عمل نسخة احتياطية لأي بيانات تريد الاحتفاظ بها قبل تشغيل هذا الأمر.
|
||||
سيؤدي هذا إلى إسقاط قاعدة البيانات وإعادة تشغيل الهجرات والبذور.
|
||||
|
||||
تأكد من عمل نسخة احتياطية لأي بيانات تريد الاحتفاظ بها قبل تشغيل هذا الأمر.
|
||||
|
||||
</Warning>
|
||||
|
||||
## "التقنية المستخدمة"
|
||||
|
||||
+3
-1
@@ -43,7 +43,9 @@ cp .env.example .env
|
||||
## التطوير
|
||||
|
||||
<Warning>
|
||||
تأكد من تشغيل `yarn build` قبل أي أمر `zapier`.
|
||||
|
||||
تأكد من تشغيل `yarn build` قبل أي أمر `zapier`.
|
||||
|
||||
</Warning>
|
||||
|
||||
### تجربة
|
||||
|
||||
+3
-1
@@ -13,7 +13,9 @@ title: أفضل الممارسات
|
||||
من الجيد إنشاء أكبر عدد ممكن من الذرات لتخزين الحالة الخاصة بك.
|
||||
|
||||
<Warning>
|
||||
من الأفضل استخدام ذرات إضافية بدلاً من محاولة أن تكون مقتضبًا باستخدام تمرير الخصائص.
|
||||
|
||||
من الأفضل استخدام ذرات إضافية بدلاً من محاولة أن تكون مقتضبًا باستخدام تمرير الخصائص.
|
||||
|
||||
</Warning>
|
||||
|
||||
```tsx
|
||||
|
||||
+3
-1
@@ -13,7 +13,9 @@ info: تعرّف على كيفية التعاون باستخدام Figma الخ
|
||||
تتوفر المميزات الرئيسية فقط للمستخدمين الذين قاموا بتسجيل الدخول، مثل وضع المطور والقدرة على اختيار إطار مخصص.
|
||||
|
||||
<Warning>
|
||||
لن تتمكن من التعاون بفعالية بدون حساب.
|
||||
|
||||
لن تتمكن من التعاون بفعالية بدون حساب.
|
||||
|
||||
</Warning>
|
||||
|
||||
## هيكل فيجما
|
||||
|
||||
@@ -6,66 +6,66 @@ description: الدليل للمساهمين (أو المطورين الفضول
|
||||
## المتطلبات الأساسية
|
||||
|
||||
<Tabs>
|
||||
<Tab title="Linux و MacOS">
|
||||
قبل أن تتمكن من تثبيت واستخدام Twenty، تأكد من تثبيت الأمور التالية على جهاز الكمبيوتر الخاص بك:
|
||||
<Tab title="Linux و MacOS">
|
||||
|
||||
* [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)
|
||||
قبل أن تتمكن من تثبيت واستخدام 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>
|
||||
<Warning>
|
||||
لن يعمل `npm` ، يجب عليك استخدام `yarn` بدلًا من ذلك. يأتي Yarn الآن مع Node.js، لذا لست بحاجة إلى تثبيته بشكل منفصل.
|
||||
عليك فقط تشغيل `corepack enable` لتفعيل Yarn إذا لم تقم بذلك بعد.
|
||||
</Warning>
|
||||
|
||||
<Tab title="ويندوز (WSL)">
|
||||
1. ثبّت WSL
|
||||
افتح PowerShell كمسؤول ثم نفّذ:
|
||||
</Tab>
|
||||
|
||||
```powershell
|
||||
wsl --install
|
||||
```
|
||||
<Tab title="ويندوز (WSL)">
|
||||
|
||||
يجب أن ترى الآن مطالبة لإعادة تشغيل جهاز الكمبيوتر الخاص بك. إذا لم يكن كذلك، فأعد تشغيله يدويًا.
|
||||
1. ثبّت WSL
|
||||
افتح PowerShell كمسؤول ثم نفّذ:
|
||||
```powershell
|
||||
wsl --install
|
||||
```
|
||||
يجب أن ترى الآن مطالبة لإعادة تشغيل جهاز الكمبيوتر الخاص بك. إذا لم يكن كذلك، فأعد تشغيله يدويًا.
|
||||
|
||||
عند إعادة التشغيل، ستُفتح نافذة PowerShell وسيتم تثبيت Ubuntu. قد يستغرق هذا وقتًا طويلاً.
|
||||
سترى مطالبة لإنشاء اسم المستخدم وكلمة المرور لتثبيت Ubuntu الخاص بك.
|
||||
عند إعادة التشغيل، ستُفتح نافذة PowerShell وسيتم تثبيت Ubuntu. قد يستغرق هذا وقتًا طويلاً.
|
||||
سترى مطالبة لإنشاء اسم المستخدم وكلمة المرور لتثبيت Ubuntu الخاص بك.
|
||||
|
||||
2. تثبيت وإعداد git
|
||||
2. تثبيت وإعداد git
|
||||
|
||||
```bash
|
||||
sudo apt-get install git
|
||||
```bash
|
||||
sudo apt-get install git
|
||||
|
||||
git config --global user.name "Your Name"
|
||||
git config --global user.name "Your Name"
|
||||
|
||||
git config --global user.email "youremail@domain.com"
|
||||
```
|
||||
git config --global user.email "youremail@domain.com"
|
||||
```
|
||||
|
||||
3. تثبيت nvm و node.js و yarn
|
||||
3. تثبيت nvm و node.js و yarn
|
||||
|
||||
<Warning>
|
||||
استخدم `nvm` لتثبيت نسخة `node` الصحيحة. الملف `.nvmrc` يضمن استخدام جميع المشاركين لنفس النسخة.
|
||||
</Warning>
|
||||
<Warning>
|
||||
استخدم `nvm` لتثبيت نسخة `node` الصحيحة. الملف `.nvmrc` يضمن استخدام جميع المشاركين لنفس النسخة.
|
||||
</Warning>
|
||||
|
||||
```bash
|
||||
sudo apt-get install curl
|
||||
```bash
|
||||
sudo apt-get install curl
|
||||
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
|
||||
```
|
||||
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
|
||||
```
|
||||
أغلق وأعد فتح برنامجك الطرفي لاستخدام nvm. ثم قم بتشغيل الأوامر التالية.
|
||||
|
||||
أغلق وأعد فتح برنامجك الطرفي لاستخدام nvm. ثم قم بتشغيل الأوامر التالية.
|
||||
```bash
|
||||
|
||||
```bash
|
||||
nvm install # يثبت إصدار node الموصى به
|
||||
|
||||
nvm install # يثبت إصدار node الموصى به
|
||||
nvm use # استخدم إصدار node الموصى به
|
||||
|
||||
nvm use # استخدم إصدار node الموصى به
|
||||
corepack enable
|
||||
```
|
||||
|
||||
corepack enable
|
||||
```
|
||||
</Tab>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
---
|
||||
@@ -75,19 +75,19 @@ description: الدليل للمساهمين (أو المطورين الفضول
|
||||
في الطرفية الخاصة بك، قم بتشغيل الأمر التالي.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="SSH (موصى به)">
|
||||
إذا لم تكن قد أعددت مفاتيح SSH بالفعل، يمكنك معرفة كيفية القيام بذلك [هنا](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/about-ssh).
|
||||
<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 git@github.com:twentyhq/twenty.git
|
||||
```
|
||||
</Tab>
|
||||
```bash
|
||||
git clone https://github.com/twentyhq/twenty.git
|
||||
```
|
||||
|
||||
<Tab title="HTTPS">
|
||||
```bash
|
||||
git clone https://github.com/twentyhq/twenty.git
|
||||
```
|
||||
</Tab>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## الخطوة 2: انتقل إلى جذر المشروع
|
||||
@@ -104,20 +104,16 @@ cd twenty
|
||||
<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`:
|
||||
|
||||
@@ -129,7 +125,6 @@ cd twenty
|
||||
```
|
||||
|
||||
يمكنك التحقق مما إذا كان خادم PostgreSQL يعمل بتنفيذ:
|
||||
|
||||
```bash
|
||||
brew services list
|
||||
```
|
||||
@@ -138,7 +133,6 @@ cd twenty
|
||||
عبر Homebrew على MacOS. بدلاً من ذلك، فإنه ينشئ دور PostgreSQL يطابق
|
||||
اسم المستخدم الخاص بك في MacOS (مثل "john").
|
||||
للتحقق وإنشاء المستخدم `postgres` إذا لزم الأمر، اتبع هذه الخطوات:
|
||||
|
||||
```bash
|
||||
# قم بالاتصال بPostgreSQL
|
||||
psql postgres
|
||||
@@ -147,59 +141,48 @@ cd twenty
|
||||
```
|
||||
|
||||
بمجرد أن تكون عند مطالبة psql (postgres=#)، قم بتشغيل:
|
||||
|
||||
```bash
|
||||
# قائمة الأدوار الموجودة في PostgreSQL
|
||||
\du
|
||||
```
|
||||
|
||||
```bash
|
||||
# قائمة الأدوار الموجودة في PostgreSQL
|
||||
\du
|
||||
```
|
||||
سترى مخرجات مشابهة ل:
|
||||
|
||||
```bash
|
||||
اسم الأدوار | الخصائص | عضو في
|
||||
-----------+-------------+-----------
|
||||
john | مشرف نظام | {}
|
||||
```
|
||||
```bash
|
||||
اسم الأدوار | الخصائص | عضو في
|
||||
-----------+-------------+-----------
|
||||
john | مشرف نظام | {}
|
||||
```
|
||||
|
||||
إذا لم ترَ دور `postgres` مدرجًا، انتقل إلى الخطوة التالية.
|
||||
قم بإنشاء دور `postgres` يدويًا:
|
||||
|
||||
```bash
|
||||
CREATE ROLE postgres WITH SUPERUSER LOGIN;
|
||||
```
|
||||
|
||||
```bash
|
||||
CREATE ROLE postgres WITH SUPERUSER LOGIN;
|
||||
```
|
||||
يقوم هذا بإنشاء دور مشرف نظام باسم `postgres` مع إمكانية تسجيل الدخول.
|
||||
|
||||
```bash
|
||||
اسم الدور | الخصائص | عضو في
|
||||
-----------+-------------+-----------
|
||||
postgres | مشرف نظام | {}
|
||||
john | مشرف نظام | {}
|
||||
```
|
||||
```bash
|
||||
اسم الدور | الخصائص | عضو في
|
||||
-----------+-------------+-----------
|
||||
postgres | مشرف نظام | {}
|
||||
john | مشرف نظام | {}
|
||||
```
|
||||
|
||||
**الخيار 2:** إذا كنت قد قمت بتثبيت docker:
|
||||
|
||||
```bash
|
||||
make -C packages/twenty-docker postgres-on-docker
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="ويندوز (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
|
||||
```
|
||||
@@ -218,35 +201,28 @@ cd twenty
|
||||
استخدم الرابط التالي لتثبيت 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 الخاص بك:
|
||||
`brew services start redis`
|
||||
|
||||
**الخيار 2:** إذا كنت قد قمت بتثبيت docker:
|
||||
|
||||
```bash
|
||||
make -C packages/twenty-docker redis-on-docker
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="ويندوز (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
|
||||
```
|
||||
@@ -267,7 +243,7 @@ cp ./packages/twenty-server/.env.example ./packages/twenty-server/.env
|
||||
```
|
||||
|
||||
<Info>
|
||||
**وضع تعدد مساحات العمل:** بشكل افتراضي، يعمل Twenty في وضع مساحة عمل واحدة حيث يمكن إنشاء مساحة عمل واحدة فقط. لتمكين دعم تعدد مساحات العمل (مفيد لاختبار الميزات المعتمدة على النطاقات الفرعية)، عيّن `IS_MULTIWORKSPACE_ENABLED=true` في ملف الخادم `.env`. راجع [وضع تعدد مساحات العمل](/l/ar/developers/self-host/capabilities/setup#multi-workspace-mode) للحصول على التفاصيل.
|
||||
**وضع تعدد مساحات العمل:** بشكل افتراضي، يعمل Twenty في وضع مساحة عمل واحدة حيث يمكن إنشاء مساحة عمل واحدة فقط. لتمكين دعم تعدد مساحات العمل (مفيد لاختبار الميزات المعتمدة على النطاقات الفرعية)، عيّن `IS_MULTIWORKSPACE_ENABLED=true` في ملف الخادم `.env`. راجع [وضع تعدد مساحات العمل](/l/ar/developers/self-host/capabilities/setup#multi-workspace-mode) للحصول على التفاصيل.
|
||||
</Info>
|
||||
|
||||
## الخطوة 6: تثبيت التبعيات
|
||||
@@ -287,15 +263,12 @@ yarn
|
||||
اعتمادًا على توزيعة Linux الخاصة بك، قد يتم بدء خادم Redis تلقائيًا.
|
||||
إذا لم يكن كذلك، تحقق من [دليل تثبيت Redis](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) لتوزيعتك.
|
||||
</Tab>
|
||||
|
||||
<Tab title="نظام Mac OS">
|
||||
من المفترض أن يكون Redis قد تم تشغيله بالفعل. إذا لم يكن كذلك، قم بتشغيل:
|
||||
|
||||
من المفترض أن يكون Redis قد تم تشغيله بالفعل. إذا لم يكن كذلك، قم بتشغيل:
|
||||
```bash
|
||||
brew services start redis
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="ويندوز (WSL)">
|
||||
اعتمادًا على توزيعة Linux الخاصة بك، قد يتم بدء خادم Redis تلقائيًا.
|
||||
إذا لم يكن كذلك، تحقق من [دليل تثبيت ريديس](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) لتوزيعتك.
|
||||
|
||||
@@ -25,7 +25,6 @@ Twenty مفتوح المصدر ويرحب بمساهمات المجتمع. سو
|
||||
<Card title="تقارير الأخطاء والطلبات" icon="bug" href="/l/ar/developers/contribute/capabilities/bug-and-requests">
|
||||
أبلغ عن المشكلات أو اطلب ميزات
|
||||
</Card>
|
||||
|
||||
<Card title="تطوير الواجهة الأمامية" icon="browser" href="/l/ar/developers/contribute/capabilities/frontend-development">
|
||||
ساهم في واجهة المستخدم
|
||||
</Card>
|
||||
|
||||
@@ -17,7 +17,7 @@ import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
* **وثائق مخصصة**: يتم إنشاؤها خصيصًا لنموذج بيانات مساحة عملك
|
||||
|
||||
<Note>
|
||||
وثائق واجهة برمجة التطبيقات المخصصة لك متاحة ضمن **الإعدادات → API & Webhooks** بعد إنشاء مفتاح API. نظرًا لأن Twenty تُنشئ واجهات برمجة تطبيقات تتطابق مع نموذج البيانات المخصص لديك، فإن الوثائق فريدة لمساحة عملك.
|
||||
وثائق واجهة برمجة التطبيقات المخصصة لك متاحة ضمن **الإعدادات → API & Webhooks** بعد إنشاء مفتاح API. نظرًا لأن Twenty تُنشئ واجهات برمجة تطبيقات تتطابق مع نموذج البيانات المخصص لديك، فإن الوثائق فريدة لمساحة عملك.
|
||||
</Note>
|
||||
|
||||
## نوعا واجهات برمجة التطبيقات
|
||||
@@ -81,7 +81,7 @@ Authorization: Bearer YOUR_API_KEY
|
||||
<VimeoEmbed videoId="928786722" title="إنشاء مفتاح API" />
|
||||
|
||||
<Warning>
|
||||
يمنح مفتاح API الخاص بك الوصول إلى بيانات حساسة. لا تشاركه مع خدمات غير موثوقة. إذا تم اختراقه، عطّلْه فوراً وأنشئ مفتاحاً جديداً.
|
||||
يمنح مفتاح API الخاص بك الوصول إلى بيانات حساسة. لا تشاركه مع خدمات غير موثوقة. إذا تم اختراقه، عطّلْه فوراً وأنشئ مفتاحاً جديداً.
|
||||
</Warning>
|
||||
|
||||
### تعيين دور لمفتاح API
|
||||
@@ -143,5 +143,5 @@ Authorization: Bearer YOUR_API_KEY
|
||||
| **حجم الدفعة** | 60 سجل لكل استدعاء |
|
||||
|
||||
<Tip>
|
||||
استخدم عمليات الدفعات لزيادة الإنتاجية — عالج ما يصل إلى 60 سجلًا في استدعاء API واحد بدلاً من إجراء طلبات فردية.
|
||||
استخدم عمليات الدفعات لزيادة الإنتاجية — عالج ما يصل إلى 60 سجلًا في استدعاء API واحد بدلاً من إجراء طلبات فردية.
|
||||
</Tip>
|
||||
|
||||
@@ -4,7 +4,7 @@ description: أنشئ وأدِر تخصيصات Twenty على هيئة كود.
|
||||
---
|
||||
|
||||
<Warning>
|
||||
التطبيقات حاليًا في مرحلة الاختبار الألفا. الميزة تعمل لكنها لا تزال قيد التطور.
|
||||
التطبيقات حاليًا في مرحلة الاختبار الألفا. الميزة تعمل لكنها لا تزال قيد التطور.
|
||||
</Warning>
|
||||
|
||||
## ما هي التطبيقات؟
|
||||
@@ -15,6 +15,7 @@ description: أنشئ وأدِر تخصيصات Twenty على هيئة كود.
|
||||
|
||||
* عرِّف كائنات وحقولًا مخصصة على شكل كود (نموذج بيانات مُدار)
|
||||
* أنشئ وظائف منطقية مع مشغلات مخصصة
|
||||
* حدد المهارات لوكلاء الذكاء الاصطناعي
|
||||
* انشر التطبيق نفسه عبر مساحات عمل متعددة
|
||||
|
||||
## المتطلبات الأساسية
|
||||
@@ -27,41 +28,47 @@ description: أنشئ وأدِر تخصيصات Twenty على هيئة كود.
|
||||
أنشئ تطبيقًا جديدًا باستخدام المُهيئ الرسمي، ثم قم بالمصادقة وابدأ التطوير:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# إنشاء تطبيق جديد
|
||||
# إنشاء تطبيق جديد (يتضمن جميع الأمثلة افتراضيًا)
|
||||
npx create-twenty-app@latest my-twenty-app
|
||||
cd my-twenty-app
|
||||
|
||||
# إذا كنت لا تستخدم yarn@4
|
||||
corepack enable
|
||||
yarn install
|
||||
|
||||
# قم بالمصادقة باستخدام مفتاح واجهة برمجة التطبيقات الخاص بك (سيُطلب منك ذلك)
|
||||
yarn auth:login
|
||||
|
||||
# ابدأ وضع التطوير: يُزامن التغييرات المحلية تلقائيًا مع مساحة العمل الخاصة بك
|
||||
yarn app:dev
|
||||
yarn twenty app:dev
|
||||
```
|
||||
|
||||
يدعم المُنشئ ثلاثة أوضاع للتحكم في ملفات الأمثلة التي سيتم تضمينها:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# الافتراضي (شامل): جميع الأمثلة (كائن، حقل، دالة منطقية، مكوّن الواجهة الأمامية، عرض، عنصر قائمة التنقل، مهارة)
|
||||
npx create-twenty-app@latest my-app
|
||||
|
||||
# الأدنى: الملفات الأساسية فقط (application-config.ts و default-role.ts)
|
||||
npx create-twenty-app@latest my-app --minimal
|
||||
|
||||
# التفاعلي: اختر الأمثلة التي تريد تضمينها
|
||||
npx create-twenty-app@latest my-app --interactive
|
||||
```
|
||||
|
||||
من هنا يمكنك:
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Add a new entity to your application (guided)
|
||||
yarn entity:add
|
||||
|
||||
# Generate a typed Twenty client and workspace entity types
|
||||
yarn app:generate
|
||||
yarn twenty entity:add
|
||||
|
||||
# Watch your application's function logs
|
||||
yarn function:logs
|
||||
yarn twenty function:logs
|
||||
|
||||
# Execute a function by name
|
||||
yarn function:execute -n my-function -p '{"name": "test"}'
|
||||
yarn twenty function:execute -n my-function -p '{"name": "test"}'
|
||||
|
||||
# Execute the post-install function
|
||||
yarn twenty function:execute --postInstall
|
||||
|
||||
# Uninstall the application from the current workspace
|
||||
yarn app:uninstall
|
||||
yarn twenty app:uninstall
|
||||
|
||||
# Display commands' help
|
||||
yarn help
|
||||
yarn twenty help
|
||||
```
|
||||
|
||||
راجع أيضًا: صفحات مرجع CLI لـ [create-twenty-app](https://www.npmjs.com/package/create-twenty-app) و[twenty-sdk CLI](https://www.npmjs.com/package/twenty-sdk).
|
||||
@@ -73,9 +80,9 @@ yarn help
|
||||
* ينسخ تطبيقًا أساسيًا مصغّرًا إلى `my-twenty-app/`
|
||||
* يضيف اعتمادًا محليًا `twenty-sdk` وتهيئة Yarn 4
|
||||
* ينشئ ملفات ضبط ونصوصًا مرتبطة بـ `twenty` CLI
|
||||
* يُولّد ضبطًا افتراضيًا للتطبيق ودورًا افتراضيًا للوظيفة
|
||||
* يُنشئ الملفات الأساسية (تهيئة التطبيق، دور الدالة الافتراضي، دالة ما بعد التثبيت) بالإضافة إلى ملفات الأمثلة بحسب وضع الإنشاء
|
||||
|
||||
يبدو التطبيق المُنشأ حديثًا بالقالب كما يلي:
|
||||
يبدو التطبيق المُنشأ حديثًا باستخدام الوضع الافتراضي `--exhaustive` كما يلي:
|
||||
|
||||
```text filename="my-twenty-app/"
|
||||
my-twenty-app/
|
||||
@@ -91,18 +98,31 @@ my-twenty-app/
|
||||
README.md
|
||||
public/ # مجلد الأصول العامة (صور، خطوط، إلخ)
|
||||
src/
|
||||
├── application-config.ts # مطلوب - التكوين الرئيسي للتطبيق
|
||||
├── application-config.ts # مطلوب - إعدادات التطبيق الرئيسية
|
||||
├── roles/
|
||||
│ └── default-role.ts # الدور الافتراضي لوظائف المنطق
|
||||
│ └── default-role.ts # الدور الافتراضي للدوال المنطقية
|
||||
├── objects/
|
||||
│ └── example-object.ts # تعريف كائن مخصص — مثال
|
||||
├── fields/
|
||||
│ └── example-field.ts # تعريف حقل مستقل — مثال
|
||||
├── logic-functions/
|
||||
│ └── hello-world.ts # مثال لوظيفة منطقية
|
||||
└── front-components/
|
||||
└── hello-world.tsx # مثال لمكوّن الواجهة الأمامية
|
||||
│ ├── hello-world.ts # دالة منطقية — مثال
|
||||
│ └── post-install.ts # دالة منطقية لما بعد التثبيت
|
||||
├── front-components/
|
||||
│ └── hello-world.tsx # مكوّن واجهة أمامية — مثال
|
||||
├── views/
|
||||
│ └── example-view.ts # تعريف عرض محفوظ — مثال
|
||||
├── navigation-menu-items/
|
||||
│ └── example-navigation-menu-item.ts # رابط تنقّل في الشريط الجانبي — مثال
|
||||
└── skills/
|
||||
└── example-skill.ts # تعريف مهارة لوكيل الذكاء الاصطناعي — مثال
|
||||
```
|
||||
|
||||
مع `--minimal`، سيتم إنشاء الملفات الأساسية فقط (`application-config.ts` و`roles/default-role.ts` و`logic-functions/post-install.ts`). مع `--interactive`، تختار ملفات الأمثلة التي تريد تضمينها.
|
||||
|
||||
بشكل عام:
|
||||
|
||||
* **package.json**: يصرّح باسم التطبيق والإصدار والمحرّكات (Node 24+، Yarn 4)، ويضيف `twenty-sdk` فضلًا عن نصوص مثل `app:dev` و`app:generate` و`entity:add` و`function:logs` و`function:execute` و`app:uninstall` وأوامر المصادقة التي تُفوِّض إلى `twenty` CLI المحلي.
|
||||
* **package.json**: يصرّح باسم التطبيق والإصدار والمحرّكات (Node 24+، Yarn 4)، ويضيف `twenty-sdk` بالإضافة إلى نص برمجي `twenty` يفوِّض إلى `twenty` CLI المحلي. شغِّل `yarn twenty help` لعرض جميع الأوامر المتاحة.
|
||||
* **.gitignore**: يتجاهل العناصر الشائعة مثل `node_modules` و`.yarn` و`generated/` (عميل مضبوط الأنواع) و`dist/` و`build/` ومجلدات التغطية وملفات السجلات وملفات `.env*`.
|
||||
* **yarn.lock**، **.yarnrc.yml**، **.yarn/**: تقوم بقفل وتكوين حزمة أدوات Yarn 4 المستخدمة في المشروع.
|
||||
* **.nvmrc**: يثبّت إصدار Node.js المتوقع للمشروع.
|
||||
@@ -115,16 +135,19 @@ my-twenty-app/
|
||||
|
||||
يكتشف SDK الكيانات عبر تحليل ملفات TypeScript الخاصة بك بحثًا عن استدعاءات **`export default define<Entity>({...})`**. يحتوي كل نوع كيان على دالة مساعدة مقابلة يتم تصديرها من `twenty-sdk`:
|
||||
|
||||
| دالة مساعدة | نوع الكيان |
|
||||
| ------------------------ | --------------------------------- |
|
||||
| `defineObject()` | تعريفات كائنات مخصصة |
|
||||
| `defineLogicFunction()` | تعريفات الوظائف المنطقية |
|
||||
| `defineFrontComponent()` | Front component definitions |
|
||||
| `defineRole()` | تعريفات الأدوار |
|
||||
| `defineField()` | امتدادات الحقول للكائنات الموجودة |
|
||||
| دالة مساعدة | نوع الكيان |
|
||||
| ---------------------------- | ------------------------------------ |
|
||||
| `defineObject()` | تعريفات كائنات مخصصة |
|
||||
| `defineLogicFunction()` | تعريفات الوظائف المنطقية |
|
||||
| `defineFrontComponent()` | Front component definitions |
|
||||
| `defineRole()` | تعريفات الأدوار |
|
||||
| `defineField()` | امتدادات الحقول للكائنات الموجودة |
|
||||
| `defineView()` | تعريفات العروض المحفوظة |
|
||||
| `defineNavigationMenuItem()` | تعريفات عناصر قائمة التنقل |
|
||||
| `defineSkill()` | تعريفات مهارات وكيل الذكاء الاصطناعي |
|
||||
|
||||
<Note>
|
||||
**تسمية الملفات مرنة.** يعتمد اكتشاف الكيانات على بنية الشجرة المجردة (AST) — إذ يقوم SDK بفحص ملفات المصدر لديك بحثًا عن النمط `export default define<Entity>({...})`. يمكنك تنظيم ملفاتك ومجلداتك كيفما تشاء. التجميع حسب نوع الكيان (مثلًا، `logic-functions/` و`roles/`) هو مجرد عرف لتنظيم الشيفرة، وليس مطلبًا إلزاميًا.
|
||||
**تسمية الملفات مرنة.** يعتمد اكتشاف الكيانات على بنية الشجرة المجردة (AST) — إذ يقوم SDK بفحص ملفات المصدر لديك بحثًا عن النمط `export default define<Entity>({...})`. يمكنك تنظيم ملفاتك ومجلداتك كيفما تشاء. التجميع حسب نوع الكيان (مثلًا، `logic-functions/` و`roles/`) هو مجرد عرف لتنظيم الشيفرة، وليس مطلبًا إلزاميًا.
|
||||
</Note>
|
||||
|
||||
مثال على كيان تم اكتشافه:
|
||||
@@ -142,12 +165,12 @@ export default defineObject({
|
||||
|
||||
ستضيف الأوامر اللاحقة مزيدًا من الملفات والمجلدات:
|
||||
|
||||
* `yarn app:generate` سيُنشئ مجلدًا `generated/` (عميل Twenty مضبوط الأنواع + أنواع مساحة العمل).
|
||||
* `yarn entity:add` سيضيف ملفات تعريف الكيانات تحت `src/` لكائناتك المخصصة أو الوظائف أو المكونات الواجهية أو الأدوار.
|
||||
* `yarn twenty app:dev` سيولّد تلقائيًا عميل API مضبوط الأنواع في `node_modules/twenty-sdk/generated` (عميل Twenty مضبوط الأنواع + أنواع مساحة العمل).
|
||||
* `yarn twenty entity:add` سيضيف ملفات تعريف الكيانات ضمن `src/` لكائناتك المخصّصة، والوظائف، ومكوّنات الواجهة الأمامية، والأدوار، والمهارات، وغير ذلك.
|
||||
|
||||
## المصادقة
|
||||
|
||||
في المرة الأولى التي تشغّل فيها `yarn auth:login`، سيُطلب منك إدخال:
|
||||
في المرة الأولى التي تشغّل فيها `yarn twenty auth:login`، سيُطلب منك إدخال:
|
||||
|
||||
* عنوان URL لواجهة برمجة التطبيقات (الافتراضي http://localhost:3000 أو ملف تعريف مساحة العمل الحالية لديك)
|
||||
* مفتاح واجهة برمجة التطبيقات
|
||||
@@ -157,26 +180,26 @@ export default defineObject({
|
||||
### Managing workspaces
|
||||
|
||||
```bash filename="Terminal"
|
||||
# Login interactively (recommended)
|
||||
yarn auth:login
|
||||
# تسجيل الدخول تفاعليًا (مُوصى به)
|
||||
yarn twenty auth:login
|
||||
|
||||
# Login to a specific workspace profile
|
||||
yarn auth:login --workspace my-custom-workspace
|
||||
# تسجيل الدخول إلى ملف تعريف لمساحة عمل محددة
|
||||
yarn twenty auth:login --workspace my-custom-workspace
|
||||
|
||||
# List all configured workspaces
|
||||
yarn auth:list
|
||||
# عرض جميع مساحات العمل المُكوَّنة
|
||||
yarn twenty auth:list
|
||||
|
||||
# Switch the default workspace (interactive)
|
||||
yarn auth:switch
|
||||
# تبديل مساحة العمل الافتراضية (تفاعليًا)
|
||||
yarn twenty auth:switch
|
||||
|
||||
# Switch to a specific workspace
|
||||
yarn auth:switch production
|
||||
# التبديل إلى مساحة عمل محددة
|
||||
yarn twenty auth:switch production
|
||||
|
||||
# Check current authentication status
|
||||
yarn auth:status
|
||||
# التحقق من حالة المصادقة الحالية
|
||||
yarn twenty auth:status
|
||||
```
|
||||
|
||||
Once you've switched workspaces with `auth:switch`, all subsequent commands will use that workspace by default. You can still override it temporarily with `--workspace <name>`.
|
||||
بمجرد أن تقوم بالتبديل بين مساحات العمل باستخدام `yarn twenty auth:switch`، ستستخدم جميع الأوامر اللاحقة تلك المساحة افتراضيًا. You can still override it temporarily with `--workspace <name>`.
|
||||
|
||||
## استخدم موارد SDK (الأنواع والتكوين)
|
||||
|
||||
@@ -186,14 +209,17 @@ Once you've switched workspaces with `auth:switch`, all subsequent commands will
|
||||
|
||||
يوفّر SDK دوالًا مساعدة لتعريف كيانات تطبيقك. كما هو موضح في [اكتشاف الكيانات](#entity-detection)، يجب استخدام `export default define<Entity>({...})` كي يتم اكتشاف كياناتك:
|
||||
|
||||
| دالة | الغرض |
|
||||
| ------------------------ | ---------------------------------------------------- |
|
||||
| `defineApplication()` | تهيئة بيانات التعريف للتطبيق (مطلوب، واحد لكل تطبيق) |
|
||||
| `defineObject()` | تعريف كائنات مخصصة مع حقول |
|
||||
| `defineLogicFunction()` | تعريف وظائف منطقية مع معالجات |
|
||||
| `defineFrontComponent()` | عرِّف مكوّنات أمامية لواجهة مستخدم مخصّصة |
|
||||
| `defineRole()` | تهيئة صلاحيات الدور والوصول إلى الكائنات |
|
||||
| `defineField()` | وسّع الكائنات الموجودة بحقول إضافية |
|
||||
| دالة | الغرض |
|
||||
| ---------------------------- | ---------------------------------------------------- |
|
||||
| `defineApplication()` | تهيئة بيانات التعريف للتطبيق (مطلوب، واحد لكل تطبيق) |
|
||||
| `defineObject()` | تعريف كائنات مخصصة مع حقول |
|
||||
| `defineLogicFunction()` | تعريف وظائف منطقية مع معالجات |
|
||||
| `defineFrontComponent()` | عرِّف مكوّنات أمامية لواجهة مستخدم مخصّصة |
|
||||
| `defineRole()` | تهيئة صلاحيات الدور والوصول إلى الكائنات |
|
||||
| `defineField()` | وسّع الكائنات الموجودة بحقول إضافية |
|
||||
| `defineView()` | تعريف العروض المحفوظة للكائنات |
|
||||
| `defineNavigationMenuItem()` | تعريف روابط التنقل في الشريط الجانبي |
|
||||
| `defineSkill()` | عرّف مهارات وكيل الذكاء الاصطناعي |
|
||||
|
||||
تتحقق هذه الدوال من تكوينك وقت البناء وتوفّر إكمالًا تلقائيًا في بيئة التطوير وأمان الأنواع.
|
||||
|
||||
@@ -276,10 +302,14 @@ export default defineObject({
|
||||
* `universalIdentifier` يجب أن يكون فريدًا وثابتًا عبر عمليات النشر.
|
||||
* يتطلب كل حقل `name` و`type` و`label` ومعرّف `universalIdentifier` ثابتًا خاصًا به.
|
||||
* المصفوفة `fields` اختيارية — يمكنك تعريف كائنات بدون حقول مخصصة.
|
||||
* يمكنك إنشاء كائنات جديدة باستخدام `yarn entity:add`، والذي يرشدك خلال التسمية والحقول والعلاقات.
|
||||
* يمكنك إنشاء كائنات جديدة باستخدام `yarn twenty entity:add`، والذي يرشدك خلال التسمية والحقول والعلاقات.
|
||||
|
||||
<Note>
|
||||
**يتم إنشاء الحقول الأساسية تلقائيًا.** عند تعريف كائن مخصص، يضيف Twenty تلقائيًا حقولًا قياسية مثل `name` و`createdAt` و`updatedAt` و`createdBy` و`position` و`deletedAt`. لا تحتاج إلى تعريف هذه في مصفوفة `fields` — أضف فقط حقولك المخصصة.
|
||||
**يتم إنشاء الحقول الأساسية تلقائيًا.** عند تعريف كائن مخصص، يضيف Twenty تلقائيًا حقولًا قياسية
|
||||
مثل `id` و`name` و`createdAt` و`updatedAt` و`createdBy` و`updatedBy` و`deletedAt`.
|
||||
لا تحتاج إلى تعريف هذه في مصفوفة `fields` — أضف فقط حقولك المخصصة.
|
||||
يمكنك تجاوز الحقول الافتراضية من خلال تعريف حقل بالاسم نفسه في مصفوفة `fields` الخاصة بك،
|
||||
لكن هذا غير مستحسن.
|
||||
</Note>
|
||||
|
||||
### تكوين التطبيق (application-config.ts)
|
||||
@@ -289,6 +319,7 @@ export default defineObject({
|
||||
* **هوية التطبيق**: المعرفات، اسم العرض، والوصف.
|
||||
* **كيفية تشغيل وظائفه**: الدور الذي تستخدمه للأذونات.
|
||||
* **متغيرات (اختياري)**: أزواج مفتاح-قيمة تُعرض لوظائفك كمتغيرات بيئة.
|
||||
* **(Optional) post-install function**: a logic function that runs after the app is installed.
|
||||
|
||||
Use `defineApplication()` to define your application configuration:
|
||||
|
||||
@@ -296,6 +327,7 @@ Use `defineApplication()` to define your application configuration:
|
||||
// src/application-config.ts
|
||||
import { defineApplication } from 'twenty-sdk';
|
||||
import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/roles/default-role';
|
||||
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
|
||||
|
||||
export default defineApplication({
|
||||
universalIdentifier: '4ec0391d-18d5-411c-b2f3-266ddc1c3ef7',
|
||||
@@ -311,6 +343,7 @@ export default defineApplication({
|
||||
},
|
||||
},
|
||||
defaultRoleUniversalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER,
|
||||
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
@@ -319,6 +352,7 @@ export default defineApplication({
|
||||
* حقول `universalIdentifier` هي معرّفات حتمية تخصك؛ أنشئها مرة واحدة واحتفظ بها ثابتة عبر عمليات المزامنة.
|
||||
* `applicationVariables` تصبح متغيرات بيئة لوظائفك (على سبيل المثال، `DEFAULT_RECIPIENT_NAME` متاح كـ `process.env.DEFAULT_RECIPIENT_NAME`).
|
||||
* `defaultRoleUniversalIdentifier` يجب أن يطابق ملف الدور (انظر أدناه).
|
||||
* `postInstallLogicFunctionUniversalIdentifier` (optional) points to a logic function that runs automatically after the app is installed. See [Post-install functions](#post-install-functions).
|
||||
|
||||
#### الأدوار والصلاحيات
|
||||
|
||||
@@ -457,30 +491,77 @@ export default defineLogicFunction({
|
||||
* المصفوفة `triggers` اختيارية. يمكن استخدام الوظائف بدون مشغلات كوظائف مساعدة تُستدعى بواسطة وظائف أخرى.
|
||||
* يمكنك مزج أنواع متعددة من المشغلات في وظيفة واحدة.
|
||||
|
||||
### Post-install functions
|
||||
|
||||
A post-install function is a logic function that runs automatically after your app is installed on a workspace. This is useful for one-time setup tasks such as seeding default data, creating initial records, or configuring workspace settings.
|
||||
|
||||
عند إنشاء هيكل تطبيق جديد باستخدام `create-twenty-app`، يتم إنشاء دالة ما بعد التثبيت لك في `src/logic-functions/post-install.ts`:
|
||||
|
||||
```typescript
|
||||
// src/logic-functions/post-install.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
|
||||
export const POST_INSTALL_UNIVERSAL_IDENTIFIER = '<generated-uuid>';
|
||||
|
||||
const handler = async (): Promise<void> => {
|
||||
console.log('Post install logic function executed successfully!');
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
|
||||
name: 'post-install',
|
||||
description: 'Runs after installation to set up the application.',
|
||||
timeoutSeconds: 300,
|
||||
handler,
|
||||
});
|
||||
```
|
||||
|
||||
يتم ربط الدالة بتطبيقك من خلال الإشارة إلى المعرِّف العالمي الخاص بها في `application-config.ts`:
|
||||
|
||||
```typescript
|
||||
import { POST_INSTALL_UNIVERSAL_IDENTIFIER } from 'src/logic-functions/post-install';
|
||||
|
||||
export default defineApplication({
|
||||
// ...
|
||||
postInstallLogicFunctionUniversalIdentifier: POST_INSTALL_UNIVERSAL_IDENTIFIER,
|
||||
});
|
||||
```
|
||||
|
||||
يمكنك أيضًا تنفيذ دالة ما بعد التثبيت يدويًا في أي وقت باستخدام CLI:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn twenty function:execute --postInstall
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
|
||||
* دوال ما بعد التثبيت هي دوال منطقية قياسية — فهي تستخدم `defineLogicFunction()` مثل أي دالة أخرى.
|
||||
* حقل `postInstallLogicFunctionUniversalIdentifier` في `defineApplication()` اختياري. إذا تم تجاهله، لن يتم تشغيل أي دالة بعد التثبيت.
|
||||
* تم تعيين مهلة افتراضية إلى 300 ثانية (5 دقائق) للسماح بمهام الإعداد الأطول مثل تهيئة البيانات.
|
||||
* لا تحتاج دوال ما بعد التثبيت إلى مُشغِّلات — حيث يستدعيها النظام الأساسي أثناء التثبيت أو يدويًا عبر `function:execute --postInstall`.
|
||||
|
||||
### حمولة مشغل المسار
|
||||
|
||||
<Warning>
|
||||
**تغيير غير متوافق (v1.16، يناير 2026):** لقد تغير تنسيق حمولة مشغل المسار. قبل v1.16، كانت معلمات الاستعلام، ومعلمات المسار، وجسم الطلب تُرسل مباشرةً كحمولة. بدءًا من v1.16، أصبحت متداخلة داخل كائن منظَّم `RoutePayload`.
|
||||
**تغيير غير متوافق (v1.16، يناير 2026):** لقد تغير تنسيق حمولة مشغل المسار. قبل v1.16، كانت معلمات الاستعلام، ومعلمات المسار، وجسم الطلب تُرسل مباشرةً كحمولة. بدءًا من v1.16، أصبحت متداخلة داخل كائن منظَّم `RoutePayload`.
|
||||
|
||||
**قبل v1.16:**
|
||||
**قبل v1.16:**
|
||||
```typescript
|
||||
const handler = async (params) => {
|
||||
const { param1, param2 } = params; // Direct access
|
||||
};
|
||||
```
|
||||
|
||||
```typescript
|
||||
const handler = async (params) => {
|
||||
const { param1, param2 } = params; // Direct access
|
||||
};
|
||||
```
|
||||
**بعد v1.16:**
|
||||
```typescript
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { param1, param2 } = event.body; // Access via .body
|
||||
const { queryParam } = event.queryStringParameters;
|
||||
const { id } = event.pathParameters;
|
||||
};
|
||||
```
|
||||
|
||||
**بعد v1.16:**
|
||||
|
||||
```typescript
|
||||
const handler = async (event: RoutePayload) => {
|
||||
const { param1, param2 } = event.body; // Access via .body
|
||||
const { queryParam } = event.queryStringParameters;
|
||||
const { id } = event.pathParameters;
|
||||
};
|
||||
```
|
||||
|
||||
**لترحيل الدوال الحالية:** حدّث المعالج لديك لفكّ البنية من `event.body` أو `event.queryStringParameters` أو `event.pathParameters` بدلاً من القراءة مباشرةً من كائن params.
|
||||
**لترحيل الدوال الحالية:** حدّث المعالج لديك لفكّ البنية من `event.body` أو `event.queryStringParameters` أو `event.pathParameters` بدلاً من القراءة مباشرةً من كائن params.
|
||||
</Warning>
|
||||
|
||||
عندما يستدعي مشغّل المسار وظيفتك المنطقية، يتلقى كائنًا من النوع `RoutePayload` يتبع تنسيق AWS HTTP API v2. استورد النوع من `twenty-sdk`:
|
||||
@@ -551,9 +632,74 @@ const handler = async (event: RoutePayload) => {
|
||||
|
||||
يمكنك إنشاء وظائف جديدة بطريقتين:
|
||||
|
||||
* **مُنشأ بالقالب**: شغّل `yarn entity:add` واختر خيار إضافة وظيفة منطقية جديدة. يُولّد هذا ملفًا مبدئيًا مع معالج وتكوين.
|
||||
* **مُنشأ بالقالب**: شغّل `yarn twenty entity:add` واختر خيار إضافة وظيفة منطقية جديدة. يُولّد هذا ملفًا مبدئيًا مع معالج وتكوين.
|
||||
* **يدوي**: أنشئ ملفًا جديدًا `*.logic-function.ts` واستخدم `defineLogicFunction()` مع اتباع النمط نفسه.
|
||||
|
||||
### تمييز دالة منطقية كأداة
|
||||
|
||||
يمكن إتاحة الدوال المنطقية بوصفها **أدوات** لوكلاء الذكاء الاصطناعي وسير العمل. عندما يتم تمييز دالة كأداة، تصبح قابلة للاكتشاف بواسطة ميزات الذكاء الاصطناعي الخاصة بـ Twenty ويمكن اختيارها كخطوة في أتمتة سير العمل.
|
||||
|
||||
لتمييز دالة منطقية كأداة، عيّن `isTool: true` وقدّم `toolInputSchema` يصف معاملات الإدخال المتوقعة باستخدام [مخطط JSON](https://json-schema.org/):
|
||||
|
||||
```typescript
|
||||
// src/logic-functions/enrich-company.logic-function.ts
|
||||
import { defineLogicFunction } from 'twenty-sdk';
|
||||
import Twenty from '~/generated';
|
||||
|
||||
const handler = async (params: { companyName: string; domain?: string }) => {
|
||||
const client = new Twenty();
|
||||
|
||||
const result = await client.mutation({
|
||||
createTask: {
|
||||
__args: {
|
||||
data: {
|
||||
title: `Enrich data for ${params.companyName}`,
|
||||
body: `Domain: ${params.domain ?? 'unknown'}`,
|
||||
},
|
||||
},
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
return { taskId: result.createTask.id };
|
||||
};
|
||||
|
||||
export default defineLogicFunction({
|
||||
universalIdentifier: 'f47ac10b-58cc-4372-a567-0e02b2c3d479',
|
||||
name: 'enrich-company',
|
||||
description: 'Enrich a company record with external data',
|
||||
timeoutSeconds: 10,
|
||||
handler,
|
||||
isTool: true,
|
||||
toolInputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
companyName: {
|
||||
type: 'string',
|
||||
description: 'The name of the company to enrich',
|
||||
},
|
||||
domain: {
|
||||
type: 'string',
|
||||
description: 'The company website domain (optional)',
|
||||
},
|
||||
},
|
||||
required: ['companyName'],
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
|
||||
* **`isTool`** (`boolean`, الافتراضي: `false`): عند ضبطه على `true`، يتم تسجيل الدالة كأداة وتصبح متاحة لوكلاء الذكاء الاصطناعي ولأتمتة سير العمل.
|
||||
* **`toolInputSchema`** (`object`, اختياري): كائن JSON Schema يصف المعلمات التي تقبلها دالتك. يستخدم وكلاء الذكاء الاصطناعي هذا المخطط لفهم المدخلات التي تتوقعها الأداة وللتحقق من صحة الاستدعاءات. إذا تم إغفاله، فالقيمة الافتراضية للمخطط هي `{ type: 'object', properties: {} }` (من دون معلمات).
|
||||
* الدوال التي لديها `isTool: false` (أو غير معيَّنة) **غير** معروضة كأدوات. لا يزال بالإمكان تنفيذها مباشرةً أو استدعاؤها بواسطة دوال أخرى، لكنها لن تظهر في اكتشاف الأدوات.
|
||||
* **تسمية الأداة**: عند كشفها كأداة، يتم تطبيع اسم الدالة تلقائيًا إلى `logic_function_<name>` (تحويله إلى أحرف صغيرة، واستبدال المحارف غير الأبجدية الرقمية بشرطات سفلية). على سبيل المثال، `enrich-company` تصبح `logic_function_enrich_company`.
|
||||
* يمكنك دمج `isTool` مع المشغِّلات — إذ يمكن للدالة أن تكون أداة (قابلة للاستدعاء من قِبل وكلاء الذكاء الاصطناعي) وأن تُشغَّل بواسطة أحداث (cron، وأحداث قاعدة البيانات، والمسارات) في الوقت نفسه.
|
||||
|
||||
<Note>
|
||||
**اكتب `description` جيدًا.** يعتمد وكلاء الذكاء الاصطناعي على حقل `description` الخاص بالدالة لتحديد وقت استخدام الأداة. كن محددًا بشأن ما تفعله الأداة ومتى ينبغي استدعاؤها.
|
||||
</Note>
|
||||
|
||||
### المكوّنات الأمامية
|
||||
|
||||
تتيح لك المكوّنات الأمامية إنشاء مكوّنات React مخصّصة تُعرَض داخل واجهة مستخدم Twenty. استخدم `defineFrontComponent()` لتعريف مكوّنات مع تحقّق مدمج:
|
||||
@@ -584,16 +730,51 @@ export default defineFrontComponent({
|
||||
* المكوّنات الأمامية هي مكوّنات React تُعرَض ضمن سياقات معزولة داخل Twenty.
|
||||
* استخدم لاحقة الملف `*.front-component.tsx` للاكتشاف التلقائي.
|
||||
* يشير الحقل `component` إلى مكوّن React الخاص بك.
|
||||
* يتم بناء المكوّنات ومزامنتها تلقائيًا أثناء `yarn app:dev`.
|
||||
* يتم بناء المكوّنات ومزامنتها تلقائيًا أثناء `yarn twenty app:dev`.
|
||||
|
||||
يمكنك إنشاء مكوّنات أمامية جديدة بطريقتين:
|
||||
|
||||
* **مُنشأ بالقالب**: شغّل `yarn entity:add` واختر خيار إضافة مكوّن أمامي جديد.
|
||||
* **مُنشأ بالقالب**: شغّل `yarn twenty entity:add` واختر خيار إضافة مكوّن أمامي جديد.
|
||||
* **يدوي**: أنشئ ملفًا جديدًا `*.front-component.tsx` واستخدم `defineFrontComponent()`.
|
||||
|
||||
### المهارات
|
||||
|
||||
تُحدِّد المهارات تعليمات وإمكانات قابلة لإعادة الاستخدام يمكن لوكلاء الذكاء الاصطناعي استخدامها داخل مساحة العمل لديك. استخدم `defineSkill()` لتعريف مهارات مع تحقّق مدمج:
|
||||
|
||||
```typescript
|
||||
// src/skills/example-skill.ts
|
||||
import { defineSkill } from 'twenty-sdk';
|
||||
|
||||
export default defineSkill({
|
||||
universalIdentifier: 'a1b2c3d4-e5f6-7890-abcd-ef1234567890',
|
||||
name: 'sales-outreach',
|
||||
label: 'التواصل البيعي',
|
||||
description: 'يرشد وكيل الذكاء الاصطناعي خلال عملية منظّمة للتواصل البيعي',
|
||||
icon: 'IconBrain',
|
||||
content: `أنت مساعد للتواصل البيعي. عند التواصل مع عميل محتمل:
|
||||
1. ابحث عن الشركة وآخر الأخبار
|
||||
2. حدِّد دور العميل المحتمل ونقاط الألم المرجّحة
|
||||
3. صِغ رسالة مخصّصة تشير إلى تفاصيل محدّدة
|
||||
4. حافظ على نبرة احترافية ولكن حوارية`,
|
||||
});
|
||||
```
|
||||
|
||||
النقاط الرئيسية:
|
||||
|
||||
* `name` هي سلسلة معرّف فريدة للمهارة (يُنصَح باستخدام kebab-case).
|
||||
* `label` هو اسم العرض المقروء للبشر الظاهر في واجهة المستخدم.
|
||||
* `content` يحتوي على تعليمات المهارة — وهو النص الذي يستخدمه وكيل الذكاء الاصطناعي.
|
||||
* `icon` (اختياري) يحدّد الأيقونة المعروضة في واجهة المستخدم.
|
||||
* `description` (اختياري) يوفّر سياقًا إضافيًا حول غرض المهارة.
|
||||
|
||||
يمكنك إنشاء مهارات جديدة بطريقتين:
|
||||
|
||||
* **مُنشأ بالقالب**: شغِّل `yarn twenty entity:add` واختر خيار إضافة مهارة جديدة.
|
||||
* **يدوي**: أنشئ ملفًا جديدًا واستخدم `defineSkill()` مع اتباع النمط نفسه.
|
||||
|
||||
### عميل مُولَّد مضبوط الأنواع
|
||||
|
||||
شغّل yarn app:generate لإنشاء عميل محلي مضبوط الأنواع في generated/ استنادًا إلى مخطط مساحة العمل لديك. استخدمه في وظائفك:
|
||||
يُولَّد العميل مضبوط الأنواع تلقائيًا بواسطة `yarn twenty app:dev` ويُخزَّن في `node_modules/twenty-sdk/generated` استنادًا إلى مخطط مساحة العمل لديك. استخدمه في وظائفك:
|
||||
|
||||
```typescript
|
||||
import Twenty from '~/generated';
|
||||
@@ -602,7 +783,7 @@ const client = new Twenty();
|
||||
const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
```
|
||||
|
||||
يُعاد توليد العميل بواسطة `yarn app:generate`. أعِد التشغيل بعد تغيير كائناتك أو عند الانضمام إلى مساحة عمل جديدة.
|
||||
يُعاد توليد العميل تلقائيًا بواسطة `yarn twenty app:dev` كلما تغيّرت كائناتك أو حقولك.
|
||||
|
||||
#### بيانات الاعتماد وقت التشغيل في الوظائف المنطقية
|
||||
|
||||
@@ -617,46 +798,82 @@ const { me } = await client.query({ me: { id: true, displayName: true } });
|
||||
* تُحدَّد أذونات مفتاح واجهة برمجة التطبيقات بواسطة الدور المشار إليه في `application-config.ts` عبر `defaultRoleUniversalIdentifier`. هذا هو الدور الافتراضي الذي تستخدمه الوظائف المنطقية في تطبيقك.
|
||||
* يمكن للتطبيقات تعريف أدوار لاتباع مبدأ أقل الامتياز. امنح فقط الأذونات التي تحتاجها وظائفك، ثم وجّه `defaultRoleUniversalIdentifier` إلى المعرّف الشامل لذلك الدور.
|
||||
|
||||
#### رفع الملفات
|
||||
|
||||
يتضمن العميل `Twenty` المُولَّد طريقة `uploadFile` لإرفاق الملفات بالحقول من نوع ملف ضمن كائنات مساحة العمل الخاصة بك. نظرًا لأن عملاء GraphQL القياسيون لا يدعمون تحميل الملفات متعددة الأجزاء افتراضيًا، يوفر العميل هذه الطريقة المخصصة التي تطبق [مواصفة طلب GraphQL متعدد الأجزاء](https://github.com/jaydenseric/graphql-multipart-request-spec) في الخلفية.
|
||||
|
||||
```typescript
|
||||
import Twenty from '~/generated';
|
||||
import * as fs from 'fs';
|
||||
|
||||
const client = new Twenty();
|
||||
|
||||
const fileBuffer = fs.readFileSync('./invoice.pdf');
|
||||
|
||||
const uploadedFile = await client.uploadFile(
|
||||
fileBuffer, // file contents as a Buffer
|
||||
'invoice.pdf', // filename
|
||||
'application/pdf', // MIME type (defaults to 'application/octet-stream')
|
||||
'58a0a314-d7ea-4865-9850-7fb84e72f30b', // field universal identifier
|
||||
);
|
||||
|
||||
console.log(uploadedFile);
|
||||
// { id: '...', path: '...', size: 12345, createdAt: '...', url: 'https://...' }
|
||||
```
|
||||
|
||||
توقيع الطريقة:
|
||||
|
||||
```typescript
|
||||
uploadFile(
|
||||
fileBuffer: Buffer,
|
||||
filename: string,
|
||||
contentType: string,
|
||||
fieldMetadataUniversalIdentifier: string,
|
||||
): Promise<{ id: string; path: string; size: number; createdAt: string; url: string }>
|
||||
```
|
||||
|
||||
| المعلمة | النوع | الوصف |
|
||||
| ---------------------------------- | -------- | ---------------------------------------------------------------------------------- |
|
||||
| `fileBuffer` | `Buffer` | المحتوى الخام للملف |
|
||||
| `filename` | `string` | اسم الملف (يُستخدم للتخزين والعرض) |
|
||||
| `contentType` | `string` | نوع MIME للملف (القيمة الافتراضية هي `application/octet-stream` إذا لم يتم تحديده) |
|
||||
| `fieldMetadataUniversalIdentifier` | `string` | قيمة `universalIdentifier` لحقل نوع الملف في كائنك |
|
||||
|
||||
النقاط الرئيسية:
|
||||
|
||||
* ترسل هذه الطريقة الملف إلى **نقطة نهاية البيانات الوصفية** (وليست نقطة النهاية الرئيسية لـ GraphQL)، حيث تُنفَّذ عملية الرفع.
|
||||
* تستخدم `universalIdentifier` الخاص بالحقل (وليس المعرّف الخاص بمساحة العمل)، ليعمل كود الرفع لديك عبر أي مساحة عمل مُثبَّت فيها تطبيقك — بما يتماشى مع كيفية إشارة التطبيقات إلى الحقول في كل مكان آخر.
|
||||
* العنوان `url` المُعاد هو عنوان URL موقّع يمكنك استخدامه للوصول إلى الملف المرفوع.
|
||||
|
||||
### مثال Hello World
|
||||
|
||||
استكشف مثالًا بسيطًا شاملًا من البداية إلى النهاية يوضح الكائنات والوظائف المنطقية والمكوّنات الأمامية ومشغّلات متعددة [هنا](https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/hello-world):
|
||||
|
||||
## إعداد يدوي (بدون المهيئ)
|
||||
|
||||
بينما نوصي باستخدام `create-twenty-app` للحصول على أفضل تجربة للبدء، يمكنك أيضًا إعداد مشروع يدويًا. لا تثبّت CLI عالميًا. بدل ذلك، أضف `twenty-sdk` كاعتماد محلي ووصل السكربتات في ملف package.json لديك:
|
||||
بينما نوصي باستخدام `create-twenty-app` للحصول على أفضل تجربة للبدء، يمكنك أيضًا إعداد مشروع يدويًا. لا تثبّت CLI عالميًا. بدل ذلك، أضف `twenty-sdk` كاعتماد محلي واربط سكربتًا واحدًا في ملف package.json لديك:
|
||||
|
||||
```bash filename="Terminal"
|
||||
yarn add -D twenty-sdk
|
||||
```
|
||||
|
||||
ثم أضف نصوصًا مثل هذه:
|
||||
ثم أضف سكربتًا باسم `twenty`:
|
||||
|
||||
```json filename="package.json"
|
||||
{
|
||||
"scripts": {
|
||||
"auth:login": "twenty auth:login",
|
||||
"auth:logout": "twenty auth:logout",
|
||||
"auth:status": "twenty auth:status",
|
||||
"auth:switch": "twenty auth:switch",
|
||||
"auth:list": "twenty auth:list",
|
||||
"app:dev": "twenty app:dev",
|
||||
"app:generate": "twenty app:generate",
|
||||
"app:uninstall": "twenty app:uninstall",
|
||||
"entity:add": "twenty entity:add",
|
||||
"function:logs": "twenty function:logs",
|
||||
"function:execute": "twenty function:execute",
|
||||
"help": "twenty help"
|
||||
"twenty": "twenty"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
يمكنك الآن تشغيل الأوامر نفسها عبر Yarn، مثل `yarn app:dev` و`yarn app:generate`، إلخ.
|
||||
الآن يمكنك تشغيل جميع الأوامر عبر `yarn twenty <command>`، مثلًا: `yarn twenty app:dev`، `yarn twenty help`، إلخ.
|
||||
|
||||
## استكشاف الأخطاء وإصلاحها
|
||||
|
||||
* أخطاء المصادقة: شغّل `yarn auth:login` وتأكد من أن مفتاح واجهة برمجة التطبيقات لديك يمتلك الأذونات المطلوبة.
|
||||
* أخطاء المصادقة: شغّل `yarn twenty auth:login` وتأكد من أن مفتاح واجهة برمجة التطبيقات لديك يمتلك الأذونات المطلوبة.
|
||||
* يتعذّر الاتصال بالخادم: تحقق من عنوان URL لواجهة البرمجة وأن خادم Twenty قابل للوصول.
|
||||
* الأنواع أو العميل مفقود/قديم: شغّل `yarn app:generate`.
|
||||
* وضع التطوير لا يزامن: تأكد من أن `yarn app:dev` قيد التشغيل وأن التغييرات ليست متجاهلة من بيئتك.
|
||||
* الأنواع أو العميل مفقود/قديم: أعد تشغيل `yarn twenty app:dev` — فهو ينشئ العميل مضبوط الأنواع بشكل تلقائي.
|
||||
* وضع التطوير لا يزامن: تأكد من أن `yarn twenty app:dev` قيد التشغيل وأن التغييرات ليست متجاهلة من بيئتك.
|
||||
|
||||
قناة المساعدة على Discord: https://discord.com/channels/1130383047699738754/1130386664812982322
|
||||
|
||||
@@ -62,7 +62,7 @@ import { VimeoEmbed } from '/snippets/vimeo-embed.mdx';
|
||||
| `الطابع الزمني` | وقت حدوث الحدث (UTC) |
|
||||
|
||||
<Note>
|
||||
استجب بحالة **HTTP 2xx** (200-299) لتأكيد الاستلام. تُسجَّل الاستجابات غير 2xx كإخفاقات في التسليم.
|
||||
استجب بحالة **HTTP 2xx** (200-299) لتأكيد الاستلام. تُسجَّل الاستجابات غير 2xx كإخفاقات في التسليم.
|
||||
</Note>
|
||||
|
||||
## التحقق من صحة خطاف الويب
|
||||
|
||||
@@ -23,11 +23,9 @@ description: وسّع وظائف Twenty باستخدام واجهات برمجة
|
||||
<Card title="واجهات برمجة التطبيقات" icon="كود" href="/l/ar/developers/extend/capabilities/apis">
|
||||
اتصل بـ Twenty برمجياً
|
||||
</Card>
|
||||
|
||||
<Card title="الويب هوكس" icon="bell" href="/l/ar/developers/extend/capabilities/webhooks">
|
||||
احصل على إشعارات بالأحداث في الوقت الفعلي
|
||||
</Card>
|
||||
|
||||
<Card title="التطبيقات" icon="puzzle-piece" href="/l/ar/developers/extend/capabilities/apps">
|
||||
أنشئ تخصيصات كرمز برمجي (ألفا)
|
||||
</Card>
|
||||
|
||||
@@ -3,7 +3,7 @@ title: بنقرة واحدة مع Docker Compose
|
||||
---
|
||||
|
||||
<Warning>
|
||||
الحاويات الخاصة بدوكر مخصصة للاستضافة الإنتاجية أو الاستضافة الذاتية، للتحقيق يرجى التحقق من [الإعداد المحلي](/l/ar/developers/contribute/capabilities/local-setup).
|
||||
الحاويات الخاصة بدوكر مخصصة للاستضافة الإنتاجية أو الاستضافة الذاتية، للتحقيق يرجى التحقق من [الإعداد المحلي](/l/ar/developers/contribute/capabilities/local-setup).
|
||||
</Warning>
|
||||
|
||||
## نظرة عامة
|
||||
|
||||
@@ -5,7 +5,7 @@ title: إعداد
|
||||
# إدارة الإعدادات
|
||||
|
||||
<Warning>
|
||||
**هل هي المرة الأولى التي تقوم فيها بالتثبيت؟** اتبع [دليل تثبيت Docker Compose](/l/ar/developers/self-host/capabilities/docker-compose) لتشغيل Twenty، ثم عد هنا للإعداد.
|
||||
**هل هي المرة الأولى التي تقوم فيها بالتثبيت؟** اتبع [دليل تثبيت Docker Compose](/l/ar/developers/self-host/capabilities/docker-compose) لتشغيل Twenty، ثم عد هنا للإعداد.
|
||||
</Warning>
|
||||
|
||||
يوفر Twenty **وضعين للإعداد** ليلائم احتياجات النشر المختلفة:
|
||||
@@ -26,7 +26,7 @@ IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # افتراضي
|
||||
4. تسري التغييرات على الفور (خلال 15 ثانية لعمليات النشر متعددة الحاويات)
|
||||
|
||||
<Warning>
|
||||
**نشرات متعددة الحاويات:** عند استخدام إعدادات قاعدة البيانات (`IS_CONFIG_VARIABLES_IN_DB_ENABLED=true`)، يقوم كل من حاويات الخادم والعامل بالقراءة من نفس قاعدة البيانات. التغييرات في لوحة الإدارة تؤثر عليهما تلقائيًا، مما يلغي الحاجة إلى تكرار متغيرات البيئة بين الحاويات (باستثناء متغيرات البنية التحتية).
|
||||
**نشرات متعددة الحاويات:** عند استخدام إعدادات قاعدة البيانات (`IS_CONFIG_VARIABLES_IN_DB_ENABLED=true`)، يقوم كل من حاويات الخادم والعامل بالقراءة من نفس قاعدة البيانات. التغييرات في لوحة الإدارة تؤثر عليهما تلقائيًا، مما يلغي الحاجة إلى تكرار متغيرات البيئة بين الحاويات (باستثناء متغيرات البنية التحتية).
|
||||
</Warning>
|
||||
|
||||
**ما يمكنك تكوينه عبر لوحة الإدارة:**
|
||||
@@ -41,10 +41,10 @@ IS_CONFIG_VARIABLES_IN_DB_ENABLED=true # افتراضي
|
||||

|
||||
|
||||
<Warning>
|
||||
كل متغير موثق بوصف في لوحة الإدارة الخاصة بك في **الإعدادات → لوحة الإدارة → متغيرات التكوين**.
|
||||
بعض إعدادات البنية التحتية مثل اتصالات قاعدة البيانات (`PG_DATABASE_URL`)، عناوين الخوادم (`SERVER_URL`)، وأسرار التطبيقات (`APP_SECRET`) يمكن ضبطها فقط عبر ملف `.env`.
|
||||
كل متغير موثق بوصف في لوحة الإدارة الخاصة بك في **الإعدادات → لوحة الإدارة → متغيرات التكوين**.
|
||||
بعض إعدادات البنية التحتية مثل اتصالات قاعدة البيانات (`PG_DATABASE_URL`)، عناوين الخوادم (`SERVER_URL`)، وأسرار التطبيقات (`APP_SECRET`) يمكن ضبطها فقط عبر ملف `.env`.
|
||||
|
||||
[مرجع تقني كامل →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
|
||||
[مرجع تقني كامل →](https://github.com/twentyhq/twenty/blob/main/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts)
|
||||
</Warning>
|
||||
|
||||
## 2. إعداد بيئي فقط
|
||||
@@ -93,7 +93,7 @@ DEFAULT_SUBDOMAIN=app # default value
|
||||
* إعدادات خاصة بمساحة العمل مثل النطاق الفرعي والنطاق المخصص تصبح متاحة ضمن إعدادات مساحة العمل
|
||||
|
||||
<Warning>
|
||||
**إعداد خاص بالبيئة فقط:** لا يمكن تكوين `IS_MULTIWORKSPACE_ENABLED` إلا عبر ملف `.env` ويتطلب إعادة تشغيل. لا يمكن تغييره عبر لوحة الإدارة.
|
||||
**إعداد خاص بالبيئة فقط:** لا يمكن تكوين `IS_MULTIWORKSPACE_ENABLED` إلا عبر ملف `.env` ويتطلب إعادة تشغيل. لا يمكن تغييره عبر لوحة الإدارة.
|
||||
</Warning>
|
||||
|
||||
### تكوين DNS لوضع تعدد مساحات العمل
|
||||
@@ -149,7 +149,7 @@ IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS=true
|
||||
* `AUTH_GOOGLE_APIS_CALLBACK_URL=https://{your-domain}/auth/google-apis/get-access-token`
|
||||
|
||||
<Warning>
|
||||
**وضع بيئي فقط:** إذا كنت قد ضبطت `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`، فأضف هذه المتغيرات إلى ملف `.env` الخاص بك بدلاً من ذلك.
|
||||
**وضع بيئي فقط:** إذا كنت قد ضبطت `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`، فأضف هذه المتغيرات إلى ملف `.env` الخاص بك بدلاً من ذلك.
|
||||
</Warning>
|
||||
|
||||
**النطاقات المطلوبة** (يتم تكوينها تلقائيًا):
|
||||
@@ -168,7 +168,7 @@ IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS=true
|
||||
## تكامل Microsoft 365
|
||||
|
||||
<Warning>
|
||||
يجب على المستخدمين الحصول على [ترخيص Microsoft 365](https://admin.microsoft.com/Adminportal/Home) ليتمكنوا من استخدام تقويم API ورسائل. لن يتمكنوا من مزامنة حسابهم في Twenty دون واحد منها.
|
||||
يجب على المستخدمين الحصول على [ترخيص Microsoft 365](https://admin.microsoft.com/Adminportal/Home) ليتمكنوا من استخدام تقويم API ورسائل. لن يتمكنوا من مزامنة حسابهم في Twenty دون واحد منها.
|
||||
</Warning>
|
||||
|
||||
### إنشاء مشروع في Microsoft Azure
|
||||
@@ -211,7 +211,7 @@ IS_WORKSPACE_CREATION_LIMITED_TO_SERVER_ADMINS=true
|
||||
* `AUTH_MICROSOFT_APIS_CALLBACK_URL=https://{your-domain}/auth/microsoft-apis/get-access-token`
|
||||
|
||||
<Warning>
|
||||
**وضع بيئي فقط:** إذا كنت قد ضبطت `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`، فأضف هذه المتغيرات إلى ملف `.env` الخاص بك بدلاً من ذلك.
|
||||
**وضع بيئي فقط:** إذا كنت قد ضبطت `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`، فأضف هذه المتغيرات إلى ملف `.env` الخاص بك بدلاً من ذلك.
|
||||
</Warning>
|
||||
|
||||
### تكوين النطاقات
|
||||
@@ -256,40 +256,45 @@ yarn command:prod cron:workflow:automated-cron-trigger
|
||||
3. قم بضبط إعدادات SMTP الخاصة بك:
|
||||
|
||||
<ArticleTabs label1="جيميل" label2="أوفيس 365" label3="Smtp4dev">
|
||||
<ArticleTab>
|
||||
ستحتاج إلى توفير [كلمة مرور التطبيق](https://support.google.com/accounts/answer/185833).
|
||||
|
||||
<ArticleTab>
|
||||
|
||||
ستحتاج إلى توفير [كلمة مرور التطبيق](https://support.google.com/accounts/answer/185833).
|
||||
* EMAIL_DRIVER=smtp
|
||||
* EMAIL_SMTP_HOST=smtp.gmail.com
|
||||
* EMAIL_SMTP_PORT=465
|
||||
* EMAIL_SMTP_USER=gmail_email_address
|
||||
* EMAIL_SMTP_PASSWORD='gmail_app_password'
|
||||
|
||||
</ArticleTab>
|
||||
|
||||
<ArticleTab>
|
||||
|
||||
تذكر أنه إذا كنت تشغل التحقق بعاملين، ستحتاج إلى توفير [كلمة مرور التطبيق](https://support.microsoft.com/en-us/account-billing/manage-app-passwords-for-two-step-verification-d6dc8c6d-4bf7-4851-ad95-6d07799387e9).
|
||||
* EMAIL_DRIVER=smtp
|
||||
* EMAIL_SMTP_HOST=smtp.office365.com
|
||||
* EMAIL_SMTP_PORT=587
|
||||
* EMAIL_SMTP_USER=office365_email_address
|
||||
* EMAIL_SMTP_PASSWORD='office365_password'
|
||||
|
||||
</ArticleTab>
|
||||
|
||||
<ArticleTab>
|
||||
|
||||
**smtp4dev** هو خادم بريد إلكتروني مزيف للتطوير والاختبار.
|
||||
* قم بتشغيل صورة smtp4dev: `docker run --rm -it -p 8090:80 -p 2525:25 rnwood/smtp4dev`
|
||||
* الوصول إلى واجهة المستخدم smtp4dev هنا: [http://localhost:8090](http://localhost:8090)
|
||||
* حدد المتغيرات التالية:
|
||||
* EMAIL_DRIVER=smtp
|
||||
* EMAIL_SMTP_HOST=smtp.gmail.com
|
||||
* EMAIL_SMTP_PORT=465
|
||||
* EMAIL_SMTP_USER=gmail_email_address
|
||||
* EMAIL_SMTP_PASSWORD='gmail_app_password'
|
||||
* EMAIL_SMTP_HOST=localhost
|
||||
* EMAIL_SMTP_PORT=2525
|
||||
|
||||
</ArticleTab>
|
||||
|
||||
<ArticleTab>
|
||||
تذكر أنه إذا كنت تشغل التحقق بعاملين، ستحتاج إلى توفير [كلمة مرور التطبيق](https://support.microsoft.com/en-us/account-billing/manage-app-passwords-for-two-step-verification-d6dc8c6d-4bf7-4851-ad95-6d07799387e9).
|
||||
|
||||
* EMAIL_DRIVER=smtp
|
||||
* EMAIL_SMTP_HOST=smtp.office365.com
|
||||
* EMAIL_SMTP_PORT=587
|
||||
* EMAIL_SMTP_USER=office365_email_address
|
||||
* EMAIL_SMTP_PASSWORD='office365_password'
|
||||
</ArticleTab>
|
||||
|
||||
<ArticleTab>
|
||||
**smtp4dev** هو خادم بريد إلكتروني مزيف للتطوير والاختبار.
|
||||
|
||||
* قم بتشغيل صورة smtp4dev: `docker run --rm -it -p 8090:80 -p 2525:25 rnwood/smtp4dev`
|
||||
* الوصول إلى واجهة المستخدم smtp4dev هنا: [http://localhost:8090](http://localhost:8090)
|
||||
* حدد المتغيرات التالية:
|
||||
* EMAIL_DRIVER=smtp
|
||||
* EMAIL_SMTP_HOST=localhost
|
||||
* EMAIL_SMTP_PORT=2525
|
||||
</ArticleTab>
|
||||
</ArticleTabs>
|
||||
|
||||
<Warning>
|
||||
**وضع بيئي فقط:** إذا كنت قد ضبطت `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`، فأضف هذه المتغيرات إلى ملف `.env` الخاص بك بدلاً من ذلك.
|
||||
**وضع بيئي فقط:** إذا كنت قد ضبطت `IS_CONFIG_VARIABLES_IN_DB_ENABLED=false`، فأضف هذه المتغيرات إلى ملف `.env` الخاص بك بدلاً من ذلك.
|
||||
</Warning>
|
||||
|
||||
## الوظائف المنطقية
|
||||
@@ -297,7 +302,7 @@ yarn command:prod cron:workflow:automated-cron-trigger
|
||||
تدعم Twenty الوظائف المنطقية لعمليات سير العمل والمنطق المخصص. يتم تكوين بيئة التنفيذ عبر متغير البيئة `SERVERLESS_TYPE`.
|
||||
|
||||
<Warning>
|
||||
**ملاحظة أمنية:** يقوم برنامج التشغيل المحلي (`SERVERLESS_TYPE=LOCAL`) بتشغيل الشيفرة مباشرةً على المضيف ضمن عملية Node.js من دون عزل. يجب استخدامه فقط للشيفرة الموثوقة أثناء التطوير. بالنسبة لعمليات النشر الإنتاجية التي تتعامل مع شيفرة غير موثوق بها، نوصي بشدة باستخدام `SERVERLESS_TYPE=LAMBDA` أو `SERVERLESS_TYPE=DISABLED`.
|
||||
**ملاحظة أمنية:** يقوم برنامج التشغيل المحلي (`SERVERLESS_TYPE=LOCAL`) بتشغيل الشيفرة مباشرةً على المضيف ضمن عملية Node.js من دون عزل. يجب استخدامه فقط للشيفرة الموثوقة أثناء التطوير. بالنسبة لعمليات النشر الإنتاجية التي تتعامل مع شيفرة غير موثوق بها، نوصي بشدة باستخدام `SERVERLESS_TYPE=LAMBDA` أو `SERVERLESS_TYPE=DISABLED`.
|
||||
</Warning>
|
||||
|
||||
### برامج التشغيل المتاحة
|
||||
@@ -333,5 +338,5 @@ SERVERLESS_TYPE=DISABLED
|
||||
```
|
||||
|
||||
<Note>
|
||||
عند استخدام `SERVERLESS_TYPE=DISABLED`، ستؤدي أي محاولة لتنفيذ وظيفة منطقية إلى إرجاع خطأ. يكون هذا مفيدًا إذا كنت ترغب في تشغيل Twenty من دون إمكانات الوظائف المنطقية.
|
||||
عند استخدام `SERVERLESS_TYPE=DISABLED`، ستؤدي أي محاولة لتنفيذ وظيفة منطقية إلى إرجاع خطأ. يكون هذا مفيدًا إذا كنت ترغب في تشغيل Twenty من دون إمكانات الوظائف المنطقية.
|
||||
</Note>
|
||||
|
||||
@@ -23,7 +23,6 @@ description: قم بنشر Twenty وإدارته على البنية التحت
|
||||
<Card title="Docker Compose" icon="docker" href="/l/ar/developers/self-host/capabilities/docker-compose">
|
||||
إعداد سريع باستخدام Docker
|
||||
</Card>
|
||||
|
||||
<Card title="مزودو الخدمات السحابية" icon="cloud" href="/l/ar/developers/self-host/capabilities/cloud-providers">
|
||||
انشر على AWS أو GCP أو Azure
|
||||
</Card>
|
||||
|
||||
@@ -10,46 +10,54 @@ image: /images/user-guide/tips/light-bulb.png
|
||||
رسالة مختصرة تعرض معلومات إضافية عند تفاعل المستخدم مع عنصر.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { AppTooltip } from "@/ui/display/tooltip/AppTooltip";
|
||||
<Tab title="استخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<>
|
||||
<p id="hoverText" style={{ display: "inline-block" }}>
|
||||
رؤى العملاء
|
||||
</p>
|
||||
<AppTooltip
|
||||
className
|
||||
anchorSelect="#hoverText"
|
||||
content="استكشاف سلوك العملاء وتفضيلاتهم"
|
||||
delayHide={0}
|
||||
offset={6}
|
||||
noArrow={false}
|
||||
isOpen={true}
|
||||
place="bottom"
|
||||
positionStrategy="absolute"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
```jsx
|
||||
import { AppTooltip } from "@/ui/display/tooltip/AppTooltip";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<>
|
||||
<p id="hoverText" style={{ display: "inline-block" }}>
|
||||
رؤى العملاء
|
||||
</p>
|
||||
<AppTooltip
|
||||
className
|
||||
anchorSelect="#hoverText"
|
||||
content="استكشاف سلوك العملاء وتفضيلاتهم"
|
||||
delayHide={0}
|
||||
offset={6}
|
||||
noArrow={false}
|
||||
isOpen={true}
|
||||
place="bottom"
|
||||
positionStrategy="absolute"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="المحددات">
|
||||
|
||||
|
||||
| المحددات | النوع | الوصف |
|
||||
| ------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| اسم الفئة | نص | فئة CSS اختيارية للتنسيق الإضافي |
|
||||
| اختيار الربط | محدد CSS | المحدِّد لمرساة التلميح (العنصر الذي يُفعّل التلميح) |
|
||||
| المحتوى | نص | المحتوى الذي تريد عرضه داخل التلميح |
|
||||
| تأخير الإخفاء | رقم | التأخير بالثواني قبل إخفاء التلميح بعد مغادرة المؤشر للمرساة |
|
||||
| الإزاحة | رقم | الإزاحة بالبكسل لتحديد موضع التلميح |
|
||||
| بدون سهم | قيمة منطقية | إذا كانت القيمة `صحيح`, سيتم إخفاء السهم في المربط التنبيهي |
|
||||
| مفتوح | قيمة منطقية | إذا كانت القيمة `صحيح`, يكون المربط التنبيهي مفتوحًا افتراضيًا |
|
||||
| المكان | نص `PlacesType` من `react-tooltip` | يحدد موضع المربط التنبيهي. تتضمن القيم `bottom`، `left`، `right`، `top`، `top-start`، `top-end`، `right-start`، `right-end`، `bottom-start`، `bottom-end`، `left-start`، و`left-end` |
|
||||
| استراتيجية الوضعية | نص `PositionStrategy` من `react-tooltip` | استراتيجية وضعية للمربط التنبيهي. له قيمتان: `absolute` و`fixed` |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="المحددات">
|
||||
| المحددات | النوع | الوصف |
|
||||
| ------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| اسم الفئة | نص | فئة CSS اختيارية للتنسيق الإضافي |
|
||||
| اختيار الربط | محدد CSS | المحدِّد لمرساة التلميح (العنصر الذي يُفعّل التلميح) |
|
||||
| المحتوى | نص | المحتوى الذي تريد عرضه داخل التلميح |
|
||||
| تأخير الإخفاء | رقم | التأخير بالثواني قبل إخفاء التلميح بعد مغادرة المؤشر للمرساة |
|
||||
| الإزاحة | رقم | الإزاحة بالبكسل لتحديد موضع التلميح |
|
||||
| بدون سهم | قيمة منطقية | إذا كانت القيمة `صحيح`, سيتم إخفاء السهم في المربط التنبيهي |
|
||||
| مفتوح | قيمة منطقية | إذا كانت القيمة `صحيح`, يكون المربط التنبيهي مفتوحًا افتراضيًا |
|
||||
| المكان | نص `PlacesType` من `react-tooltip` | يحدد موضع المربط التنبيهي. تتضمن القيم `bottom`، `left`، `right`، `top`، `top-start`، `top-end`، `right-start`، `right-end`، `bottom-start`، `bottom-end`، `left-start`، و`left-end` |
|
||||
| استراتيجية الوضعية | نص `PositionStrategy` من `react-tooltip` | استراتيجية وضعية للمربط التنبيهي. له قيمتان: `absolute` و`fixed` |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## نص متجاوز مع تلميح
|
||||
@@ -57,22 +65,30 @@ image: /images/user-guide/tips/light-bulb.png
|
||||
يعالج النص الزائد ويعرض مربط تنبيهي عند فيضان النص.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
<Tab title="استخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
const crmTaskDescription =
|
||||
'المتابعة مع العميل بشأن استفساره الأخير عن المنتج. مناقشة خيارات التسعير، ومعالجة أي مخاوف، وتقديم معلومات إضافية عن المنتج. تسجيل تفاصيل المحادثة في نظام إدارة علاقات العملاء (CRM) للرجوع إليها لاحقاً.';
|
||||
```jsx
|
||||
import { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
|
||||
return <OverflowingTextWithTooltip text={crmTaskDescription} />;
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
const crmTaskDescription =
|
||||
'المتابعة مع العميل بشأن استفساره الأخير عن المنتج. مناقشة خيارات التسعير، ومعالجة أي مخاوف، وتقديم معلومات إضافية عن المنتج. تسجيل تفاصيل المحادثة في نظام إدارة علاقات العملاء (CRM) للرجوع إليها لاحقاً.';
|
||||
|
||||
return <OverflowingTextWithTooltip text={crmTaskDescription} />;
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="الخصائص">
|
||||
|
||||
|
||||
| الخصائص | النوع | الوصف |
|
||||
| ------- | ------ | --------------------------------------------- |
|
||||
| نص | string | المحتوى الذي تريد عرضه في منطقة النص المتجاوز |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="الخصائص">
|
||||
| الخصائص | النوع | الوصف |
|
||||
| ------- | ------ | --------------------------------------------- |
|
||||
| نص | string | المحتوى الذي تريد عرضه في منطقة النص المتجاوز |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -10,19 +10,25 @@ image: /images/user-guide/tasks/tasks_header.png
|
||||
يمثل إجراءً ناجحًا أو مكتملًا.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { Checkmark } from 'twenty-ui/display';
|
||||
<Tab title="استخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
return <Checkmark />;
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
```jsx
|
||||
import { Checkmark } from 'twenty-ui/display';
|
||||
|
||||
export const MyComponent = () => {
|
||||
return <Checkmark />;
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="المحددات">
|
||||
|
||||
يمتد `React.ComponentPropsWithoutRef<'div'>` و يقبل جميع خصائص عنصر `div` العادي.
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="المحددات">
|
||||
يمتد `React.ComponentPropsWithoutRef<'div'>` و يقبل جميع خصائص عنصر `div` العادي.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## علامة صحيح متحركة
|
||||
@@ -30,29 +36,38 @@ image: /images/user-guide/tasks/tasks_header.png
|
||||
يمثل رمز علامة صحيح مع ميزة الإضافة للحركة.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { AnimatedCheckmark } from 'twenty-ui/display';
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<AnimatedCheckmark
|
||||
isAnimating={true}
|
||||
color="green"
|
||||
duration={0.5}
|
||||
size={30}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="استخدام">
|
||||
|
||||
```jsx
|
||||
import { AnimatedCheckmark } from 'twenty-ui/display';
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<AnimatedCheckmark
|
||||
isAnimating={true}
|
||||
color="green"
|
||||
duration={0.5}
|
||||
size={30}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="الخصائص">
|
||||
|
||||
|
||||
| الخصائص | النوع | الوصف | الإعداد الافتراضي |
|
||||
| ----------- | ----------- | ------------------------------------- | ----------------- |
|
||||
| isAnimating | قيمة منطقية | يتحكم فيما إذا كانت علامة صحيح متحركة | خاطئ |
|
||||
| اللون | string | لون علامة الاختيار | |
|
||||
| المدة | رقم | مدة الحركة بالثواني | 0.5 ثانية |
|
||||
| الحجم | رقم | حجم علامة الاختيار | 28 بكسل |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="الخصائص">
|
||||
| الخصائص | النوع | الوصف | الإعداد الافتراضي |
|
||||
| ----------- | ----------- | ------------------------------------- | ----------------- |
|
||||
| isAnimating | قيمة منطقية | يتحكم فيما إذا كانت علامة صحيح متحركة | خاطئ |
|
||||
| اللون | string | لون علامة الاختيار | |
|
||||
| المدة | رقم | مدة الحركة بالثواني | 0.5 ثانية |
|
||||
| الحجم | رقم | حجم علامة الاختيار | 28 بكسل |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -10,40 +10,48 @@ image: /images/user-guide/github/github-header.png
|
||||
عنصر مرئي يمكن استخدامه كحاوية قابلة للنقر أو غير قابلة للنقر، مع علامة وعناصر اختيارية يسار ويمين، وخيارات تصميم متنوعة لعرض العلامات والبطاقات.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { Chip } from 'twenty-ui/components';
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<Chip
|
||||
size="large"
|
||||
label="Clickable Chip"
|
||||
clickable={true}
|
||||
variant="highlighted"
|
||||
accent="text-primary"
|
||||
leftComponent
|
||||
rightComponent
|
||||
maxWidth="200px"
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
<Tab title="استخدام">
|
||||
|
||||
```
|
||||
</Tab>
|
||||
```jsx
|
||||
import { Chip } from 'twenty-ui/components';
|
||||
|
||||
<Tab title="المحددات">
|
||||
| المحددات | النوع | الوصف |
|
||||
| ------------------ | ----------------------- | ---------------------------------------------------------------------- |
|
||||
| linkToEntity | نص | الرابط إلى الكيان |
|
||||
| معرف الكيان | نص | المعرف الفريد للكيان |
|
||||
| الاسم | نص | اسم الكيان |
|
||||
| رابط الصورة | نص | s picture", |
|
||||
| نوع الصورة الرمزية | نوع الصورة الرمزية | نوع الصورة الرمزية التي تريد عرضها. لديه خياران: `مستدير` و `مربع` |
|
||||
| التنوع | تعداد EntityChipVariant | تنوع الرقاقة الكيانية التي ترغب في عرضها. لديه خياران: `عادي` و `شفاف` |
|
||||
| الأيقونة اليسرى | مكون رمز | مكون React يمثل رمزًا. يظهر على الجانب الأيسر من الرقاقة |
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<Chip
|
||||
size="large"
|
||||
label="Clickable Chip"
|
||||
clickable={true}
|
||||
variant="highlighted"
|
||||
accent="text-primary"
|
||||
leftComponent
|
||||
rightComponent
|
||||
maxWidth="200px"
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="المحددات">
|
||||
|
||||
|
||||
| المحددات | النوع | الوصف |
|
||||
| ------------------ | ----------------------- | ---------------------------------------------------------------------- |
|
||||
| linkToEntity | نص | الرابط إلى الكيان |
|
||||
| معرف الكيان | نص | المعرف الفريد للكيان |
|
||||
| الاسم | نص | اسم الكيان |
|
||||
| رابط الصورة | نص | s picture", |
|
||||
| نوع الصورة الرمزية | نوع الصورة الرمزية | نوع الصورة الرمزية التي تريد عرضها. لديه خياران: `مستدير` و `مربع` |
|
||||
| التنوع | تعداد EntityChipVariant | تنوع الرقاقة الكيانية التي ترغب في عرضها. لديه خياران: `عادي` و `شفاف` |
|
||||
| الأيقونة اليسرى | مكون رمز | مكون React يمثل رمزًا. يظهر على الجانب الأيسر من الرقاقة |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## الأمثلة
|
||||
@@ -100,39 +108,47 @@ export const MyComponent = () => {
|
||||
عنصر يشبه الرقاقة لعرض معلومات عن كيان.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { IconTwentyStar } from 'twenty-ui/display';
|
||||
import { Chip } from 'twenty-ui/components';
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<Router>
|
||||
<Chip
|
||||
linkToEntity="/entity-link"
|
||||
entityId="entityTest"
|
||||
name="Entity name"
|
||||
pictureUrl=""
|
||||
avatarType="rounded"
|
||||
variant="regular"
|
||||
LeftIcon={IconTwentyStar}
|
||||
/>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
<Tab title="الخصائص">
|
||||
| الخصائص | النوع | الوصف |
|
||||
| ------------------ | ----------------------- | ---------------------------------------------------------------------- |
|
||||
| linkToEntity | نص | الرابط إلى الكيان |
|
||||
| معرف الكيان | نص | المعرف الفريد للكيان |
|
||||
| الاسم | نص | اسم الكيان |
|
||||
| رابط الصورة | نص | s picture", |
|
||||
| نوع الصورة الرمزية | نوع الصورة الرمزية | نوع الصورة الرمزية التي تريد عرضها. لديه خياران: `مستدير` و `مربع` |
|
||||
| التنوع | تعداد EntityChipVariant | تنوع الرقاقة الكيانية التي ترغب في عرضها. لديه خياران: `عادي` و `شفاف` |
|
||||
| الأيقونة اليسرى | مكون رمز | مكون React يمثل رمزًا. يظهر على الجانب الأيسر من الرقاقة |
|
||||
</Tab>
|
||||
```jsx
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { IconTwentyStar } from 'twenty-ui/display';
|
||||
import { Chip } from 'twenty-ui/components';
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<Router>
|
||||
<Chip
|
||||
linkToEntity="/entity-link"
|
||||
entityId="entityTest"
|
||||
name="Entity name"
|
||||
pictureUrl=""
|
||||
avatarType="rounded"
|
||||
variant="regular"
|
||||
LeftIcon={IconTwentyStar}
|
||||
/>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="الخصائص">
|
||||
|
||||
|
||||
| الخصائص | النوع | الوصف |
|
||||
| ------------------ | ----------------------- | ---------------------------------------------------------------------- |
|
||||
| linkToEntity | نص | الرابط إلى الكيان |
|
||||
| معرف الكيان | نص | المعرف الفريد للكيان |
|
||||
| الاسم | نص | اسم الكيان |
|
||||
| رابط الصورة | نص | s picture", |
|
||||
| نوع الصورة الرمزية | نوع الصورة الرمزية | نوع الصورة الرمزية التي تريد عرضها. لديه خياران: `مستدير` و `مربع` |
|
||||
| التنوع | تعداد EntityChipVariant | تنوع الرقاقة الكيانية التي ترغب في عرضها. لديه خياران: `عادي` و `شفاف` |
|
||||
| الأيقونة اليسرى | مكون رمز | مكون React يمثل رمزًا. يظهر على الجانب الأيسر من الرقاقة |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -14,35 +14,44 @@ image: /images/user-guide/objects/objects.png
|
||||
نستخدم أيقونات Tabler لـ React في جميع أنحاء التطبيق.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="التثبيت">
|
||||
<br />
|
||||
|
||||
```
|
||||
yarn add @tabler/icons-react
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="التثبيت">
|
||||
<br />
|
||||
|
||||
<Tab title="الإزاحة">
|
||||
يمكنك استيراد كل أيقونة كمكون. إليك مثال:
|
||||
```
|
||||
yarn add @tabler/icons-react
|
||||
```
|
||||
|
||||
<br />
|
||||
</Tab>
|
||||
|
||||
```jsx
|
||||
import { IconArrowLeft } from "@tabler/icons-react";
|
||||
<Tab title="الإزاحة">
|
||||
|
||||
export const MyComponent = () => {
|
||||
return <IconArrowLeft color="red" size={48} />;
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
يمكنك استيراد كل أيقونة كمكون. إليك مثال:
|
||||
<br />
|
||||
|
||||
```jsx
|
||||
import { IconArrowLeft } from "@tabler/icons-react";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return <IconArrowLeft color="red" size={48} />;
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="الإزاحة">
|
||||
|
||||
|
||||
| الإزاحة | النوع | الوصف | الإعداد الافتراضي |
|
||||
| ----------- | ------ | -------------------------------- | ----------------- |
|
||||
| الحجم | رقم | ارتفاع وعرض الأيقونة بالبكسل | 24 |
|
||||
| اللون | string | لون الأيقونات | اللون الحالي |
|
||||
| الخط العريض | رقم | عرض الخط العريض للأيقونة بالبكسل | 2 |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="الإزاحة">
|
||||
| الإزاحة | النوع | الوصف | الإعداد الافتراضي |
|
||||
| ----------- | ------ | -------------------------------- | ----------------- |
|
||||
| الحجم | رقم | ارتفاع وعرض الأيقونة بالبكسل | 24 |
|
||||
| اللون | string | لون الأيقونات | اللون الحالي |
|
||||
| الخط العريض | رقم | عرض الخط العريض للأيقونة بالبكسل | 2 |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## أيقونات مخصصة
|
||||
@@ -54,20 +63,29 @@ image: /images/user-guide/objects/objects.png
|
||||
يعرض أيقونة دفتر العناوين.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { IconAddressBook } from 'twenty-ui/display';
|
||||
|
||||
export const MyComponent = () => {
|
||||
return <IconAddressBook size={24} stroke={2} />;
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
```jsx
|
||||
import { IconAddressBook } from 'twenty-ui/display';
|
||||
|
||||
export const MyComponent = () => {
|
||||
return <IconAddressBook size={24} stroke={2} />;
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="خصائص">
|
||||
|
||||
|
||||
| خصائص | النوع | الوصف | الإعداد الافتراضي |
|
||||
| ----------- | ----- | -------------------------------- | ----------------- |
|
||||
| الحجم | رقم | ارتفاع وعرض الأيقونة بالبكسل | 24 |
|
||||
| الخط العريض | رقم | عرض الخط العريض للأيقونة بالبكسل | 2 |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="خصائص">
|
||||
| خصائص | النوع | الوصف | الإعداد الافتراضي |
|
||||
| ----------- | ----- | -------------------------------- | ----------------- |
|
||||
| الحجم | رقم | ارتفاع وعرض الأيقونة بالبكسل | 24 |
|
||||
| الخط العريض | رقم | عرض الخط العريض للأيقونة بالبكسل | 2 |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -10,29 +10,38 @@ image: /images/user-guide/table-views/table.png
|
||||
مكوّن لتصنيف المحتوى أو وسمه بصريًا.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { Tag } from "@/ui/display/tag/components/Tag";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<Tag
|
||||
className
|
||||
color="red"
|
||||
text="Urgent"
|
||||
onClick={() => console.log("click")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="استخدام">
|
||||
|
||||
```jsx
|
||||
import { Tag } from "@/ui/display/tag/components/Tag";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<Tag
|
||||
className
|
||||
color="red"
|
||||
text="Urgent"
|
||||
onClick={() => console.log("click")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="المحددات">
|
||||
|
||||
|
||||
| المحددات | النوع | الوصف |
|
||||
| --------- | ----- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| اسم الفئة | نص | اسم اختياري لتنسيقات إضافية |
|
||||
| اللون | نص | لون العلامة. الخيارات تشمل: `أخضر`, `تركواز`, `سماوي`, `أزرق`, `أرجواني`, `وردي`, `أحمر`, `برتقالي`, `أصفر`, `رمادي`. |
|
||||
| نص | نص | محتوى العلامة |
|
||||
| عند_النقر | دالة | دالة اختيارية تُستدعى عند نقر المستخدم على العلامة |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="المحددات">
|
||||
| المحددات | النوع | الوصف |
|
||||
| --------- | ----- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| اسم الفئة | نص | اسم اختياري لتنسيقات إضافية |
|
||||
| اللون | نص | لون العلامة. الخيارات تشمل: `أخضر`, `تركواز`, `سماوي`, `أزرق`, `أرجواني`, `وردي`, `أحمر`, `برتقالي`, `أصفر`, `رمادي`. |
|
||||
| نص | نص | محتوى العلامة |
|
||||
| عند_النقر | دالة | دالة اختيارية تُستدعى عند نقر المستخدم على العلامة |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -10,22 +10,28 @@ image: /images/user-guide/api/api.png
|
||||
يستخدم محرر نصوص غني يعتمد على الكتل من [BlockNote](https://www.blocknotejs.org/) للسماح للمستخدمين بتحرير وعرض كتل المحتوى.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { useBlockNote } from "@blocknote/react";
|
||||
import { BlockEditor } from "@/ui/input/editor/components/BlockEditor";
|
||||
<Tab title="استخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
const BlockNoteEditor = useBlockNote();
|
||||
```jsx
|
||||
import { useBlockNote } from "@blocknote/react";
|
||||
import { BlockEditor } from "@/ui/input/editor/components/BlockEditor";
|
||||
|
||||
return <BlockEditor editor={BlockNoteEditor} />;
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
const BlockNoteEditor = useBlockNote();
|
||||
|
||||
<Tab title="المحددات">
|
||||
| المحددات | النوع | الوصف |
|
||||
| -------- | ----------------- | ------------------------ |
|
||||
| محرر | `BlockNoteEditor` | مثيل أو تكوين محرر الكتل |
|
||||
</Tab>
|
||||
return <BlockEditor editor={BlockNoteEditor} />;
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="المحددات">
|
||||
|
||||
|
||||
| المحددات | النوع | الوصف |
|
||||
| -------- | ----------------- | ------------------------ |
|
||||
| محرر | `BlockNoteEditor` | مثيل أو تكوين محرر الكتل |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -12,428 +12,511 @@ image: /images/user-guide/views/filter.png
|
||||
## زر
|
||||
|
||||
<Tabs>
|
||||
<Tab title="27332A2E2F2745">
|
||||
```jsx
|
||||
import { Button } from "@/ui/input/button/components/Button";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<Button
|
||||
className
|
||||
Icon={null}
|
||||
title="Title"
|
||||
fullWidth={false}
|
||||
variant="primary"
|
||||
size="medium"
|
||||
position="standalone"
|
||||
accent="default"
|
||||
soon={false}
|
||||
disabled={false}
|
||||
focus={true}
|
||||
onClick={() => console.log("click")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="27332A2E2F2745">
|
||||
|
||||
```jsx
|
||||
import { Button } from "@/ui/input/button/components/Button";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<Button
|
||||
className
|
||||
Icon={null}
|
||||
title="Title"
|
||||
fullWidth={false}
|
||||
variant="primary"
|
||||
size="medium"
|
||||
position="standalone"
|
||||
accent="default"
|
||||
soon={false}
|
||||
disabled={false}
|
||||
focus={true}
|
||||
onClick={() => console.log("click")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="خصائص">
|
||||
|
||||
|
||||
| خصائص | النوع | الوصف |
|
||||
| --------- | --------------------- | ---------------------------------------------------------------------------------- |
|
||||
| className | string | اسم فئة اختياري لتنسيقات إضافية |
|
||||
| أيقونة | `React.ComponentType` | مكون رمز اختياري يُعرض داخل الزر |
|
||||
| العنوان | string | محتوى نص الزر |
|
||||
| عرض كامل | قيمة منطقية | يُحدد إذا كان الزر يجب أن يمتد ليغطي العرض الكامل للحاوية الخاصة به |
|
||||
| التنوع | string | النمط المرئي للزر. تشمل الخيارات `primary`، `secondary`، و`tertiary`. |
|
||||
| الحجم | string | حجم الزر. يوجد خياران: `صغير` و `متوسط` |
|
||||
| الموقع | string | موقع الزر بالنسبة لأخوته. تشمل الخيارات: `standalone`، `left`، `right`، و`middle`. |
|
||||
| accent | string | موقع الزر بالنسبة لأخوته. تشمل الخيارات: `default`، `blue`، `danger` |
|
||||
| قريباً | قيمة منطقية | يشير إلى ما إذا كان الزر معلمًا "قريبًا" (مثل الميزات القادمة) |
|
||||
| معطل | قيمة منطقية | يحدد إذا كان الزر معطل أم لا |
|
||||
| تركيز | قيمة منطقية | يحدد إذا كان الزر في وضع التركيز |
|
||||
| عند النقر | وظيفة | وظيفة رد فعل تنطلق عند نقر المستخدم على الزر |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="خصائص">
|
||||
| خصائص | النوع | الوصف |
|
||||
| --------- | --------------------- | ---------------------------------------------------------------------------------- |
|
||||
| className | string | اسم فئة اختياري لتنسيقات إضافية |
|
||||
| أيقونة | `React.ComponentType` | مكون رمز اختياري يُعرض داخل الزر |
|
||||
| العنوان | string | محتوى نص الزر |
|
||||
| عرض كامل | قيمة منطقية | يُحدد إذا كان الزر يجب أن يمتد ليغطي العرض الكامل للحاوية الخاصة به |
|
||||
| التنوع | string | النمط المرئي للزر. تشمل الخيارات `primary`، `secondary`، و`tertiary`. |
|
||||
| الحجم | string | حجم الزر. يوجد خياران: `صغير` و `متوسط` |
|
||||
| الموقع | string | موقع الزر بالنسبة لأخوته. تشمل الخيارات: `standalone`، `left`، `right`، و`middle`. |
|
||||
| accent | string | موقع الزر بالنسبة لأخوته. تشمل الخيارات: `default`، `blue`، `danger` |
|
||||
| قريباً | قيمة منطقية | يشير إلى ما إذا كان الزر معلمًا "قريبًا" (مثل الميزات القادمة) |
|
||||
| معطل | قيمة منطقية | يحدد إذا كان الزر معطل أم لا |
|
||||
| تركيز | قيمة منطقية | يحدد إذا كان الزر في وضع التركيز |
|
||||
| عند النقر | وظيفة | وظيفة رد فعل تنطلق عند نقر المستخدم على الزر |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## مجموعة الأزرار
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { Button } from "@/ui/input/button/components/Button";
|
||||
import { ButtonGroup } from "@/ui/input/button/components/ButtonGroup";
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { Button } from "@/ui/input/button/components/Button";
|
||||
import { ButtonGroup } from "@/ui/input/button/components/ButtonGroup";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<ButtonGroup variant="primary" size="large" accent="blue" className>
|
||||
<Button
|
||||
className
|
||||
Icon={null}
|
||||
title="Button 1"
|
||||
fullWidth={false}
|
||||
variant="primary"
|
||||
size="medium"
|
||||
position="standalone"
|
||||
accent="blue"
|
||||
soon={false}
|
||||
disabled={false}
|
||||
focus={false}
|
||||
onClick={() => console.log("click")}
|
||||
/>
|
||||
<Button
|
||||
className
|
||||
Icon={null}
|
||||
title="Button 2"
|
||||
fullWidth={false}
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
position="left"
|
||||
accent="blue"
|
||||
soon={false}
|
||||
disabled={false}
|
||||
focus={false}
|
||||
onClick={() => console.log("click")}
|
||||
/>
|
||||
<Button
|
||||
className
|
||||
Icon={null}
|
||||
title="Button 3"
|
||||
fullWidth={false}
|
||||
variant="tertiary"
|
||||
size="medium"
|
||||
position="right"
|
||||
accent="blue"
|
||||
soon={false}
|
||||
disabled={false}
|
||||
focus={false}
|
||||
onClick={() => console.log("click")}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
};
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<ButtonGroup variant="primary" size="large" accent="blue" className>
|
||||
<Button
|
||||
className
|
||||
Icon={null}
|
||||
title="Button 1"
|
||||
fullWidth={false}
|
||||
variant="primary"
|
||||
size="medium"
|
||||
position="standalone"
|
||||
accent="blue"
|
||||
soon={false}
|
||||
disabled={false}
|
||||
focus={false}
|
||||
onClick={() => console.log("click")}
|
||||
/>
|
||||
<Button
|
||||
className
|
||||
Icon={null}
|
||||
title="Button 2"
|
||||
fullWidth={false}
|
||||
variant="secondary"
|
||||
size="medium"
|
||||
position="left"
|
||||
accent="blue"
|
||||
soon={false}
|
||||
disabled={false}
|
||||
focus={false}
|
||||
onClick={() => console.log("click")}
|
||||
/>
|
||||
<Button
|
||||
className
|
||||
Icon={null}
|
||||
title="Button 3"
|
||||
fullWidth={false}
|
||||
variant="tertiary"
|
||||
size="medium"
|
||||
position="right"
|
||||
accent="blue"
|
||||
soon={false}
|
||||
disabled={false}
|
||||
focus={false}
|
||||
onClick={() => console.log("click")}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
};
|
||||
|
||||
```
|
||||
</Tab>
|
||||
```
|
||||
</Tab>
|
||||
|
||||
<Tab title="الخصائص">
|
||||
| الخصائص | النوع | الوصف |
|
||||
| --------- | --------- | -------------------------------------------------------------------------------------- |
|
||||
| التنوع | string | النمط المرئي للأزرار داخل المجموعة. تشمل الخيارات `primary`، `secondary`، و`tertiary`. |
|
||||
| الحجم | string | حجم الأزرار داخل المجموعة. يوجد خياران: `medium` و`small`. |
|
||||
| accent | نص | لون تمييز الأزرار داخل المجموعة. تشمل الخيارات `default`، `blue` و`danger`. |
|
||||
| className | string | اسم فئة اختياري لتنسيقات إضافية |
|
||||
| الأبناء | ReactNode | مجموعة من عناصر React تمثل الأزرار الفردية داخل المجموعة |
|
||||
</Tab>
|
||||
<Tab title="الخصائص">
|
||||
|
||||
|
||||
| الخصائص | النوع | الوصف |
|
||||
| --------- | --------- | -------------------------------------------------------------------------------------- |
|
||||
| التنوع | string | النمط المرئي للأزرار داخل المجموعة. تشمل الخيارات `primary`، `secondary`، و`tertiary`. |
|
||||
| الحجم | string | حجم الأزرار داخل المجموعة. يوجد خياران: `medium` و`small`. |
|
||||
| accent | نص | لون تمييز الأزرار داخل المجموعة. تشمل الخيارات `default`، `blue` و`danger`. |
|
||||
| className | string | اسم فئة اختياري لتنسيقات إضافية |
|
||||
| الأبناء | ReactNode | مجموعة من عناصر React تمثل الأزرار الفردية داخل المجموعة |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## زر عائم
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { FloatingButton } from "@/ui/input/button/components/FloatingButton";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<FloatingButton
|
||||
className
|
||||
Icon={IconSearch}
|
||||
title="Title"
|
||||
size="medium"
|
||||
position="standalone"
|
||||
applyShadow={true}
|
||||
applyBlur={true}
|
||||
disabled={false}
|
||||
focus={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
```jsx
|
||||
import { FloatingButton } from "@/ui/input/button/components/FloatingButton";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
|
||||
<Tab title="الخصائص">
|
||||
| الخصائص | النوع | الوصف |
|
||||
| ------------ | --------------------- | --------------------------------------------------------------------------------- |
|
||||
| className | string | اسم اختياري لتنسيقات إضافية |
|
||||
| أيقونة | `React.ComponentType` | مكون أيقونة اختياري يظهر داخل الزر |
|
||||
| العنوان | string | محتوى نص الزر |
|
||||
| الحجم | string | حجم الزر. يوجد خياران: `صغير` و `متوسط` |
|
||||
| الموقع | string | موقع الزر بالنسبة لأخوته. تشمل الخيارات: `standalone`، `left`، `middle`، `right`. |
|
||||
| تطبيق الظل | قيمة منطقية | يحدد إذا ما سيتم تطبيق الظلال على الزر |
|
||||
| تطبيق الضباب | قيمة منطقية | يحدد ما إذا كان ينبغي تطبيق تأثير الضباب على الزر |
|
||||
| معطل | قيمة منطقية | يحدد ما إذا كان الزر معطل |
|
||||
| تركيز | قيمة منطقية | يحدد إذا كان الزر في وضع التركيز |
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<FloatingButton
|
||||
className
|
||||
Icon={IconSearch}
|
||||
title="Title"
|
||||
size="medium"
|
||||
position="standalone"
|
||||
applyShadow={true}
|
||||
applyBlur={true}
|
||||
disabled={false}
|
||||
focus={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="الخصائص">
|
||||
|
||||
|
||||
| الخصائص | النوع | الوصف |
|
||||
| ------------ | --------------------- | --------------------------------------------------------------------------------- |
|
||||
| className | string | اسم اختياري لتنسيقات إضافية |
|
||||
| أيقونة | `React.ComponentType` | مكون أيقونة اختياري يظهر داخل الزر |
|
||||
| العنوان | string | محتوى نص الزر |
|
||||
| الحجم | string | حجم الزر. يوجد خياران: `صغير` و `متوسط` |
|
||||
| الموقع | string | موقع الزر بالنسبة لأخوته. تشمل الخيارات: `standalone`، `left`، `middle`، `right`. |
|
||||
| تطبيق الظل | قيمة منطقية | يحدد إذا ما سيتم تطبيق الظلال على الزر |
|
||||
| تطبيق الضباب | قيمة منطقية | يحدد ما إذا كان ينبغي تطبيق تأثير الضباب على الزر |
|
||||
| معطل | قيمة منطقية | يحدد ما إذا كان الزر معطل |
|
||||
| تركيز | قيمة منطقية | يحدد إذا كان الزر في وضع التركيز |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## مجموعة الأزرار العائمة
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { FloatingButton } from "@/ui/input/button/components/FloatingButton";
|
||||
import { FloatingButtonGroup } from "@/ui/input/button/components/FloatingButtonGroup";
|
||||
import { IconClipboardText, IconCheckbox } from "@tabler/icons-react";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<FloatingButtonGroup size="small">
|
||||
<FloatingButton
|
||||
className
|
||||
Icon={IconClipboardText}
|
||||
title
|
||||
size="small"
|
||||
position="standalone"
|
||||
applyShadow={true}
|
||||
applyBlur={true}
|
||||
disabled={false}
|
||||
focus={true}
|
||||
/>
|
||||
<FloatingButton
|
||||
className
|
||||
Icon={IconCheckbox}
|
||||
title
|
||||
size="small"
|
||||
position="standalone"
|
||||
applyShadow={true}
|
||||
applyBlur={true}
|
||||
disabled={false}
|
||||
/>
|
||||
</FloatingButtonGroup>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
```jsx
|
||||
import { FloatingButton } from "@/ui/input/button/components/FloatingButton";
|
||||
import { FloatingButtonGroup } from "@/ui/input/button/components/FloatingButtonGroup";
|
||||
import { IconClipboardText, IconCheckbox } from "@tabler/icons-react";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<FloatingButtonGroup size="small">
|
||||
<FloatingButton
|
||||
className
|
||||
Icon={IconClipboardText}
|
||||
title
|
||||
size="small"
|
||||
position="standalone"
|
||||
applyShadow={true}
|
||||
applyBlur={true}
|
||||
disabled={false}
|
||||
focus={true}
|
||||
/>
|
||||
<FloatingButton
|
||||
className
|
||||
Icon={IconCheckbox}
|
||||
title
|
||||
size="small"
|
||||
position="standalone"
|
||||
applyShadow={true}
|
||||
applyBlur={true}
|
||||
disabled={false}
|
||||
/>
|
||||
</FloatingButtonGroup>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="خصائص">
|
||||
|
||||
|
||||
| خصائص | النوع | الوصف | الإعداد الافتراضي |
|
||||
| ------- | --------- | -------------------------------------------------------- | ----------------- |
|
||||
| الحجم | string | حجم الزر. يوجد خياران: `صغير` و `متوسط` | صغير |
|
||||
| الأبناء | ReactNode | مجموعة من عناصر React تمثل الأزرار الفردية داخل المجموعة | |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="خصائص">
|
||||
| خصائص | النوع | الوصف | الإعداد الافتراضي |
|
||||
| ------- | --------- | -------------------------------------------------------- | ----------------- |
|
||||
| الحجم | string | حجم الزر. يوجد خياران: `صغير` و `متوسط` | صغير |
|
||||
| الأبناء | ReactNode | مجموعة من عناصر React تمثل الأزرار الفردية داخل المجموعة | |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## زر رمز عائم
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { FloatingIconButton } from "@/ui/input/button/components/FloatingIconButton";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<FloatingIconButton
|
||||
className
|
||||
Icon={IconSearch}
|
||||
size="small"
|
||||
position="standalone"
|
||||
applyShadow={true}
|
||||
applyBlur={true}
|
||||
disabled={false}
|
||||
focus={false}
|
||||
onClick={() => console.log("click")}
|
||||
isActive={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
```jsx
|
||||
import { FloatingIconButton } from "@/ui/input/button/components/FloatingIconButton";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<FloatingIconButton
|
||||
className
|
||||
Icon={IconSearch}
|
||||
size="small"
|
||||
position="standalone"
|
||||
applyShadow={true}
|
||||
applyBlur={true}
|
||||
disabled={false}
|
||||
focus={false}
|
||||
onClick={() => console.log("click")}
|
||||
isActive={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="الخصائص">
|
||||
|
||||
|
||||
| الخصائص | النوع | الوصف |
|
||||
| ------------ | --------------------- | ---------------------------------------------------------------------------------- |
|
||||
| className | نص | اسم اختياري لتنسيقات إضافية |
|
||||
| أيقونة | `React.ComponentType` | مكون أيقونة اختياري يظهر داخل الزر |
|
||||
| الحجم | نص | حجم الزر. يوجد خياران: `صغير` و `متوسط` |
|
||||
| الموقع | نص | موقع الزر بالنسبة لأخوته. تشمل الخيارات: `standalone`، `left`، `right`، و`middle`. |
|
||||
| تطبيق الظل | قيمة منطقية | يحدد إذا ما سيتم تطبيق الظلال على الزر |
|
||||
| تطبيق الضباب | قيمة منطقية | يحدد ما إذا كان ينبغي تطبيق تأثير الضباب على الزر |
|
||||
| معطل | قيمة منطقية | يحدد ما إذا كان الزر معطل |
|
||||
| تركيز | قيمة منطقية | يحدد إذا كان الزر في وضع التركيز |
|
||||
| عند النقر | وظيفة | وظيفة رد فعل تنطلق عند نقر المستخدم على الزر |
|
||||
| فعّال | قيمة منطقية | يحدد إذا كان الزر في وضع فعّال |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="الخصائص">
|
||||
| الخصائص | النوع | الوصف |
|
||||
| ------------ | --------------------- | ---------------------------------------------------------------------------------- |
|
||||
| className | نص | اسم اختياري لتنسيقات إضافية |
|
||||
| أيقونة | `React.ComponentType` | مكون أيقونة اختياري يظهر داخل الزر |
|
||||
| الحجم | نص | حجم الزر. يوجد خياران: `صغير` و `متوسط` |
|
||||
| الموقع | نص | موقع الزر بالنسبة لأخوته. تشمل الخيارات: `standalone`، `left`، `right`، و`middle`. |
|
||||
| تطبيق الظل | قيمة منطقية | يحدد إذا ما سيتم تطبيق الظلال على الزر |
|
||||
| تطبيق الضباب | قيمة منطقية | يحدد ما إذا كان ينبغي تطبيق تأثير الضباب على الزر |
|
||||
| معطل | قيمة منطقية | يحدد ما إذا كان الزر معطل |
|
||||
| تركيز | قيمة منطقية | يحدد إذا كان الزر في وضع التركيز |
|
||||
| عند النقر | وظيفة | وظيفة رد فعل تنطلق عند نقر المستخدم على الزر |
|
||||
| فعّال | قيمة منطقية | يحدد إذا كان الزر في وضع فعّال |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## مجموعة أزرار الرموز العائمة
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { FloatingIconButtonGroup } from "@/ui/input/button/components/FloatingIconButtonGroup";
|
||||
import { IconClipboardText, IconCheckbox } from "@tabler/icons-react";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
const iconButtons = [
|
||||
{
|
||||
Icon: IconClipboardText,
|
||||
onClick: () => console.log("Button 1 clicked"),
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
Icon: IconCheckbox,
|
||||
onClick: () => console.log("Button 2 clicked"),
|
||||
isActive: true,
|
||||
},
|
||||
];
|
||||
```jsx
|
||||
import { FloatingIconButtonGroup } from "@/ui/input/button/components/FloatingIconButtonGroup";
|
||||
import { IconClipboardText, IconCheckbox } from "@tabler/icons-react";
|
||||
|
||||
return (
|
||||
<FloatingIconButtonGroup
|
||||
className
|
||||
size="small"
|
||||
iconButtons={iconButtons} />
|
||||
);
|
||||
};
|
||||
export const MyComponent = () => {
|
||||
const iconButtons = [
|
||||
{
|
||||
Icon: IconClipboardText,
|
||||
onClick: () => console.log("Button 1 clicked"),
|
||||
isActive: true,
|
||||
},
|
||||
{
|
||||
Icon: IconCheckbox,
|
||||
onClick: () => console.log("Button 2 clicked"),
|
||||
isActive: true,
|
||||
},
|
||||
];
|
||||
|
||||
```
|
||||
</Tab>
|
||||
return (
|
||||
<FloatingIconButtonGroup
|
||||
className
|
||||
size="small"
|
||||
iconButtons={iconButtons} />
|
||||
);
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="الخصائص">
|
||||
|
||||
|
||||
| الخصائص | النوع | الوصف |
|
||||
| ------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| className | string | اسم اختياري لتنسيقات إضافية |
|
||||
| الحجم | string | حجم الزر. يوجد خياران: `صغير` و `متوسط` |
|
||||
| أزرار الرموز | array | مجموعة من الكائنات، يمثل كل منها زر رمز في المجموعة. يجب أن يشمل كل كائن مكون الرمز الذي تريد عرضه في الزر، الوظيفة التي ترغب في استدعائها عند نقر المستخدم على الزر، وما إذا كان الزر ينبغي أن يكون نشطًا أم لا. |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="الخصائص">
|
||||
| الخصائص | النوع | الوصف |
|
||||
| ------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| className | string | اسم اختياري لتنسيقات إضافية |
|
||||
| الحجم | string | حجم الزر. يوجد خياران: `صغير` و `متوسط` |
|
||||
| أزرار الرموز | array | مجموعة من الكائنات، يمثل كل منها زر رمز في المجموعة. يجب أن يشمل كل كائن مكون الرمز الذي تريد عرضه في الزر، الوظيفة التي ترغب في استدعائها عند نقر المستخدم على الزر، وما إذا كان الزر ينبغي أن يكون نشطًا أم لا. |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## زر خفيف
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { LightButton } from "@/ui/input/button/components/LightButton";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
return <LightButton
|
||||
className
|
||||
icon={null}
|
||||
title="Title"
|
||||
accent="secondary"
|
||||
active={false}
|
||||
disabled={false}
|
||||
focus={true}
|
||||
onClick={()=>console.log('click')}
|
||||
/>;
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
```jsx
|
||||
import { LightButton } from "@/ui/input/button/components/LightButton";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return <LightButton
|
||||
className
|
||||
icon={null}
|
||||
title="Title"
|
||||
accent="secondary"
|
||||
active={false}
|
||||
disabled={false}
|
||||
focus={true}
|
||||
onClick={()=>console.log('click')}
|
||||
/>;
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="الإزاحة">
|
||||
|
||||
|
||||
| الإزاحة | النوع | الوصف |
|
||||
| --------- | ----------------- | -------------------------------------------------------------------------- |
|
||||
| className | string | اسم اختياري لتنسيقات إضافية |
|
||||
| أيقونة | `React.ReactNode` | الرمز الذي تريد عرضه في الزر |
|
||||
| العنوان | string | محتوى نص الزر |
|
||||
| accent | string | موقع الزر بالنسبة لأخوته. لون الزر المميز تشمل الخيارات: `ثانوي` و `ثالثي` |
|
||||
| نشط | قيمة منطقية | يحدد إذا كان الزر في وضع فعّال |
|
||||
| معطل | قيمة منطقية | يحدد ما إذا كان الزر معطل |
|
||||
| تركيز | قيمة منطقية | يحدد إذا كان الزر في وضع التركيز |
|
||||
| عند النقر | وظيفة | وظيفة رد فعل تنطلق عند نقر المستخدم على الزر |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="الإزاحة">
|
||||
| الإزاحة | النوع | الوصف |
|
||||
| --------- | ----------------- | -------------------------------------------------------------------------- |
|
||||
| className | string | اسم اختياري لتنسيقات إضافية |
|
||||
| أيقونة | `React.ReactNode` | الرمز الذي تريد عرضه في الزر |
|
||||
| العنوان | string | محتوى نص الزر |
|
||||
| accent | string | موقع الزر بالنسبة لأخوته. لون الزر المميز تشمل الخيارات: `ثانوي` و `ثالثي` |
|
||||
| نشط | قيمة منطقية | يحدد إذا كان الزر في وضع فعّال |
|
||||
| معطل | قيمة منطقية | يحدد ما إذا كان الزر معطل |
|
||||
| تركيز | قيمة منطقية | يحدد إذا كان الزر في وضع التركيز |
|
||||
| عند النقر | وظيفة | وظيفة رد فعل تنطلق عند نقر المستخدم على الزر |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## زر أيقونة خفيف
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { LightIconButton } from "@/ui/input/button/components/LightIconButton";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<LightIconButton
|
||||
className
|
||||
testId="test1"
|
||||
Icon={IconSearch}
|
||||
title="Title"
|
||||
size="small"
|
||||
accent="secondary"
|
||||
active={true}
|
||||
disabled={false}
|
||||
focus={true}
|
||||
onClick={() => console.log("click")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
```jsx
|
||||
import { LightIconButton } from "@/ui/input/button/components/LightIconButton";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<LightIconButton
|
||||
className
|
||||
testId="test1"
|
||||
Icon={IconSearch}
|
||||
title="Title"
|
||||
size="small"
|
||||
accent="secondary"
|
||||
active={true}
|
||||
disabled={false}
|
||||
focus={true}
|
||||
onClick={() => console.log("click")}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="العناصر">
|
||||
|
||||
|
||||
| العناصر | النوع | الوصف |
|
||||
| --------- | --------------------- | ------------------------------------------------ |
|
||||
| className | string | اسم اختياري لتنسيقات إضافية |
|
||||
| testId | string | معرف اختبار للزر |
|
||||
| أيقونة | `React.ComponentType` | مكون أيقونة اختياري يظهر داخل الزر |
|
||||
| العنوان | string | محتوى نصي للزر |
|
||||
| الحجم | string | حجم الزر. يوجد خياران: `صغير` و `متوسط` |
|
||||
| accent | string | لون الزر المميز تشمل الخيارات: `ثانوي` و `ثالثي` |
|
||||
| نشط | قيمة منطقية | يحدد ما إذا كان الزر في حالة نشطة |
|
||||
| معطل | قيمة منطقية | يحدد ما إذا كان الزر معطلاً |
|
||||
| التركيز | قيمة منطقية | يشير إلى ما إذا كان الزر لديه تركيز |
|
||||
| عند النقر | function | وظيفة رد اتصال تتفعّل عند نقر المستخدم على الزر |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="العناصر">
|
||||
| العناصر | النوع | الوصف |
|
||||
| --------- | --------------------- | ------------------------------------------------ |
|
||||
| className | string | اسم اختياري لتنسيقات إضافية |
|
||||
| testId | string | معرف اختبار للزر |
|
||||
| أيقونة | `React.ComponentType` | مكون أيقونة اختياري يظهر داخل الزر |
|
||||
| العنوان | string | محتوى نصي للزر |
|
||||
| الحجم | string | حجم الزر. يوجد خياران: `صغير` و `متوسط` |
|
||||
| accent | string | لون الزر المميز تشمل الخيارات: `ثانوي` و `ثالثي` |
|
||||
| نشط | قيمة منطقية | يحدد ما إذا كان الزر في حالة نشطة |
|
||||
| معطل | قيمة منطقية | يحدد ما إذا كان الزر معطلاً |
|
||||
| التركيز | قيمة منطقية | يشير إلى ما إذا كان الزر لديه تركيز |
|
||||
| عند النقر | function | وظيفة رد اتصال تتفعّل عند نقر المستخدم على الزر |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## الزر الرئيسي
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { MainButton } from "@/ui/input/button/components/MainButton";
|
||||
import { IconCheckbox } from "@tabler/icons-react";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<MainButton
|
||||
title="Checkbox"
|
||||
fullWidth={false}
|
||||
variant="primary"
|
||||
soon={false}
|
||||
Icon={IconCheckbox}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
```jsx
|
||||
import { MainButton } from "@/ui/input/button/components/MainButton";
|
||||
import { IconCheckbox } from "@tabler/icons-react";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<MainButton
|
||||
title="Checkbox"
|
||||
fullWidth={false}
|
||||
variant="primary"
|
||||
soon={false}
|
||||
Icon={IconCheckbox}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="العناصر">
|
||||
|
||||
|
||||
| العناصر | النوع | الوصف |
|
||||
| -------------- | -------------------------------- | -------------------------------------------------------------- |
|
||||
| العنوان | string | محتوى نصي للزر |
|
||||
| عرض كامل | قيمة منطقية | يحدد ما إذا كان الزر يجب أن يمتد على كامل عرض الحاوية |
|
||||
| التنوع | string | النمط البصري للزر. تشمل الخيارات `أساسي` و `ثانوي` |
|
||||
| قريباً | قيمة منطقية | يشير إلى ما إذا كان الزر معلمًا "قريبًا" (مثل الميزات القادمة) |
|
||||
| أيقونة | `React.ComponentType` | مكون أيقونة اختياري يظهر داخل الزر |
|
||||
| خصائص زر React | `React.ComponentProps<'button'>` | كل خصائص زر HTML القياسية مدعومة |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="العناصر">
|
||||
| العناصر | النوع | الوصف |
|
||||
| -------------- | -------------------------------- | -------------------------------------------------------------- |
|
||||
| العنوان | string | محتوى نصي للزر |
|
||||
| عرض كامل | قيمة منطقية | يحدد ما إذا كان الزر يجب أن يمتد على كامل عرض الحاوية |
|
||||
| التنوع | string | النمط البصري للزر. تشمل الخيارات `أساسي` و `ثانوي` |
|
||||
| قريباً | قيمة منطقية | يشير إلى ما إذا كان الزر معلمًا "قريبًا" (مثل الميزات القادمة) |
|
||||
| أيقونة | `React.ComponentType` | مكون أيقونة اختياري يظهر داخل الزر |
|
||||
| خصائص زر React | `React.ComponentProps<'button'>` | كل خصائص زر HTML القياسية مدعومة |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## زر أيقونة دائري
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { RoundedIconButton } from "@/ui/input/button/components/RoundedIconButton";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<RoundedIconButton
|
||||
Icon={IconSearch}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
```jsx
|
||||
import { RoundedIconButton } from "@/ui/input/button/components/RoundedIconButton";
|
||||
import { IconSearch } from "@tabler/icons-react";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<RoundedIconButton
|
||||
Icon={IconSearch}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="العناصر">
|
||||
|
||||
|
||||
| العناصر | النوع | الوصف |
|
||||
| -------------- | ----------------------------------------------- | ----- |
|
||||
| أيقونة | `React.ComponentType` | |
|
||||
| خصائص زر React | `React.ButtonHTMLAttributes<HTMLButtonElement>` | |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="العناصر">
|
||||
| العناصر | النوع | الوصف |
|
||||
| -------------- | ----------------------------------------------- | ----- |
|
||||
| أيقونة | `React.ComponentType` | |
|
||||
| خصائص زر React | `React.ButtonHTMLAttributes<HTMLButtonElement>` | |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -10,35 +10,41 @@ image: /images/user-guide/tasks/tasks_header.png
|
||||
يُستخدم عندما يحتاج المستخدم إلى اختيار قيم متعددة من بين عدة خيارات.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { Checkbox } from "twenty-ui/display";
|
||||
<Tab title="استخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={true}
|
||||
indeterminate={false}
|
||||
onChange={() => console.log("onChange function fired")}
|
||||
onCheckedChange={() => console.log("onCheckedChange function fired")}
|
||||
variant="primary"
|
||||
size="small"
|
||||
shape="squared"
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
```jsx
|
||||
import { Checkbox } from "twenty-ui/display";
|
||||
|
||||
<Tab title="المحددات">
|
||||
| المحددات | النوع | الوصف |
|
||||
| ------------------------ | ----------- | ----------------------------------------------------------------------------- |
|
||||
| مختار | قيمة منطقية | يشير إلى ما إذا كان مربع الاختيار محددًا |
|
||||
| غير محدد | قيمة منطقية | يشير إلى ما إذا كان مربع الاختيار في حالة غير محددة (لا هو محدد ولا غير محدد) |
|
||||
| عند التغيير | دالة | الدالة التي ترغب في تفعيلها عند تغيير حالة مربع الاختيار |
|
||||
| عند تغيير الحالة المحددة | دالة | الدالة التي ترغب في تفعيلها عند تغيّر حالة `checked` |
|
||||
| نموذج | نص | النمط البصري للصندوق. تتضمن الخيارات: 'أساسي'، 'ثانوي'، و 'ثالثي' |
|
||||
| الحجم | نص | حجم مربع الاختيار. له خياران: `small` و `large` |
|
||||
| الشكل | نص | شكل مربع الاختيار. لديه خياران: 'مربع' و 'مدور' |
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<Checkbox
|
||||
checked={true}
|
||||
indeterminate={false}
|
||||
onChange={() => console.log("onChange function fired")}
|
||||
onCheckedChange={() => console.log("onCheckedChange function fired")}
|
||||
variant="primary"
|
||||
size="small"
|
||||
shape="squared"
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="المحددات">
|
||||
|
||||
|
||||
| المحددات | النوع | الوصف |
|
||||
| ------------------------ | ----------- | ----------------------------------------------------------------------------- |
|
||||
| مختار | قيمة منطقية | يشير إلى ما إذا كان مربع الاختيار محددًا |
|
||||
| غير محدد | قيمة منطقية | يشير إلى ما إذا كان مربع الاختيار في حالة غير محددة (لا هو محدد ولا غير محدد) |
|
||||
| عند التغيير | دالة | الدالة التي ترغب في تفعيلها عند تغيير حالة مربع الاختيار |
|
||||
| عند تغيير الحالة المحددة | دالة | الدالة التي ترغب في تفعيلها عند تغيّر حالة `checked` |
|
||||
| نموذج | نص | النمط البصري للصندوق. تتضمن الخيارات: 'أساسي'، 'ثانوي'، و 'ثالثي' |
|
||||
| الحجم | نص | حجم مربع الاختيار. له خياران: `small` و `large` |
|
||||
| الشكل | نص | شكل مربع الاختيار. لديه خياران: 'مربع' و 'مدور' |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -12,28 +12,36 @@ image: /images/user-guide/fields/field.png
|
||||
يمثل مخططات ألوان مختلفة ومخصص بشكل خاص للمواضيع الفاتحة والداكنة.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="27332A2E2F2745">
|
||||
```jsx
|
||||
import { ColorSchemeCard } from "twenty-ui/display";
|
||||
<Tab title="27332A2E2F2745">
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<ColorSchemeCard
|
||||
variant="Dark"
|
||||
selected={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
```jsx
|
||||
import { ColorSchemeCard } from "twenty-ui/display";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<ColorSchemeCard
|
||||
variant="Dark"
|
||||
selected={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="خصائص">
|
||||
|
||||
|
||||
| خصائص | النوع | الوصف | الإعداد الافتراضي |
|
||||
| ------------ | --------------------------------------- | ---------------------------------------------------------------------- | ----------------- |
|
||||
| التنوع | string | نوع مخطط الألوان. تشمل الخيارات `داكنة`, `فاتحة`, و `النظام` | فاتح |
|
||||
| المحدد | قيمة منطقية | إذا كان `صحيح`, يتم عرض علامة الاختيار للدلالة على مخطط الألوان المحدد | |
|
||||
| خصائص إضافية | `React.ComponentPropsWithoutRef<'div'>` | خصائص عنصر `div` العادي في HTML | |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="خصائص">
|
||||
| خصائص | النوع | الوصف | الإعداد الافتراضي |
|
||||
| ------------ | --------------------------------------- | ---------------------------------------------------------------------- | ----------------- |
|
||||
| التنوع | string | نوع مخطط الألوان. تشمل الخيارات `داكنة`, `فاتحة`, و `النظام` | فاتح |
|
||||
| المحدد | قيمة منطقية | إذا كان `صحيح`, يتم عرض علامة الاختيار للدلالة على مخطط الألوان المحدد | |
|
||||
| خصائص إضافية | `React.ComponentPropsWithoutRef<'div'>` | خصائص عنصر `div` العادي في HTML | |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## منتقي مخطط الألوان
|
||||
@@ -41,23 +49,31 @@ image: /images/user-guide/fields/field.png
|
||||
يتيح للمستخدمين اختيار بين مخططات الألوان المختلفة.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { ColorSchemePicker } from "twenty-ui/display";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
return <ColorSchemePicker
|
||||
value="Dark"
|
||||
onChange
|
||||
/>;
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
```jsx
|
||||
import { ColorSchemePicker } from "twenty-ui/display";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return <ColorSchemePicker
|
||||
value="Dark"
|
||||
onChange
|
||||
/>;
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="خصائص">
|
||||
|
||||
|
||||
| خصائص | النوع | الوصف |
|
||||
| ----------- | ------------------- | ------------------------------------------------------------- |
|
||||
| القيمة | `طريقة عرض الألوان` | مخطط الألوان المحدد حاليًا |
|
||||
| عند التغيير | function | الدالة التي ترغب في تفعيلها عندما يختار المستخدم نظام الألوان |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="خصائص">
|
||||
| خصائص | النوع | الوصف |
|
||||
| ----------- | ------------------- | ------------------------------------------------------------- |
|
||||
| القيمة | `طريقة عرض الألوان` | مخطط الألوان المحدد حاليًا |
|
||||
| عند التغيير | function | الدالة التي ترغب في تفعيلها عندما يختار المستخدم نظام الألوان |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -10,43 +10,52 @@ image: /images/user-guide/github/github-header.png
|
||||
منتقى الأيقونات المعتمد على القائمة المنسدلة الذي يتيح للمستخدمين اختيار أيقونة من قائمة.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { RecoilRoot } from "recoil";
|
||||
import React, { useState } from "react";
|
||||
import { IconPicker } from "@/ui/input/components/IconPicker";
|
||||
|
||||
export const MyComponent = () => {
|
||||
<Tab title="استخدام">
|
||||
|
||||
const [selectedIcon, setSelectedIcon] = useState("");
|
||||
const handleIconChange = ({ iconKey, Icon }) => {
|
||||
console.log("Selected Icon:", iconKey);
|
||||
setSelectedIcon(iconKey);
|
||||
};
|
||||
```jsx
|
||||
import { RecoilRoot } from "recoil";
|
||||
import React, { useState } from "react";
|
||||
import { IconPicker } from "@/ui/input/components/IconPicker";
|
||||
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<IconPicker
|
||||
disabled={false}
|
||||
onChange={handleIconChange}
|
||||
selectedIconKey={selectedIcon}
|
||||
variant="primary"
|
||||
/>
|
||||
</RecoilRoot>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
|
||||
<Tab title="المحددات">
|
||||
| المحددات | النوع | الوصف |
|
||||
| ----------------------- | ----------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| معطل | قيمة منطقية | يقوم بتعطيل منتقى الأيقونات إذا تم تعيينه إلى `true` |
|
||||
| عند التغيير | دالة | الدالة الارتجاعية التي تُفعل عندما يختار المستخدم أيقونة. يستقبل كائنًا يحتوي على الخصائص `iconKey` و `Icon` |
|
||||
| مفتاح الأيقونة المختارة | نص | مفتاح الأيقونة المختارة في البداية |
|
||||
| النقر بالخارج | دالة | الدالة الارتجاعية التي تُفعل عندما ينقر المستخدم خارج القائمة المنسدلة |
|
||||
| عند الإغلاق | دالة | الدالة الارتجاعية التي تُفعل عند إغلاق القائمة المنسدلة |
|
||||
| عند الفتح | دالة | الدالة الارتجاعية التي تُفعل عند فتح القائمة المنسدلة |
|
||||
| التنوع | نص | متغير النمط البصري للأيقونة القابلة للنقر. تشمل الخيارات: `رئيسي`, `ثانوي`, و `ثالثي` |
|
||||
</Tab>
|
||||
const [selectedIcon, setSelectedIcon] = useState("");
|
||||
const handleIconChange = ({ iconKey, Icon }) => {
|
||||
console.log("Selected Icon:", iconKey);
|
||||
setSelectedIcon(iconKey);
|
||||
};
|
||||
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<IconPicker
|
||||
disabled={false}
|
||||
onChange={handleIconChange}
|
||||
selectedIconKey={selectedIcon}
|
||||
variant="primary"
|
||||
/>
|
||||
</RecoilRoot>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
</Tab>
|
||||
<Tab title="المحددات">
|
||||
|
||||
|
||||
| المحددات | النوع | الوصف |
|
||||
| ----------------------- | ----------- | ------------------------------------------------------------------------------------------------------------ |
|
||||
| معطل | قيمة منطقية | يقوم بتعطيل منتقى الأيقونات إذا تم تعيينه إلى `true` |
|
||||
| عند التغيير | دالة | الدالة الارتجاعية التي تُفعل عندما يختار المستخدم أيقونة. يستقبل كائنًا يحتوي على الخصائص `iconKey` و `Icon` |
|
||||
| مفتاح الأيقونة المختارة | نص | مفتاح الأيقونة المختارة في البداية |
|
||||
| النقر بالخارج | دالة | الدالة الارتجاعية التي تُفعل عندما ينقر المستخدم خارج القائمة المنسدلة |
|
||||
| عند الإغلاق | دالة | الدالة الارتجاعية التي تُفعل عند إغلاق القائمة المنسدلة |
|
||||
| عند الفتح | دالة | الدالة الارتجاعية التي تُفعل عند فتح القائمة المنسدلة |
|
||||
| التنوع | نص | متغير النمط البصري للأيقونة القابلة للنقر. تشمل الخيارات: `رئيسي`, `ثانوي`, و `ثالثي` |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -10,25 +10,32 @@ image: /images/user-guide/objects/objects.png
|
||||
4A4F33452D 44445245332A2E2F454A46 28452F 482532274429 35483129.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { ImageInput } from "@/ui/input/components/ImageInput";
|
||||
<Tab title="استخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
return <ImageInput/>;
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
```jsx
|
||||
import { ImageInput } from "@/ui/input/components/ImageInput";
|
||||
|
||||
<Tab title="المحددات">
|
||||
| المحددات | النوع | الوصف |
|
||||
| ------------ | ----------- | --------------------------------------------------------------------------------- |
|
||||
| صورة | نص | 3946482746 45352F31 274435483129 27442544432A3148464A |
|
||||
| onUpload | دالة | الدالة التي تُستدعى عند قيام المستخدم بتحميل صورة جديدة. تستقبل كائن `File` كوسيط |
|
||||
| onRemove | دالة | الدالة التي تُستدعى عند نقر المستخدم على زر الإزالة |
|
||||
| onAbort | دالة | الدالة التي تُستدعى عند نقر المستخدم على زر الإلغاء أثناء تحميل الصورة |
|
||||
| isUploading | قيمة منطقية | يشير إلى ما إذا كان يتم تحميل صورة حاليًا |
|
||||
| errorMessage | نص | رسالة خطأ اختيارية لعرضها أسفل حقل إدخال الصورة |
|
||||
| معطل | قيمة منطقية | إذا كانت `true`، فسيكون حقل الإدخال بأكمله معطلاً، ولن تكون الأزرار قابلة للنقر |
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
return <ImageInput/>;
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="المحددات">
|
||||
|
||||
|
||||
| المحددات | النوع | الوصف |
|
||||
| ------------ | ----------- | --------------------------------------------------------------------------------- |
|
||||
| صورة | نص | 3946482746 45352F31 274435483129 27442544432A3148464A |
|
||||
| onUpload | دالة | الدالة التي تُستدعى عند قيام المستخدم بتحميل صورة جديدة. تستقبل كائن `File` كوسيط |
|
||||
| onRemove | دالة | الدالة التي تُستدعى عند نقر المستخدم على زر الإزالة |
|
||||
| onAbort | دالة | الدالة التي تُستدعى عند نقر المستخدم على زر الإلغاء أثناء تحميل الصورة |
|
||||
| isUploading | قيمة منطقية | يشير إلى ما إذا كان يتم تحميل صورة حاليًا |
|
||||
| errorMessage | نص | رسالة خطأ اختيارية لعرضها أسفل حقل إدخال الصورة |
|
||||
| معطل | قيمة منطقية | إذا كانت `true`، فسيكون حقل الإدخال بأكمله معطلاً، ولن تكون الأزرار قابلة للنقر |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -10,50 +10,58 @@ image: /images/user-guide/create-workspace/workspace-cover.png
|
||||
تستخدم عندما يمكن للمستخدمين اختيار خيار واحد فقط من سلسلة من الخيارات.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { Radio } from "twenty-ui/display";
|
||||
<Tab title="استخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
```jsx
|
||||
import { Radio } from "twenty-ui/display";
|
||||
|
||||
const handleRadioChange = (event) => {
|
||||
console.log("Radio button changed:", event.target.checked);
|
||||
};
|
||||
export const MyComponent = () => {
|
||||
|
||||
const handleCheckedChange = (checked) => {
|
||||
console.log("Checked state changed:", checked);
|
||||
};
|
||||
const handleRadioChange = (event) => {
|
||||
console.log("Radio button changed:", event.target.checked);
|
||||
};
|
||||
|
||||
const handleCheckedChange = (checked) => {
|
||||
console.log("Checked state changed:", checked);
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<Radio
|
||||
checked={true}
|
||||
value="Option 1"
|
||||
onChange={handleRadioChange}
|
||||
onCheckedChange={handleCheckedChange}
|
||||
size="large"
|
||||
disabled={false}
|
||||
labelPosition="right"
|
||||
/>
|
||||
);
|
||||
};
|
||||
},{
|
||||
```
|
||||
</Tab>
|
||||
return (
|
||||
<Radio
|
||||
checked={true}
|
||||
value="Option 1"
|
||||
onChange={handleRadioChange}
|
||||
onCheckedChange={handleCheckedChange}
|
||||
size="large"
|
||||
disabled={false}
|
||||
labelPosition="right"
|
||||
/>
|
||||
);
|
||||
};
|
||||
},{
|
||||
```
|
||||
|
||||
<Tab title="المحددات">
|
||||
| المحددات | النوع | الوصف |
|
||||
| ------------------------ | ----------------- | ------------------------------------------------------------------ |
|
||||
| النمط | خصائص `React.CSS` | أنماط إضافية مضمنة للمكون |
|
||||
| اسم الفئة | نص | فئة CSS اختيارية لتصميم إضافي |
|
||||
| مختار | قيمة منطقية | يشير إلى ما إذا كان زر الراديو محددًا |
|
||||
| القيمة | نص | التسمية أو النص المرتبط بزر الراديو |
|
||||
| عند التغيير | دالة | الدالة التي تُستدعى عند تغيير زر الاختيار المحدد. |
|
||||
| عند تغيير الحالة المحددة | دالة | الدالة التي تُستدعى عند تغيير حالة `checked` لزر الاختيار. |
|
||||
| الحجم | نص | حجم زر الراديو. تشمل الخيارات: `large` و `small` |
|
||||
| معطل | قيمة منطقية | إذا كانت `true`، فسيكون زر الاختيار معطلاً وغير قابل للنقر |
|
||||
| موضع التسمية | نص | موضع نص التسمية بالنسبة لزر الراديو. يوجد خياران: `left` و `right` |
|
||||
</Tab>
|
||||
|
||||
</Tab>
|
||||
<Tab title="المحددات">
|
||||
|
||||
|
||||
| المحددات | النوع | الوصف |
|
||||
| ------------------------ | ----------------- | ------------------------------------------------------------------ |
|
||||
| النمط | خصائص `React.CSS` | أنماط إضافية مضمنة للمكون |
|
||||
| اسم الفئة | نص | فئة CSS اختيارية لتصميم إضافي |
|
||||
| مختار | قيمة منطقية | يشير إلى ما إذا كان زر الراديو محددًا |
|
||||
| القيمة | نص | التسمية أو النص المرتبط بزر الراديو |
|
||||
| عند التغيير | دالة | الدالة التي تُستدعى عند تغيير زر الاختيار المحدد. |
|
||||
| عند تغيير الحالة المحددة | دالة | الدالة التي تُستدعى عند تغيير حالة `checked` لزر الاختيار. |
|
||||
| الحجم | نص | حجم زر الراديو. تشمل الخيارات: `large` و `small` |
|
||||
| معطل | قيمة منطقية | إذا كانت `true`، فسيكون زر الاختيار معطلاً وغير قابل للنقر |
|
||||
| موضع التسمية | نص | موضع نص التسمية بالنسبة لزر الراديو. يوجد خياران: `left` و `right` |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## مجموعة الراديو
|
||||
@@ -61,37 +69,45 @@ image: /images/user-guide/create-workspace/workspace-cover.png
|
||||
يجمع أزرار الراديو ذات الصلة معًا.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import React, { useState } from "react";
|
||||
import { Radio, RadioGroup } from "twenty-ui/display";
|
||||
<Tab title="استخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
```jsx
|
||||
import React, { useState } from "react";
|
||||
import { Radio, RadioGroup } from "twenty-ui/display";
|
||||
|
||||
const [selectedValue, setSelectedValue] = useState("Option 1");
|
||||
export const MyComponent = () => {
|
||||
|
||||
const handleChange = (event) => {
|
||||
setSelectedValue(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<RadioGroup value={selectedValue} onChange={handleChange}>
|
||||
<Radio value="Option 1" />
|
||||
<Radio value="Option 2" />
|
||||
<Radio value="Option 3" />
|
||||
</RadioGroup>
|
||||
);
|
||||
};
|
||||
const [selectedValue, setSelectedValue] = useState("Option 1");
|
||||
|
||||
```
|
||||
</Tab>
|
||||
const handleChange = (event) => {
|
||||
setSelectedValue(event.target.value);
|
||||
};
|
||||
|
||||
return (
|
||||
<RadioGroup value={selectedValue} onChange={handleChange}>
|
||||
<Radio value="Option 1" />
|
||||
<Radio value="Option 2" />
|
||||
<Radio value="Option 3" />
|
||||
</RadioGroup>
|
||||
);
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="الخصائص">
|
||||
|
||||
|
||||
| الخصائص | النوع | الوصف |
|
||||
| ------------- | ----------------- | --------------------------------------------------------------------- |
|
||||
| القيمة | string | قيمة زر الراديو المحدد حاليًا |
|
||||
| عند التغيير | دالة | دالة الاستدعاء التي يتم تشغيلها عند تغيير زر الاختيار. |
|
||||
| onValueChange | دالة | دالة الاستدعاء التي يتم تشغيلها عند تغيير القيمة المحددة في المجموعة. |
|
||||
| الأبناء | `React.ReactNode` | يتيح لك تمرير مكوّنات React (مثل Radio) كعناصر فرعية إلى Radio Group |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="الخصائص">
|
||||
| الخصائص | النوع | الوصف |
|
||||
| ------------- | ----------------- | --------------------------------------------------------------------- |
|
||||
| القيمة | string | قيمة زر الراديو المحدد حاليًا |
|
||||
| عند التغيير | دالة | دالة الاستدعاء التي يتم تشغيلها عند تغيير زر الاختيار. |
|
||||
| onValueChange | دالة | دالة الاستدعاء التي يتم تشغيلها عند تغيير القيمة المحددة في المجموعة. |
|
||||
| الأبناء | `React.ReactNode` | يتيح لك تمرير مكوّنات React (مثل Radio) كعناصر فرعية إلى Radio Group |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -10,42 +10,49 @@ image: /images/user-guide/what-is-twenty/20.png
|
||||
يتيح للمستخدمين اختيار قيمة من قائمة من الخيارات المحددة مسبقًا.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { IconTwentyStar } from 'twenty-ui/display';
|
||||
<Tab title="استخدام">
|
||||
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
```jsx
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { IconTwentyStar } from 'twenty-ui/display';
|
||||
|
||||
export const MyComponent = () => {
|
||||
import { Select } from '@/ui/input/components/Select';
|
||||
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<Select
|
||||
className
|
||||
disabled={false}
|
||||
label="Select an option"
|
||||
options={[
|
||||
{ value: 'option1', label: 'Option A', Icon: IconTwentyStar },
|
||||
{ value: 'option2', label: 'Option B', Icon: IconTwentyStar },
|
||||
]}
|
||||
value="option1"
|
||||
/>
|
||||
</RecoilRoot>
|
||||
);
|
||||
};
|
||||
export const MyComponent = () => {
|
||||
|
||||
```
|
||||
</Tab>
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<Select
|
||||
className
|
||||
disabled={false}
|
||||
label="Select an option"
|
||||
options={[
|
||||
{ value: 'option1', label: 'Option A', Icon: IconTwentyStar },
|
||||
{ value: 'option2', label: 'Option B', Icon: IconTwentyStar },
|
||||
]}
|
||||
value="option1"
|
||||
/>
|
||||
</RecoilRoot>
|
||||
);
|
||||
};
|
||||
|
||||
<Tab title="المحددات">
|
||||
| المحددات | النوع | الوصف |
|
||||
| ----------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| اسم الفئة | نص | فئة CSS اختيارية للتنسيق الإضافي |
|
||||
| معطل | قيمة منطقية | عند ضبطها على `true`، يتم تعطيل تفاعل المستخدم مع المكون |
|
||||
| التسمية | نص | التسمية التي تصف غرض مكوّن `Select` |
|
||||
| عند التغيير | دالة | الدالة التي تُستدعى عند تغيير القيم المحددة |
|
||||
| خيارات | مصفوفة | تمثل الخيارات المتاحة في مكون `الاختيار`. إنها مصفوفة من الكائنات حيث يحتوي كل كائن على `قيمة` (معرف فريد)، `تسمية` (معرف فريد)، و`أيقونة` اختيارية |
|
||||
| القيمة | نص | تمثل القيمة المحددة حاليًا. يجب أن تطابق إحدى خصائص `القيمة` في مصفوفة `الخيارات` |
|
||||
</Tab>
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="المحددات">
|
||||
|
||||
|
||||
| المحددات | النوع | الوصف |
|
||||
| ----------- | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| اسم الفئة | نص | فئة CSS اختيارية للتنسيق الإضافي |
|
||||
| معطل | قيمة منطقية | عند ضبطها على `true`، يتم تعطيل تفاعل المستخدم مع المكون |
|
||||
| التسمية | نص | التسمية التي تصف غرض مكوّن `Select` |
|
||||
| عند التغيير | دالة | الدالة التي تُستدعى عند تغيير القيم المحددة |
|
||||
| خيارات | مصفوفة | تمثل الخيارات المتاحة في مكون `الاختيار`. إنها مصفوفة من الكائنات حيث يحتوي كل كائن على `قيمة` (معرف فريد)، `تسمية` (معرف فريد)، و`أيقونة` اختيارية |
|
||||
| القيمة | نص | تمثل القيمة المحددة حاليًا. يجب أن تطابق إحدى خصائص `القيمة` في مصفوفة `الخيارات` |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -12,53 +12,64 @@ image: /images/user-guide/notes/notes_header.png
|
||||
يسمح للمستخدمين بإدخال وتحرير النص.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="27332A2E2F2745">
|
||||
```jsx
|
||||
import { RecoilRoot } from "recoil";
|
||||
import { TextInput } from "@/ui/input/components/TextInput";
|
||||
|
||||
export const MyComponent = () => {
|
||||
const handleChange = (text) => {
|
||||
console.log("تم تغيير الإدخال:", text);
|
||||
};
|
||||
<Tab title="27332A2E2F2745">
|
||||
|
||||
const handleKeyDown = (event) => {
|
||||
console.log("تم ضغط المفتاح:", event.key);
|
||||
};
|
||||
```jsx
|
||||
import { RecoilRoot } from "recoil";
|
||||
import { TextInput } from "@/ui/input/components/TextInput";
|
||||
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<TextInput
|
||||
className
|
||||
label="اسم المستخدم"
|
||||
onChange={handleChange}
|
||||
fullWidth={false}
|
||||
disableHotkeys={false}
|
||||
error="اسم مستخدم غير صالح"
|
||||
onKeyDown={handleKeyDown}
|
||||
RightIcon={null}
|
||||
/>
|
||||
</RecoilRoot>
|
||||
);
|
||||
};
|
||||
},{
|
||||
```
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
const handleChange = (text) => {
|
||||
console.log("تم تغيير الإدخال:", text);
|
||||
};
|
||||
|
||||
<Tab title="خصائص">
|
||||
| خصائص | النوع | الوصف |
|
||||
| ---------------- | ------------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| className | string | اسم اختياري للتنسيق الإضافي. |
|
||||
| التسمية | نص | يمثل التسمية للإدخال. |
|
||||
| onChange | وظيفة | الدالة التي تُستدعى عند تغيير قيمة الإدخال. |
|
||||
| عرض كامل | قيمة منطقية | يشير إلى ما إذا كان الإدخال يجب أن يشغل 100% من العرض. |
|
||||
| تعطيل الإختصارات | قيمة منطقية | يشير إلى ما إذا كانت الاختصارات ممكنة للإدخال. |
|
||||
| خطأ | string | يمثل رسالة الخطأ التي سيتم عرضها. عند توفرها، تضيف رمز خطأ على الجانب الأيمن من الإدخال. |
|
||||
| onKeyDown | دالة | يتم الاستدعاء عندما يتم الضغط على مفتاح عند التركيز على حقل الإدخال. يتلقى `React.KeyboardEvent` كمعلمة |
|
||||
| أيقونة يمين | مكون الأيقونة | مكون أيقونة اختياري معروض على الجانب الأيمن من الإدخال. |
|
||||
const handleKeyDown = (event) => {
|
||||
console.log("تم ضغط المفتاح:", event.key);
|
||||
};
|
||||
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<TextInput
|
||||
className
|
||||
label="اسم المستخدم"
|
||||
onChange={handleChange}
|
||||
fullWidth={false}
|
||||
disableHotkeys={false}
|
||||
error="اسم مستخدم غير صالح"
|
||||
onKeyDown={handleKeyDown}
|
||||
RightIcon={null}
|
||||
/>
|
||||
</RecoilRoot>
|
||||
);
|
||||
};
|
||||
},{
|
||||
```
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="خصائص">
|
||||
|
||||
|
||||
| خصائص | النوع | الوصف |
|
||||
| ---------------- | ------------- | ------------------------------------------------------------------------------------------------------- |
|
||||
| className | string | اسم اختياري للتنسيق الإضافي. |
|
||||
| التسمية | نص | يمثل التسمية للإدخال. |
|
||||
| onChange | وظيفة | الدالة التي تُستدعى عند تغيير قيمة الإدخال. |
|
||||
| عرض كامل | قيمة منطقية | يشير إلى ما إذا كان الإدخال يجب أن يشغل 100% من العرض. |
|
||||
| تعطيل الإختصارات | قيمة منطقية | يشير إلى ما إذا كانت الاختصارات ممكنة للإدخال. |
|
||||
| خطأ | string | يمثل رسالة الخطأ التي سيتم عرضها. عند توفرها، تضيف رمز خطأ على الجانب الأيمن من الإدخال. |
|
||||
| onKeyDown | دالة | يتم الاستدعاء عندما يتم الضغط على مفتاح عند التركيز على حقل الإدخال. يتلقى `React.KeyboardEvent` كمعلمة |
|
||||
| أيقونة يمين | مكون الأيقونة | مكون أيقونة اختياري معروض على الجانب الأيمن من الإدخال. |
|
||||
|
||||
|
||||
|
||||
|
||||
يقبل المكون أيضًا دعم خصائص HTML أخرى لعناصر الإدخال.
|
||||
|
||||
</Tab>
|
||||
|
||||
يقبل المكون أيضًا دعم خصائص HTML أخرى لعناصر الإدخال.
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## إدخال نص بالحجم التلقائي
|
||||
@@ -66,40 +77,51 @@ image: /images/user-guide/notes/notes_header.png
|
||||
مكون إدخال نصي يعدل ارتفاعه تلقائيًا بناءً على المحتوى.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { RecoilRoot } from "recoil";
|
||||
import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<AutosizeTextInput
|
||||
onValidate={() => console.log("تم تشغيل الدالة onValidate")}
|
||||
minRows={1}
|
||||
placeholder="اكتب تعليقًا"
|
||||
onFocus={() => console.log("تم تشغيل الدالة onFocus")}
|
||||
variant="icon"
|
||||
buttonTitle
|
||||
value="المهمة: "
|
||||
/>
|
||||
</RecoilRoot>
|
||||
);
|
||||
};},{
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
```jsx
|
||||
import { RecoilRoot } from "recoil";
|
||||
import { AutosizeTextInput } from "@/ui/input/components/AutosizeTextInput";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<RecoilRoot>
|
||||
<AutosizeTextInput
|
||||
onValidate={() => console.log("تم تشغيل الدالة onValidate")}
|
||||
minRows={1}
|
||||
placeholder="اكتب تعليقًا"
|
||||
onFocus={() => console.log("تم تشغيل الدالة onFocus")}
|
||||
variant="icon"
|
||||
buttonTitle
|
||||
value="المهمة: "
|
||||
/>
|
||||
</RecoilRoot>
|
||||
);
|
||||
};},{
|
||||
```
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="خصائص">
|
||||
|
||||
|
||||
| خصائص | النوع | الوصف |
|
||||
| ------------------ | ----- | ------------------------------------------------------------- |
|
||||
| onValidate | دالة | الدالة التي ترغب في تفعيلها عند تصديق المستخدم الإدخال. |
|
||||
| الحد الأدنى للأسطر | رقم | عدد الأسطر الأدنى للمساحة النصية. |
|
||||
| النص التوضيحي | نص | النص التوضيحي الذي ترغب في عرضه عند كون المساحة النصية فارغة. |
|
||||
| onFocus | دالة | الدالة التي ترغب في تفعيلها عند تركيز المساحة النصية. |
|
||||
| البديل | نص | البديل للإدخال. تشمل الخيارات: `افتراضي`، `أيقونة`، و`زر`. |
|
||||
| عنوان الزر | نص | العنوان للزر (فقط للبديل الزر). |
|
||||
| القيمة | نص | القيمة الأولية للمساحة النصية. |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="خصائص">
|
||||
| خصائص | النوع | الوصف |
|
||||
| ------------------ | ----- | ------------------------------------------------------------- |
|
||||
| onValidate | دالة | الدالة التي ترغب في تفعيلها عند تصديق المستخدم الإدخال. |
|
||||
| الحد الأدنى للأسطر | رقم | عدد الأسطر الأدنى للمساحة النصية. |
|
||||
| النص التوضيحي | نص | النص التوضيحي الذي ترغب في عرضه عند كون المساحة النصية فارغة. |
|
||||
| onFocus | دالة | الدالة التي ترغب في تفعيلها عند تركيز المساحة النصية. |
|
||||
| البديل | نص | البديل للإدخال. تشمل الخيارات: `افتراضي`، `أيقونة`، و`زر`. |
|
||||
| عنوان الزر | نص | العنوان للزر (فقط للبديل الزر). |
|
||||
| القيمة | نص | القيمة الأولية للمساحة النصية. |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## مساحة نصية
|
||||
@@ -107,31 +129,40 @@ image: /images/user-guide/notes/notes_header.png
|
||||
تتيح لك إنشاء إدخالات نصية متعددة الأسطر.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { TextArea } from "@/ui/input/components/TextArea";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<TextArea
|
||||
disabled={false}
|
||||
minRows={4}
|
||||
onChange={()=>console.log('تم تشغيل الدالة onChange')}
|
||||
placeholder="أدخل النص هنا"
|
||||
value=""
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
```jsx
|
||||
import { TextArea } from "@/ui/input/components/TextArea";
|
||||
|
||||
<Tab title="خصائص">
|
||||
| خصائص | النوع | الوصف |
|
||||
| ------------------ | ----------- | ------------------------------------------------ |
|
||||
| تعطيل | قيمة منطقية | يشير إلى ما إذا كانت المساحة النصية معطلة. |
|
||||
| الحد الأدنى للأسطر | رقم | العدد الأدنى للأسطر الظاهرة للمساحة النصية. |
|
||||
| onChange | وظيفة | دالة الاستدعاء تُشغّل عند تغيّر محتوى منطقة النص |
|
||||
| نص توضيحي | نص | النص المُوضّح عندما تكون منطقة النص فارغة |
|
||||
| القيمة | نص | القيمة الحالية لمنطقة النص |
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<TextArea
|
||||
disabled={false}
|
||||
minRows={4}
|
||||
onChange={()=>console.log('تم تشغيل الدالة onChange')}
|
||||
placeholder="أدخل النص هنا"
|
||||
value=""
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="خصائص">
|
||||
|
||||
|
||||
| خصائص | النوع | الوصف |
|
||||
| ------------------ | ----------- | ------------------------------------------------ |
|
||||
| تعطيل | قيمة منطقية | يشير إلى ما إذا كانت المساحة النصية معطلة. |
|
||||
| الحد الأدنى للأسطر | رقم | العدد الأدنى للأسطر الظاهرة للمساحة النصية. |
|
||||
| onChange | وظيفة | دالة الاستدعاء تُشغّل عند تغيّر محتوى منطقة النص |
|
||||
| نص توضيحي | نص | النص المُوضّح عندما تكون منطقة النص فارغة |
|
||||
| القيمة | نص | القيمة الحالية لمنطقة النص |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -8,29 +8,35 @@ image: /images/user-guide/table-views/table.png
|
||||
</Frame>
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { Toggle } from "twenty-ui/input";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<Toggle
|
||||
value = {true}
|
||||
onChange = {()=>console.log('تم تشغيل حدث onChange')}
|
||||
color="green"
|
||||
toggleSize = "medium"
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
```jsx
|
||||
import { Toggle } from "twenty-ui/input";
|
||||
|
||||
<Tab title="الخصائص">
|
||||
| الخصائص | النوع | الوصف | الإعداد الافتراضي |
|
||||
| ----------- | ----------- | --------------------------------------------------------------------------- | ----------------- |
|
||||
| القيمة | قيمة منطقية | الحالة الحالية لمفتاح التبديل | `خاطئ` |
|
||||
| عند التغيير | دالة | دالة الاستدعاء التي يتم تحفيزها عند تغيير حالة مفتاح التبديل | |
|
||||
| اللون | string | لون التبديل عند كونه | لون أزرق |
|
||||
| حجم التبديل | نص | حجم التبديل الذي يؤثر على كل من الطول والوزن. لديها خياران: `صغير` و`متوسط` | متوسط |
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<Toggle
|
||||
value = {true}
|
||||
onChange = {()=>console.log('تم تشغيل حدث onChange')}
|
||||
color="green"
|
||||
toggleSize = "medium"
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="الخصائص">
|
||||
|
||||
|
||||
| الخصائص | النوع | الوصف | الإعداد الافتراضي |
|
||||
| ----------- | ----------- | --------------------------------------------------------------------------- | ----------------- |
|
||||
| القيمة | قيمة منطقية | الحالة الحالية لمفتاح التبديل | `خاطئ` |
|
||||
| عند التغيير | دالة | دالة الاستدعاء التي يتم تحفيزها عند تغيير حالة مفتاح التبديل | |
|
||||
| اللون | string | لون التبديل عند كونه | لون أزرق |
|
||||
| حجم التبديل | نص | حجم التبديل الذي يؤثر على كل من الطول والوزن. لديها خياران: `صغير` و`متوسط` | متوسط |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -10,32 +10,38 @@ image: /images/user-guide/fields/field.png
|
||||
يعرض شريط مسار التنقّل.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { Breadcrumb } from "@/ui/navigation/bread-crumb/components/Breadcrumb";
|
||||
<Tab title="استخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
const breadcrumbLinks = [
|
||||
{ children: "Home", href: "/" },
|
||||
{ children: "Category", href: "/category" },
|
||||
{ children: "Subcategory", href: "/category/subcategory" },
|
||||
{ children: "Current Page" },
|
||||
];
|
||||
```jsx
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { Breadcrumb } from "@/ui/navigation/bread-crumb/components/Breadcrumb";
|
||||
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Breadcrumb className links={breadcrumbLinks} />
|
||||
</BrowserRouter>
|
||||
)
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
const breadcrumbLinks = [
|
||||
{ children: "Home", href: "/" },
|
||||
{ children: "Category", href: "/category" },
|
||||
{ children: "Subcategory", href: "/category/subcategory" },
|
||||
{ children: "Current Page" },
|
||||
];
|
||||
|
||||
<Tab title="المحددات">
|
||||
| المحددات | النوع | الوصف |
|
||||
| --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| اسم الفئة | نص | اسم فئة اختياري لتنسيقات إضافية |
|
||||
| روابط | مصفوفة | مصفوفة من الكائنات، يمثّل كلٌّ منها رابطًا في مسار التنقّل. كل كائن يحتوي على خاصية `children` (محتوى النص للرابط) وخاصية `href` اختيارية (رابط URL للتنقل إليه عند النقر على الرابط) |
|
||||
</Tab>
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<Breadcrumb className links={breadcrumbLinks} />
|
||||
</BrowserRouter>
|
||||
)
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="المحددات">
|
||||
|
||||
|
||||
| المحددات | النوع | الوصف |
|
||||
| --------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| اسم الفئة | نص | اسم فئة اختياري لتنسيقات إضافية |
|
||||
| روابط | مصفوفة | مصفوفة من الكائنات، يمثّل كلٌّ منها رابطًا في مسار التنقّل. كل كائن يحتوي على خاصية `children` (محتوى النص للرابط) وخاصية `href` اختيارية (رابط URL للتنقل إليه عند النقر على الرابط) |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -12,40 +12,49 @@ image: /images/user-guide/what-is-twenty/20.png
|
||||
مكون رابط منمق لعرض معلومات الاتصال.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="27332A2E2F2745">
|
||||
```jsx
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
<Tab title="27332A2E2F2745">
|
||||
|
||||
import { ContactLink } from 'twenty-ui/navigation';
|
||||
```jsx
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
export const MyComponent = () => {
|
||||
const handleLinkClick = (event) => {
|
||||
console.log('تم النقر على رابط الاتصال!', event);
|
||||
};
|
||||
import { ContactLink } from 'twenty-ui/navigation';
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<ContactLink
|
||||
className
|
||||
href="mailto:example@example.com"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
example@example.com
|
||||
</ContactLink>
|
||||
</Router>
|
||||
);
|
||||
};},{
|
||||
```
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
const handleLinkClick = (event) => {
|
||||
console.log('تم النقر على رابط الاتصال!', event);
|
||||
};
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<ContactLink
|
||||
className
|
||||
href="mailto:example@example.com"
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
example@example.com
|
||||
</ContactLink>
|
||||
</Router>
|
||||
);
|
||||
};},{
|
||||
```
|
||||
|
||||
|
||||
</Tab>
|
||||
<Tab title="خصائص">
|
||||
|
||||
|
||||
| خصائص | النوع | الوصف |
|
||||
| --------- | ----------------- | ------------------------------------------------ |
|
||||
| className | string | اسم اختياري للتنسيق الإضافي. |
|
||||
| رابط | نص | عنوان URL المستهدف أو المسار للرابط |
|
||||
| عند النقر | وظيفة | دالة رد النداء ليتم تفعيلها عند النقر على الرابط |
|
||||
| الأبناء | `React.ReactNode` | المحتوى ليتم عرضه داخل الرابط |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="خصائص">
|
||||
| خصائص | النوع | الوصف |
|
||||
| --------- | ----------------- | ------------------------------------------------ |
|
||||
| className | string | اسم اختياري للتنسيق الإضافي. |
|
||||
| رابط | نص | عنوان URL المستهدف أو المسار للرابط |
|
||||
| عند النقر | وظيفة | دالة رد النداء ليتم تفعيلها عند النقر على الرابط |
|
||||
| الأبناء | `React.ReactNode` | المحتوى ليتم عرضه داخل الرابط |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## رابط خام
|
||||
@@ -53,36 +62,44 @@ image: /images/user-guide/what-is-twenty/20.png
|
||||
مكون رابط منمق لعرض الروابط.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { RawLink } from "/navigation";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
const handleLinkClick = (event) => {
|
||||
console.log("Contact link clicked!", event);
|
||||
};
|
||||
```jsx
|
||||
import { RawLink } from "/navigation";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<RawLink className href="/contact" onClick={handleLinkClick}>
|
||||
Contact Us
|
||||
</RawLink>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
export const MyComponent = () => {
|
||||
const handleLinkClick = (event) => {
|
||||
console.log("Contact link clicked!", event);
|
||||
};
|
||||
|
||||
```
|
||||
</Tab>
|
||||
return (
|
||||
<Router>
|
||||
<RawLink className href="/contact" onClick={handleLinkClick}>
|
||||
Contact Us
|
||||
</RawLink>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
|
||||
<Tab title="خصائص">
|
||||
| خصائص | النوع | الوصف |
|
||||
| --------- | ----------------- | ------------------------------------------------ |
|
||||
| اسم الصنف | string | اسم اختياري لتنسيقات إضافية |
|
||||
| رابط | string | عنوان URL المستهدف أو المسار للرابط |
|
||||
| عند النقر | وظيفة | دالة رد النداء ليتم تفعيلها عند النقر على الرابط |
|
||||
| الأبناء | `React.ReactNode` | المحتوى ليتم عرضه داخل الرابط |
|
||||
</Tab>
|
||||
```
|
||||
|
||||
|
||||
</Tab>
|
||||
<Tab title="خصائص">
|
||||
|
||||
|
||||
| خصائص | النوع | الوصف |
|
||||
| --------- | ----------------- | ------------------------------------------------ |
|
||||
| اسم الصنف | string | اسم اختياري لتنسيقات إضافية |
|
||||
| رابط | string | عنوان URL المستهدف أو المسار للرابط |
|
||||
| عند النقر | وظيفة | دالة رد النداء ليتم تفعيلها عند النقر على الرابط |
|
||||
| الأبناء | `React.ReactNode` | المحتوى ليتم عرضه داخل الرابط |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## رابط مستدير
|
||||
@@ -90,34 +107,41 @@ image: /images/user-guide/what-is-twenty/20.png
|
||||
رابط مستدير مثبت مع مكون Chip للروابط.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { RoundedLink } from "/navigation";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
const handleLinkClick = (event) => {
|
||||
console.log("Contact link clicked!", event);
|
||||
};
|
||||
```jsx
|
||||
import { RoundedLink } from "/navigation";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
|
||||
return (
|
||||
<Router>
|
||||
<RoundedLink href="/contact" onClick={handleLinkClick}>
|
||||
Contact Us
|
||||
</RoundedLink>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
const handleLinkClick = (event) => {
|
||||
console.log("Contact link clicked!", event);
|
||||
};
|
||||
|
||||
<Tab title="خصائص">
|
||||
| خصائص | النوع | الوصف |
|
||||
| --------- | ----------------- | ------------------------------------------------ |
|
||||
| رابط | string | عنوان URL المستهدف أو المسار للرابط |
|
||||
| الأبناء | `React.ReactNode` | المحتوى ليتم عرضه داخل الرابط |
|
||||
| عند النقر | وظيفة | دالة رد النداء ليتم تفعيلها عند النقر على الرابط |
|
||||
</Tab>
|
||||
return (
|
||||
<Router>
|
||||
<RoundedLink href="/contact" onClick={handleLinkClick}>
|
||||
Contact Us
|
||||
</RoundedLink>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="خصائص">
|
||||
|
||||
|
||||
| خصائص | النوع | الوصف |
|
||||
| --------- | ----------------- | ------------------------------------------------ |
|
||||
| رابط | string | عنوان URL المستهدف أو المسار للرابط |
|
||||
| الأبناء | `React.ReactNode` | المحتوى ليتم عرضه داخل الرابط |
|
||||
| عند النقر | وظيفة | دالة رد النداء ليتم تفعيلها عند النقر على الرابط |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## رابط التواصل الاجتماعي
|
||||
@@ -125,30 +149,37 @@ image: /images/user-guide/what-is-twenty/20.png
|
||||
روابط اجتماعية منمقة، مع دعم لأنواع متعددة من الروابط الاجتماعية، مثل العناوين الإلكترونية، LinkedIn، وX (أو Twitter).
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { SocialLink } from "twenty-ui/navigation";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
<Tab title="استخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<Router>
|
||||
<SocialLink
|
||||
type="twitter"
|
||||
href="https://twitter.com/twentycrm"
|
||||
></SocialLink>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
```jsx
|
||||
import { SocialLink } from "twenty-ui/navigation";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
|
||||
<Tab title="خصائص">
|
||||
| خصائص | النوع | الوصف |
|
||||
| --------- | ----------------- | -------------------------------------------------------------------- |
|
||||
| رابط | string | عنوان URL المستهدف أو المسار للرابط |
|
||||
| الأبناء | `React.ReactNode` | المحتوى ليتم عرضه داخل الرابط |
|
||||
| النوع | string | نوع الروابط الاجتماعية. تشمل الخيارات: `url`, `LinkedIn`, و`Twitter` |
|
||||
| عند النقر | وظيفة | دالة رد النداء ليتم تفعيلها عند النقر على الرابط |
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<Router>
|
||||
<SocialLink
|
||||
type="twitter"
|
||||
href="https://twitter.com/twentycrm"
|
||||
></SocialLink>
|
||||
</Router>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="خصائص">
|
||||
|
||||
|
||||
| خصائص | النوع | الوصف |
|
||||
| --------- | ----------------- | -------------------------------------------------------------------- |
|
||||
| رابط | string | عنوان URL المستهدف أو المسار للرابط |
|
||||
| الأبناء | `React.ReactNode` | المحتوى ليتم عرضه داخل الرابط |
|
||||
| النوع | string | نوع الروابط الاجتماعية. تشمل الخيارات: `url`, `LinkedIn`, و`Twitter` |
|
||||
| عند النقر | وظيفة | دالة رد النداء ليتم تفعيلها عند النقر على الرابط |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -10,49 +10,57 @@ image: /images/user-guide/kanban-views/kanban.png
|
||||
عنصر قائمة متعدد الاستخدامات مصمم للاستخدام في قائمة أو قائمة تنقل.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { IconBell } from "@tabler/icons-react";
|
||||
import { IconAlertCircle } from "@tabler/icons-react";
|
||||
import { MenuItem } from "twenty-ui/display";
|
||||
<Tab title="استخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
const handleMenuItemClick = (event) => {
|
||||
console.log("Menu item clicked!", event);
|
||||
};
|
||||
```jsx
|
||||
import { IconBell } from "@tabler/icons-react";
|
||||
import { IconAlertCircle } from "@tabler/icons-react";
|
||||
import { MenuItem } from "twenty-ui/display";
|
||||
|
||||
const handleButtonClick = (event) => {
|
||||
console.log("Icon button clicked!", event);
|
||||
};
|
||||
export const MyComponent = () => {
|
||||
const handleMenuItemClick = (event) => {
|
||||
console.log("Menu item clicked!", event);
|
||||
};
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
LeftIcon={IconBell}
|
||||
accent="default"
|
||||
text="Menu item text"
|
||||
iconButtons={[{ Icon: IconAlertCircle, onClick: handleButtonClick }]}
|
||||
isTooltipOpen={true}
|
||||
testId="menu-item-1"
|
||||
onClick={handleMenuItemClick}
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
const handleButtonClick = (event) => {
|
||||
console.log("Icon button clicked!", event);
|
||||
};
|
||||
|
||||
<Tab title="المحددات">
|
||||
| المحددات | النوع | الوصف |
|
||||
| -------------- | ------------- | ---------------------------------------------------------------------------- |
|
||||
| أيقونة اليسار | مكون الأيقونة | أيقونة اختيارية تظهر قبل النص في عنصر القائمة |
|
||||
| accent | نص | يحدد لون الإبراز لعنصر القائمة. تشمل الخيارات: `افتراضي`, `خطر`, `موضع مؤقت` |
|
||||
| نص | نص | المحتوى النصي لعنصر القائمة |
|
||||
| أزرار الأيقونة | array | مجموعة من الكائنات التي تمثل أيقونات إضافية مرتبطة بعنصر القائمة |
|
||||
| isTooltipOpen | قيمة منطقية | يتحكم في ظهور تلميح الأدوات المرتبط بعنصر القائمة |
|
||||
| معرف الفحص | نص | السمة data-testid لأغراض الاختبار |
|
||||
| عند النقر | function | دالة الاستدعاء يتم تنشيطها عند النقر فوق عنصر القائمة |
|
||||
| اسم الفئة | نص | اسم اختياري لتصميم إضافي |
|
||||
</Tab>
|
||||
return (
|
||||
<MenuItem
|
||||
LeftIcon={IconBell}
|
||||
accent="default"
|
||||
text="Menu item text"
|
||||
iconButtons={[{ Icon: IconAlertCircle, onClick: handleButtonClick }]}
|
||||
isTooltipOpen={true}
|
||||
testId="menu-item-1"
|
||||
onClick={handleMenuItemClick}
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
</Tab>
|
||||
<Tab title="المحددات">
|
||||
|
||||
|
||||
| المحددات | النوع | الوصف |
|
||||
| -------------- | ------------- | ---------------------------------------------------------------------------- |
|
||||
| أيقونة اليسار | مكون الأيقونة | أيقونة اختيارية تظهر قبل النص في عنصر القائمة |
|
||||
| accent | نص | يحدد لون الإبراز لعنصر القائمة. تشمل الخيارات: `افتراضي`, `خطر`, `موضع مؤقت` |
|
||||
| نص | نص | المحتوى النصي لعنصر القائمة |
|
||||
| أزرار الأيقونة | array | مجموعة من الكائنات التي تمثل أيقونات إضافية مرتبطة بعنصر القائمة |
|
||||
| isTooltipOpen | قيمة منطقية | يتحكم في ظهور تلميح الأدوات المرتبط بعنصر القائمة |
|
||||
| معرف الفحص | نص | السمة data-testid لأغراض الاختبار |
|
||||
| عند النقر | function | دالة الاستدعاء يتم تنشيطها عند النقر فوق عنصر القائمة |
|
||||
| اسم الفئة | نص | اسم اختياري لتصميم إضافي |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## الأشكال
|
||||
@@ -64,42 +72,49 @@ image: /images/user-guide/kanban-views/kanban.png
|
||||
عنصر قائمة على نمط الأوامر داخل القائمة للإشارة إلى اختصارات لوحة المفاتيح.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { IconBell } from "@tabler/icons-react";
|
||||
import { MenuItemCommand } from "twenty-ui/display";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
const handleCommandClick = () => {
|
||||
console.log("Command clicked!");
|
||||
};
|
||||
```jsx
|
||||
import { IconBell } from "@tabler/icons-react";
|
||||
import { MenuItemCommand } from "twenty-ui/display";
|
||||
|
||||
return (
|
||||
<MenuItemCommand
|
||||
LeftIcon={IconBell}
|
||||
text="First Option"
|
||||
firstHotKey="⌘"
|
||||
secondHotKey="1"
|
||||
isSelected={true}
|
||||
onClick={handleCommandClick}
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
const handleCommandClick = () => {
|
||||
console.log("Command clicked!");
|
||||
};
|
||||
|
||||
<Tab title="الخصائص">
|
||||
| الخصائص | النوع | الوصف |
|
||||
| --------------- | ------------- | ----------------------------------------------------- |
|
||||
| الأيقونة اليسرى | مكون الأيقونة | أيقونة اختيارية إذا ظهرت قبل النص في عنصر القائمة |
|
||||
| نص | نص | محتوى النص لعنصر القائمة |
|
||||
| firstHotKey | string | أول اختصار لوحة مفاتيح مرتبط بالأمر |
|
||||
| secondHotKey | string | اختصار لوحة المفاتيح الثاني المرتبط بالأمر |
|
||||
| isSelected | قيمة منطقية | يشير إلى ما إذا كان عنصر القائمة محددا أو مميزا |
|
||||
| عند النقر | دالة | دالة الاستدعاء يتم تنشيطها عند النقر فوق عنصر القائمة |
|
||||
| اسم الفئة | نص | اسم اختياري لإضافة التنسيق |
|
||||
</Tab>
|
||||
return (
|
||||
<MenuItemCommand
|
||||
LeftIcon={IconBell}
|
||||
text="First Option"
|
||||
firstHotKey="⌘"
|
||||
secondHotKey="1"
|
||||
isSelected={true}
|
||||
onClick={handleCommandClick}
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
|
||||
</Tab>
|
||||
<Tab title="الخصائص">
|
||||
|
||||
|
||||
| الخصائص | النوع | الوصف |
|
||||
| --------------- | ------------- | ----------------------------------------------------- |
|
||||
| الأيقونة اليسرى | مكون الأيقونة | أيقونة اختيارية إذا ظهرت قبل النص في عنصر القائمة |
|
||||
| نص | نص | محتوى النص لعنصر القائمة |
|
||||
| firstHotKey | string | أول اختصار لوحة مفاتيح مرتبط بالأمر |
|
||||
| secondHotKey | string | اختصار لوحة المفاتيح الثاني المرتبط بالأمر |
|
||||
| isSelected | قيمة منطقية | يشير إلى ما إذا كان عنصر القائمة محددا أو مميزا |
|
||||
| عند النقر | دالة | دالة الاستدعاء يتم تنشيطها عند النقر فوق عنصر القائمة |
|
||||
| اسم الفئة | نص | اسم اختياري لإضافة التنسيق |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### قابلة للسحب
|
||||
@@ -107,45 +122,52 @@ image: /images/user-guide/kanban-views/kanban.png
|
||||
مكون عنصر قائمة قابل للسحب مصمم ليتم استخدامه في قائمة أو قائمة حيث يمكن سحب العناصر، ويتم تنفيذ إجراءات إضافية عبر أزرار الأيقونات.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { IconBell } from "@tabler/icons-react";
|
||||
import { IconAlertCircle } from "@tabler/icons-react";
|
||||
import { MenuItemDraggable } from "twenty-ui/display";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
const handleMenuItemClick = (event) => {
|
||||
console.log("Menu item clicked!", event);
|
||||
};
|
||||
```jsx
|
||||
import { IconBell } from "@tabler/icons-react";
|
||||
import { IconAlertCircle } from "@tabler/icons-react";
|
||||
import { MenuItemDraggable } from "twenty-ui/display";
|
||||
|
||||
return (
|
||||
<MenuItemDraggable
|
||||
LeftIcon={IconBell}
|
||||
accent="default"
|
||||
iconButtons={[{ Icon: IconAlertCircle, onClick: handleButtonClick }]}
|
||||
isTooltipOpen={false}
|
||||
onClick={handleMenuItemClick}
|
||||
text="Menu item draggable"
|
||||
isDragDisabled={false}
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
const handleMenuItemClick = (event) => {
|
||||
console.log("Menu item clicked!", event);
|
||||
};
|
||||
|
||||
<Tab title="الخصائص">
|
||||
| الخصائص | النوع | الوصف |
|
||||
| --------------- | ------------- | ------------------------------------------------------------------------------------- |
|
||||
| الأيقونة اليسرى | مكون الأيقونة | أيقونة اختيارية تظهر قبل النص في عنصر القائمة |
|
||||
| accent | نص | لون العنصر لهجة القائمة. يمكن أن يكون إحدى القيم: `default`، `placeholder`، و`danger` |
|
||||
| أزرار الأيقونات | array | مصفوفة الكائنات التي تمثل أزرار الأيقونات الإضافية المرتبطة بعنصر القائمة |
|
||||
| isTooltipOpen | قيمة منطقية | يتحكم في ظهور تلميح الأدوات المرتبط بعنصر القائمة |
|
||||
| عند_النقر | دالة | وظيفة استدعاء ليتم تشغيلها عند النقر فوق الرابط |
|
||||
| نص | نص | محتوى النص لعنصر القائمة |
|
||||
| isDragDisabled | قيمة منطقية | يشير إلى ما إذا كان تم تعطيل السحب |
|
||||
| اسم الفئة | نص | اسم اختياري لإضافة التنسيق |
|
||||
</Tab>
|
||||
return (
|
||||
<MenuItemDraggable
|
||||
LeftIcon={IconBell}
|
||||
accent="default"
|
||||
iconButtons={[{ Icon: IconAlertCircle, onClick: handleButtonClick }]}
|
||||
isTooltipOpen={false}
|
||||
onClick={handleMenuItemClick}
|
||||
text="Menu item draggable"
|
||||
isDragDisabled={false}
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="الخصائص">
|
||||
|
||||
|
||||
| الخصائص | النوع | الوصف |
|
||||
| --------------- | ------------- | ------------------------------------------------------------------------------------- |
|
||||
| الأيقونة اليسرى | مكون الأيقونة | أيقونة اختيارية تظهر قبل النص في عنصر القائمة |
|
||||
| accent | نص | لون العنصر لهجة القائمة. يمكن أن يكون إحدى القيم: `default`، `placeholder`، و`danger` |
|
||||
| أزرار الأيقونات | array | مصفوفة الكائنات التي تمثل أزرار الأيقونات الإضافية المرتبطة بعنصر القائمة |
|
||||
| isTooltipOpen | قيمة منطقية | يتحكم في ظهور تلميح الأدوات المرتبط بعنصر القائمة |
|
||||
| عند_النقر | دالة | وظيفة استدعاء ليتم تشغيلها عند النقر فوق الرابط |
|
||||
| نص | نص | محتوى النص لعنصر القائمة |
|
||||
| isDragDisabled | قيمة منطقية | يشير إلى ما إذا كان تم تعطيل السحب |
|
||||
| اسم الفئة | نص | اسم اختياري لإضافة التنسيق |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### التحديد المتعدد
|
||||
@@ -153,34 +175,41 @@ image: /images/user-guide/kanban-views/kanban.png
|
||||
يوفر طريقة لتنفيذ وظيفة التحديد المتعدد مع مربع اختيار مصاحب.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { IconBell } from "@tabler/icons-react";
|
||||
import { MenuItemMultiSelect } from "twenty-ui/display";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
```jsx
|
||||
import { IconBell } from "@tabler/icons-react";
|
||||
import { MenuItemMultiSelect } from "twenty-ui/display";
|
||||
|
||||
return (
|
||||
<MenuItemMultiSelect
|
||||
LeftIcon={IconBell}
|
||||
text="First Option"
|
||||
selected={false}
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
|
||||
<Tab title="الخصائص">
|
||||
| الخصائص | النوع | الوصف |
|
||||
| --------------- | ------------- | ------------------------------------------------------ |
|
||||
| الأيقونة اليسرى | مكون الأيقونة | أيقونة اختيارية تظهر قبل النص في عنصر القائمة |
|
||||
| نص | string | محتوى النص لعنصر القائمة |
|
||||
| المحدد | قيمة منطقية | يشير إلى ما إذا كان عنصر القائمة محددًا (مفحوص) |
|
||||
| onSelectChange | دالة | وظيفة استدعاء يتم تشغيلها عند تغيير حالة مربع الاختيار |
|
||||
| اسم الفئة | نص | اسم اختياري لتنسيقات إضافية |
|
||||
</Tab>
|
||||
return (
|
||||
<MenuItemMultiSelect
|
||||
LeftIcon={IconBell}
|
||||
text="First Option"
|
||||
selected={false}
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="الخصائص">
|
||||
|
||||
|
||||
| الخصائص | النوع | الوصف |
|
||||
| --------------- | ------------- | ------------------------------------------------------ |
|
||||
| الأيقونة اليسرى | مكون الأيقونة | أيقونة اختيارية تظهر قبل النص في عنصر القائمة |
|
||||
| نص | string | محتوى النص لعنصر القائمة |
|
||||
| المحدد | قيمة منطقية | يشير إلى ما إذا كان عنصر القائمة محددًا (مفحوص) |
|
||||
| onSelectChange | دالة | وظيفة استدعاء يتم تشغيلها عند تغيير حالة مربع الاختيار |
|
||||
| اسم الفئة | نص | اسم اختياري لتنسيقات إضافية |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### التحديد المتعدد للصورة الرمزية
|
||||
@@ -188,35 +217,42 @@ image: /images/user-guide/kanban-views/kanban.png
|
||||
عنصر قائمة متعدد الخيارات مع صورة رمزية، ومربع اختيار للتحديد، ومحتوى نصي.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { MenuItemMultiSelectAvatar } from "twenty-ui/display";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
const imageUrl =
|
||||
"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAYABgAAD/4QCMRXhpZgAATU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABgAAAAAQAAAGAAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAABSgAwAEAAAAAQAAABQAAAAA/8AAEQgAFAAUAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMACwgICggHCwoJCg0MCw0RHBIRDw8RIhkaFBwpJCsqKCQnJy0yQDctMD0wJyc4TDk9Q0VISUgrNk9VTkZUQEdIRf/bAEMBDA0NEQ8RIRISIUUuJy5FRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRf/dAAQAAv/aAAwDAQACEQMRAD8Ava1q728otYY98joSCTgZrnbXWdTtrhrfVZXWLafmcAEkdgR/hVltQku9Q8+OIEBcGOT+ID0PY1ka1KH2u8ToqnPLbmIqG7u6LtbQ7RXBRec4Uck9eKXcPWsKDWVnhWSL5kYcFelSf2m3901POh8jP//QoyIAnTuKpXsY82NsksUyWPU5q/L9z8RVK++/F/uCsVsaEURwgA4HtT9x9TUcf3KfUGh//9k=";
|
||||
```jsx
|
||||
import { MenuItemMultiSelectAvatar } from "twenty-ui/display";
|
||||
|
||||
return (
|
||||
<MenuItemMultiSelectAvatar
|
||||
avatar={<img src={imageUrl} alt="Avatar" />}
|
||||
text="First Option"
|
||||
selected={false}
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
const imageUrl =
|
||||
"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAYABgAAD/4QCMRXhpZgAATU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABgAAAAAQAAAGAAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAABSgAwAEAAAAAQAAABQAAAAA/8AAEQgAFAAUAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMACwgICggHCwoJCg0MCw0RHBIRDw8RIhkaFBwpJCsqKCQnJy0yQDctMD0wJyc4TDk9Q0VISUgrNk9VTkZUQEdIRf/bAEMBDA0NEQ8RIRISIUUuJy5FRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRf/dAAQAAv/aAAwDAQACEQMRAD8Ava1q728otYY98joSCTgZrnbXWdTtrhrfVZXWLafmcAEkdgR/hVltQku9Q8+OIEBcGOT+ID0PY1ka1KH2u8ToqnPLbmIqG7u6LtbQ7RXBRec4Uck9eKXcPWsKDWVnhWSL5kYcFelSf2m3901POh8jP//QoyIAnTuKpXsY82NsksUyWPU5q/L9z8RVK++/F/uCsVsaEURwgA4HtT9x9TUcf3KfUGh//9k=";
|
||||
|
||||
<Tab title="الخصائص">
|
||||
| الخصائص | النوع | الوصف |
|
||||
| --------------- | ----------- | -------------------------------------------------------------------- |
|
||||
| الصورة الرمزبية | `ReactNode` | الصورة الرمزبية أو الأيقونة لعرضها على الجانب الأيسر من عنصر القائمة |
|
||||
| نص | نص | محتوى النص لعنصر القائمة |
|
||||
| المحدد | قيمة منطقية | يشير إلى ما إذا كان عنصر القائمة محددًا (مفحوص) |
|
||||
| onSelectChange | دالة | وظيفة استدعاء يتم تشغيلها عند تغيير حالة مربع الاختيار |
|
||||
| اسم الفئة | نص | اسم اختياري لتنسيقات إضافية |
|
||||
</Tab>
|
||||
return (
|
||||
<MenuItemMultiSelectAvatar
|
||||
avatar={<img src={imageUrl} alt="Avatar" />}
|
||||
text="First Option"
|
||||
selected={false}
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="الخصائص">
|
||||
|
||||
|
||||
| الخصائص | النوع | الوصف |
|
||||
| --------------- | ----------- | -------------------------------------------------------------------- |
|
||||
| الصورة الرمزبية | `ReactNode` | الصورة الرمزبية أو الأيقونة لعرضها على الجانب الأيسر من عنصر القائمة |
|
||||
| نص | نص | محتوى النص لعنصر القائمة |
|
||||
| المحدد | قيمة منطقية | يشير إلى ما إذا كان عنصر القائمة محددًا (مفحوص) |
|
||||
| onSelectChange | دالة | وظيفة استدعاء يتم تشغيلها عند تغيير حالة مربع الاختيار |
|
||||
| اسم الفئة | نص | اسم اختياري لتنسيقات إضافية |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### التنقل
|
||||
@@ -224,36 +260,43 @@ image: /images/user-guide/kanban-views/kanban.png
|
||||
عنصر قائمة يتضمن أيقونة يسارية اختيارية، محتوى نصي، وأيقونة سهم إلى اليمين.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { IconBell } from "@tabler/icons-react";
|
||||
import { MenuItemNavigate } from "twenty-ui/display";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
const handleNavigation = () => {
|
||||
console.log("Navigate to another page");
|
||||
};
|
||||
```jsx
|
||||
import { IconBell } from "@tabler/icons-react";
|
||||
import { MenuItemNavigate } from "twenty-ui/display";
|
||||
|
||||
return (
|
||||
<MenuItemNavigate
|
||||
LeftIcon={IconBell}
|
||||
text="First Option"
|
||||
onClick={handleNavigation}
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
const handleNavigation = () => {
|
||||
console.log("Navigate to another page");
|
||||
};
|
||||
|
||||
<Tab title="العناصر">
|
||||
| العناصر | النوع | الوصف |
|
||||
| --------------- | ------------- | ------------------------------------------------------ |
|
||||
| الأيقونة اليسرى | مكون الأيقونة | أيقونة اختيارية تظهر قبل النص في عنصر القائمة |
|
||||
| نص | نص | محتوى النص لعنصر القائمة |
|
||||
| عند_النقر | دالة | وظيفة الاستدعاء يتم تنشيطها عند النقر على عنصر القائمة |
|
||||
| اسم الفئة | نص | اسم اختياري لتنسيقات إضافية |
|
||||
</Tab>
|
||||
return (
|
||||
<MenuItemNavigate
|
||||
LeftIcon={IconBell}
|
||||
text="First Option"
|
||||
onClick={handleNavigation}
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="العناصر">
|
||||
|
||||
|
||||
| العناصر | النوع | الوصف |
|
||||
| --------------- | ------------- | ------------------------------------------------------ |
|
||||
| الأيقونة اليسرى | مكون الأيقونة | أيقونة اختيارية تظهر قبل النص في عنصر القائمة |
|
||||
| نص | نص | محتوى النص لعنصر القائمة |
|
||||
| عند_النقر | دالة | وظيفة الاستدعاء يتم تنشيطها عند النقر على عنصر القائمة |
|
||||
| اسم الفئة | نص | اسم اختياري لتنسيقات إضافية |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### اختيار
|
||||
@@ -261,42 +304,49 @@ image: /images/user-guide/kanban-views/kanban.png
|
||||
عنصر قائمة يمكن تحديده، مع شكل محتوى متاح (أيقونة ونص) ومُؤشر (أيقونة تحقق) لحالة الاختيار.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { IconBell } from "@tabler/icons-react";
|
||||
import { MenuItemSelect } from "twenty-ui/display";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
const handleSelection = () => {
|
||||
console.log("Menu item selected");
|
||||
};
|
||||
```jsx
|
||||
import { IconBell } from "@tabler/icons-react";
|
||||
import { MenuItemSelect } from "twenty-ui/display";
|
||||
|
||||
return (
|
||||
<MenuItemSelect
|
||||
LeftIcon={IconBell}
|
||||
text="First Option"
|
||||
selected={true}
|
||||
disabled={false}
|
||||
hovered={false}
|
||||
onClick={handleSelection}
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
const handleSelection = () => {
|
||||
console.log("Menu item selected");
|
||||
};
|
||||
|
||||
<Tab title="الخصائص">
|
||||
| الخصائص | النوع | الوصف |
|
||||
| --------------- | ------------- | ---------------------------------------------------------- |
|
||||
| الأيقونة اليسرى | مكون الأيقونة | أيقونة اختيارية تظهر قبل النص في عنصر القائمة |
|
||||
| نص | نص | محتوى النص لعنصر القائمة |
|
||||
| المحدد | قيمة منطقية | يشير إلى ما إذا كان عنصر القائمة محددًا (مفحوص) |
|
||||
| تعطيل | قيمة منطقية | يشير إلى ما إذا كان عنصر القائمة معطلا |
|
||||
| معطل | قيمة منطقية | يشير إلى ما إذا كان يتم التحويم حاليًا على عنصر القائمة |
|
||||
| عند_النقر | دالة | دالة الاستدعاء التي يتم تحفيزها عند النقر على عنصر القائمة |
|
||||
| اسم الفئة | نص | اسم اختياري لتنسيقات إضافية |
|
||||
</Tab>
|
||||
return (
|
||||
<MenuItemSelect
|
||||
LeftIcon={IconBell}
|
||||
text="First Option"
|
||||
selected={true}
|
||||
disabled={false}
|
||||
hovered={false}
|
||||
onClick={handleSelection}
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="الخصائص">
|
||||
|
||||
|
||||
| الخصائص | النوع | الوصف |
|
||||
| --------------- | ------------- | ---------------------------------------------------------- |
|
||||
| الأيقونة اليسرى | مكون الأيقونة | أيقونة اختيارية تظهر قبل النص في عنصر القائمة |
|
||||
| نص | نص | محتوى النص لعنصر القائمة |
|
||||
| المحدد | قيمة منطقية | يشير إلى ما إذا كان عنصر القائمة محددًا (مفحوص) |
|
||||
| تعطيل | قيمة منطقية | يشير إلى ما إذا كان عنصر القائمة معطلا |
|
||||
| معطل | قيمة منطقية | يشير إلى ما إذا كان يتم التحويم حاليًا على عنصر القائمة |
|
||||
| عند_النقر | دالة | دالة الاستدعاء التي يتم تحفيزها عند النقر على عنصر القائمة |
|
||||
| اسم الفئة | نص | اسم اختياري لتنسيقات إضافية |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### اختيار الصورة الرمزية
|
||||
@@ -304,47 +354,54 @@ image: /images/user-guide/kanban-views/kanban.png
|
||||
عنصر قائمة قابل للتحديد مع صورة رمزية، يتضمن محتوى يسارياً اختيارياً (صورة رمزية ونصاً) ومؤشراً (أيقونة التحقق) لحالة التحديد.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { MenuItemSelectAvatar } from "twenty-ui/display";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
const imageUrl =
|
||||
"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAYABgAAD/4QCMRXhpZgAATU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABgAAAAAQAAAGAAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAABSgAwAEAAAAAQAAABQAAAAA/8AAEQgAFAAUAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6OnqvLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMACwgICggHCwoJCg0MCw0RHBIRDw8RIhkaFBwpJCsqKCQnJy0yQDctMD0wJyc4TDk9Q0VISUgrNk9VTkZUQEdIRf/bAEMBDA0NEQ8RIRISIUUuJy5FRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRf/dAAQAAv/aAAwDAQACEQMRAD8Ava1q728otYY98joSCTgZrnbXWdTtrhrfVZXWLafmcAEkdgR/hVltQku9Q8+OIEBcGOT+ID0PY1ka1KH2u8ToqnPLbmIqG7u6LtbQ7RXBRec4Uck9eKXcPWsKDWVnhWSL5kYcFelSf2m3901POh8jP//QoyIAnTuKpXsY82NsksUyWPU5q/L9z8RVK++/F/uCsVsaEURwgA4HtT9x9TUcf3KfUGh//9k=";
|
||||
```jsx
|
||||
import { MenuItemSelectAvatar } from "twenty-ui/display";
|
||||
|
||||
const handleSelection = () => {
|
||||
console.log("تم تحديد عنصر القائمة");
|
||||
};
|
||||
export const MyComponent = () => {
|
||||
const imageUrl =
|
||||
"data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAYABgAAD/4QCMRXhpZgAATU0AKgAAAAgABQESAAMAAAABAAEAAAEaAAUAAAABAAAASgEbAAUAAAABAAAAUgEoAAMAAAABAAIAAIdpAAQAAAABAAAAWgAAAAAAAABgAAAAAQAAAGAAAAABAAOgAQADAAAAAQABAACgAgAEAAAAAQAAABSgAwAEAAAAAQAAABQAAAAA/8AAEQgAFAAUAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6OnqvLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMACwgICggHCwoJCg0MCw0RHBIRDw8RIhkaFBwpJCsqKCQnJy0yQDctMD0wJyc4TDk9Q0VISUgrNk9VTkZUQEdIRf/bAEMBDA0NEQ8RIRISIUUuJy5FRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRUVFRf/dAAQAAv/aAAwDAQACEQMRAD8Ava1q728otYY98joSCTgZrnbXWdTtrhrfVZXWLafmcAEkdgR/hVltQku9Q8+OIEBcGOT+ID0PY1ka1KH2u8ToqnPLbmIqG7u6LtbQ7RXBRec4Uck9eKXcPWsKDWVnhWSL5kYcFelSf2m3901POh8jP//QoyIAnTuKpXsY82NsksUyWPU5q/L9z8RVK++/F/uCsVsaEURwgA4HtT9x9TUcf3KfUGh//9k=";
|
||||
|
||||
return (
|
||||
<MenuItemSelectAvatar
|
||||
avatar={<img src={imageUrl} alt="الصورة الرمزية" />}
|
||||
text="الخيار الأول"
|
||||
selected={true}
|
||||
disabled={false}
|
||||
hovered={false}
|
||||
testId="menu-item-test"
|
||||
onClick={handleSelection}
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
const handleSelection = () => {
|
||||
console.log("تم تحديد عنصر القائمة");
|
||||
};
|
||||
|
||||
```
|
||||
</Tab>
|
||||
return (
|
||||
<MenuItemSelectAvatar
|
||||
avatar={<img src={imageUrl} alt="الصورة الرمزية" />}
|
||||
text="الخيار الأول"
|
||||
selected={true}
|
||||
disabled={false}
|
||||
hovered={false}
|
||||
testId="menu-item-test"
|
||||
onClick={handleSelection}
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
<Tab title="الخصائص">
|
||||
| الخصائص | النوع | الوصف |
|
||||
| -------------- | ------------ | ---------------------------------------------------------------------------- |
|
||||
| الصورة الرمزية | `مكون React` | الصورة الرمزية أو الأيقونة التي سيتم عرضها على الجانب الأيسر من عنصر القائمة |
|
||||
| نص | نص | محتوى النص في عنصر القائمة |
|
||||
| المحدد | قيمة منطقية | يشير إلى ما إذا كان عنصر القائمة محددًا (مفحوص) |
|
||||
| تعطيل | قيمة منطقية | يشير إلى ما إذا كان عنصر القائمة معطلاً |
|
||||
| معلق عليه | قيمة منطقية | يشير إلى ما إذا كان يتم التحويم حاليًا على عنصر القائمة |
|
||||
| testId | نص | سمة data-testid لأغراض الاختبار |
|
||||
| عند_النقر | دالة | دالة الاستدعاء التي يتم تحفيزها عند النقر على عنصر القائمة |
|
||||
| اسم الفئة | نص | اسم اختياري للتنسيق الإضافي. |
|
||||
</Tab>
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="الخصائص">
|
||||
|
||||
|
||||
| الخصائص | النوع | الوصف |
|
||||
| -------------- | ------------ | ---------------------------------------------------------------------------- |
|
||||
| الصورة الرمزية | `مكون React` | الصورة الرمزية أو الأيقونة التي سيتم عرضها على الجانب الأيسر من عنصر القائمة |
|
||||
| نص | نص | محتوى النص في عنصر القائمة |
|
||||
| المحدد | قيمة منطقية | يشير إلى ما إذا كان عنصر القائمة محددًا (مفحوص) |
|
||||
| تعطيل | قيمة منطقية | يشير إلى ما إذا كان عنصر القائمة معطلاً |
|
||||
| معلق عليه | قيمة منطقية | يشير إلى ما إذا كان يتم التحويم حاليًا على عنصر القائمة |
|
||||
| testId | نص | سمة data-testid لأغراض الاختبار |
|
||||
| عند_النقر | دالة | دالة الاستدعاء التي يتم تحفيزها عند النقر على عنصر القائمة |
|
||||
| اسم الفئة | نص | اسم اختياري للتنسيق الإضافي. |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### اختيار اللون
|
||||
@@ -352,41 +409,48 @@ image: /images/user-guide/kanban-views/kanban.png
|
||||
عنصر قائمة قابل للتحديد يتضمن عينة لون للسيناريوهات التي تريد فيها من المستخدمين اختيار لون من القائمة.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { MenuItemSelectColor } from "twenty-ui/display";
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
const handleSelection = () => {
|
||||
console.log("Menu item selected");
|
||||
};
|
||||
```jsx
|
||||
import { MenuItemSelectColor } from "twenty-ui/display";
|
||||
|
||||
return (
|
||||
<MenuItemSelectColor
|
||||
color="green"
|
||||
selected={true}
|
||||
disabled={false}
|
||||
hovered={true}
|
||||
variant="default"
|
||||
onClick={handleSelection}
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
const handleSelection = () => {
|
||||
console.log("Menu item selected");
|
||||
};
|
||||
|
||||
<Tab title="الخصائص">
|
||||
| الخصائص | النوع | الوصف |
|
||||
| --------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| اللون | نص | لون الثيم المعروض كعينة في عنصر القائمة. الخيارات تشمل: `أخضر`, `تركواز`, `سماوي`, `أزرق`, `أرجواني`, `وردي`, `أحمر`, `برتقالي`, `أصفر`, `رمادي`. |
|
||||
| المحدد | قيمة منطقية | يشير إلى ما إذا كان عنصر القائمة محددًا (مفحوص) |
|
||||
| تعطيل | قيمة منطقية | يشير إلى ما إذا كان عنصر القائمة معطلاً |
|
||||
| معلق عليه | قيمة منطقية | يشير إلى ما إذا كان يتم التحويم حاليًا على عنصر القائمة |
|
||||
| البديل | نص | المتغير لعينة اللون. يمكن أن يكون إما `افتراضي` أو `خط أنابيب` |
|
||||
| عند_النقر | دالة | دالة الاستدعاء التي يتم تحفيزها عند النقر على عنصر القائمة |
|
||||
| اسم الفئة | نص | اسم اختياري للتنسيق الإضافي. |
|
||||
</Tab>
|
||||
return (
|
||||
<MenuItemSelectColor
|
||||
color="green"
|
||||
selected={true}
|
||||
disabled={false}
|
||||
hovered={true}
|
||||
variant="default"
|
||||
onClick={handleSelection}
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="الخصائص">
|
||||
|
||||
|
||||
| الخصائص | النوع | الوصف |
|
||||
| --------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| اللون | نص | لون الثيم المعروض كعينة في عنصر القائمة. الخيارات تشمل: `أخضر`, `تركواز`, `سماوي`, `أزرق`, `أرجواني`, `وردي`, `أحمر`, `برتقالي`, `أصفر`, `رمادي`. |
|
||||
| المحدد | قيمة منطقية | يشير إلى ما إذا كان عنصر القائمة محددًا (مفحوص) |
|
||||
| تعطيل | قيمة منطقية | يشير إلى ما إذا كان عنصر القائمة معطلاً |
|
||||
| معلق عليه | قيمة منطقية | يشير إلى ما إذا كان يتم التحويم حاليًا على عنصر القائمة |
|
||||
| البديل | نص | المتغير لعينة اللون. يمكن أن يكون إما `افتراضي` أو `خط أنابيب` |
|
||||
| عند_النقر | دالة | دالة الاستدعاء التي يتم تحفيزها عند النقر على عنصر القائمة |
|
||||
| اسم الفئة | نص | اسم اختياري للتنسيق الإضافي. |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
### تبديل
|
||||
@@ -394,35 +458,42 @@ image: /images/user-guide/kanban-views/kanban.png
|
||||
عنصر قائمة مع مفتاح تبديل مرتبط للسماح للمستخدمين بتمكين أو تعطيل ميزة معينة
|
||||
|
||||
<Tabs>
|
||||
<Tab title="الاستخدام">
|
||||
```jsx
|
||||
import { IconBell } from '@tabler/icons-react';
|
||||
<Tab title="الاستخدام">
|
||||
|
||||
import { MenuItemToggle } from 'twenty-ui/display';
|
||||
```jsx
|
||||
import { IconBell } from '@tabler/icons-react';
|
||||
|
||||
export const MyComponent = () => {
|
||||
import { MenuItemToggle } from 'twenty-ui/display';
|
||||
|
||||
return (
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconBell}
|
||||
text="First Option"
|
||||
toggled={true}
|
||||
toggleSize="small"
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
|
||||
<Tab title="الخصائص">
|
||||
| الخصائص | النوع | الوصف |
|
||||
| --------------- | ------------- | ------------------------------------------------------------ |
|
||||
| الأيقونة اليسرى | مكون الأيقونة | أيقونة اختيارية تُعرض قبل النص في عنصر القائمة |
|
||||
| نص | نص | محتوى النص في عنصر القائمة |
|
||||
| مبدل | قيمة منطقية | يشير إلى ما إذا كان مفتاح التبديل في حالة "تشغيل" أو "إيقاف" |
|
||||
| onToggleChange | دالة | دالة الاستدعاء التي يتم تحفيزها عند تغيير حالة مفتاح التبديل |
|
||||
| حجم التبديل | نص | حجم مفتاح التبديل. يمكن أن يكون إما \ |
|
||||
| اسم الفئة | نص | اسم اختياري لتنسيقات إضافية |
|
||||
</Tab>
|
||||
return (
|
||||
<MenuItemToggle
|
||||
LeftIcon={IconBell}
|
||||
text="First Option"
|
||||
toggled={true}
|
||||
toggleSize="small"
|
||||
className
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="الخصائص">
|
||||
|
||||
|
||||
| الخصائص | النوع | الوصف |
|
||||
| --------------- | ------------- | ------------------------------------------------------------ |
|
||||
| الأيقونة اليسرى | مكون الأيقونة | أيقونة اختيارية تُعرض قبل النص في عنصر القائمة |
|
||||
| نص | نص | محتوى النص في عنصر القائمة |
|
||||
| مبدل | قيمة منطقية | يشير إلى ما إذا كان مفتاح التبديل في حالة "تشغيل" أو "إيقاف" |
|
||||
| onToggleChange | دالة | دالة الاستدعاء التي يتم تحفيزها عند تغيير حالة مفتاح التبديل |
|
||||
| حجم التبديل | نص | حجم مفتاح التبديل. يمكن أن يكون إما \ |
|
||||
| اسم الفئة | نص | اسم اختياري لتنسيقات إضافية |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -10,40 +10,47 @@ image: /images/user-guide/table-views/table.png
|
||||
يستعرض شريط التنقل الذي يحتوي على عدة مكونات `NavigationBarItem`.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { IconHome, IconUser, IconSettings } from '@tabler/icons-react';
|
||||
import { NavigationBar } from "@/ui/navigation/navigation-bar/components/NavigationBar";
|
||||
<Tab title="استخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
```jsx
|
||||
import { IconHome, IconUser, IconSettings } from '@tabler/icons-react';
|
||||
import { NavigationBar } from "@/ui/navigation/navigation-bar/components/NavigationBar";
|
||||
|
||||
const navigationItems = [
|
||||
{
|
||||
name: "Home",
|
||||
Icon: IconHome,
|
||||
onClick: () => console.log("Home clicked"),
|
||||
},
|
||||
{
|
||||
name: "Profile",
|
||||
Icon: IconUser,
|
||||
onClick: () => console.log("Profile clicked"),
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
Icon: IconSettings,
|
||||
onClick: () => console.log("Settings clicked"),
|
||||
},
|
||||
];
|
||||
export const MyComponent = () => {
|
||||
|
||||
return <NavigationBar activeItemName="Home" items={navigationItems}/>;
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
const navigationItems = [
|
||||
{
|
||||
name: "Home",
|
||||
Icon: IconHome,
|
||||
onClick: () => console.log("Home clicked"),
|
||||
},
|
||||
{
|
||||
name: "Profile",
|
||||
Icon: IconUser,
|
||||
onClick: () => console.log("Profile clicked"),
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
Icon: IconSettings,
|
||||
onClick: () => console.log("Settings clicked"),
|
||||
},
|
||||
];
|
||||
|
||||
<Tab title="المحددات">
|
||||
| المحددات | النوع | الوصف |
|
||||
| ---------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| اسم العنصر النشط | نص | اسم العنصر النشط حاليًا في قائمة التنقل |
|
||||
| العناصر | مصفوفة | مصفوفة من الكائنات التي تمثل كل عنصر من عناصر التنقل. كل كائن يحتوي على `الاسم` للعنصر، ومكون `الأيقونة` للعرض، ودالة `onClick` التي تستدعي عند النقر على العنصر |
|
||||
</Tab>
|
||||
return <NavigationBar activeItemName="Home" items={navigationItems}/>;
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab title="المحددات">
|
||||
|
||||
|
||||
| المحددات | النوع | الوصف |
|
||||
| ---------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| اسم العنصر النشط | نص | اسم العنصر النشط حاليًا في قائمة التنقل |
|
||||
| العناصر | مصفوفة | مصفوفة من الكائنات التي تمثل كل عنصر من عناصر التنقل. كل كائن يحتوي على `الاسم` للعنصر، ومكون `الأيقونة` للعرض، ودالة `onClick` التي تستدعي عند النقر على العنصر |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -10,25 +10,33 @@ image: /images/user-guide/api/api.png
|
||||
يعرض التقدم من خلال سلسلة من الخطوات المرقمة عن طريق تمييز الخطوة النشطة. يولد حاوية تحتوي على خطوات، يتم تمثيل كل منها بواسطة مكون 'Step'.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { StepBar } from "@/ui/navigation/step-bar/components/StepBar";
|
||||
<Tab title="استخدام">
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<StepBar activeStep={2}>
|
||||
<StepBar.Step>Step 1</StepBar.Step>
|
||||
<StepBar.Step>Step 2</StepBar.Step>
|
||||
<StepBar.Step>Step 3</StepBar.Step>
|
||||
</StepBar>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
```jsx
|
||||
import { StepBar } from "@/ui/navigation/step-bar/components/StepBar";
|
||||
|
||||
<Tab title="المحددات">
|
||||
| المحددات | النوع | الوصف |
|
||||
| ------------- | ----- | ----------------------------------------------------------------- |
|
||||
| الخطوة النشطة | رقم | مؤشر للخطوة النشطة حاليًا. هذا يحدد أي خطوة يجب إبرازها بشكل مرئي |
|
||||
</Tab>
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<StepBar activeStep={2}>
|
||||
<StepBar.Step>Step 1</StepBar.Step>
|
||||
<StepBar.Step>Step 2</StepBar.Step>
|
||||
<StepBar.Step>Step 3</StepBar.Step>
|
||||
</StepBar>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="المحددات">
|
||||
|
||||
|
||||
| المحددات | النوع | الوصف |
|
||||
| ------------- | ----- | ----------------------------------------------------------------- |
|
||||
| الخطوة النشطة | رقم | مؤشر للخطوة النشطة حاليًا. هذا يحدد أي خطوة يجب إبرازها بشكل مرئي |
|
||||
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -10,35 +10,43 @@ image: /images/user-guide/emails/emails_header.png
|
||||
يشير إلى تقدم أو عد تنازلي ويتحرك من اليمين إلى اليسار.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { ProgressBar } from "twenty-ui/feedback";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<ProgressBar
|
||||
duration={6000}
|
||||
delay={0}
|
||||
easing="easeInOut"
|
||||
barHeight={10}
|
||||
barColor="#4bb543"
|
||||
autoStart={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="استخدام">
|
||||
|
||||
<Tab title="المحددات">
|
||||
| المحددات | النوع | الوصف | الإعداد الافتراضي |
|
||||
| ---------------- | ----------- | ------------------------------------------------------------------------------- | ----------------- |
|
||||
| المدة | رقم | إجمالي مدة الرسوم المتحركة لشريط التقدم بالميلي ثانية | 3 |
|
||||
| التأخير | رقم | التأخير في بدء الرسوم المتحركة لشريط التقدم بالميلي ثانية | 0 |
|
||||
| التخفيف | نص | وظيفة التخفيف للرسوم المتحركة لشريط التقدم | easeInOut |
|
||||
| ارتفاع الشريط | رقم | ارتفاع الشريط بالبكسل | 24 |
|
||||
| لون الشريط | نص | لون الشريط | gray80 |
|
||||
| التشغيل التلقائي | قيمة منطقية | إذا كان `true`، فإن الرسوم المتحركة لشريط التقدم تبدأ تلقائيًا عند تحميل المكون | `صحيح` |
|
||||
</Tab>
|
||||
```jsx
|
||||
import { ProgressBar } from "twenty-ui/feedback";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return (
|
||||
<ProgressBar
|
||||
duration={6000}
|
||||
delay={0}
|
||||
easing="easeInOut"
|
||||
barHeight={10}
|
||||
barColor="#4bb543"
|
||||
autoStart={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="المحددات">
|
||||
|
||||
|
||||
| المحددات | النوع | الوصف | الإعداد الافتراضي |
|
||||
| ---------------- | ----------- | ------------------------------------------------------------------------------- | ----------------- |
|
||||
| المدة | رقم | إجمالي مدة الرسوم المتحركة لشريط التقدم بالميلي ثانية | 3 |
|
||||
| التأخير | رقم | التأخير في بدء الرسوم المتحركة لشريط التقدم بالميلي ثانية | 0 |
|
||||
| التخفيف | نص | وظيفة التخفيف للرسوم المتحركة لشريط التقدم | easeInOut |
|
||||
| ارتفاع الشريط | رقم | ارتفاع الشريط بالبكسل | 24 |
|
||||
| لون الشريط | نص | لون الشريط | gray80 |
|
||||
| التشغيل التلقائي | قيمة منطقية | إذا كان `true`، فإن الرسوم المتحركة لشريط التقدم تبدأ تلقائيًا عند تحميل المكون | `صحيح` |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
## شريط التقدم الدائري
|
||||
@@ -46,21 +54,30 @@ image: /images/user-guide/emails/emails_header.png
|
||||
يشير إلى تقدم المهمة، ويستخدم غالباً في شاشات التحميل أو الأماكن التي ترغب فيها في إبلاغ العمليات الجارية إلى المستخدم.
|
||||
|
||||
<Tabs>
|
||||
<Tab title="استخدام">
|
||||
```jsx
|
||||
import { CircularProgressBar } from "@/ui/feedback/progress-bar/components/CircularProgressBar";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return <CircularProgressBar size={80} barWidth={6} barColor="green" />;
|
||||
};
|
||||
```
|
||||
</Tab>
|
||||
<Tab title="استخدام">
|
||||
|
||||
```jsx
|
||||
import { CircularProgressBar } from "@/ui/feedback/progress-bar/components/CircularProgressBar";
|
||||
|
||||
export const MyComponent = () => {
|
||||
return <CircularProgressBar size={80} barWidth={6} barColor="green" />;
|
||||
};
|
||||
```
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="الخصائص">
|
||||
|
||||
|
||||
| الخصائص | النوع | الوصف | الإعداد الافتراضي |
|
||||
| ---------- | ----- | ----------------------- | ----------------- |
|
||||
| الحجم | رقم | حجم شريط التقدم الدائري | 50 |
|
||||
| عرض الشريط | رقم | عرض خط شريط التقدم | 5 |
|
||||
| لون الشريط | نص | لون شريط التقدم | currentColor |
|
||||
|
||||
|
||||
|
||||
</Tab>
|
||||
|
||||
<Tab title="الخصائص">
|
||||
| الخصائص | النوع | الوصف | الإعداد الافتراضي |
|
||||
| ---------- | ----- | ----------------------- | ----------------- |
|
||||
| الحجم | رقم | حجم شريط التقدم الدائري | 50 |
|
||||
| عرض الشريط | رقم | عرض خط شريط التقدم | 5 |
|
||||
| لون الشريط | نص | لون شريط التقدم | currentColor |
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
@@ -4,7 +4,7 @@ description: ادمج قدرات الذكاء الاصطناعي مباشرةً
|
||||
---
|
||||
|
||||
<Note>
|
||||
هذه الميزة قيد التطوير حالياً وستكون متاحة في النسخة التجريبية قريباً.
|
||||
هذه الميزة قيد التطوير حالياً وستكون متاحة في النسخة التجريبية قريباً.
|
||||
</Note>
|
||||
|
||||
## نظرة عامة
|
||||
|
||||
@@ -4,7 +4,7 @@ description: مساعد ذكي يساعدك على التفاعل مع بيان
|
||||
---
|
||||
|
||||
<Note>
|
||||
هذه الميزة قيد التطوير حالياً وستكون متاحة في النسخة التجريبية قريباً.
|
||||
هذه الميزة قيد التطوير حالياً وستكون متاحة في النسخة التجريبية قريباً.
|
||||
</Note>
|
||||
|
||||
## نظرة عامة
|
||||
|
||||
@@ -26,7 +26,7 @@ description: تحكّم بما يمكن لوكلاء الذكاء الاصطنا
|
||||
| **إمكانية التدقيق** | تتبُّع الإجراءات التي نفّذها كل وكيل |
|
||||
|
||||
<Note>
|
||||
بالنسبة لوكلاء الذكاء الاصطناعي الذين يعملون ضمن سير العمل، يضمن تعيين الدور ألا يتمكّن الوكيل من الوصول إلى البيانات أو تعديلها خارج نطاقه المقصود — حتى إذا كانت لسير العمل أذونات أوسع.
|
||||
بالنسبة لوكلاء الذكاء الاصطناعي الذين يعملون ضمن سير العمل، يضمن تعيين الدور ألا يتمكّن الوكيل من الوصول إلى البيانات أو تعديلها خارج نطاقه المقصود — حتى إذا كانت لسير العمل أذونات أوسع.
|
||||
</Note>
|
||||
|
||||
## ذات صلة
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user