Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
571e9ba4e5 | ||
|
|
7e67b1c5a6 | ||
|
|
57869d3c8c | ||
|
|
257834ea71 | ||
|
|
9579f22bc2 | ||
|
|
b10d831371 | ||
|
|
07aaf0801c | ||
|
|
d56c815897 | ||
|
|
2990d23411 | ||
|
|
77c2961912 | ||
|
|
05cd0d1803 | ||
|
|
5f2a39d9e9 | ||
|
|
d7da73f0ec | ||
|
|
182ebb6394 | ||
|
|
bce5be85a3 | ||
|
|
c776179ecc | ||
|
|
90c26643a8 | ||
|
|
183fd877c4 | ||
|
|
07de458fda | ||
|
|
4d9facb9bd | ||
|
|
2c4a77a7b7 | ||
|
|
224b6d1334 | ||
|
|
ad04040ea6 | ||
|
|
6067852c1c | ||
|
|
841b1f0e11 | ||
|
|
5c60a5511e | ||
|
|
89f6f32243 | ||
|
|
c1fff908fe | ||
|
|
8ecf07f112 | ||
|
|
b6e02b630d | ||
|
|
96d56f8883 | ||
|
|
013eda4a32 | ||
|
|
b0595e452a | ||
|
|
90d47a8fca | ||
|
|
afd3252cbe | ||
|
|
0a8960c2ed | ||
|
|
f0dbeea704 | ||
|
|
c553361f25 | ||
|
|
d70987653b | ||
|
|
228b7386ea | ||
|
|
49c3250519 | ||
|
|
28ff5610f0 | ||
|
|
0a61550f43 | ||
|
|
f4ef51fbe5 | ||
|
|
0654d814c6 | ||
|
|
bb5cc7e30c | ||
|
|
23015de996 | ||
|
|
310bba758b | ||
|
|
8d469b9bc3 | ||
|
|
387f5259a7 | ||
|
|
ef0ae2568d | ||
|
|
1749793bc1 | ||
|
|
2524d64687 | ||
|
|
99f53a5030 | ||
|
|
ab8ad46685 | ||
|
|
f36555bdc0 | ||
|
|
b2be2fb398 | ||
|
|
9bbb963a08 | ||
|
|
6285186ead | ||
|
|
4208bd2b83 | ||
|
|
9c67a109af | ||
|
|
e1a0259154 | ||
|
|
229a93e41a | ||
|
|
a9cb20f317 | ||
|
|
14b7bcf262 | ||
|
|
36fb14179b | ||
|
|
5c565345ae | ||
|
|
ab22fd560a | ||
|
|
2b3b073570 | ||
|
|
a8867fd090 | ||
|
|
50af41f170 | ||
|
|
4623b6127b | ||
|
|
f60ce384c6 | ||
|
|
815e5dfa16 | ||
|
|
c23896c5dc | ||
|
|
de56c01206 | ||
|
|
680366e998 | ||
|
|
f4c5d03b98 | ||
|
|
11d244194f | ||
|
|
c993f2de0b | ||
|
|
26ff344f56 | ||
|
|
9ed9b4746a | ||
|
|
6c78cac908 | ||
|
|
455e548bea | ||
|
|
081ecbcfaf | ||
|
|
7ab00a4c82 | ||
|
|
b686e0b2e6 | ||
|
|
36e4357bb1 | ||
|
|
33e69805cb | ||
|
|
99caab1412 | ||
|
|
02f34211b5 | ||
|
|
078e23154a | ||
|
|
2fa5fb7de7 | ||
|
|
9142bdfb92 | ||
|
|
2c0d3e93d2 | ||
|
|
c735026f6c | ||
|
|
743169a260 | ||
|
|
2fc247cb21 | ||
|
|
5e891a135b | ||
|
|
7943141d03 | ||
|
|
9a65e80566 | ||
|
|
3c7805c6d0 | ||
|
|
7e4277fbe4 | ||
|
|
32194a88b3 | ||
|
|
7b2d9894f3 | ||
|
|
d0ff1ffd5f | ||
|
|
9d7632cb4f | ||
|
|
64500d07d3 | ||
|
|
b6701a81e1 | ||
|
|
2b0f67191a | ||
|
|
0527bc296e | ||
|
|
39a9cd0d51 | ||
|
|
69f052e3ad | ||
|
|
b542b43878 | ||
|
|
29eb9fe77b | ||
|
|
ebc381f009 | ||
|
|
a7bbc9e00d | ||
|
|
27eae53d0a | ||
|
|
0136be65d1 | ||
|
|
a2d55a8694 | ||
|
|
05149feb00 | ||
|
|
c878f09d72 | ||
|
|
83223eeae3 | ||
|
|
d73dc1a728 | ||
|
|
3573d89c3c | ||
|
|
38b83f0866 | ||
|
|
b857d45182 | ||
|
|
541bbb93cb | ||
|
|
33159e29b7 | ||
|
|
75f5afb968 | ||
|
|
fbe042db3b | ||
|
|
041b1f21bf | ||
|
|
8aab063da3 | ||
|
|
812ed6ed69 | ||
|
|
e96ad9a1f2 | ||
|
|
abe9185f48 | ||
|
|
525a2c2bed | ||
|
|
c9fd194695 | ||
|
|
2fab2266d5 | ||
|
|
3ad1113173 | ||
|
|
a9cb1e9b0d | ||
|
|
0d6a6ec678 | ||
|
|
149ce680d5 | ||
|
|
315938215e | ||
|
|
dfb966d47e | ||
|
|
12467d67c8 | ||
|
|
eb39288583 | ||
|
|
08f163592b | ||
|
|
182f9b12da | ||
|
|
88e63262cd | ||
|
|
4c413d4802 | ||
|
|
a026cde46a | ||
|
|
d85279c55e | ||
|
|
eea2885cbd | ||
|
|
64b8fd544c | ||
|
|
a16b0d233e | ||
|
|
9f2e774113 | ||
|
|
2e75fae3ad | ||
|
|
7bde2006c5 | ||
|
|
49526937fa | ||
|
|
2e2b27912b | ||
|
|
8c7d1f923d | ||
|
|
c3d96b1dd8 | ||
|
|
37970c08a9 | ||
|
|
3f1683f8b9 | ||
|
|
08852dd1b1 | ||
|
|
a19349bdae | ||
|
|
00791c3cd5 | ||
|
|
bad7ad464b | ||
|
|
fd8e0d04a2 | ||
|
|
96b5d01ff5 | ||
|
|
5ec6cb0e6f | ||
|
|
4d8445a34a | ||
|
|
35f2d7a004 |
@@ -21,7 +21,7 @@ jobs:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: twenty
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: "true"
|
||||
SPILO_PROVIDER: "local"
|
||||
ports:
|
||||
@@ -73,11 +73,32 @@ jobs:
|
||||
- name: Server / Create DB
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: |
|
||||
PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
npx nx run twenty-server:database:init:prod
|
||||
npx nx run twenty-server:database:migrate:prod
|
||||
- name: Worker / Run
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: npx nx run twenty-server:worker:ci
|
||||
- name: Server / Check for Pending Migrations
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: |
|
||||
METADATA_MIGRATION_OUTPUT=$(npx nx run twenty-server:typeorm migration:generate metadata-migration-check -d src/database/typeorm/metadata/metadata.datasource.ts || true)
|
||||
|
||||
CORE_MIGRATION_OUTPUT=$(npx nx run twenty-server:typeorm migration:generate core-migration-check -d src/database/typeorm/core/core.datasource.ts || true)
|
||||
|
||||
METADATA_MIGRATION_FILE=$(ls packages/twenty-server/*metadata-migration-check.ts 2>/dev/null || echo "")
|
||||
CORE_MIGRATION_FILE=$(ls packages/twenty-server/*core-migration-check.ts 2>/dev/null || echo "")
|
||||
|
||||
if [ -n "$METADATA_MIGRATION_FILE" ] || [ -n "$CORE_MIGRATION_FILE" ]; then
|
||||
echo "::error::Unexpected migration files were generated. Please create a proper migration manually."
|
||||
echo "$METADATA_MIGRATION_OUTPUT"
|
||||
echo "$CORE_MIGRATION_OUTPUT"
|
||||
|
||||
rm -f packages/twenty-server/*metadata-migration-check.ts packages/twenty-server/*core-migration-check.ts
|
||||
|
||||
exit 1
|
||||
fi
|
||||
|
||||
server-test:
|
||||
timeout-minutes: 30
|
||||
@@ -124,7 +145,7 @@ jobs:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: twenty
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: "true"
|
||||
SPILO_PROVIDER: "local"
|
||||
ports:
|
||||
|
||||
@@ -20,7 +20,7 @@ jobs:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: twenty
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: "true"
|
||||
SPILO_PROVIDER: "local"
|
||||
ports:
|
||||
@@ -46,18 +46,18 @@ jobs:
|
||||
|
||||
- name: Server / Create DB
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
run: PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
run: PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "default";'
|
||||
|
||||
- name: Website / Run migrations
|
||||
if: steps.changed-files.outputs.changed == 'true'
|
||||
run: npx nx database:migrate twenty-website
|
||||
env:
|
||||
DATABASE_PG_URL: postgres://postgres:twenty@localhost:5432/default
|
||||
DATABASE_PG_URL: postgres://postgres:postgres@localhost:5432/default
|
||||
- name: Website / Build Website
|
||||
if: steps.changed-files.outputs.changed == 'true'
|
||||
run: npx nx build twenty-website
|
||||
env:
|
||||
DATABASE_PG_URL: postgres://postgres:twenty@localhost:5432/default
|
||||
DATABASE_PG_URL: postgres://postgres:postgres@localhost:5432/default
|
||||
|
||||
- name: Mark as VALID
|
||||
if: steps.changed-files.outputs.changed != 'true' # If no changes, mark as valid
|
||||
|
||||
+10
@@ -30,3 +30,13 @@ storybook-static
|
||||
test-results/
|
||||
dump.rdb
|
||||
.tinyb
|
||||
|
||||
.notes
|
||||
/data/
|
||||
/.devenv/
|
||||
/.direnv/
|
||||
/.pre-commit-config.yaml
|
||||
/.envrc
|
||||
/devenv.nix
|
||||
/flake.lock
|
||||
/flake.nix
|
||||
|
||||
@@ -2,17 +2,17 @@ postgres-on-docker:
|
||||
docker run -d \
|
||||
--name twenty_pg \
|
||||
-e PGUSER_SUPERUSER=postgres \
|
||||
-e PGPASSWORD_SUPERUSER=twenty \
|
||||
-e PGPASSWORD_SUPERUSER=postgres \
|
||||
-e ALLOW_NOSSL=true \
|
||||
-v twenty_db_data:/home/postgres/pgdata \
|
||||
-p 5432:5432 \
|
||||
twentycrm/twenty-postgres-spilo:latest
|
||||
@echo "Waiting for PostgreSQL to be ready..."
|
||||
@until PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres \
|
||||
@until docker exec twenty_pg psql -U postgres -d postgres \
|
||||
-c 'SELECT pg_is_in_recovery();' 2>/dev/null | grep -q 'f'; do \
|
||||
sleep 1; \
|
||||
done
|
||||
PGPASSWORD=twenty psql -h localhost -p 5432 -U postgres -d postgres \
|
||||
docker exec twenty_pg psql -U postgres -d postgres \
|
||||
-c "CREATE DATABASE \"default\" WITH OWNER postgres;" \
|
||||
-c "CREATE DATABASE \"test\" WITH OWNER postgres;"
|
||||
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
"outputs": ["{projectRoot}/{options.output-dir}"],
|
||||
"options": {
|
||||
"cwd": "{projectRoot}",
|
||||
"command": "VITE_DISABLE_ESLINT_CHECKER=true storybook build",
|
||||
"command": "VITE_DISABLE_TYPESCRIPT_CHECKER=true VITE_DISABLE_ESLINT_CHECKER=true storybook build",
|
||||
"output-dir": "storybook-static",
|
||||
"config-dir": ".storybook"
|
||||
}
|
||||
|
||||
+3
-3
@@ -60,6 +60,7 @@
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/passport-microsoft": "^1.0.3",
|
||||
"@wyw-in-js/vite": "^0.5.3",
|
||||
"@xyflow/react": "^12.3.5",
|
||||
"add": "^2.0.6",
|
||||
"addressparser": "^1.0.1",
|
||||
"afterframe": "^1.0.2",
|
||||
@@ -70,7 +71,7 @@
|
||||
"bcrypt": "^5.1.1",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"body-parser": "^1.20.2",
|
||||
"bullmq": "^4.14.0",
|
||||
"bullmq": "^4.15.0",
|
||||
"bytes": "^3.1.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -168,7 +169,6 @@
|
||||
"react-router-dom": "^6.4.4",
|
||||
"react-textarea-autosize": "^8.4.1",
|
||||
"react-tooltip": "^5.13.1",
|
||||
"reactflow": "^11.11.3",
|
||||
"recoil": "^0.7.7",
|
||||
"rehype-slug": "^6.0.0",
|
||||
"remark-behead": "^3.1.0",
|
||||
@@ -179,7 +179,7 @@
|
||||
"semver": "^7.5.4",
|
||||
"sharp": "^0.32.1",
|
||||
"slash": "^5.1.0",
|
||||
"stripe": "^14.17.0",
|
||||
"stripe": "^17.3.1",
|
||||
"ts-key-enum": "^2.0.12",
|
||||
"tslib": "^2.3.0",
|
||||
"tsup": "^8.2.4",
|
||||
|
||||
@@ -7,6 +7,9 @@ PG_DATABASE_HOST=db:5432
|
||||
REDIS_URL=redis://redis:6379
|
||||
|
||||
SERVER_URL=http://localhost:3000
|
||||
FRONT_DOMAIN=localhost
|
||||
FRONT_PORT=3000
|
||||
FRONT_PROTOCOL=http
|
||||
|
||||
# Use openssl rand -base64 32 for each secret
|
||||
# APP_SECRET=replace_me_with_a_random_string
|
||||
|
||||
@@ -22,9 +22,8 @@ services:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
PORT: 3000
|
||||
PG_DATABASE_URL: postgres://${PGUSER_SUPERUSER:-postgres}:${PGPASSWORD_SUPERUSER:-twenty}@${PG_DATABASE_HOST:-db:5432}/default
|
||||
PG_DATABASE_URL: postgres://${PGUSER_SUPERUSER:-postgres}:${PGPASSWORD_SUPERUSER:-postgres}@${PG_DATABASE_HOST:-db:5432}/default
|
||||
SERVER_URL: ${SERVER_URL}
|
||||
FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL}
|
||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
||||
|
||||
ENABLE_DB_MIGRATIONS: "true"
|
||||
@@ -52,11 +51,9 @@ services:
|
||||
image: twentycrm/twenty:${TAG}
|
||||
command: ["yarn", "worker:prod"]
|
||||
environment:
|
||||
PG_DATABASE_URL: postgres://${PGUSER_SUPERUSER:-postgres}:${PGPASSWORD_SUPERUSER:-twenty}@${PG_DATABASE_HOST:-db:5432}/default
|
||||
PG_DATABASE_URL: postgres://${PGUSER_SUPERUSER:-postgres}:${PGPASSWORD_SUPERUSER:-postgres}@${PG_DATABASE_HOST:-db:5432}/default
|
||||
SERVER_URL: ${SERVER_URL}
|
||||
FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL}
|
||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379}
|
||||
|
||||
ENABLE_DB_MIGRATIONS: "false" # it already runs on the server
|
||||
|
||||
STORAGE_TYPE: ${STORAGE_TYPE}
|
||||
@@ -78,7 +75,7 @@ services:
|
||||
- db-data:/home/postgres/pgdata
|
||||
environment:
|
||||
PGUSER_SUPERUSER: ${PGUSER_SUPERUSER:-postgres}
|
||||
PGPASSWORD_SUPERUSER: ${PGPASSWORD_SUPERUSER:-twenty}
|
||||
PGPASSWORD_SUPERUSER: ${PGPASSWORD_SUPERUSER:-postgres}
|
||||
ALLOW_NOSSL: "true"
|
||||
SPILO_PROVIDER: "local"
|
||||
healthcheck:
|
||||
|
||||
@@ -33,7 +33,7 @@ spec:
|
||||
- name: PGUSER_SUPERUSER
|
||||
value: "postgres"
|
||||
- name: PGPASSWORD_SUPERUSER
|
||||
value: "twenty"
|
||||
value: "postgres"
|
||||
ports:
|
||||
- containerPort: 5432
|
||||
name: tcp
|
||||
|
||||
@@ -37,10 +37,8 @@ spec:
|
||||
value: 3000
|
||||
- name: SERVER_URL
|
||||
value: "https://crm.example.com:443"
|
||||
- name: FRONT_BASE_URL
|
||||
value: "https://crm.example.com:443"
|
||||
- name: "PG_DATABASE_URL"
|
||||
value: "postgres://postgres:twenty@twenty-db.twentycrm.svc.cluster.local/default"
|
||||
value: "postgres://postgres:postgres@twentycrm-db.twentycrm.svc.cluster.local/default"
|
||||
- name: "REDIS_URL"
|
||||
value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379"
|
||||
- name: ENABLE_DB_MIGRATIONS
|
||||
|
||||
@@ -28,10 +28,8 @@ spec:
|
||||
env:
|
||||
- name: SERVER_URL
|
||||
value: "https://crm.example.com:443"
|
||||
- name: FRONT_BASE_URL
|
||||
value: "https://crm.example.com:443"
|
||||
- name: PG_DATABASE_URL
|
||||
value: "postgres://postgres:twenty@twenty-db.twentycrm.svc.cluster.local/default"
|
||||
value: "postgres://postgres:postgres@twentycrm-db.twentycrm.svc.cluster.local/default"
|
||||
- name: ENABLE_DB_MIGRATIONS
|
||||
value: "false" # it already runs on the server
|
||||
- name: STORAGE_TYPE
|
||||
|
||||
@@ -51,11 +51,6 @@ resource "kubernetes_deployment" "twentycrm_server" {
|
||||
value = var.twentycrm_app_hostname
|
||||
}
|
||||
|
||||
env {
|
||||
name = "FRONT_BASE_URL"
|
||||
value = var.twentycrm_app_hostname
|
||||
}
|
||||
|
||||
env {
|
||||
name = "PG_DATABASE_URL"
|
||||
value = "postgres://twenty:${var.twentycrm_pgdb_admin_password}@${kubernetes_service.twentycrm_db.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local/default"
|
||||
|
||||
@@ -43,11 +43,6 @@ resource "kubernetes_deployment" "twentycrm_worker" {
|
||||
value = var.twentycrm_app_hostname
|
||||
}
|
||||
|
||||
env {
|
||||
name = "FRONT_BASE_URL"
|
||||
value = var.twentycrm_app_hostname
|
||||
}
|
||||
|
||||
env {
|
||||
name = "PG_DATABASE_URL"
|
||||
value = "postgres://twenty:${var.twentycrm_pgdb_admin_password}@${kubernetes_service.twentycrm_db.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local/default"
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
pull_version=${VERSION:-$(curl -s https://api.github.com/repos/twentyhq/twenty/releases/latest | grep '"tag_name":' | cut -d '"' -f 4)}
|
||||
pull_version=${VERSION:-$(curl -s https://api.github.com/repos/twentyhq/twenty/tags | grep '"name":' | head -n 1 | cut -d '"' -f 4)}
|
||||
|
||||
if [[ -z "$pull_version" ]]; then
|
||||
echo "Error: Unable to fetch the latest version tag. Please check your network connection or the GitHub API response."
|
||||
exit 1
|
||||
fi
|
||||
pull_branch=${BRANCH:-$pull_version}
|
||||
|
||||
version_num=${pull_version#v}
|
||||
|
||||
@@ -44,7 +44,7 @@ function on_exit {
|
||||
trap on_exit EXIT
|
||||
|
||||
# Use environment variables VERSION and BRANCH, with defaults if not set
|
||||
version=${VERSION:-$(curl -s https://api.github.com/repos/twentyhq/twenty/releases/latest | grep '"tag_name":' | cut -d '"' -f 4)}
|
||||
version=${VERSION:-$(curl -s https://api.github.com/repos/twentyhq/twenty/tags | grep '"name":' | head -n 1 | cut -d '"' -f 4)}
|
||||
branch=${BRANCH:-$version}
|
||||
|
||||
echo "🚀 Using version $version and branch $branch"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# Note that provide always without trailing forward slash to have expected behaviour
|
||||
FRONTEND_BASE_URL=http://localhost:3001
|
||||
FRONTEND_BASE_URL=http://app.localhost:3001
|
||||
CI_DEFAULT_BASE_URL=https://demo.twenty.com
|
||||
DEFAULT_LOGIN=tim@apple.dev
|
||||
NEW_WORKSPACE_LOGIN=test@apple.dev
|
||||
|
||||
@@ -3,8 +3,7 @@ import path from 'path';
|
||||
|
||||
export const envVariables = (variables: string) => {
|
||||
let payload = `
|
||||
PG_DATABASE_URL=postgres://postgres:twenty@localhost:5432/default
|
||||
FRONT_BASE_URL=http://localhost:3001
|
||||
PG_DATABASE_URL=postgres://postgres:postgres@localhost:5432/default
|
||||
ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access
|
||||
LOGIN_TOKEN_SECRET=replace_me_with_a_random_string_login
|
||||
REFRESH_TOKEN_SECRET=replace_me_with_a_random_string_refresh
|
||||
|
||||
@@ -10,7 +10,7 @@ export class LoginPage {
|
||||
private readonly forgotPasswordButton: Locator;
|
||||
private readonly passwordField: Locator;
|
||||
private readonly revealPasswordButton: Locator;
|
||||
private readonly signInButton: Locator;
|
||||
readonly signInButton: Locator;
|
||||
private readonly signUpButton: Locator;
|
||||
private readonly previewImageButton: Locator;
|
||||
private readonly uploadImageButton: Locator;
|
||||
|
||||
@@ -16,12 +16,12 @@ export default defineConfig({
|
||||
fullyParallel: true, // false only for specific tests, overwritten in specific projects or global setups of projects
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: process.env.CI ? 1 : undefined, // undefined = amount of projects * amount of tests
|
||||
workers: 1, // 1 worker = 1 test at the time, tests can't be parallelized
|
||||
timeout: 30 * 1000, // timeout can be changed
|
||||
use: {
|
||||
baseURL: process.env.CI
|
||||
? process.env.CI_DEFAULT_BASE_URL
|
||||
: (process.env.FRONTEND_BASE_URL ?? 'http://localhost:3001'),
|
||||
: (process.env.FRONTEND_BASE_URL ?? 'http://app.localhost:3001'),
|
||||
trace: 'retain-on-failure', // trace takes EVERYTHING from page source, records every single step, should be used only when normal debugging won't work
|
||||
screenshot: 'on', // either 'on' here or in different method in modules, if 'on' all screenshots are overwritten each time the test is run
|
||||
headless: true, // instead of changing it to false, run 'yarn test:e2e:debug' or 'yarn test:e2e:ui'
|
||||
@@ -56,6 +56,10 @@ export default defineConfig({
|
||||
},
|
||||
dependencies: ['Login setup'],
|
||||
},
|
||||
{
|
||||
name: 'Authentication',
|
||||
testMatch: /authentication\/.*\.spec\.ts/,
|
||||
},
|
||||
|
||||
//{
|
||||
// name: 'webkit',
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { test as base, expect } from '../../lib/fixtures/screenshot';
|
||||
import { LoginPage } from '../../lib/pom/loginPage';
|
||||
|
||||
// fixture
|
||||
const test = base.extend<{ loginPage: LoginPage }>({
|
||||
loginPage: async ({ page }, use) => {
|
||||
await use(new LoginPage(page));
|
||||
},
|
||||
});
|
||||
|
||||
test('Check login with email', async ({ loginPage }) => {
|
||||
await loginPage.typeEmail(process.env.DEFAULT_LOGIN);
|
||||
await loginPage.clickContinueButton();
|
||||
await loginPage.typePassword(process.env.DEFAULT_PASSWORD);
|
||||
await loginPage.clickSignInButton();
|
||||
await expect(loginPage.signInButton).not.toBeVisible();
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-emails",
|
||||
"version": "0.33.5",
|
||||
"version": "0.34.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
|
||||
@@ -18,8 +18,9 @@ type SendInviteLinkEmailProps = {
|
||||
sender: {
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
serverUrl?: string;
|
||||
serverUrl: string;
|
||||
};
|
||||
|
||||
export const SendInviteLinkEmail = ({
|
||||
@@ -28,14 +29,17 @@ export const SendInviteLinkEmail = ({
|
||||
sender,
|
||||
serverUrl,
|
||||
}: SendInviteLinkEmailProps) => {
|
||||
const workspaceLogo = getImageAbsoluteURI(workspace.logo, serverUrl);
|
||||
const workspaceLogo = workspace.logo
|
||||
? getImageAbsoluteURI(workspace.logo, serverUrl)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<BaseEmail width={333}>
|
||||
<Title value="Join your team on Twenty" />
|
||||
<MainText>
|
||||
{capitalize(sender.firstName)} (
|
||||
<Link
|
||||
href={sender.email}
|
||||
href={`mailto:${sender.email}`}
|
||||
value={sender.email}
|
||||
color={emailTheme.font.colors.blue}
|
||||
/>
|
||||
|
||||
@@ -1,16 +1,9 @@
|
||||
export const getImageAbsoluteURI = (
|
||||
imageUrl?: string | null,
|
||||
serverUrl?: string,
|
||||
) => {
|
||||
if (!imageUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (imageUrl?.startsWith('https:')) {
|
||||
export const getImageAbsoluteURI = (imageUrl: string, serverUrl: string) => {
|
||||
if (imageUrl.startsWith('https:') || imageUrl.startsWith('http:')) {
|
||||
return imageUrl;
|
||||
}
|
||||
|
||||
return serverUrl?.endsWith('/')
|
||||
return serverUrl.endsWith('/')
|
||||
? `${serverUrl.substring(0, serverUrl.length - 1)}/files/${imageUrl}`
|
||||
: `${serverUrl || ''}/files/${imageUrl}`;
|
||||
: `${serverUrl}/files/${imageUrl}`;
|
||||
};
|
||||
|
||||
@@ -7,5 +7,6 @@ GENERATE_SOURCEMAP=false
|
||||
# VITE_DISABLE_TYPESCRIPT_CHECKER=true
|
||||
# VITE_DISABLE_ESLINT_CHECKER=true
|
||||
# VITE_ENABLE_SSL=false
|
||||
# VITE_HOST=localhost.com
|
||||
# SSL_KEY_PATH="./certs/your-cert.key"
|
||||
# SSL_CERT_PATH="./certs/your-cert.crt"
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "twenty-front",
|
||||
"version": "0.33.5",
|
||||
"version": "0.34.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -69,7 +69,7 @@
|
||||
"test": {},
|
||||
"storybook:build": {
|
||||
"options": {
|
||||
"env": { "NODE_OPTIONS": "--max_old_space_size=6500" }
|
||||
"env": { "NODE_OPTIONS": "--max_old_space_size=8000" }
|
||||
},
|
||||
"configurations": {
|
||||
"docs": { "env": { "STORYBOOK_SCOPE": "ui-docs" } },
|
||||
|
||||
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.8 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.9 KiB |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -234,17 +234,6 @@ const testCases = [
|
||||
{ loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
|
||||
{ loc: AppPath.DevelopersCatchAll, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined },
|
||||
|
||||
{ loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
|
||||
{ loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
|
||||
{ loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
|
||||
{ loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.PastDue, onboardingStatus: OnboardingStatus.Completed, res: undefined },
|
||||
{ loc: AppPath.Impersonate, isLoggedIn: false, subscriptionStatus: undefined, onboardingStatus: undefined, res: AppPath.SignInUp },
|
||||
{ loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.WorkspaceActivation, res: AppPath.CreateWorkspace },
|
||||
{ loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.ProfileCreation, res: AppPath.CreateProfile },
|
||||
{ loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.SyncEmail, res: AppPath.SyncEmails },
|
||||
{ loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.InviteTeam, res: AppPath.InviteTeam },
|
||||
{ loc: AppPath.Impersonate, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Active, onboardingStatus: OnboardingStatus.Completed, res: undefined },
|
||||
|
||||
{ loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: undefined, onboardingStatus: OnboardingStatus.PlanRequired, res: AppPath.PlanRequired },
|
||||
{ loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Canceled, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
|
||||
{ loc: AppPath.Authorize, isLoggedIn: true, subscriptionStatus: SubscriptionStatus.Unpaid, onboardingStatus: OnboardingStatus.Completed, res: '/settings/billing' },
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import React, { Ref, RefCallback } from 'react';
|
||||
import { isFunction } from '@sniptt/guards';
|
||||
import { Ref, RefCallback } from 'react';
|
||||
import { combineRefs } from '~/utils/combineRefs';
|
||||
|
||||
export const useCombinedRefs =
|
||||
<T>(...refs: (Ref<T> | undefined)[]): RefCallback<T> =>
|
||||
(node: T) => {
|
||||
for (const ref of refs) {
|
||||
if (isFunction(ref)) {
|
||||
ref(node);
|
||||
} else if (ref !== null && ref !== undefined) {
|
||||
(ref as React.MutableRefObject<T | null>).current = node;
|
||||
}
|
||||
}
|
||||
};
|
||||
export const useCombinedRefs = <T>(
|
||||
...refs: (Ref<T> | undefined)[]
|
||||
): RefCallback<T> => combineRefs<T>(...refs);
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
|
||||
export const usePreventOverlapCallback = (
|
||||
callback: () => Promise<void>,
|
||||
wait?: number,
|
||||
) => {
|
||||
const [isRunning, setIsRunning] = useState(false);
|
||||
const [pendingRun, setPendingRun] = useState(false);
|
||||
|
||||
const handleCallback = async () => {
|
||||
if (isRunning) {
|
||||
setPendingRun(true);
|
||||
return;
|
||||
}
|
||||
setIsRunning(true);
|
||||
try {
|
||||
await callback();
|
||||
} finally {
|
||||
setIsRunning(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRunning && pendingRun) {
|
||||
setPendingRun(false);
|
||||
callback();
|
||||
}
|
||||
}, [callback, isRunning, pendingRun, setPendingRun]);
|
||||
|
||||
return useDebouncedCallback(handleCallback, wait);
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { BACKGROUND_LIGHT, GRAY_SCALE } from 'twenty-ui';
|
||||
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
|
||||
import Skeleton, { SkeletonTheme } from 'react-loading-skeleton';
|
||||
|
||||
const StyledRightDrawerContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
padding: ${({ theme }) => theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledSkeletonLoader = () => {
|
||||
return (
|
||||
<SkeletonTheme
|
||||
baseColor={GRAY_SCALE.gray15}
|
||||
highlightColor={BACKGROUND_LIGHT.transparent.lighter}
|
||||
borderRadius={4}
|
||||
>
|
||||
<Skeleton height={SKELETON_LOADER_HEIGHT_SIZES.standard.m} width={140} />
|
||||
</SkeletonTheme>
|
||||
);
|
||||
};
|
||||
|
||||
export const RightDrawerSkeletonLoader = () => {
|
||||
return (
|
||||
<StyledRightDrawerContainer>
|
||||
<StyledSkeletonLoader />
|
||||
</StyledRightDrawerContainer>
|
||||
);
|
||||
};
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
import { WorkflowRunActionEffect } from '@/action-menu/actions/global-actions/workflow-run-actions/components/WorkflowRunActionEffect';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
|
||||
export const GlobalActionMenuEntriesSetter = () => {
|
||||
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
|
||||
|
||||
return <>{isWorkflowEnabled && <WorkflowRunActionEffect />}</>;
|
||||
};
|
||||
-75
@@ -1,75 +0,0 @@
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useAllActiveWorkflowVersions } from '@/workflow/hooks/useAllActiveWorkflowVersions';
|
||||
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
|
||||
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useEffect } from 'react';
|
||||
import { IconSettingsAutomation } from 'twenty-ui';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const WorkflowRunActionEffect = () => {
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
|
||||
const { records: activeWorkflowVersions } = useAllActiveWorkflowVersions({
|
||||
triggerType: 'MANUAL',
|
||||
});
|
||||
|
||||
const { runWorkflowVersion } = useRunWorkflowVersion();
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
for (const [
|
||||
index,
|
||||
activeWorkflowVersion,
|
||||
] of activeWorkflowVersions.entries()) {
|
||||
addActionMenuEntry({
|
||||
type: ActionMenuEntryType.WorkflowRun,
|
||||
key: `workflow-run-${activeWorkflowVersion.id}`,
|
||||
scope: ActionMenuEntryScope.Global,
|
||||
label: capitalize(activeWorkflowVersion.workflow.name),
|
||||
position: index,
|
||||
Icon: IconSettingsAutomation,
|
||||
onClick: async () => {
|
||||
await runWorkflowVersion({
|
||||
workflowVersionId: activeWorkflowVersion.id,
|
||||
});
|
||||
|
||||
enqueueSnackBar('', {
|
||||
variant: SnackBarVariant.Success,
|
||||
title: `${capitalize(activeWorkflowVersion.workflow.name)} starting...`,
|
||||
icon: (
|
||||
<IconSettingsAutomation
|
||||
size={16}
|
||||
color={theme.snackBar.success.color}
|
||||
/>
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return () => {
|
||||
for (const activeWorkflowVersion of activeWorkflowVersions) {
|
||||
removeActionMenuEntry(`workflow-run-${activeWorkflowVersion.id}`);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
activeWorkflowVersions,
|
||||
addActionMenuEntry,
|
||||
enqueueSnackBar,
|
||||
removeActionMenuEntry,
|
||||
runWorkflowVersion,
|
||||
theme.snackBar.success.color,
|
||||
]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+40
-36
@@ -1,32 +1,30 @@
|
||||
import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect';
|
||||
import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect';
|
||||
import { ManageFavoritesActionEffect } from '@/action-menu/actions/record-actions/components/ManageFavoritesActionEffect';
|
||||
import { WorkflowRunRecordActionEffect } from '@/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionEffect';
|
||||
import { MultipleRecordsActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/multiple-records/components/MultipleRecordsActionMenuEntrySetterEffect';
|
||||
import { NoSelectionActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/no-selection/components/NoSelectionActionMenuEntrySetterEffect';
|
||||
import { SingleRecordActionMenuEntrySetter } from '@/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetter';
|
||||
import { WorkflowRunRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/workflow-run-record-actions/components/WorkflowRunRecordActionMenuEntrySetter';
|
||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
const noSelectionRecordActionEffects = [ExportRecordsActionEffect];
|
||||
|
||||
const singleRecordActionEffects = [
|
||||
ManageFavoritesActionEffect,
|
||||
DeleteRecordsActionEffect,
|
||||
];
|
||||
|
||||
const multipleRecordActionEffects = [
|
||||
ExportRecordsActionEffect,
|
||||
DeleteRecordsActionEffect,
|
||||
];
|
||||
|
||||
export const RecordActionMenuEntriesSetter = () => {
|
||||
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
|
||||
contextStoreCurrentObjectMetadataIdComponentState,
|
||||
);
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
|
||||
if (!isDefined(contextStoreCurrentObjectMetadataId)) {
|
||||
const objectMetadataItem = objectMetadataItems.find(
|
||||
(item) => item.id === contextStoreCurrentObjectMetadataId,
|
||||
);
|
||||
|
||||
if (
|
||||
!isDefined(contextStoreCurrentObjectMetadataId) ||
|
||||
!isDefined(objectMetadataItem)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -44,30 +42,36 @@ const ActionEffects = ({
|
||||
objectId: objectMetadataItemId,
|
||||
});
|
||||
|
||||
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
||||
contextStoreNumberOfSelectedRecordsComponentState,
|
||||
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
);
|
||||
|
||||
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
|
||||
|
||||
const actions =
|
||||
contextStoreNumberOfSelectedRecords === 0
|
||||
? noSelectionRecordActionEffects
|
||||
: contextStoreNumberOfSelectedRecords === 1
|
||||
? singleRecordActionEffects
|
||||
: multipleRecordActionEffects;
|
||||
|
||||
return (
|
||||
<>
|
||||
{actions.map((ActionEffect, index) => (
|
||||
<ActionEffect
|
||||
key={index}
|
||||
position={index}
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
))}
|
||||
{contextStoreNumberOfSelectedRecords === 1 && isWorkflowEnabled && (
|
||||
<WorkflowRunRecordActionEffect
|
||||
{contextStoreTargetedRecordsRule.mode === 'selection' &&
|
||||
contextStoreTargetedRecordsRule.selectedRecordIds.length === 0 && (
|
||||
<NoSelectionActionMenuEntrySetterEffect
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
)}
|
||||
{contextStoreTargetedRecordsRule.mode === 'selection' &&
|
||||
contextStoreTargetedRecordsRule.selectedRecordIds.length === 1 && (
|
||||
<>
|
||||
<SingleRecordActionMenuEntrySetter
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
{isWorkflowEnabled && (
|
||||
<WorkflowRunRecordActionMenuEntrySetterEffect
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{(contextStoreTargetedRecordsRule.mode === 'exclusion' ||
|
||||
contextStoreTargetedRecordsRule.selectedRecordIds.length > 1) && (
|
||||
<MultipleRecordsActionMenuEntrySetterEffect
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
)}
|
||||
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { useMultipleRecordsActions } from '@/action-menu/actions/record-actions/multiple-records/hooks/useMultipleRecordsActions';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const MultipleRecordsActionMenuEntrySetterEffect = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const { registerMultipleRecordsActions, unregisterMultipleRecordsActions } =
|
||||
useMultipleRecordsActions({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
registerMultipleRecordsActions();
|
||||
|
||||
return () => {
|
||||
unregisterMultipleRecordsActions();
|
||||
};
|
||||
}, [registerMultipleRecordsActions, unregisterMultipleRecordsActions]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+132
@@ -0,0 +1,132 @@
|
||||
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { expect } from '@storybook/test';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import { useDeleteMultipleRecordsAction } from '../useDeleteMultipleRecordsAction';
|
||||
|
||||
jest.mock('@/object-record/hooks/useDeleteManyRecords', () => ({
|
||||
useDeleteManyRecords: () => ({
|
||||
deleteManyRecords: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
jest.mock('@/favorites/hooks/useDeleteFavorite', () => ({
|
||||
useDeleteFavorite: () => ({
|
||||
deleteFavorite: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
jest.mock('@/favorites/hooks/useFavorites', () => ({
|
||||
useFavorites: () => ({
|
||||
sortedFavorites: [],
|
||||
}),
|
||||
}));
|
||||
jest.mock('@/object-record/record-table/hooks/useRecordTable', () => ({
|
||||
useRecordTable: () => ({
|
||||
resetTableRowSelection: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'company',
|
||||
)!;
|
||||
|
||||
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: [],
|
||||
onInitializeRecoilSnapshot: ({ set }) => {
|
||||
set(
|
||||
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
|
||||
instanceId: '1',
|
||||
}),
|
||||
3,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
describe('useDeleteMultipleRecordsAction', () => {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<JestMetadataAndApolloMocksWrapper>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: '1',
|
||||
}}
|
||||
>
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: '1',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</JestMetadataAndApolloMocksWrapper>
|
||||
);
|
||||
|
||||
it('should register delete action', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
actionMenuEntries,
|
||||
useDeleteMultipleRecordsAction: useDeleteMultipleRecordsAction({
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.useDeleteMultipleRecordsAction.registerDeleteMultipleRecordsAction(
|
||||
{ position: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(1);
|
||||
expect(
|
||||
result.current.actionMenuEntries.get('delete-multiple-records'),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
result.current.actionMenuEntries.get('delete-multiple-records')?.position,
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('should unregister delete action', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
actionMenuEntries,
|
||||
useDeleteMultipleRecordsAction: useDeleteMultipleRecordsAction({
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.useDeleteMultipleRecordsAction.registerDeleteMultipleRecordsAction(
|
||||
{ position: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(1);
|
||||
|
||||
act(() => {
|
||||
result.current.useDeleteMultipleRecordsAction.unregisterDeleteMultipleRecordsAction();
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(0);
|
||||
});
|
||||
});
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { expect } from '@storybook/test';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import { useExportMultipleRecordsAction } from '../useExportMultipleRecordsAction';
|
||||
|
||||
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'company',
|
||||
)!;
|
||||
|
||||
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: [],
|
||||
});
|
||||
|
||||
describe('useExportMultipleRecordsAction', () => {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<JestMetadataAndApolloMocksWrapper>
|
||||
<JestObjectMetadataItemSetter>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: '1',
|
||||
}}
|
||||
>
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: '1',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</JestObjectMetadataItemSetter>
|
||||
</JestMetadataAndApolloMocksWrapper>
|
||||
);
|
||||
|
||||
it('should register export multiple records action', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
actionMenuEntries,
|
||||
useExportMultipleRecordsAction: useExportMultipleRecordsAction({
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.useExportMultipleRecordsAction.registerExportMultipleRecordsAction(
|
||||
{ position: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(1);
|
||||
expect(
|
||||
result.current.actionMenuEntries.get('export-multiple-records'),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
result.current.actionMenuEntries.get('export-multiple-records')?.position,
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('should unregister export multiple records action', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
actionMenuEntries,
|
||||
useExportMultipleRecordsAction: useExportMultipleRecordsAction({
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.useExportMultipleRecordsAction.registerExportMultipleRecordsAction(
|
||||
{ position: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(1);
|
||||
|
||||
act(() => {
|
||||
result.current.useExportMultipleRecordsAction.unregisterExportMultipleRecordsAction();
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(0);
|
||||
});
|
||||
});
|
||||
+19
-37
@@ -18,14 +18,12 @@ import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTabl
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useCallback, useContext, useEffect, useState } from 'react';
|
||||
import { useCallback, useContext, useState } from 'react';
|
||||
import { IconTrash, isDefined } from 'twenty-ui';
|
||||
|
||||
export const DeleteRecordsActionEffect = ({
|
||||
position,
|
||||
export const useDeleteMultipleRecordsAction = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
position: number;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
@@ -106,12 +104,16 @@ export const DeleteRecordsActionEffect = ({
|
||||
const { isInRightDrawer, onActionExecutedCallback } =
|
||||
useContext(ActionMenuContext);
|
||||
|
||||
useEffect(() => {
|
||||
const registerDeleteMultipleRecordsAction = ({
|
||||
position,
|
||||
}: {
|
||||
position: number;
|
||||
}) => {
|
||||
if (canDelete) {
|
||||
addActionMenuEntry({
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'delete',
|
||||
key: 'delete-multiple-records',
|
||||
label: 'Delete',
|
||||
position,
|
||||
Icon: IconTrash,
|
||||
@@ -124,16 +126,8 @@ export const DeleteRecordsActionEffect = ({
|
||||
<ConfirmationModal
|
||||
isOpen={isDeleteRecordsModalOpen}
|
||||
setIsOpen={setIsDeleteRecordsModalOpen}
|
||||
title={`Delete ${contextStoreNumberOfSelectedRecords} ${
|
||||
contextStoreNumberOfSelectedRecords === 1 ? `record` : 'records'
|
||||
}`}
|
||||
subtitle={`Are you sure you want to delete ${
|
||||
contextStoreNumberOfSelectedRecords === 1
|
||||
? 'this record'
|
||||
: 'these records'
|
||||
}? ${
|
||||
contextStoreNumberOfSelectedRecords === 1 ? 'It' : 'They'
|
||||
} can be recovered from the Options menu.`}
|
||||
title={'Delete Records'}
|
||||
subtitle={`Are you sure you want to delete these records? They can be recovered from the Options menu.`}
|
||||
onConfirmClick={() => {
|
||||
handleDeleteClick();
|
||||
onActionExecutedCallback?.();
|
||||
@@ -141,31 +135,19 @@ export const DeleteRecordsActionEffect = ({
|
||||
closeRightDrawer();
|
||||
}
|
||||
}}
|
||||
deleteButtonText={`Delete ${
|
||||
contextStoreNumberOfSelectedRecords > 1 ? 'Records' : 'Record'
|
||||
}`}
|
||||
deleteButtonText={'Delete Records'}
|
||||
/>
|
||||
),
|
||||
});
|
||||
} else {
|
||||
removeActionMenuEntry('delete');
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
removeActionMenuEntry('delete');
|
||||
};
|
||||
}, [
|
||||
addActionMenuEntry,
|
||||
canDelete,
|
||||
closeRightDrawer,
|
||||
contextStoreNumberOfSelectedRecords,
|
||||
handleDeleteClick,
|
||||
isDeleteRecordsModalOpen,
|
||||
isInRightDrawer,
|
||||
onActionExecutedCallback,
|
||||
position,
|
||||
removeActionMenuEntry,
|
||||
]);
|
||||
const unregisterDeleteMultipleRecordsAction = () => {
|
||||
removeActionMenuEntry('delete-multiple-records');
|
||||
};
|
||||
|
||||
return null;
|
||||
return {
|
||||
registerDeleteMultipleRecordsAction,
|
||||
unregisterDeleteMultipleRecordsAction,
|
||||
};
|
||||
};
|
||||
+16
-27
@@ -1,7 +1,5 @@
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { IconDatabaseExport } from 'twenty-ui';
|
||||
|
||||
import {
|
||||
@@ -12,19 +10,13 @@ import {
|
||||
displayedExportProgress,
|
||||
useExportRecords,
|
||||
} from '@/object-record/record-index/export/hooks/useExportRecords';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const ExportRecordsActionEffect = ({
|
||||
position,
|
||||
export const useExportMultipleRecordsAction = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
position: number;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
||||
contextStoreNumberOfSelectedRecordsComponentState,
|
||||
);
|
||||
|
||||
const { progress, download } = useExportRecords({
|
||||
delayMs: 100,
|
||||
@@ -33,32 +25,29 @@ export const ExportRecordsActionEffect = ({
|
||||
filename: `${objectMetadataItem.nameSingular}.csv`,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const registerExportMultipleRecordsAction = ({
|
||||
position,
|
||||
}: {
|
||||
position: number;
|
||||
}) => {
|
||||
addActionMenuEntry({
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope:
|
||||
contextStoreNumberOfSelectedRecords > 0
|
||||
? ActionMenuEntryScope.RecordSelection
|
||||
: ActionMenuEntryScope.Global,
|
||||
key: 'export',
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'export-multiple-records',
|
||||
position,
|
||||
label: displayedExportProgress(progress),
|
||||
Icon: IconDatabaseExport,
|
||||
accent: 'default',
|
||||
onClick: () => download(),
|
||||
});
|
||||
};
|
||||
|
||||
return () => {
|
||||
removeActionMenuEntry('export');
|
||||
};
|
||||
}, [
|
||||
contextStoreNumberOfSelectedRecords,
|
||||
download,
|
||||
progress,
|
||||
addActionMenuEntry,
|
||||
removeActionMenuEntry,
|
||||
position,
|
||||
]);
|
||||
const unregisterExportMultipleRecordsAction = () => {
|
||||
removeActionMenuEntry('export-multiple-records');
|
||||
};
|
||||
|
||||
return null;
|
||||
return {
|
||||
registerExportMultipleRecordsAction,
|
||||
unregisterExportMultipleRecordsAction,
|
||||
};
|
||||
};
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { useDeleteMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/hooks/useDeleteMultipleRecordsAction';
|
||||
import { useExportMultipleRecordsAction } from '@/action-menu/actions/record-actions/multiple-records/hooks/useExportMultipleRecordsAction';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
export const useMultipleRecordsActions = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const {
|
||||
registerDeleteMultipleRecordsAction,
|
||||
unregisterDeleteMultipleRecordsAction,
|
||||
} = useDeleteMultipleRecordsAction({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const {
|
||||
registerExportMultipleRecordsAction,
|
||||
unregisterExportMultipleRecordsAction,
|
||||
} = useExportMultipleRecordsAction({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const registerMultipleRecordsActions = () => {
|
||||
registerDeleteMultipleRecordsAction({ position: 1 });
|
||||
registerExportMultipleRecordsAction({ position: 2 });
|
||||
};
|
||||
|
||||
const unregisterMultipleRecordsActions = () => {
|
||||
unregisterDeleteMultipleRecordsAction();
|
||||
unregisterExportMultipleRecordsAction();
|
||||
};
|
||||
|
||||
return {
|
||||
registerMultipleRecordsActions,
|
||||
unregisterMultipleRecordsActions,
|
||||
};
|
||||
};
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { useNoSelectionRecordActions } from '@/action-menu/actions/record-actions/no-selection/hooks/useNoSelectionRecordActions';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const NoSelectionActionMenuEntrySetterEffect = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const {
|
||||
registerNoSelectionRecordActions,
|
||||
unregisterNoSelectionRecordActions,
|
||||
} = useNoSelectionRecordActions({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
registerNoSelectionRecordActions();
|
||||
|
||||
return () => {
|
||||
unregisterNoSelectionRecordActions();
|
||||
};
|
||||
}, [registerNoSelectionRecordActions, unregisterNoSelectionRecordActions]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { expect } from '@storybook/test';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
|
||||
import { useExportViewNoSelectionRecordAction } from '@/action-menu/actions/record-actions/no-selection/hooks/useExportViewNoSelectionRecordAction';
|
||||
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'company',
|
||||
)!;
|
||||
|
||||
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: [],
|
||||
});
|
||||
|
||||
describe('useExportViewNoSelectionRecordAction', () => {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<JestMetadataAndApolloMocksWrapper>
|
||||
<JestObjectMetadataItemSetter>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: '1',
|
||||
}}
|
||||
>
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: '1',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</JestObjectMetadataItemSetter>
|
||||
</JestMetadataAndApolloMocksWrapper>
|
||||
);
|
||||
|
||||
it('should register export view action', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
actionMenuEntries,
|
||||
useExportViewNoSelectionRecordAction:
|
||||
useExportViewNoSelectionRecordAction({
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.useExportViewNoSelectionRecordAction.registerExportViewNoSelectionRecordsAction(
|
||||
{ position: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(1);
|
||||
expect(
|
||||
result.current.actionMenuEntries.get('export-view-no-selection'),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
result.current.actionMenuEntries.get('export-view-no-selection')
|
||||
?.position,
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('should unregister export view action', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
actionMenuEntries,
|
||||
useExportViewNoSelectionRecordAction:
|
||||
useExportViewNoSelectionRecordAction({
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.useExportViewNoSelectionRecordAction.registerExportViewNoSelectionRecordsAction(
|
||||
{ position: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(1);
|
||||
|
||||
act(() => {
|
||||
result.current.useExportViewNoSelectionRecordAction.unregisterExportViewNoSelectionRecordsAction();
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(0);
|
||||
});
|
||||
});
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { IconDatabaseExport } from 'twenty-ui';
|
||||
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import {
|
||||
displayedExportProgress,
|
||||
useExportRecords,
|
||||
} from '@/object-record/record-index/export/hooks/useExportRecords';
|
||||
|
||||
export const useExportViewNoSelectionRecordAction = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
|
||||
const { progress, download } = useExportRecords({
|
||||
delayMs: 100,
|
||||
objectMetadataItem,
|
||||
recordIndexId: objectMetadataItem.namePlural,
|
||||
filename: `${objectMetadataItem.nameSingular}.csv`,
|
||||
});
|
||||
|
||||
const registerExportViewNoSelectionRecordsAction = ({
|
||||
position,
|
||||
}: {
|
||||
position: number;
|
||||
}) => {
|
||||
addActionMenuEntry({
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.Global,
|
||||
key: 'export-view-no-selection',
|
||||
position,
|
||||
label: displayedExportProgress(progress),
|
||||
Icon: IconDatabaseExport,
|
||||
accent: 'default',
|
||||
onClick: () => download(),
|
||||
});
|
||||
};
|
||||
|
||||
const unregisterExportViewNoSelectionRecordsAction = () => {
|
||||
removeActionMenuEntry('export-view-no-selection');
|
||||
};
|
||||
|
||||
return {
|
||||
registerExportViewNoSelectionRecordsAction,
|
||||
unregisterExportViewNoSelectionRecordsAction,
|
||||
};
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { useExportViewNoSelectionRecordAction } from '@/action-menu/actions/record-actions/no-selection/hooks/useExportViewNoSelectionRecordAction';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
export const useNoSelectionRecordActions = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const {
|
||||
registerExportViewNoSelectionRecordsAction,
|
||||
unregisterExportViewNoSelectionRecordsAction,
|
||||
} = useExportViewNoSelectionRecordAction({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const registerNoSelectionRecordActions = () => {
|
||||
registerExportViewNoSelectionRecordsAction({ position: 1 });
|
||||
};
|
||||
|
||||
const unregisterNoSelectionRecordActions = () => {
|
||||
unregisterExportViewNoSelectionRecordsAction();
|
||||
};
|
||||
|
||||
return {
|
||||
registerNoSelectionRecordActions,
|
||||
unregisterNoSelectionRecordActions,
|
||||
};
|
||||
};
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { SingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/components/SingleRecordActionMenuEntrySetterEffect';
|
||||
import { WorkflowSingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/workflow-actions/components/WorkflowSingleRecordActionMenuEntrySetterEffect';
|
||||
import { WorkflowVersionsSingleRecordActionMenuEntrySetterEffect } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/components/WorkflowVersionsSingleRecordActionMenuEntrySetterEffect';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
export const SingleRecordActionMenuEntrySetter = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<SingleRecordActionMenuEntrySetterEffect
|
||||
objectMetadataItem={objectMetadataItem}
|
||||
/>
|
||||
{objectMetadataItem.nameSingular === CoreObjectNameSingular.Workflow && (
|
||||
<WorkflowSingleRecordActionMenuEntrySetterEffect />
|
||||
)}
|
||||
{objectMetadataItem.nameSingular ===
|
||||
CoreObjectNameSingular.WorkflowVersion && (
|
||||
<WorkflowVersionsSingleRecordActionMenuEntrySetterEffect />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useEffect } from 'react';
|
||||
import { useSingleRecordActions } from '../hooks/useSingleRecordActions';
|
||||
|
||||
export const SingleRecordActionMenuEntrySetterEffect = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const { registerSingleRecordActions, unregisterSingleRecordActions } =
|
||||
useSingleRecordActions({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
registerSingleRecordActions();
|
||||
|
||||
return () => {
|
||||
unregisterSingleRecordActions();
|
||||
};
|
||||
}, [registerSingleRecordActions, unregisterSingleRecordActions]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+1
@@ -0,0 +1 @@
|
||||
export const NUMBER_OF_STANDARD_SINGLE_RECORD_ACTIONS_ON_ALL_OBJECTS = 2;
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { expect } from '@storybook/test';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import { useDeleteSingleRecordAction } from '../useDeleteSingleRecordAction';
|
||||
|
||||
jest.mock('@/object-record/hooks/useDeleteOneRecord', () => ({
|
||||
useDeleteOneRecord: () => ({
|
||||
deleteOneRecord: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
jest.mock('@/favorites/hooks/useDeleteFavorite', () => ({
|
||||
useDeleteFavorite: () => ({
|
||||
deleteFavorite: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
jest.mock('@/favorites/hooks/useFavorites', () => ({
|
||||
useFavorites: () => ({
|
||||
sortedFavorites: [],
|
||||
}),
|
||||
}));
|
||||
jest.mock('@/object-record/record-table/hooks/useRecordTable', () => ({
|
||||
useRecordTable: () => ({
|
||||
resetTableRowSelection: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'company',
|
||||
)!;
|
||||
|
||||
describe('useDeleteSingleRecordAction', () => {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<RecoilRoot>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: '1',
|
||||
}}
|
||||
>
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: '1',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</RecoilRoot>
|
||||
);
|
||||
|
||||
it('should register delete action', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
actionMenuEntries,
|
||||
useDeleteSingleRecordAction: useDeleteSingleRecordAction({
|
||||
recordId: 'record1',
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.useDeleteSingleRecordAction.registerDeleteSingleRecordAction(
|
||||
{ position: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(1);
|
||||
expect(
|
||||
result.current.actionMenuEntries.get('delete-single-record'),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
result.current.actionMenuEntries.get('delete-single-record')?.position,
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('should unregister delete action', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
actionMenuEntries,
|
||||
useDeleteSingleRecordAction: useDeleteSingleRecordAction({
|
||||
recordId: 'record1',
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.useDeleteSingleRecordAction.registerDeleteSingleRecordAction(
|
||||
{ position: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(1);
|
||||
|
||||
act(() => {
|
||||
result.current.useDeleteSingleRecordAction.unregisterDeleteSingleRecordAction();
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(0);
|
||||
});
|
||||
});
|
||||
+108
@@ -0,0 +1,108 @@
|
||||
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { expect } from '@storybook/test';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems';
|
||||
import { useManageFavoritesSingleRecordAction } from '../useManageFavoritesSingleRecordAction';
|
||||
|
||||
const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find(
|
||||
(item) => item.nameSingular === 'company',
|
||||
)!;
|
||||
|
||||
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: [],
|
||||
});
|
||||
|
||||
describe('useManageFavoritesSingleRecordAction', () => {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<JestMetadataAndApolloMocksWrapper>
|
||||
<JestObjectMetadataItemSetter>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: '1',
|
||||
}}
|
||||
>
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: '1',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</JestObjectMetadataItemSetter>
|
||||
</JestMetadataAndApolloMocksWrapper>
|
||||
);
|
||||
|
||||
it('should register manage favorites action', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
actionMenuEntries,
|
||||
useManageFavoritesSingleRecordAction:
|
||||
useManageFavoritesSingleRecordAction({
|
||||
recordId: 'record1',
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.useManageFavoritesSingleRecordAction.registerManageFavoritesSingleRecordAction(
|
||||
{ position: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(1);
|
||||
expect(
|
||||
result.current.actionMenuEntries.get('manage-favorites-single-record'),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
result.current.actionMenuEntries.get('manage-favorites-single-record')
|
||||
?.position,
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('should unregister manage favorites action', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
actionMenuEntries,
|
||||
useManageFavoritesSingleRecordAction:
|
||||
useManageFavoritesSingleRecordAction({
|
||||
recordId: 'record1',
|
||||
objectMetadataItem: companyMockObjectMetadataItem,
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.useManageFavoritesSingleRecordAction.registerManageFavoritesSingleRecordAction(
|
||||
{ position: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.useManageFavoritesSingleRecordAction.unregisterManageFavoritesSingleRecordAction();
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(0);
|
||||
});
|
||||
});
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable';
|
||||
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
|
||||
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
|
||||
import { useCallback, useContext, useState } from 'react';
|
||||
import { IconTrash, isDefined } from 'twenty-ui';
|
||||
|
||||
export const useDeleteSingleRecordAction = ({
|
||||
recordId,
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
recordId: string;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
|
||||
const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] =
|
||||
useState(false);
|
||||
|
||||
const { resetTableRowSelection } = useRecordTable({
|
||||
recordTableId: objectMetadataItem.namePlural,
|
||||
});
|
||||
|
||||
const { deleteOneRecord } = useDeleteOneRecord({
|
||||
objectNameSingular: objectMetadataItem.nameSingular,
|
||||
});
|
||||
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
const { deleteFavorite } = useDeleteFavorite();
|
||||
|
||||
const { closeRightDrawer } = useRightDrawer();
|
||||
|
||||
const handleDeleteClick = useCallback(async () => {
|
||||
resetTableRowSelection();
|
||||
|
||||
const foundFavorite = favorites?.find(
|
||||
(favorite) => favorite.recordId === recordId,
|
||||
);
|
||||
|
||||
if (isDefined(foundFavorite)) {
|
||||
deleteFavorite(foundFavorite.id);
|
||||
}
|
||||
|
||||
await deleteOneRecord(recordId);
|
||||
}, [
|
||||
deleteFavorite,
|
||||
deleteOneRecord,
|
||||
favorites,
|
||||
resetTableRowSelection,
|
||||
recordId,
|
||||
]);
|
||||
|
||||
const isRemoteObject = objectMetadataItem.isRemote;
|
||||
|
||||
const { isInRightDrawer, onActionExecutedCallback } =
|
||||
useContext(ActionMenuContext);
|
||||
|
||||
const registerDeleteSingleRecordAction = ({
|
||||
position,
|
||||
}: {
|
||||
position: number;
|
||||
}) => {
|
||||
if (isRemoteObject) {
|
||||
return;
|
||||
}
|
||||
|
||||
addActionMenuEntry({
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'delete-single-record',
|
||||
label: 'Delete',
|
||||
position,
|
||||
Icon: IconTrash,
|
||||
accent: 'danger',
|
||||
isPinned: true,
|
||||
onClick: () => {
|
||||
setIsDeleteRecordsModalOpen(true);
|
||||
},
|
||||
ConfirmationModal: (
|
||||
<ConfirmationModal
|
||||
isOpen={isDeleteRecordsModalOpen}
|
||||
setIsOpen={setIsDeleteRecordsModalOpen}
|
||||
title={'Delete Record'}
|
||||
subtitle={
|
||||
'Are you sure you want to delete this record? It can be recovered from the Options menu.'
|
||||
}
|
||||
onConfirmClick={() => {
|
||||
handleDeleteClick();
|
||||
onActionExecutedCallback?.();
|
||||
if (isInRightDrawer) {
|
||||
closeRightDrawer();
|
||||
}
|
||||
}}
|
||||
deleteButtonText={'Delete Record'}
|
||||
/>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const unregisterDeleteSingleRecordAction = () => {
|
||||
removeActionMenuEntry('delete-single-record');
|
||||
};
|
||||
|
||||
return {
|
||||
registerDeleteSingleRecordAction,
|
||||
unregisterDeleteSingleRecordAction,
|
||||
};
|
||||
};
|
||||
+26
-37
@@ -3,52 +3,47 @@ import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { useCreateFavorite } from '@/favorites/hooks/useCreateFavorite';
|
||||
import { useDeleteFavorite } from '@/favorites/hooks/useDeleteFavorite';
|
||||
import { useFavorites } from '@/favorites/hooks/useFavorites';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useEffect } from 'react';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconHeart, IconHeartOff, isDefined } from 'twenty-ui';
|
||||
|
||||
export const ManageFavoritesActionEffect = ({
|
||||
position,
|
||||
export const useManageFavoritesSingleRecordAction = ({
|
||||
recordId,
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
position: number;
|
||||
recordId: string;
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
|
||||
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
);
|
||||
|
||||
const { sortedFavorites: favorites } = useFavorites();
|
||||
|
||||
const { createFavorite } = useCreateFavorite();
|
||||
|
||||
const { deleteFavorite } = useDeleteFavorite();
|
||||
|
||||
const selectedRecordId =
|
||||
contextStoreTargetedRecordsRule.mode === 'selection'
|
||||
? contextStoreTargetedRecordsRule.selectedRecordIds[0]
|
||||
: undefined;
|
||||
|
||||
const selectedRecord = useRecoilValue(
|
||||
recordStoreFamilyState(selectedRecordId ?? ''),
|
||||
);
|
||||
const selectedRecord = useRecoilValue(recordStoreFamilyState(recordId));
|
||||
|
||||
const foundFavorite = favorites?.find(
|
||||
(favorite) => favorite.recordId === selectedRecordId,
|
||||
(favorite) => favorite.recordId === recordId,
|
||||
);
|
||||
|
||||
const isFavorite = !!selectedRecordId && !!foundFavorite;
|
||||
const isFavorite = !!foundFavorite;
|
||||
|
||||
useEffect(() => {
|
||||
const isPageHeaderV2Enabled = useIsFeatureEnabled(
|
||||
'IS_PAGE_HEADER_V2_ENABLED',
|
||||
);
|
||||
|
||||
const registerManageFavoritesSingleRecordAction = ({
|
||||
position,
|
||||
}: {
|
||||
position: number;
|
||||
}) => {
|
||||
if (!isDefined(objectMetadataItem) || objectMetadataItem.isRemote) {
|
||||
return;
|
||||
}
|
||||
@@ -56,7 +51,8 @@ export const ManageFavoritesActionEffect = ({
|
||||
addActionMenuEntry({
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
key: 'manage-favorites',
|
||||
key: 'manage-favorites-single-record',
|
||||
isPinned: isPageHeaderV2Enabled,
|
||||
label: isFavorite ? 'Remove from favorites' : 'Add to favorites',
|
||||
position,
|
||||
Icon: isFavorite ? IconHeartOff : IconHeart,
|
||||
@@ -68,21 +64,14 @@ export const ManageFavoritesActionEffect = ({
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
return () => {
|
||||
removeActionMenuEntry('manage-favorites');
|
||||
};
|
||||
}, [
|
||||
addActionMenuEntry,
|
||||
createFavorite,
|
||||
deleteFavorite,
|
||||
foundFavorite?.id,
|
||||
isFavorite,
|
||||
objectMetadataItem,
|
||||
position,
|
||||
removeActionMenuEntry,
|
||||
selectedRecord,
|
||||
]);
|
||||
const unregisterManageFavoritesSingleRecordAction = () => {
|
||||
removeActionMenuEntry('manage-favorites-single-record');
|
||||
};
|
||||
|
||||
return null;
|
||||
return {
|
||||
registerManageFavoritesSingleRecordAction,
|
||||
unregisterManageFavoritesSingleRecordAction,
|
||||
};
|
||||
};
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import { useDeleteSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useDeleteSingleRecordAction';
|
||||
import { useManageFavoritesSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/hooks/useManageFavoritesSingleRecordAction';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const useSingleRecordActions = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
);
|
||||
|
||||
const selectedRecordId =
|
||||
contextStoreTargetedRecordsRule.mode === 'selection'
|
||||
? contextStoreTargetedRecordsRule.selectedRecordIds[0]
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(selectedRecordId)) {
|
||||
throw new Error('Selected record ID is required');
|
||||
}
|
||||
|
||||
const {
|
||||
registerManageFavoritesSingleRecordAction,
|
||||
unregisterManageFavoritesSingleRecordAction,
|
||||
} = useManageFavoritesSingleRecordAction({
|
||||
recordId: selectedRecordId,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const {
|
||||
registerDeleteSingleRecordAction,
|
||||
unregisterDeleteSingleRecordAction,
|
||||
} = useDeleteSingleRecordAction({
|
||||
recordId: selectedRecordId,
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
const registerSingleRecordActions = () => {
|
||||
registerManageFavoritesSingleRecordAction({ position: 1 });
|
||||
registerDeleteSingleRecordAction({ position: 2 });
|
||||
};
|
||||
|
||||
const unregisterSingleRecordActions = () => {
|
||||
unregisterManageFavoritesSingleRecordAction();
|
||||
unregisterDeleteSingleRecordAction();
|
||||
};
|
||||
|
||||
return {
|
||||
registerSingleRecordActions,
|
||||
unregisterSingleRecordActions,
|
||||
};
|
||||
};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { NUMBER_OF_STANDARD_SINGLE_RECORD_ACTIONS_ON_ALL_OBJECTS } from '@/action-menu/actions/record-actions/single-record/constants/NumberOfStandardSingleRecordActionsOnAllObjects';
|
||||
import { useEffect } from 'react';
|
||||
import { useWorkflowSingleRecordActions } from '../hooks/useWorkflowSingleRecordActions';
|
||||
|
||||
export const WorkflowSingleRecordActionMenuEntrySetterEffect = () => {
|
||||
const { registerSingleRecordActions, unregisterSingleRecordActions } =
|
||||
useWorkflowSingleRecordActions();
|
||||
|
||||
useEffect(() => {
|
||||
registerSingleRecordActions({
|
||||
startPosition:
|
||||
NUMBER_OF_STANDARD_SINGLE_RECORD_ACTIONS_ON_ALL_OBJECTS + 1,
|
||||
});
|
||||
|
||||
return () => {
|
||||
unregisterSingleRecordActions();
|
||||
};
|
||||
}, [registerSingleRecordActions, unregisterSingleRecordActions]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { expect } from '@storybook/test';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { useActivateWorkflowDraftWorkflowSingleRecordAction } from '../useActivateWorkflowDraftWorkflowSingleRecordAction';
|
||||
|
||||
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: [],
|
||||
});
|
||||
|
||||
jest.mock('@/workflow/hooks/useWorkflowWithCurrentVersion', () => ({
|
||||
useWorkflowWithCurrentVersion: () => ({
|
||||
id: 'workflowId',
|
||||
currentVersion: {
|
||||
id: 'currentVersionId',
|
||||
trigger: 'trigger',
|
||||
status: 'DRAFT',
|
||||
steps: [
|
||||
{
|
||||
id: 'stepId1',
|
||||
},
|
||||
{
|
||||
id: 'stepId2',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useActivateWorkflowDraftWorkflowSingleRecordAction', () => {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<JestMetadataAndApolloMocksWrapper>
|
||||
<JestObjectMetadataItemSetter>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: '1',
|
||||
}}
|
||||
>
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: '1',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</JestObjectMetadataItemSetter>
|
||||
</JestMetadataAndApolloMocksWrapper>
|
||||
);
|
||||
|
||||
it('should register activate workflow draft workflow action', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
actionMenuEntries,
|
||||
useActivateWorkflowDraftWorkflowSingleRecordAction:
|
||||
useActivateWorkflowDraftWorkflowSingleRecordAction({
|
||||
workflowId: 'workflowId',
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.useActivateWorkflowDraftWorkflowSingleRecordAction.registerActivateWorkflowDraftWorkflowSingleRecordAction(
|
||||
{ position: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(1);
|
||||
expect(
|
||||
result.current.actionMenuEntries.get(
|
||||
'activate-workflow-draft-single-record',
|
||||
),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
result.current.actionMenuEntries.get(
|
||||
'activate-workflow-draft-single-record',
|
||||
)?.position,
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('should unregister activate workflow draft workflow action', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
actionMenuEntries,
|
||||
useActivateWorkflowDraftWorkflowSingleRecordAction:
|
||||
useActivateWorkflowDraftWorkflowSingleRecordAction({
|
||||
workflowId: 'workflow1',
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.useActivateWorkflowDraftWorkflowSingleRecordAction.registerActivateWorkflowDraftWorkflowSingleRecordAction(
|
||||
{ position: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.useActivateWorkflowDraftWorkflowSingleRecordAction.unregisterActivateWorkflowDraftWorkflowSingleRecordAction();
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(0);
|
||||
});
|
||||
});
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { expect } from '@storybook/test';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction } from '../useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction';
|
||||
|
||||
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: [],
|
||||
});
|
||||
|
||||
jest.mock('@/workflow/hooks/useWorkflowWithCurrentVersion', () => ({
|
||||
useWorkflowWithCurrentVersion: () => ({
|
||||
id: 'workflowId',
|
||||
currentVersion: {
|
||||
id: 'currentVersionId',
|
||||
trigger: 'trigger',
|
||||
status: 'DEACTIVATED',
|
||||
steps: [
|
||||
{
|
||||
id: 'stepId1',
|
||||
},
|
||||
{
|
||||
id: 'stepId2',
|
||||
},
|
||||
],
|
||||
},
|
||||
lastPublishedVersionId: 'lastPublishedVersionId',
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction', () => {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<JestMetadataAndApolloMocksWrapper>
|
||||
<JestObjectMetadataItemSetter>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: '1',
|
||||
}}
|
||||
>
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: '1',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</JestObjectMetadataItemSetter>
|
||||
</JestMetadataAndApolloMocksWrapper>
|
||||
);
|
||||
|
||||
it('should register activate workflow last published version workflow action', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
actionMenuEntries,
|
||||
useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction:
|
||||
useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction({
|
||||
workflowId: 'workflowId',
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction.registerActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction(
|
||||
{ position: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(1);
|
||||
expect(
|
||||
result.current.actionMenuEntries.get(
|
||||
'activate-workflow-last-published-version-single-record',
|
||||
),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
result.current.actionMenuEntries.get(
|
||||
'activate-workflow-last-published-version-single-record',
|
||||
)?.position,
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('should unregister activate workflow last published version workflow action', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
actionMenuEntries,
|
||||
useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction:
|
||||
useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction({
|
||||
workflowId: 'workflow1',
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction.registerActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction(
|
||||
{ position: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction.unregisterActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction();
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(0);
|
||||
});
|
||||
});
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { expect } from '@storybook/test';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { useDeactivateWorkflowWorkflowSingleRecordAction } from '../useDeactivateWorkflowWorkflowSingleRecordAction';
|
||||
|
||||
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: [],
|
||||
});
|
||||
|
||||
jest.mock('@/workflow/hooks/useWorkflowWithCurrentVersion', () => ({
|
||||
useWorkflowWithCurrentVersion: () => ({
|
||||
id: 'workflowId',
|
||||
currentVersion: {
|
||||
id: 'currentVersionId',
|
||||
trigger: 'trigger',
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
lastPublishedVersionId: 'lastPublishedVersionId',
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useDeactivateWorkflowWorkflowSingleRecordAction', () => {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<JestMetadataAndApolloMocksWrapper>
|
||||
<JestObjectMetadataItemSetter>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: '1',
|
||||
}}
|
||||
>
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: '1',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</JestObjectMetadataItemSetter>
|
||||
</JestMetadataAndApolloMocksWrapper>
|
||||
);
|
||||
|
||||
it('should register activate workflow last published version workflow action', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
actionMenuEntries,
|
||||
useDeactivateWorkflowWorkflowSingleRecordAction:
|
||||
useDeactivateWorkflowWorkflowSingleRecordAction({
|
||||
workflowId: 'workflowId',
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.useDeactivateWorkflowWorkflowSingleRecordAction.registerDeactivateWorkflowWorkflowSingleRecordAction(
|
||||
{ position: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(1);
|
||||
expect(
|
||||
result.current.actionMenuEntries.get('deactivate-workflow-single-record'),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
result.current.actionMenuEntries.get('deactivate-workflow-single-record')
|
||||
?.position,
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('should unregister deactivate workflow workflow action', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
actionMenuEntries,
|
||||
useDeactivateWorkflowWorkflowSingleRecordAction:
|
||||
useDeactivateWorkflowWorkflowSingleRecordAction({
|
||||
workflowId: 'workflow1',
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.useDeactivateWorkflowWorkflowSingleRecordAction.registerDeactivateWorkflowWorkflowSingleRecordAction(
|
||||
{ position: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.useDeactivateWorkflowWorkflowSingleRecordAction.unregisterDeactivateWorkflowWorkflowSingleRecordAction();
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(0);
|
||||
});
|
||||
});
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { expect } from '@storybook/test';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { act } from 'react';
|
||||
import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter';
|
||||
import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper';
|
||||
import { useDiscardDraftWorkflowSingleRecordAction } from '../useDiscardDraftWorkflowSingleRecordAction';
|
||||
|
||||
const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({
|
||||
apolloMocks: [],
|
||||
});
|
||||
|
||||
jest.mock('@/workflow/hooks/useWorkflowWithCurrentVersion', () => ({
|
||||
useWorkflowWithCurrentVersion: () => ({
|
||||
id: 'workflowId',
|
||||
currentVersion: {
|
||||
id: 'currentVersionId',
|
||||
trigger: 'trigger',
|
||||
status: 'DRAFT',
|
||||
},
|
||||
lastPublishedVersionId: 'lastPublishedVersionId',
|
||||
versions: [
|
||||
{
|
||||
id: 'currentVersionId',
|
||||
trigger: 'trigger',
|
||||
status: 'DRAFT',
|
||||
},
|
||||
{
|
||||
id: 'lastPublishedVersionId',
|
||||
trigger: 'trigger',
|
||||
status: 'ACTIVE',
|
||||
},
|
||||
],
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('useDiscardDraftWorkflowSingleRecordAction', () => {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<JestMetadataAndApolloMocksWrapper>
|
||||
<JestObjectMetadataItemSetter>
|
||||
<ContextStoreComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: '1',
|
||||
}}
|
||||
>
|
||||
<ActionMenuComponentInstanceContext.Provider
|
||||
value={{
|
||||
instanceId: '1',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ActionMenuComponentInstanceContext.Provider>
|
||||
</ContextStoreComponentInstanceContext.Provider>
|
||||
</JestObjectMetadataItemSetter>
|
||||
</JestMetadataAndApolloMocksWrapper>
|
||||
);
|
||||
|
||||
it('should register discard workflow draft workflow action', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
actionMenuEntries,
|
||||
useDiscardDraftWorkflowSingleRecordAction:
|
||||
useDiscardDraftWorkflowSingleRecordAction({
|
||||
workflowId: 'workflowId',
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.useDiscardDraftWorkflowSingleRecordAction.registerDiscardDraftWorkflowSingleRecordAction(
|
||||
{ position: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(1);
|
||||
expect(
|
||||
result.current.actionMenuEntries.get(
|
||||
'discard-workflow-draft-single-record',
|
||||
),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
result.current.actionMenuEntries.get(
|
||||
'discard-workflow-draft-single-record',
|
||||
)?.position,
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('should unregister deactivate workflow workflow action', () => {
|
||||
const { result } = renderHook(
|
||||
() => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentState,
|
||||
);
|
||||
|
||||
return {
|
||||
actionMenuEntries,
|
||||
useDiscardDraftWorkflowSingleRecordAction:
|
||||
useDiscardDraftWorkflowSingleRecordAction({
|
||||
workflowId: 'workflow1',
|
||||
}),
|
||||
};
|
||||
},
|
||||
{ wrapper },
|
||||
);
|
||||
|
||||
act(() => {
|
||||
result.current.useDiscardDraftWorkflowSingleRecordAction.registerDiscardDraftWorkflowSingleRecordAction(
|
||||
{ position: 1 },
|
||||
);
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.useDiscardDraftWorkflowSingleRecordAction.unregisterDiscardDraftWorkflowSingleRecordAction();
|
||||
});
|
||||
|
||||
expect(result.current.actionMenuEntries.size).toBe(0);
|
||||
});
|
||||
});
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { IconPower, isDefined } from 'twenty-ui';
|
||||
|
||||
export const useActivateWorkflowDraftWorkflowSingleRecordAction = ({
|
||||
workflowId,
|
||||
}: {
|
||||
workflowId: string;
|
||||
}) => {
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
|
||||
const { activateWorkflowVersion } = useActivateWorkflowVersion();
|
||||
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
const registerActivateWorkflowDraftWorkflowSingleRecordAction = ({
|
||||
position,
|
||||
}: {
|
||||
position: number;
|
||||
}) => {
|
||||
if (
|
||||
!isDefined(workflowWithCurrentVersion?.currentVersion?.trigger) ||
|
||||
!isDefined(workflowWithCurrentVersion.currentVersion?.steps)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isDraft =
|
||||
workflowWithCurrentVersion.currentVersion.status === 'DRAFT';
|
||||
|
||||
if (!isDraft) {
|
||||
return;
|
||||
}
|
||||
|
||||
addActionMenuEntry({
|
||||
key: 'activate-workflow-draft-single-record',
|
||||
label: 'Activate Draft',
|
||||
position,
|
||||
Icon: IconPower,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
onClick: () => {
|
||||
activateWorkflowVersion({
|
||||
workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
|
||||
workflowId: workflowWithCurrentVersion.id,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const unregisterActivateWorkflowDraftWorkflowSingleRecordAction = () => {
|
||||
removeActionMenuEntry('activate-workflow-draft-single-record');
|
||||
};
|
||||
|
||||
return {
|
||||
registerActivateWorkflowDraftWorkflowSingleRecordAction,
|
||||
unregisterActivateWorkflowDraftWorkflowSingleRecordAction,
|
||||
};
|
||||
};
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { IconPower, isDefined } from 'twenty-ui';
|
||||
|
||||
export const useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction =
|
||||
({ workflowId }: { workflowId: string }) => {
|
||||
const { addActionMenuEntry, removeActionMenuEntry } =
|
||||
useActionMenuEntries();
|
||||
|
||||
const { activateWorkflowVersion } = useActivateWorkflowVersion();
|
||||
|
||||
const workflowWithCurrentVersion =
|
||||
useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
const registerActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction =
|
||||
({ position }: { position: number }) => {
|
||||
if (
|
||||
!isDefined(workflowWithCurrentVersion) ||
|
||||
!isDefined(workflowWithCurrentVersion.currentVersion.trigger) ||
|
||||
!isDefined(workflowWithCurrentVersion.lastPublishedVersionId) ||
|
||||
workflowWithCurrentVersion.currentVersion.status === 'ACTIVE' ||
|
||||
!isDefined(workflowWithCurrentVersion.currentVersion?.steps) ||
|
||||
workflowWithCurrentVersion.currentVersion?.steps.length === 0
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
addActionMenuEntry({
|
||||
key: 'activate-workflow-last-published-version-single-record',
|
||||
label: 'Activate last published version',
|
||||
position,
|
||||
Icon: IconPower,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
onClick: () => {
|
||||
activateWorkflowVersion({
|
||||
workflowVersionId:
|
||||
workflowWithCurrentVersion.lastPublishedVersionId,
|
||||
workflowId: workflowWithCurrentVersion.id,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const unregisterActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction =
|
||||
() => {
|
||||
removeActionMenuEntry(
|
||||
'activate-workflow-last-published-version-single-record',
|
||||
);
|
||||
};
|
||||
|
||||
return {
|
||||
registerActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction,
|
||||
unregisterActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction,
|
||||
};
|
||||
};
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { useDeactivateWorkflowVersion } from '@/workflow/hooks/useDeactivateWorkflowVersion';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { IconPlayerPause, isDefined } from 'twenty-ui';
|
||||
|
||||
export const useDeactivateWorkflowWorkflowSingleRecordAction = ({
|
||||
workflowId,
|
||||
}: {
|
||||
workflowId: string;
|
||||
}) => {
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
|
||||
const { deactivateWorkflowVersion } = useDeactivateWorkflowVersion();
|
||||
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
const isWorkflowActive =
|
||||
isDefined(workflowWithCurrentVersion) &&
|
||||
workflowWithCurrentVersion.currentVersion.status === 'ACTIVE';
|
||||
|
||||
const registerDeactivateWorkflowWorkflowSingleRecordAction = ({
|
||||
position,
|
||||
}: {
|
||||
position: number;
|
||||
}) => {
|
||||
if (!isDefined(workflowWithCurrentVersion) || !isWorkflowActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
addActionMenuEntry({
|
||||
key: 'deactivate-workflow-single-record',
|
||||
label: 'Deactivate Workflow',
|
||||
position,
|
||||
Icon: IconPlayerPause,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
onClick: () => {
|
||||
deactivateWorkflowVersion(workflowWithCurrentVersion.currentVersion.id);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const unregisterDeactivateWorkflowWorkflowSingleRecordAction = () => {
|
||||
removeActionMenuEntry('deactivate-workflow-single-record');
|
||||
};
|
||||
|
||||
return {
|
||||
registerDeactivateWorkflowWorkflowSingleRecordAction,
|
||||
unregisterDeactivateWorkflowWorkflowSingleRecordAction,
|
||||
};
|
||||
};
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { useDeleteOneWorkflowVersion } from '@/workflow/hooks/useDeleteOneWorkflowVersion';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { IconTrash, isDefined } from 'twenty-ui';
|
||||
|
||||
export const useDiscardDraftWorkflowSingleRecordAction = ({
|
||||
workflowId,
|
||||
}: {
|
||||
workflowId: string;
|
||||
}) => {
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
|
||||
const { deleteOneWorkflowVersion } = useDeleteOneWorkflowVersion();
|
||||
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
const registerDiscardDraftWorkflowSingleRecordAction = ({
|
||||
position,
|
||||
}: {
|
||||
position: number;
|
||||
}) => {
|
||||
if (
|
||||
!isDefined(workflowWithCurrentVersion) ||
|
||||
workflowWithCurrentVersion.versions.length < 2
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isDraft =
|
||||
workflowWithCurrentVersion.currentVersion.status === 'DRAFT';
|
||||
|
||||
if (!isDraft) {
|
||||
return;
|
||||
}
|
||||
|
||||
addActionMenuEntry({
|
||||
key: 'discard-workflow-draft-single-record',
|
||||
label: 'Discard Draft',
|
||||
position,
|
||||
Icon: IconTrash,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
onClick: () => {
|
||||
deleteOneWorkflowVersion({
|
||||
workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const unregisterDiscardDraftWorkflowSingleRecordAction = () => {
|
||||
removeActionMenuEntry('discard-workflow-draft-single-record');
|
||||
};
|
||||
|
||||
return {
|
||||
registerDiscardDraftWorkflowSingleRecordAction,
|
||||
unregisterDiscardDraftWorkflowSingleRecordAction,
|
||||
};
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useActiveWorkflowVersion } from '@/workflow/hooks/useActiveWorkflowVersion';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconHistory, isDefined } from 'twenty-ui';
|
||||
|
||||
export const useSeeWorkflowActiveVersionWorkflowSingleRecordAction = ({
|
||||
workflowId,
|
||||
}: {
|
||||
workflowId: string;
|
||||
}) => {
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
|
||||
const workflow = useRecoilValue(recordStoreFamilyState(workflowId));
|
||||
|
||||
const isDraft = workflow?.statuses?.includes('DRAFT');
|
||||
|
||||
const workflowActiveVersion = useActiveWorkflowVersion(workflowId);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const registerSeeWorkflowActiveVersionWorkflowSingleRecordAction = ({
|
||||
position,
|
||||
}: {
|
||||
position: number;
|
||||
}) => {
|
||||
if (!isDefined(workflowActiveVersion) || !isDraft) {
|
||||
return;
|
||||
}
|
||||
|
||||
addActionMenuEntry({
|
||||
key: 'see-workflow-active-version-single-record',
|
||||
label: 'See active version',
|
||||
position,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
Icon: IconHistory,
|
||||
onClick: () => {
|
||||
navigate(
|
||||
`/object/${CoreObjectNameSingular.WorkflowVersion}/${workflowActiveVersion.id}`,
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const unregisterSeeWorkflowActiveVersionWorkflowSingleRecordAction = () => {
|
||||
removeActionMenuEntry('see-workflow-active-version-single-record');
|
||||
};
|
||||
|
||||
return {
|
||||
registerSeeWorkflowActiveVersionWorkflowSingleRecordAction,
|
||||
unregisterSeeWorkflowActiveVersionWorkflowSingleRecordAction,
|
||||
};
|
||||
};
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import qs from 'qs';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { IconHistoryToggle, isDefined } from 'twenty-ui';
|
||||
|
||||
export const useSeeWorkflowRunsWorkflowSingleRecordAction = ({
|
||||
workflowId,
|
||||
}: {
|
||||
workflowId: string;
|
||||
}) => {
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const registerSeeWorkflowRunsWorkflowSingleRecordAction = ({
|
||||
position,
|
||||
}: {
|
||||
position: number;
|
||||
}) => {
|
||||
if (!isDefined(workflowWithCurrentVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filterQueryParams: FilterQueryParams = {
|
||||
filter: {
|
||||
workflow: {
|
||||
[ViewFilterOperand.Is]: [workflowWithCurrentVersion.id],
|
||||
},
|
||||
},
|
||||
};
|
||||
const filterLinkHref = `/objects/${CoreObjectNamePlural.WorkflowRun}?${qs.stringify(
|
||||
filterQueryParams,
|
||||
)}`;
|
||||
|
||||
addActionMenuEntry({
|
||||
key: 'see-workflow-runs-single-record',
|
||||
label: 'See runs',
|
||||
position,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
Icon: IconHistoryToggle,
|
||||
onClick: () => {
|
||||
navigate(filterLinkHref);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const unregisterSeeWorkflowRunsWorkflowSingleRecordAction = () => {
|
||||
removeActionMenuEntry('see-workflow-runs-single-record');
|
||||
};
|
||||
|
||||
return {
|
||||
registerSeeWorkflowRunsWorkflowSingleRecordAction,
|
||||
unregisterSeeWorkflowRunsWorkflowSingleRecordAction,
|
||||
};
|
||||
};
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import qs from 'qs';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { IconHistory, isDefined } from 'twenty-ui';
|
||||
|
||||
export const useSeeWorkflowVersionsHistoryWorkflowSingleRecordAction = ({
|
||||
workflowId,
|
||||
}: {
|
||||
workflowId: string;
|
||||
}) => {
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const registerSeeWorkflowVersionsHistoryWorkflowSingleRecordAction = ({
|
||||
position,
|
||||
}: {
|
||||
position: number;
|
||||
}) => {
|
||||
if (!isDefined(workflowWithCurrentVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filterQueryParams: FilterQueryParams = {
|
||||
filter: {
|
||||
workflow: {
|
||||
[ViewFilterOperand.Is]: [workflowWithCurrentVersion.id],
|
||||
},
|
||||
},
|
||||
};
|
||||
const filterLinkHref = `/objects/${CoreObjectNamePlural.WorkflowVersion}?${qs.stringify(
|
||||
filterQueryParams,
|
||||
)}`;
|
||||
|
||||
addActionMenuEntry({
|
||||
key: 'see-workflow-versions-history-single-record',
|
||||
label: 'See versions history',
|
||||
position,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
Icon: IconHistory,
|
||||
onClick: () => {
|
||||
navigate(filterLinkHref);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const unregisterSeeWorkflowVersionsHistoryWorkflowSingleRecordAction = () => {
|
||||
removeActionMenuEntry('see-workflow-versions-history-single-record');
|
||||
};
|
||||
|
||||
return {
|
||||
registerSeeWorkflowVersionsHistoryWorkflowSingleRecordAction,
|
||||
unregisterSeeWorkflowVersionsHistoryWorkflowSingleRecordAction,
|
||||
};
|
||||
};
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { IconPlayerPlay, isDefined } from 'twenty-ui';
|
||||
|
||||
export const useTestWorkflowWorkflowSingleRecordAction = ({
|
||||
workflowId,
|
||||
}: {
|
||||
workflowId: string;
|
||||
}) => {
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId);
|
||||
|
||||
const { runWorkflowVersion } = useRunWorkflowVersion();
|
||||
|
||||
const registerTestWorkflowWorkflowSingleRecordAction = ({
|
||||
position,
|
||||
}: {
|
||||
position: number;
|
||||
}) => {
|
||||
if (
|
||||
!isDefined(workflowWithCurrentVersion?.currentVersion?.trigger) ||
|
||||
workflowWithCurrentVersion.currentVersion.trigger.type !== 'MANUAL' ||
|
||||
isDefined(
|
||||
workflowWithCurrentVersion.currentVersion.trigger.settings.objectType,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
addActionMenuEntry({
|
||||
key: 'test-workflow-single-record',
|
||||
label: 'Test workflow',
|
||||
position,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
Icon: IconPlayerPlay,
|
||||
onClick: () => {
|
||||
runWorkflowVersion({
|
||||
workflowVersionId: workflowWithCurrentVersion.currentVersion.id,
|
||||
workflowName: workflowWithCurrentVersion.name,
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const unregisterTestWorkflowWorkflowSingleRecordAction = () => {
|
||||
removeActionMenuEntry('test-workflow-single-record');
|
||||
};
|
||||
|
||||
return {
|
||||
registerTestWorkflowWorkflowSingleRecordAction,
|
||||
unregisterTestWorkflowWorkflowSingleRecordAction,
|
||||
};
|
||||
};
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
import { useActivateWorkflowDraftWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateWorkflowDraftWorkflowSingleRecordAction';
|
||||
import { useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction';
|
||||
import { useDeactivateWorkflowWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useDeactivateWorkflowWorkflowSingleRecordAction';
|
||||
import { useDiscardDraftWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useDiscardDraftWorkflowSingleRecordAction';
|
||||
import { useSeeWorkflowActiveVersionWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeWorkflowActiveVersionWorkflowSingleRecordAction';
|
||||
import { useSeeWorkflowRunsWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeWorkflowRunsWorkflowSingleRecordAction';
|
||||
import { useSeeWorkflowVersionsHistoryWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeWorkflowVersionsHistoryWorkflowSingleRecordAction';
|
||||
import { useTestWorkflowWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useTestWorkflowWorkflowSingleRecordAction';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const useWorkflowSingleRecordActions = () => {
|
||||
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
);
|
||||
|
||||
const selectedRecordId =
|
||||
contextStoreTargetedRecordsRule.mode === 'selection'
|
||||
? contextStoreTargetedRecordsRule.selectedRecordIds?.[0]
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(selectedRecordId)) {
|
||||
throw new Error('Selected record ID is required');
|
||||
}
|
||||
|
||||
const {
|
||||
registerTestWorkflowWorkflowSingleRecordAction,
|
||||
unregisterTestWorkflowWorkflowSingleRecordAction,
|
||||
} = useTestWorkflowWorkflowSingleRecordAction({
|
||||
workflowId: selectedRecordId,
|
||||
});
|
||||
|
||||
const {
|
||||
registerActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction,
|
||||
unregisterActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction,
|
||||
} = useActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction({
|
||||
workflowId: selectedRecordId,
|
||||
});
|
||||
|
||||
const {
|
||||
registerDeactivateWorkflowWorkflowSingleRecordAction,
|
||||
unregisterDeactivateWorkflowWorkflowSingleRecordAction,
|
||||
} = useDeactivateWorkflowWorkflowSingleRecordAction({
|
||||
workflowId: selectedRecordId,
|
||||
});
|
||||
|
||||
const {
|
||||
registerSeeWorkflowRunsWorkflowSingleRecordAction,
|
||||
unregisterSeeWorkflowRunsWorkflowSingleRecordAction,
|
||||
} = useSeeWorkflowRunsWorkflowSingleRecordAction({
|
||||
workflowId: selectedRecordId,
|
||||
});
|
||||
|
||||
const {
|
||||
registerSeeWorkflowVersionsHistoryWorkflowSingleRecordAction,
|
||||
unregisterSeeWorkflowVersionsHistoryWorkflowSingleRecordAction,
|
||||
} = useSeeWorkflowVersionsHistoryWorkflowSingleRecordAction({
|
||||
workflowId: selectedRecordId,
|
||||
});
|
||||
|
||||
const {
|
||||
registerSeeWorkflowActiveVersionWorkflowSingleRecordAction,
|
||||
unregisterSeeWorkflowActiveVersionWorkflowSingleRecordAction,
|
||||
} = useSeeWorkflowActiveVersionWorkflowSingleRecordAction({
|
||||
workflowId: selectedRecordId,
|
||||
});
|
||||
|
||||
const {
|
||||
registerActivateWorkflowDraftWorkflowSingleRecordAction,
|
||||
unregisterActivateWorkflowDraftWorkflowSingleRecordAction,
|
||||
} = useActivateWorkflowDraftWorkflowSingleRecordAction({
|
||||
workflowId: selectedRecordId,
|
||||
});
|
||||
|
||||
const {
|
||||
registerDiscardDraftWorkflowSingleRecordAction,
|
||||
unregisterDiscardDraftWorkflowSingleRecordAction,
|
||||
} = useDiscardDraftWorkflowSingleRecordAction({
|
||||
workflowId: selectedRecordId,
|
||||
});
|
||||
|
||||
const registerSingleRecordActions = ({
|
||||
startPosition,
|
||||
}: {
|
||||
startPosition: number;
|
||||
}) => {
|
||||
registerTestWorkflowWorkflowSingleRecordAction({ position: startPosition });
|
||||
registerDiscardDraftWorkflowSingleRecordAction({
|
||||
position: startPosition + 1,
|
||||
});
|
||||
registerActivateWorkflowDraftWorkflowSingleRecordAction({
|
||||
position: startPosition + 2,
|
||||
});
|
||||
registerActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction({
|
||||
position: startPosition + 3,
|
||||
});
|
||||
registerDeactivateWorkflowWorkflowSingleRecordAction({
|
||||
position: startPosition + 4,
|
||||
});
|
||||
registerSeeWorkflowRunsWorkflowSingleRecordAction({
|
||||
position: startPosition + 5,
|
||||
});
|
||||
registerSeeWorkflowActiveVersionWorkflowSingleRecordAction({
|
||||
position: startPosition + 6,
|
||||
});
|
||||
registerSeeWorkflowVersionsHistoryWorkflowSingleRecordAction({
|
||||
position: startPosition + 7,
|
||||
});
|
||||
};
|
||||
|
||||
const unregisterSingleRecordActions = () => {
|
||||
unregisterTestWorkflowWorkflowSingleRecordAction();
|
||||
unregisterActivateWorkflowLastPublishedVersionWorkflowSingleRecordAction();
|
||||
unregisterDiscardDraftWorkflowSingleRecordAction();
|
||||
unregisterActivateWorkflowDraftWorkflowSingleRecordAction();
|
||||
unregisterDeactivateWorkflowWorkflowSingleRecordAction();
|
||||
unregisterSeeWorkflowRunsWorkflowSingleRecordAction();
|
||||
unregisterSeeWorkflowActiveVersionWorkflowSingleRecordAction();
|
||||
unregisterSeeWorkflowVersionsHistoryWorkflowSingleRecordAction();
|
||||
};
|
||||
|
||||
return {
|
||||
registerSingleRecordActions,
|
||||
unregisterSingleRecordActions,
|
||||
};
|
||||
};
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { NUMBER_OF_STANDARD_SINGLE_RECORD_ACTIONS_ON_ALL_OBJECTS } from '@/action-menu/actions/record-actions/single-record/constants/NumberOfStandardSingleRecordActionsOnAllObjects';
|
||||
import { useWorkflowVersionsSingleRecordActions } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useWorkflowVersionsSingleRecordActions';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const WorkflowVersionsSingleRecordActionMenuEntrySetterEffect = () => {
|
||||
const { registerSingleRecordActions, unregisterSingleRecordActions } =
|
||||
useWorkflowVersionsSingleRecordActions();
|
||||
|
||||
useEffect(() => {
|
||||
registerSingleRecordActions({
|
||||
startPosition:
|
||||
NUMBER_OF_STANDARD_SINGLE_RECORD_ACTIONS_ON_ALL_OBJECTS + 1,
|
||||
});
|
||||
|
||||
return () => {
|
||||
unregisterSingleRecordActions();
|
||||
};
|
||||
}, [registerSingleRecordActions, unregisterSingleRecordActions]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { CoreObjectNamePlural } from '@/object-metadata/types/CoreObjectNamePlural';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams';
|
||||
import { ViewFilterOperand } from '@/views/types/ViewFilterOperand';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import qs from 'qs';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconHistoryToggle, isDefined } from 'twenty-ui';
|
||||
|
||||
export const useSeeWorkflowExecutionsWorkflowVersionSingleRecordAction = ({
|
||||
workflowVersionId,
|
||||
}: {
|
||||
workflowVersionId: string;
|
||||
}) => {
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
|
||||
const workflowVersion = useRecoilValue(
|
||||
recordStoreFamilyState(workflowVersionId),
|
||||
);
|
||||
|
||||
const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(
|
||||
workflowVersion?.workflow.id,
|
||||
);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
const registerSeeWorkflowExecutionsWorkflowVersionSingleRecordAction = ({
|
||||
position,
|
||||
}: {
|
||||
position: number;
|
||||
}) => {
|
||||
if (!isDefined(workflowWithCurrentVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const filterQueryParams: FilterQueryParams = {
|
||||
filter: {
|
||||
workflow: {
|
||||
[ViewFilterOperand.Is]: [workflowWithCurrentVersion.id],
|
||||
},
|
||||
workflowVersion: {
|
||||
[ViewFilterOperand.Is]: [workflowVersionId],
|
||||
},
|
||||
},
|
||||
};
|
||||
const filterLinkHref = `/objects/${CoreObjectNamePlural.WorkflowRun}?${qs.stringify(
|
||||
filterQueryParams,
|
||||
)}`;
|
||||
|
||||
addActionMenuEntry({
|
||||
key: 'see-workflow-executions-single-record',
|
||||
label: 'See executions',
|
||||
position,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
Icon: IconHistoryToggle,
|
||||
onClick: () => {
|
||||
navigate(filterLinkHref);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const unregisterSeeWorkflowExecutionsWorkflowVersionSingleRecordAction =
|
||||
() => {
|
||||
removeActionMenuEntry('see-workflow-executions-single-record');
|
||||
};
|
||||
|
||||
return {
|
||||
registerSeeWorkflowExecutionsWorkflowVersionSingleRecordAction,
|
||||
unregisterSeeWorkflowExecutionsWorkflowVersionSingleRecordAction,
|
||||
};
|
||||
};
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { useSeeWorkflowVersionsHistoryWorkflowSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-actions/hooks/useSeeWorkflowVersionsHistoryWorkflowSingleRecordAction';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
export const useSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction = ({
|
||||
workflowVersionId,
|
||||
}: {
|
||||
workflowVersionId: string;
|
||||
}) => {
|
||||
const workflowVersion = useRecoilValue(
|
||||
recordStoreFamilyState(workflowVersionId),
|
||||
);
|
||||
|
||||
const {
|
||||
registerSeeWorkflowVersionsHistoryWorkflowSingleRecordAction:
|
||||
registerSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction,
|
||||
unregisterSeeWorkflowVersionsHistoryWorkflowSingleRecordAction:
|
||||
unregisterSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction,
|
||||
} = useSeeWorkflowVersionsHistoryWorkflowSingleRecordAction({
|
||||
workflowId: workflowVersion?.workflow.id,
|
||||
});
|
||||
|
||||
return {
|
||||
registerSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction,
|
||||
unregisterSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction,
|
||||
};
|
||||
};
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { OverrideWorkflowDraftConfirmationModal } from '@/workflow/components/OverrideWorkflowDraftConfirmationModal';
|
||||
import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion';
|
||||
import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion';
|
||||
import { openOverrideWorkflowDraftConfirmationModalState } from '@/workflow/states/openOverrideWorkflowDraftConfirmationModalState';
|
||||
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||
import { IconPencil, isDefined } from 'twenty-ui';
|
||||
|
||||
export const useUseAsDraftWorkflowVersionSingleRecordAction = ({
|
||||
workflowVersionId,
|
||||
}: {
|
||||
workflowVersionId: string;
|
||||
}) => {
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
|
||||
const workflowVersion = useRecoilValue(
|
||||
recordStoreFamilyState(workflowVersionId),
|
||||
);
|
||||
|
||||
const workflow = useWorkflowWithCurrentVersion(
|
||||
workflowVersion?.workflow?.id ?? '',
|
||||
);
|
||||
|
||||
const { createNewWorkflowVersion } = useCreateNewWorkflowVersion();
|
||||
|
||||
const setOpenOverrideWorkflowDraftConfirmationModal = useSetRecoilState(
|
||||
openOverrideWorkflowDraftConfirmationModalState,
|
||||
);
|
||||
|
||||
const registerUseAsDraftWorkflowVersionSingleRecordAction = ({
|
||||
position,
|
||||
}: {
|
||||
position: number;
|
||||
}) => {
|
||||
if (
|
||||
!isDefined(workflowVersion) ||
|
||||
!isDefined(workflow) ||
|
||||
!isDefined(workflow.statuses) ||
|
||||
workflowVersion.status === 'DRAFT'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hasAlreadyDraftVersion = workflow.statuses.includes('DRAFT');
|
||||
|
||||
addActionMenuEntry({
|
||||
key: 'use-workflow-version-as-draft-single-record',
|
||||
label: 'Use as draft',
|
||||
position,
|
||||
Icon: IconPencil,
|
||||
type: ActionMenuEntryType.Standard,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
onClick: async () => {
|
||||
if (hasAlreadyDraftVersion) {
|
||||
setOpenOverrideWorkflowDraftConfirmationModal(true);
|
||||
} else {
|
||||
await createNewWorkflowVersion({
|
||||
workflowId: workflowVersion.workflow.id,
|
||||
name: `v${workflow.versions.length + 1}`,
|
||||
status: 'DRAFT',
|
||||
trigger: workflowVersion.trigger,
|
||||
steps: workflowVersion.steps,
|
||||
});
|
||||
}
|
||||
},
|
||||
ConfirmationModal: (
|
||||
<OverrideWorkflowDraftConfirmationModal
|
||||
draftWorkflowVersionId={workflow?.currentVersion?.id ?? ''}
|
||||
workflowId={workflow?.id ?? ''}
|
||||
workflowVersionUpdateInput={{
|
||||
steps: workflowVersion.steps,
|
||||
trigger: workflowVersion.trigger,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
const unregisterUseAsDraftWorkflowVersionSingleRecordAction = () => {
|
||||
removeActionMenuEntry('use-workflow-version-as-draft-single-record');
|
||||
};
|
||||
|
||||
return {
|
||||
registerUseAsDraftWorkflowVersionSingleRecordAction,
|
||||
unregisterUseAsDraftWorkflowVersionSingleRecordAction,
|
||||
};
|
||||
};
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
import { useSeeWorkflowExecutionsWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeWorkflowExecutionsWorkflowVersionSingleRecordAction';
|
||||
import { useSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction';
|
||||
import { useUseAsDraftWorkflowVersionSingleRecordAction } from '@/action-menu/actions/record-actions/single-record/workflow-version-actions/hooks/useUseAsDraftWorkflowVersionSingleRecordAction';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { isDefined } from 'twenty-ui';
|
||||
|
||||
export const useWorkflowVersionsSingleRecordActions = () => {
|
||||
const contextStoreTargetedRecordsRule = useRecoilComponentValueV2(
|
||||
contextStoreTargetedRecordsRuleComponentState,
|
||||
);
|
||||
|
||||
const selectedRecordId =
|
||||
contextStoreTargetedRecordsRule.mode === 'selection'
|
||||
? contextStoreTargetedRecordsRule.selectedRecordIds?.[0]
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(selectedRecordId)) {
|
||||
throw new Error('Selected record ID is required');
|
||||
}
|
||||
|
||||
const {
|
||||
registerSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction,
|
||||
unregisterSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction,
|
||||
} = useSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction({
|
||||
workflowVersionId: selectedRecordId,
|
||||
});
|
||||
|
||||
const {
|
||||
registerUseAsDraftWorkflowVersionSingleRecordAction,
|
||||
unregisterUseAsDraftWorkflowVersionSingleRecordAction,
|
||||
} = useUseAsDraftWorkflowVersionSingleRecordAction({
|
||||
workflowVersionId: selectedRecordId,
|
||||
});
|
||||
|
||||
const {
|
||||
registerSeeWorkflowExecutionsWorkflowVersionSingleRecordAction,
|
||||
unregisterSeeWorkflowExecutionsWorkflowVersionSingleRecordAction,
|
||||
} = useSeeWorkflowExecutionsWorkflowVersionSingleRecordAction({
|
||||
workflowVersionId: selectedRecordId,
|
||||
});
|
||||
|
||||
const registerSingleRecordActions = ({
|
||||
startPosition,
|
||||
}: {
|
||||
startPosition: number;
|
||||
}) => {
|
||||
registerUseAsDraftWorkflowVersionSingleRecordAction({
|
||||
position: startPosition,
|
||||
});
|
||||
registerSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction({
|
||||
position: startPosition + 1,
|
||||
});
|
||||
|
||||
registerSeeWorkflowExecutionsWorkflowVersionSingleRecordAction({
|
||||
position: startPosition + 2,
|
||||
});
|
||||
};
|
||||
|
||||
const unregisterSingleRecordActions = () => {
|
||||
unregisterUseAsDraftWorkflowVersionSingleRecordAction();
|
||||
unregisterSeeWorkflowVersionsHistoryWorkflowVersionSingleRecordAction();
|
||||
unregisterSeeWorkflowExecutionsWorkflowVersionSingleRecordAction();
|
||||
};
|
||||
|
||||
return {
|
||||
registerSingleRecordActions,
|
||||
unregisterSingleRecordActions,
|
||||
};
|
||||
};
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useEffect } from 'react';
|
||||
import { useWorkflowRunRecordActions } from '../hooks/useWorkflowRunRecordActions';
|
||||
|
||||
export const WorkflowRunRecordActionMenuEntrySetterEffect = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
}) => {
|
||||
const {
|
||||
registerWorkflowRunRecordActions,
|
||||
unregisterWorkflowRunRecordActions,
|
||||
} = useWorkflowRunRecordActions({
|
||||
objectMetadataItem,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
registerWorkflowRunRecordActions();
|
||||
|
||||
return () => {
|
||||
unregisterWorkflowRunRecordActions();
|
||||
};
|
||||
}, [registerWorkflowRunRecordActions, unregisterWorkflowRunRecordActions]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+23
-39
@@ -6,19 +6,15 @@ import {
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useAllActiveWorkflowVersions } from '@/workflow/hooks/useAllActiveWorkflowVersions';
|
||||
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
|
||||
|
||||
import { useTheme } from '@emotion/react';
|
||||
import { useEffect } from 'react';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { IconSettingsAutomation, isDefined } from 'twenty-ui';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const WorkflowRunRecordActionEffect = ({
|
||||
export const useWorkflowRunRecordActions = ({
|
||||
objectMetadataItem,
|
||||
}: {
|
||||
objectMetadataItem: ObjectMetadataItem;
|
||||
@@ -34,8 +30,12 @@ export const WorkflowRunRecordActionEffect = ({
|
||||
? contextStoreTargetedRecordsRule.selectedRecordIds[0]
|
||||
: undefined;
|
||||
|
||||
if (!isDefined(selectedRecordId)) {
|
||||
throw new Error('Selected record ID is required');
|
||||
}
|
||||
|
||||
const selectedRecord = useRecoilValue(
|
||||
recordStoreFamilyState(selectedRecordId ?? ''),
|
||||
recordStoreFamilyState(selectedRecordId),
|
||||
);
|
||||
|
||||
const { records: activeWorkflowVersions } = useAllActiveWorkflowVersions({
|
||||
@@ -45,11 +45,7 @@ export const WorkflowRunRecordActionEffect = ({
|
||||
|
||||
const { runWorkflowVersion } = useRunWorkflowVersion();
|
||||
|
||||
const { enqueueSnackBar } = useSnackBar();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
const registerWorkflowRunRecordActions = () => {
|
||||
if (!isDefined(objectMetadataItem) || objectMetadataItem.isRemote) {
|
||||
return;
|
||||
}
|
||||
@@ -58,11 +54,15 @@ export const WorkflowRunRecordActionEffect = ({
|
||||
index,
|
||||
activeWorkflowVersion,
|
||||
] of activeWorkflowVersions.entries()) {
|
||||
if (!isDefined(activeWorkflowVersion.workflow)) {
|
||||
continue;
|
||||
}
|
||||
const name = capitalize(activeWorkflowVersion.workflow.name);
|
||||
addActionMenuEntry({
|
||||
type: ActionMenuEntryType.WorkflowRun,
|
||||
key: `workflow-run-${activeWorkflowVersion.id}`,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
label: capitalize(activeWorkflowVersion.workflow.name),
|
||||
label: name,
|
||||
position: index,
|
||||
Icon: IconSettingsAutomation,
|
||||
onClick: async () => {
|
||||
@@ -72,38 +72,22 @@ export const WorkflowRunRecordActionEffect = ({
|
||||
|
||||
await runWorkflowVersion({
|
||||
workflowVersionId: activeWorkflowVersion.id,
|
||||
workflowName: name,
|
||||
payload: selectedRecord,
|
||||
});
|
||||
|
||||
enqueueSnackBar('', {
|
||||
variant: SnackBarVariant.Success,
|
||||
title: `${capitalize(activeWorkflowVersion.workflow.name)} starting...`,
|
||||
icon: (
|
||||
<IconSettingsAutomation
|
||||
size={16}
|
||||
color={theme.snackBar.success.color}
|
||||
/>
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
for (const activeWorkflowVersion of activeWorkflowVersions) {
|
||||
removeActionMenuEntry(`workflow-run-${activeWorkflowVersion.id}`);
|
||||
}
|
||||
};
|
||||
}, [
|
||||
activeWorkflowVersions,
|
||||
addActionMenuEntry,
|
||||
enqueueSnackBar,
|
||||
objectMetadataItem,
|
||||
removeActionMenuEntry,
|
||||
runWorkflowVersion,
|
||||
selectedRecord,
|
||||
theme.snackBar.success.color,
|
||||
]);
|
||||
const unregisterWorkflowRunRecordActions = () => {
|
||||
for (const activeWorkflowVersion of activeWorkflowVersions) {
|
||||
removeActionMenuEntry(`workflow-run-${activeWorkflowVersion.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
return null;
|
||||
return {
|
||||
registerWorkflowRunRecordActions,
|
||||
unregisterWorkflowRunRecordActions,
|
||||
};
|
||||
};
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { useRecordAgnosticActions } from '@/action-menu/actions/record-agnostic-actions/hooks/useRecordAgnosticActions';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export const RecordAgnosticActionsSetterEffect = () => {
|
||||
const { registerRecordAgnosticActions, unregisterRecordAgnosticActions } =
|
||||
useRecordAgnosticActions();
|
||||
|
||||
useEffect(() => {
|
||||
registerRecordAgnosticActions();
|
||||
|
||||
return () => {
|
||||
unregisterRecordAgnosticActions();
|
||||
};
|
||||
}, [registerRecordAgnosticActions, unregisterRecordAgnosticActions]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import { useWorkflowRunActions } from '@/action-menu/actions/record-agnostic-actions/workflow-run-actions/hooks/useWorkflowRunActions';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
|
||||
export const useRecordAgnosticActions = () => {
|
||||
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
|
||||
|
||||
const { addWorkflowRunActions, removeWorkflowRunActions } =
|
||||
useWorkflowRunActions();
|
||||
|
||||
const registerRecordAgnosticActions = () => {
|
||||
if (isWorkflowEnabled) {
|
||||
addWorkflowRunActions();
|
||||
}
|
||||
};
|
||||
|
||||
const unregisterRecordAgnosticActions = () => {
|
||||
if (isWorkflowEnabled) {
|
||||
removeWorkflowRunActions();
|
||||
}
|
||||
};
|
||||
|
||||
return { registerRecordAgnosticActions, unregisterRecordAgnosticActions };
|
||||
};
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries';
|
||||
import {
|
||||
ActionMenuEntryScope,
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { useAllActiveWorkflowVersions } from '@/workflow/hooks/useAllActiveWorkflowVersions';
|
||||
import { useRunWorkflowVersion } from '@/workflow/hooks/useRunWorkflowVersion';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
|
||||
import { IconSettingsAutomation, isDefined } from 'twenty-ui';
|
||||
import { capitalize } from '~/utils/string/capitalize';
|
||||
|
||||
export const useWorkflowRunActions = () => {
|
||||
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
|
||||
|
||||
const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries();
|
||||
|
||||
const { records: activeWorkflowVersions } = useAllActiveWorkflowVersions({
|
||||
triggerType: 'MANUAL',
|
||||
});
|
||||
|
||||
const { runWorkflowVersion } = useRunWorkflowVersion();
|
||||
|
||||
const addWorkflowRunActions = () => {
|
||||
if (!isWorkflowEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [
|
||||
index,
|
||||
activeWorkflowVersion,
|
||||
] of activeWorkflowVersions.entries()) {
|
||||
if (!isDefined(activeWorkflowVersion.workflow)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const name = capitalize(activeWorkflowVersion.workflow.name);
|
||||
|
||||
addActionMenuEntry({
|
||||
type: ActionMenuEntryType.WorkflowRun,
|
||||
key: `workflow-run-${activeWorkflowVersion.id}`,
|
||||
scope: ActionMenuEntryScope.Global,
|
||||
label: name,
|
||||
position: index,
|
||||
Icon: IconSettingsAutomation,
|
||||
onClick: async () => {
|
||||
await runWorkflowVersion({
|
||||
workflowVersionId: activeWorkflowVersion.id,
|
||||
workflowName: name,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const removeWorkflowRunActions = () => {
|
||||
for (const activeWorkflowVersion of activeWorkflowVersions) {
|
||||
removeActionMenuEntry(`workflow-run-${activeWorkflowVersion.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
return { addWorkflowRunActions, removeWorkflowRunActions };
|
||||
};
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Button } from 'twenty-ui';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
|
||||
|
||||
export const CmdEnterActionButton = ({
|
||||
title,
|
||||
onClick,
|
||||
}: {
|
||||
title: string;
|
||||
onClick: () => void;
|
||||
}) => {
|
||||
useScopedHotkeys(
|
||||
[`${Key.Control}+${Key.Enter}`, `${Key.Meta}+${Key.Enter}`],
|
||||
() => onClick(),
|
||||
RightDrawerHotkeyScope.RightDrawer,
|
||||
[onClick],
|
||||
);
|
||||
|
||||
return (
|
||||
<Button
|
||||
title={title}
|
||||
variant="primary"
|
||||
accent="blue"
|
||||
size="medium"
|
||||
onClick={onClick}
|
||||
shortcut={'⌘⏎'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,19 +1,30 @@
|
||||
import { GlobalActionMenuEntriesSetter } from '@/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter';
|
||||
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
|
||||
import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect';
|
||||
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
|
||||
import { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar';
|
||||
import { RecordIndexActionMenuButtons } from '@/action-menu/components/RecordIndexActionMenuButtons';
|
||||
import { RecordIndexActionMenuDropdown } from '@/action-menu/components/RecordIndexActionMenuDropdown';
|
||||
import { RecordIndexActionMenuEffect } from '@/action-menu/components/RecordIndexActionMenuEffect';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
|
||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useIsMobile } from 'twenty-ui';
|
||||
|
||||
export const RecordIndexActionMenu = () => {
|
||||
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
|
||||
contextStoreCurrentObjectMetadataIdComponentState,
|
||||
);
|
||||
|
||||
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
|
||||
|
||||
const isPageHeaderV2Enabled = useIsFeatureEnabled(
|
||||
'IS_PAGE_HEADER_V2_ENABLED',
|
||||
);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextStoreCurrentObjectMetadataId && (
|
||||
@@ -23,12 +34,16 @@ export const RecordIndexActionMenu = () => {
|
||||
onActionExecutedCallback: () => {},
|
||||
}}
|
||||
>
|
||||
<RecordIndexActionMenuBar />
|
||||
{isPageHeaderV2Enabled ? (
|
||||
<>{!isMobile && <RecordIndexActionMenuButtons />}</>
|
||||
) : (
|
||||
<RecordIndexActionMenuBar />
|
||||
)}
|
||||
<RecordIndexActionMenuDropdown />
|
||||
<ActionMenuConfirmationModals />
|
||||
<RecordIndexActionMenuEffect />
|
||||
<RecordActionMenuEntriesSetter />
|
||||
<GlobalActionMenuEntriesSetter />
|
||||
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
|
||||
</ActionMenuContext.Provider>
|
||||
)}
|
||||
</>
|
||||
|
||||
+1
-3
@@ -1,5 +1,3 @@
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { RecordIndexActionMenuBarAllActionsButton } from '@/action-menu/components/RecordIndexActionMenuBarAllActionsButton';
|
||||
import { RecordIndexActionMenuBarEntry } from '@/action-menu/components/RecordIndexActionMenuBarEntry';
|
||||
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
|
||||
@@ -10,6 +8,7 @@ import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-sto
|
||||
import { BottomBar } from '@/ui/layout/bottom-bar/components/BottomBar';
|
||||
import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
const StyledLabel = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
@@ -33,7 +32,6 @@ export const RecordIndexActionMenuBar = () => {
|
||||
);
|
||||
|
||||
const pinnedEntries = actionMenuEntries.filter((entry) => entry.isPinned);
|
||||
|
||||
if (contextStoreNumberOfSelectedRecords === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
+2
-4
@@ -1,8 +1,7 @@
|
||||
import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
|
||||
import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry';
|
||||
|
||||
type RecordIndexActionMenuBarEntryProps = {
|
||||
entry: ActionMenuEntry;
|
||||
};
|
||||
@@ -13,11 +12,9 @@ const StyledButton = styled.div`
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
||||
padding: ${({ theme }) => theme.spacing(2)};
|
||||
transition: background ${({ theme }) => theme.animation.duration.fast} ease;
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.tertiary};
|
||||
}
|
||||
@@ -32,6 +29,7 @@ export const RecordIndexActionMenuBarEntry = ({
|
||||
entry,
|
||||
}: RecordIndexActionMenuBarEntryProps) => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<StyledButton onClick={() => entry.onClick?.()}>
|
||||
{entry.Icon && <entry.Icon size={theme.icon.size.md} />}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { Button } from 'twenty-ui';
|
||||
|
||||
export const RecordIndexActionMenuButtons = () => {
|
||||
const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2(
|
||||
contextStoreNumberOfSelectedRecordsComponentState,
|
||||
);
|
||||
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentSelector,
|
||||
);
|
||||
|
||||
const pinnedEntries = actionMenuEntries.filter((entry) => entry.isPinned);
|
||||
|
||||
if (contextStoreNumberOfSelectedRecords === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{pinnedEntries.map((entry, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
Icon={entry.Icon}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
title={entry.label}
|
||||
onClick={() => entry.onClick?.()}
|
||||
ariaLabel={entry.label}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
+3
-5
@@ -21,10 +21,6 @@ type StyledContainerProps = {
|
||||
|
||||
const StyledContainerActionMenuDropdown = styled.div<StyledContainerProps>`
|
||||
align-items: flex-start;
|
||||
background: ${({ theme }) => theme.background.secondary};
|
||||
border: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
border-radius: ${({ theme }) => theme.border.radius.md};
|
||||
box-shadow: ${({ theme }) => theme.boxShadow.strong};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -33,7 +29,8 @@ const StyledContainerActionMenuDropdown = styled.div<StyledContainerProps>`
|
||||
top: ${(props) => `${props.position.y}px`};
|
||||
|
||||
transform: translateX(-50%);
|
||||
width: auto;
|
||||
width: 0;
|
||||
height: 0;
|
||||
`;
|
||||
|
||||
export const RecordIndexActionMenuDropdown = () => {
|
||||
@@ -84,6 +81,7 @@ export const RecordIndexActionMenuDropdown = () => {
|
||||
))}
|
||||
</DropdownMenuItemsContainer>
|
||||
}
|
||||
avoidPortal
|
||||
/>
|
||||
</StyledContainerActionMenuDropdown>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { GlobalActionMenuEntriesSetter } from '@/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter';
|
||||
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
|
||||
import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect';
|
||||
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
|
||||
import { RecordShowActionMenuButtons } from '@/action-menu/components/RecordShowActionMenuButtons';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
|
||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { RecordShowPageBaseHeader } from '~/pages/object-record/RecordShowPageBaseHeader';
|
||||
|
||||
export const RecordShowActionMenu = ({
|
||||
@@ -26,6 +28,12 @@ export const RecordShowActionMenu = ({
|
||||
contextStoreCurrentObjectMetadataIdComponentState,
|
||||
);
|
||||
|
||||
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
|
||||
|
||||
const isPageHeaderV2Enabled = useIsFeatureEnabled(
|
||||
'IS_PAGE_HEADER_V2_ENABLED',
|
||||
);
|
||||
|
||||
// TODO: refactor RecordShowPageBaseHeader to use the context store
|
||||
|
||||
return (
|
||||
@@ -37,18 +45,22 @@ export const RecordShowActionMenu = ({
|
||||
onActionExecutedCallback: () => {},
|
||||
}}
|
||||
>
|
||||
<RecordShowPageBaseHeader
|
||||
{...{
|
||||
isFavorite,
|
||||
record,
|
||||
objectMetadataItem,
|
||||
objectNameSingular,
|
||||
handleFavoriteButtonClick,
|
||||
}}
|
||||
/>
|
||||
{isPageHeaderV2Enabled ? (
|
||||
<RecordShowActionMenuButtons />
|
||||
) : (
|
||||
<RecordShowPageBaseHeader
|
||||
{...{
|
||||
isFavorite,
|
||||
record,
|
||||
objectMetadataItem,
|
||||
objectNameSingular,
|
||||
handleFavoriteButtonClick,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ActionMenuConfirmationModals />
|
||||
<RecordActionMenuEntriesSetter />
|
||||
<GlobalActionMenuEntriesSetter />
|
||||
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
|
||||
</ActionMenuContext.Provider>
|
||||
)}
|
||||
</>
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector';
|
||||
import { PageHeaderOpenCommandMenuButton } from '@/ui/layout/page-header/components/PageHeaderOpenCommandMenuButton';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { Button, useIsMobile } from 'twenty-ui';
|
||||
|
||||
export const RecordShowActionMenuButtons = () => {
|
||||
const actionMenuEntries = useRecoilComponentValueV2(
|
||||
actionMenuEntriesComponentSelector,
|
||||
);
|
||||
|
||||
const pinnedEntries = actionMenuEntries.filter((entry) => entry.isPinned);
|
||||
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
return (
|
||||
<>
|
||||
{!isMobile &&
|
||||
pinnedEntries.map((entry, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
Icon={entry.Icon}
|
||||
size="small"
|
||||
variant="secondary"
|
||||
accent="default"
|
||||
title={entry.label}
|
||||
onClick={() => entry.onClick?.()}
|
||||
ariaLabel={entry.label}
|
||||
/>
|
||||
))}
|
||||
<PageHeaderOpenCommandMenuButton key="more" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
+5
-2
@@ -1,17 +1,20 @@
|
||||
import { GlobalActionMenuEntriesSetter } from '@/action-menu/actions/global-actions/components/GlobalActionMenuEntriesSetter';
|
||||
import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter';
|
||||
import { RecordAgnosticActionsSetterEffect } from '@/action-menu/actions/record-agnostic-actions/components/RecordAgnosticActionsSetterEffect';
|
||||
import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals';
|
||||
import { RightDrawerActionMenuDropdown } from '@/action-menu/components/RightDrawerActionMenuDropdown';
|
||||
import { ActionMenuContext } from '@/action-menu/contexts/ActionMenuContext';
|
||||
|
||||
import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
|
||||
export const RecordShowRightDrawerActionMenu = () => {
|
||||
const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2(
|
||||
contextStoreCurrentObjectMetadataIdComponentState,
|
||||
);
|
||||
|
||||
const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED');
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextStoreCurrentObjectMetadataId && (
|
||||
@@ -24,7 +27,7 @@ export const RecordShowRightDrawerActionMenu = () => {
|
||||
<RightDrawerActionMenuDropdown />
|
||||
<ActionMenuConfirmationModals />
|
||||
<RecordActionMenuEntriesSetter />
|
||||
<GlobalActionMenuEntriesSetter />
|
||||
{isWorkflowEnabled && <RecordAgnosticActionsSetterEffect />}
|
||||
</ActionMenuContext.Provider>
|
||||
)}
|
||||
</>
|
||||
|
||||
+4
-12
@@ -1,7 +1,3 @@
|
||||
import { expect, jest } from '@storybook/jest';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
|
||||
import { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar';
|
||||
import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState';
|
||||
import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext';
|
||||
@@ -11,11 +7,14 @@ import {
|
||||
ActionMenuEntryType,
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { getActionBarIdFromActionMenuId } from '@/action-menu/utils/getActionBarIdFromActionMenuId';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
|
||||
import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState';
|
||||
import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext';
|
||||
import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState';
|
||||
import { expect, jest } from '@storybook/jest';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { userEvent, waitFor, within } from '@storybook/test';
|
||||
import { RecoilRoot } from 'recoil';
|
||||
import { IconTrash, RouterDecorator } from 'twenty-ui';
|
||||
|
||||
const deleteMock = jest.fn();
|
||||
@@ -40,16 +39,13 @@ const meta: Meta<typeof RecordIndexActionMenuBar> = {
|
||||
selectedRecordIds: ['1', '2', '3'],
|
||||
},
|
||||
);
|
||||
|
||||
set(
|
||||
contextStoreNumberOfSelectedRecordsComponentState.atomFamily({
|
||||
instanceId: 'story-action-menu',
|
||||
}),
|
||||
3,
|
||||
);
|
||||
|
||||
const map = new Map<string, ActionMenuEntry>();
|
||||
|
||||
map.set('delete', {
|
||||
isPinned: true,
|
||||
scope: ActionMenuEntryScope.RecordSelection,
|
||||
@@ -60,14 +56,12 @@ const meta: Meta<typeof RecordIndexActionMenuBar> = {
|
||||
Icon: IconTrash,
|
||||
onClick: deleteMock,
|
||||
});
|
||||
|
||||
set(
|
||||
actionMenuEntriesComponentState.atomFamily({
|
||||
instanceId: 'story-action-menu',
|
||||
}),
|
||||
map,
|
||||
);
|
||||
|
||||
set(
|
||||
isBottomBarOpenedComponentState.atomFamily({
|
||||
instanceId: getActionBarIdFromActionMenuId('story-action-menu'),
|
||||
@@ -117,10 +111,8 @@ export const WithButtonClicks: Story = {
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const deleteButton = await canvas.findByText('Delete');
|
||||
await userEvent.click(deleteButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(deleteMock).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
-2
@@ -6,7 +6,6 @@ import {
|
||||
import { expect, jest } from '@storybook/jest';
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { userEvent, within } from '@storybook/testing-library';
|
||||
|
||||
import { ComponentDecorator, IconCheckbox, IconTrash } from 'twenty-ui';
|
||||
|
||||
const meta: Meta<typeof RecordIndexActionMenuBarEntry> = {
|
||||
@@ -14,7 +13,6 @@ const meta: Meta<typeof RecordIndexActionMenuBarEntry> = {
|
||||
component: RecordIndexActionMenuBarEntry,
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof RecordIndexActionMenuBarEntry>;
|
||||
|
||||
+9
-2
@@ -14,7 +14,12 @@ import {
|
||||
} from '@/action-menu/types/ActionMenuEntry';
|
||||
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
|
||||
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
|
||||
import { IconCheckbox, IconHeart, IconTrash } from 'twenty-ui';
|
||||
import {
|
||||
IconCheckbox,
|
||||
IconHeart,
|
||||
IconTrash,
|
||||
getCanvasElementForDropdownTesting,
|
||||
} from 'twenty-ui';
|
||||
|
||||
const deleteMock = jest.fn();
|
||||
const markAsDoneMock = jest.fn();
|
||||
@@ -107,7 +112,9 @@ export const WithInteractions: Story = {
|
||||
args: {
|
||||
actionMenuId: 'story',
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
play: async () => {
|
||||
const canvasElement = getCanvasElementForDropdownTesting();
|
||||
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const deleteButton = await canvas.findByText('Delete');
|
||||
|
||||
+3
-2
@@ -15,6 +15,7 @@ import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/s
|
||||
import { userEvent, waitFor, within } from '@storybook/test';
|
||||
import {
|
||||
ComponentDecorator,
|
||||
getCanvasElementForDropdownTesting,
|
||||
IconFileExport,
|
||||
IconHeart,
|
||||
IconTrash,
|
||||
@@ -117,8 +118,8 @@ export const WithButtonClicks: Story = {
|
||||
args: {
|
||||
actionMenuId: 'story-action-menu',
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
play: async () => {
|
||||
const canvas = within(getCanvasElementForDropdownTesting());
|
||||
|
||||
let actionButton = await canvas.findByText('Actions');
|
||||
await userEvent.click(actionButton);
|
||||
+19
-10
@@ -20,6 +20,13 @@ type CalendarCurrentEventCursorProps = {
|
||||
calendarEvent: TimelineCalendarEvent;
|
||||
};
|
||||
|
||||
const StyledDot = styled(motion.div)`
|
||||
background-color: ${({ theme }) => theme.font.color.danger};
|
||||
border-radius: 1px;
|
||||
height: ${({ theme }) => theme.spacing(1)};
|
||||
width: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
const StyledCurrentEventCursor = styled(motion.div)`
|
||||
align-items: center;
|
||||
background-color: ${({ theme }) => theme.font.color.danger};
|
||||
@@ -30,15 +37,6 @@ const StyledCurrentEventCursor = styled(motion.div)`
|
||||
right: 0;
|
||||
border-radius: ${({ theme }) => theme.border.radius.sm};
|
||||
transform: translateY(-50%);
|
||||
|
||||
&::before {
|
||||
background-color: ${({ theme }) => theme.font.color.danger};
|
||||
border-radius: 1px;
|
||||
content: '';
|
||||
display: block;
|
||||
height: ${({ theme }) => theme.spacing(1)};
|
||||
width: ${({ theme }) => theme.spacing(1)};
|
||||
}
|
||||
`;
|
||||
|
||||
export const CalendarCurrentEventCursor = ({
|
||||
@@ -166,7 +164,18 @@ export const CalendarCurrentEventCursor = ({
|
||||
transition={{
|
||||
duration: theme.animation.duration.normal,
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
transition={{
|
||||
delay: theme.animation.duration.normal,
|
||||
duration: theme.animation.duration.normal,
|
||||
}}
|
||||
>
|
||||
<StyledDot />
|
||||
</motion.div>
|
||||
</StyledCurrentEventCursor>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
|
||||
+13
-11
@@ -29,7 +29,7 @@ import { recordStoreFamilyState } from '@/object-record/record-store/states/reco
|
||||
import { ActivityTargetInlineCellEditModeMultiRecordsEffect } from '@/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsEffect';
|
||||
import { ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect } from '@/object-record/relation-picker/components/ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect';
|
||||
import { MultiRecordSelect } from '@/object-record/relation-picker/components/MultiRecordSelect';
|
||||
import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope';
|
||||
import { RecordPickerComponentInstanceContext } from '@/object-record/relation-picker/states/contexts/RecordPickerComponentInstanceContext';
|
||||
import { prefillRecord } from '@/object-record/utils/prefillRecord';
|
||||
|
||||
const StyledSelectContainer = styled.div`
|
||||
@@ -52,7 +52,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
activityObjectNameSingular,
|
||||
}: ActivityTargetInlineCellEditModeProps) => {
|
||||
const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
|
||||
const relationPickerScopeId = `relation-picker-${activity.id}`;
|
||||
const recordPickerInstanceId = `record-picker-${activity.id}`;
|
||||
|
||||
const selectedTargetObjectIds = activityTargetWithTargetRecords.map(
|
||||
(activityTarget) => ({
|
||||
@@ -101,7 +101,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
const recordSelectedInMultiSelect = snapshot
|
||||
.getLoadable(
|
||||
objectRecordMultiSelectComponentFamilyState({
|
||||
scopeId: relationPickerScopeId,
|
||||
scopeId: recordPickerInstanceId,
|
||||
familyKey: activityTarget.targetObject.id,
|
||||
}),
|
||||
)
|
||||
@@ -126,7 +126,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
[
|
||||
activityTargetWithTargetRecords,
|
||||
closeEditableField,
|
||||
relationPickerScopeId,
|
||||
recordPickerInstanceId,
|
||||
setActivityFromStore,
|
||||
],
|
||||
);
|
||||
@@ -144,7 +144,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
const previouslyCheckedRecordsIds = snapshot
|
||||
.getLoadable(
|
||||
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
||||
scopeId: relationPickerScopeId,
|
||||
scopeId: recordPickerInstanceId,
|
||||
}),
|
||||
)
|
||||
.getValue();
|
||||
@@ -155,7 +155,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
const record = snapshot
|
||||
.getLoadable(
|
||||
objectRecordMultiSelectComponentFamilyState({
|
||||
scopeId: relationPickerScopeId,
|
||||
scopeId: recordPickerInstanceId,
|
||||
familyKey: recordId,
|
||||
}),
|
||||
)
|
||||
@@ -169,7 +169,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
|
||||
set(
|
||||
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
||||
scopeId: relationPickerScopeId,
|
||||
scopeId: recordPickerInstanceId,
|
||||
}),
|
||||
(prev) => [...prev, recordId],
|
||||
);
|
||||
@@ -239,7 +239,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
|
||||
set(
|
||||
objectRecordMultiSelectCheckedRecordsIdsComponentState({
|
||||
scopeId: relationPickerScopeId,
|
||||
scopeId: recordPickerInstanceId,
|
||||
}),
|
||||
previouslyCheckedRecordsIds.filter((id) => id !== recordId),
|
||||
);
|
||||
@@ -275,7 +275,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
deleteManyActivityTargets,
|
||||
isActivityInCreateMode,
|
||||
objectMetadataItemActivityTarget,
|
||||
relationPickerScopeId,
|
||||
recordPickerInstanceId,
|
||||
upsertActivity,
|
||||
activityObjectNameSingular,
|
||||
],
|
||||
@@ -283,7 +283,9 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
|
||||
return (
|
||||
<StyledSelectContainer>
|
||||
<RelationPickerScope relationPickerScopeId={relationPickerScopeId}>
|
||||
<RecordPickerComponentInstanceContext.Provider
|
||||
value={{ instanceId: recordPickerInstanceId }}
|
||||
>
|
||||
<ActivityTargetObjectRecordEffect
|
||||
activityTargetWithTargetRecords={activityTargetWithTargetRecords}
|
||||
/>
|
||||
@@ -292,7 +294,7 @@ export const ActivityTargetInlineCellEditMode = ({
|
||||
/>
|
||||
<ActivityTargetInlineCellEditModeMultiRecordsSearchFilterEffect />
|
||||
<MultiRecordSelect onSubmit={handleSubmit} onChange={handleChange} />
|
||||
</RelationPickerScope>
|
||||
</RecordPickerComponentInstanceContext.Provider>
|
||||
</StyledSelectContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import {
|
||||
AnimatedPlaceholder,
|
||||
AnimatedPlaceholderEmptyContainer,
|
||||
@@ -43,8 +42,7 @@ export const TaskGroups = ({ targetableObjects }: TaskGroupsProps) => {
|
||||
activityObjectNameSingular: CoreObjectNameSingular.Task,
|
||||
});
|
||||
|
||||
const { activeTabIdState } = useTabList(TASKS_TAB_LIST_COMPONENT_ID);
|
||||
const activeTabId = useRecoilValue(activeTabIdState);
|
||||
const { activeTabId } = useTabList(TASKS_TAB_LIST_COMPONENT_ID);
|
||||
|
||||
const isLoading =
|
||||
(activeTabId !== 'done' && tasksLoading) ||
|
||||
|
||||
+2
-6
@@ -3,7 +3,6 @@ import styled from '@emotion/styled';
|
||||
import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader';
|
||||
import { SkeletonLoader } from '@/activities/components/SkeletonLoader';
|
||||
import { EventList } from '@/activities/timeline-activities/components/EventList';
|
||||
import { TimelineCreateButtonGroup } from '@/activities/timeline-activities/components/TimelineCreateButtonGroup';
|
||||
import { useTimelineActivities } from '@/activities/timeline-activities/hooks/useTimelineActivities';
|
||||
import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
|
||||
@@ -41,10 +40,8 @@ const StyledMainContainer = styled.div`
|
||||
|
||||
export const TimelineActivities = ({
|
||||
targetableObject,
|
||||
isInRightDrawer = false,
|
||||
}: {
|
||||
targetableObject: ActivityTargetableObject;
|
||||
isInRightDrawer?: boolean;
|
||||
}) => {
|
||||
const { timelineActivities, loading, fetchMoreRecords } =
|
||||
useTimelineActivities(targetableObject);
|
||||
@@ -65,13 +62,12 @@ export const TimelineActivities = ({
|
||||
<AnimatedPlaceholder type="emptyTimeline" />
|
||||
<AnimatedPlaceholderEmptyTextContainer>
|
||||
<AnimatedPlaceholderEmptyTitle>
|
||||
Add your first Activity
|
||||
No activity yet
|
||||
</AnimatedPlaceholderEmptyTitle>
|
||||
<AnimatedPlaceholderEmptySubTitle>
|
||||
There are no activities associated with this record.{' '}
|
||||
There is no activity associated with this record.
|
||||
</AnimatedPlaceholderEmptySubTitle>
|
||||
</AnimatedPlaceholderEmptyTextContainer>
|
||||
<TimelineCreateButtonGroup isInRightDrawer={isInRightDrawer} />
|
||||
</AnimatedPlaceholderEmptyContainer>
|
||||
);
|
||||
}
|
||||
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
import { TAB_LIST_COMPONENT_ID } from '@/ui/layout/show-page/components/ShowPageSubContainer';
|
||||
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
|
||||
import { useSetRecoilState } from 'recoil';
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
IconCheckbox,
|
||||
IconNotes,
|
||||
IconPaperclip,
|
||||
} from 'twenty-ui';
|
||||
|
||||
export const TimelineCreateButtonGroup = ({
|
||||
isInRightDrawer = false,
|
||||
}: {
|
||||
isInRightDrawer?: boolean;
|
||||
}) => {
|
||||
const { activeTabIdState } = useTabList(
|
||||
`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`,
|
||||
);
|
||||
const setActiveTabId = useSetRecoilState(activeTabIdState);
|
||||
|
||||
return (
|
||||
<ButtonGroup variant={'secondary'}>
|
||||
<Button
|
||||
Icon={IconNotes}
|
||||
title="Note"
|
||||
onClick={() => {
|
||||
setActiveTabId('notes');
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
Icon={IconCheckbox}
|
||||
title="Task"
|
||||
onClick={() => {
|
||||
setActiveTabId('tasks');
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
Icon={IconPaperclip}
|
||||
title="File"
|
||||
onClick={() => setActiveTabId('files')}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useCreateAppRouter } from '@/app/hooks/useCreateAppRouter';
|
||||
import { currentUserState } from '@/auth/states/currentUserState';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { RouterProvider } from 'react-router-dom';
|
||||
@@ -16,6 +17,10 @@ export const AppRouter = () => {
|
||||
const isBillingPageEnabled =
|
||||
billing?.isBillingEnabled && !isFreeAccessEnabled;
|
||||
|
||||
const currentUser = useRecoilValue(currentUserState);
|
||||
|
||||
const isAdminPageEnabled = currentUser?.canImpersonate;
|
||||
|
||||
return (
|
||||
<RouterProvider
|
||||
router={useCreateAppRouter(
|
||||
@@ -23,6 +28,7 @@ export const AppRouter = () => {
|
||||
isCRMMigrationEnabled,
|
||||
isServerlessFunctionSettingsEnabled,
|
||||
isSSOEnabled,
|
||||
isAdminPageEnabled,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -8,6 +8,7 @@ import { ClientConfigProvider } from '@/client-config/components/ClientConfigPro
|
||||
import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect';
|
||||
import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
|
||||
import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider';
|
||||
import { ObjectMetadataItemsGater } from '@/object-metadata/components/ObjectMetadataItemsGater';
|
||||
import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
|
||||
import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider';
|
||||
import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager';
|
||||
@@ -15,9 +16,11 @@ import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogMa
|
||||
import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider';
|
||||
import { UserThemeProviderEffect } from '@/ui/theme/components/AppThemeProvider';
|
||||
import { BaseThemeProvider } from '@/ui/theme/components/BaseThemeProvider';
|
||||
import { PageFavicon } from '@/ui/utilities/page-favicon/components/PageFavicon';
|
||||
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
|
||||
import { UserProvider } from '@/users/components/UserProvider';
|
||||
import { UserProviderEffect } from '@/users/components/UserProviderEffect';
|
||||
import { WorkspaceProviderEffect } from '@/workspace/components/WorkspaceProviderEffect';
|
||||
import { StrictMode } from 'react';
|
||||
import { Outlet, useLocation } from 'react-router-dom';
|
||||
import { getPageTitleFromPath } from '~/utils/title-utils';
|
||||
@@ -34,25 +37,29 @@ export const AppRouterProviders = () => {
|
||||
<ChromeExtensionSidecarEffect />
|
||||
<ChromeExtensionSidecarProvider>
|
||||
<UserProviderEffect />
|
||||
<WorkspaceProviderEffect />
|
||||
<UserProvider>
|
||||
<AuthProvider>
|
||||
<ApolloMetadataClientProvider>
|
||||
<ObjectMetadataItemsProvider>
|
||||
<PrefetchDataProvider>
|
||||
<UserThemeProviderEffect />
|
||||
<SnackBarProvider>
|
||||
<DialogManagerScope dialogManagerScopeId="dialog-manager">
|
||||
<DialogManager>
|
||||
<StrictMode>
|
||||
<PromiseRejectionEffect />
|
||||
<GotoHotkeysEffectsProvider />
|
||||
<PageTitle title={pageTitle} />
|
||||
<Outlet />
|
||||
</StrictMode>
|
||||
</DialogManager>
|
||||
</DialogManagerScope>
|
||||
</SnackBarProvider>
|
||||
</PrefetchDataProvider>
|
||||
<ObjectMetadataItemsGater>
|
||||
<PrefetchDataProvider>
|
||||
<UserThemeProviderEffect />
|
||||
<SnackBarProvider>
|
||||
<DialogManagerScope dialogManagerScopeId="dialog-manager">
|
||||
<DialogManager>
|
||||
<StrictMode>
|
||||
<PromiseRejectionEffect />
|
||||
<GotoHotkeysEffectsProvider />
|
||||
<PageTitle title={pageTitle} />
|
||||
<PageFavicon />
|
||||
<Outlet />
|
||||
</StrictMode>
|
||||
</DialogManager>
|
||||
</DialogManagerScope>
|
||||
</SnackBarProvider>
|
||||
</PrefetchDataProvider>
|
||||
</ObjectMetadataItemsGater>
|
||||
<PageChangeEffect />
|
||||
</ObjectMetadataItemsProvider>
|
||||
</ApolloMetadataClientProvider>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user