Compare commits
149
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5717752be | ||
|
|
24796e9355 | ||
|
|
15fc850212 | ||
|
|
e6d5df751b | ||
|
|
5b4fed1afe | ||
|
|
e757a1418d | ||
|
|
2f298307f9 | ||
|
|
c15536f9f8 | ||
|
|
b2f7c745f8 | ||
|
|
0ee63a0525 | ||
|
|
18880f0385 | ||
|
|
0902579fbe | ||
|
|
52e57e70fd | ||
|
|
1e01f15182 | ||
|
|
d0c1841f0f | ||
|
|
5be64bf4be | ||
|
|
d9dab75052 | ||
|
|
2237273869 | ||
|
|
9e21e55db4 | ||
|
|
859241d237 | ||
|
|
41e413d7b1 | ||
|
|
b0cb29c11c | ||
|
|
b4d957306e | ||
|
|
9c984b137f | ||
|
|
05a6a96b13 | ||
|
|
16d414590b | ||
|
|
bc72879c70 | ||
|
|
bb037e13dd | ||
|
|
17786a3298 | ||
|
|
8f91529153 | ||
|
|
e995e84621 | ||
|
|
148584c730 | ||
|
|
b7ff587b5e | ||
|
|
2e77a68daf | ||
|
|
382746dd52 | ||
|
|
202a130ac8 | ||
|
|
24c6d3fef3 | ||
|
|
52fe21c04b | ||
|
|
ee0474e287 | ||
|
|
3f202c5c6a | ||
|
|
a63b31931f | ||
|
|
3c2aec1894 | ||
|
|
b250de216e | ||
|
|
2d42b0a726 | ||
|
|
7e5e800f63 | ||
|
|
8c70a875f8 | ||
|
|
a40612ef9e | ||
|
|
1979e013e9 | ||
|
|
3747005fd5 | ||
|
|
268cc40d80 | ||
|
|
11fd1a41f3 | ||
|
|
05eca08ad2 | ||
|
|
ef1464db73 | ||
|
|
5ccf18fdf5 | ||
|
|
9e6f19d16e | ||
|
|
987ed845ac | ||
|
|
f51291704d | ||
|
|
37b9a55382 | ||
|
|
aa7973e5b8 | ||
|
|
8c951d3623 | ||
|
|
2a76e1791e | ||
|
|
4ae375308f | ||
|
|
c01853c349 | ||
|
|
21b2b65dbe | ||
|
|
c6d04ccced | ||
|
|
c18726d712 | ||
|
|
96bc3594a3 | ||
|
|
617f634b6d | ||
|
|
8e977912e2 | ||
|
|
4cdb7d61b2 | ||
|
|
7fbd50e5a0 | ||
|
|
0b4cebfe0e | ||
|
|
d7c28d6455 | ||
|
|
2b29918bf8 | ||
|
|
2106f46e9e | ||
|
|
1edce5088c | ||
|
|
3216b634a3 | ||
|
|
6c7c389785 | ||
|
|
9162685b2e | ||
|
|
bf4c348c8b | ||
|
|
ece265c6e4 | ||
|
|
15f09736b2 | ||
|
|
bade5289b6 | ||
|
|
497230a052 | ||
|
|
9aa63f7ddc | ||
|
|
40d7e740ef | ||
|
|
7402edb887 | ||
|
|
d7132b35d3 | ||
|
|
bc6268bb29 | ||
|
|
c44d61f324 | ||
|
|
cc86b9acae | ||
|
|
30cbf0e40e | ||
|
|
4acca5b10d | ||
|
|
3dc5b162c7 | ||
|
|
a494a7a902 | ||
|
|
f6beb06364 | ||
|
|
7074d9ce1b | ||
|
|
b3c95744ef | ||
|
|
10de51fcbd | ||
|
|
d537144ab1 | ||
|
|
955c1b4916 | ||
|
|
e505eba978 | ||
|
|
77d15356e1 | ||
|
|
e382641ac3 | ||
|
|
04b8f2aaf5 | ||
|
|
23df33172a | ||
|
|
5b701f5ba4 | ||
|
|
df516a904b | ||
|
|
c0fd4c7fed | ||
|
|
c521406bf8 | ||
|
|
27da9d60eb | ||
|
|
0473efcef0 | ||
|
|
de5764ede0 | ||
|
|
f899804db3 | ||
|
|
66b87c641c | ||
|
|
309785f7ff | ||
|
|
60f2a74bf0 | ||
|
|
6851085143 | ||
|
|
9604dbe78a | ||
|
|
f733f30b62 | ||
|
|
1d6dc04cda | ||
|
|
67a98f77e3 | ||
|
|
752359335e | ||
|
|
64700a00b0 | ||
|
|
8674cf973e | ||
|
|
96c37b30c5 | ||
|
|
bef643970b | ||
|
|
6a5974e06c | ||
|
|
d4303469ff | ||
|
|
4629fb90be | ||
|
|
48c8fa6809 | ||
|
|
b03e3772e6 | ||
|
|
660a15b721 | ||
|
|
59c36736d4 | ||
|
|
92359603c1 | ||
|
|
177cae8c53 | ||
|
|
6407474461 | ||
|
|
bcfacdead9 | ||
|
|
4fcc424e24 | ||
|
|
9b8eacb7f7 | ||
|
|
f6b210dd0d | ||
|
|
50ef231704 | ||
|
|
aa06dab920 | ||
|
|
414f68fb63 | ||
|
|
0edc3a385c | ||
|
|
29093fdbf1 | ||
|
|
402d149ee1 | ||
|
|
e10e0b337e | ||
|
|
7867617385 |
@@ -913,7 +913,7 @@ export class WorkspaceMigrationMyEntityActionsBuilderService extends WorkspaceEn
|
||||
type: 'update',
|
||||
metadataName: 'myEntity',
|
||||
entityId: flatEntityId,
|
||||
updates: flatEntityUpdates,
|
||||
update: flatEntityUpdates,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -25,7 +25,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
task: [lint, typecheck, test:unit]
|
||||
task: [lint, typecheck, test:unit, storybook:build, storybook:test, test:integration]
|
||||
steps:
|
||||
- name: Cancel Previous Runs
|
||||
uses: styfle/[email protected]
|
||||
@@ -39,72 +39,62 @@ jobs:
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build
|
||||
run: npx nx build twenty-sdk
|
||||
- name: Install Playwright
|
||||
if: contains(matrix.task, 'storybook')
|
||||
run: npx playwright install chromium
|
||||
- name: Run ${{ matrix.task }} task
|
||||
uses: ./.github/actions/nx-affected
|
||||
with:
|
||||
tag: scope:sdk
|
||||
tasks: ${{ matrix.task }}
|
||||
# TODO uncomment when syncApplication resolver is fixed
|
||||
# sdk-e2e-integration-test:
|
||||
# timeout-minutes: 30
|
||||
# runs-on: ubuntu-latest-8-cores
|
||||
# needs: [changed-files-check, sdk-test]
|
||||
# strategy:
|
||||
# matrix:
|
||||
# task: [test:integration, test:e2e]
|
||||
# if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
# services:
|
||||
# postgres:
|
||||
# image: twentycrm/twenty-postgres-spilo
|
||||
# env:
|
||||
# PGUSER_SUPERUSER: postgres
|
||||
# PGPASSWORD_SUPERUSER: postgres
|
||||
# ALLOW_NOSSL: 'true'
|
||||
# SPILO_PROVIDER: 'local'
|
||||
# ports:
|
||||
# - 5432:5432
|
||||
# options: >-
|
||||
# --health-cmd pg_isready
|
||||
# --health-interval 10s
|
||||
# --health-timeout 5s
|
||||
# --health-retries 5
|
||||
# redis:
|
||||
# image: redis
|
||||
# ports:
|
||||
# - 6379:6379
|
||||
# env:
|
||||
# NODE_ENV: test
|
||||
# steps:
|
||||
# - name: Fetch custom Github Actions and base branch history
|
||||
# uses: actions/checkout@v4
|
||||
# with:
|
||||
# fetch-depth: 0
|
||||
# - name: Install dependencies
|
||||
# uses: ./.github/actions/yarn-install
|
||||
# - name: Server / Append billing config to .env.test
|
||||
# working-directory: packages/twenty-server
|
||||
# run: |
|
||||
# echo "" >> .env.test
|
||||
# echo "IS_BILLING_ENABLED=true" >> .env.test
|
||||
# echo "BILLING_STRIPE_API_KEY=test-api-key" >> .env.test
|
||||
# echo "BILLING_STRIPE_BASE_PLAN_PRODUCT_ID=test-base-plan-product-id" >> .env.test
|
||||
# echo "BILLING_STRIPE_WEBHOOK_SECRET=test-webhook-secret" >> .env.test
|
||||
# echo "BILLING_PLAN_REQUIRED_LINK=http://localhost:3001/stripe-redirection" >> .env.test
|
||||
# - name: Server / Create Test DB
|
||||
# run: |
|
||||
# PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
# - name: SDK / Run ${{ matrix.task }} Tests
|
||||
# uses: ./.github/actions/nx-affected
|
||||
# with:
|
||||
# tag: scope:sdk
|
||||
# tasks: ${{ matrix.task }}
|
||||
sdk-e2e-test:
|
||||
timeout-minutes: 30
|
||||
runs-on: ubuntu-latest-8-cores
|
||||
needs: [changed-files-check, sdk-test]
|
||||
if: needs.changed-files-check.outputs.any_changed == 'true'
|
||||
services:
|
||||
postgres:
|
||||
image: twentycrm/twenty-postgres-spilo
|
||||
env:
|
||||
PGUSER_SUPERUSER: postgres
|
||||
PGPASSWORD_SUPERUSER: postgres
|
||||
ALLOW_NOSSL: 'true'
|
||||
SPILO_PROVIDER: 'local'
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
redis:
|
||||
image: redis
|
||||
ports:
|
||||
- 6379:6379
|
||||
env:
|
||||
NODE_ENV: test
|
||||
steps:
|
||||
- name: Fetch custom Github Actions and base branch history
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Install dependencies
|
||||
uses: ./.github/actions/yarn-install
|
||||
- name: Build
|
||||
run: npx nx build twenty-sdk
|
||||
- name: Server / Create Test DB
|
||||
run: |
|
||||
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d postgres -c 'CREATE DATABASE "test";'
|
||||
- name: SDK / Run e2e Tests
|
||||
uses: ./.github/actions/nx-affected
|
||||
with:
|
||||
tag: scope:sdk
|
||||
tasks: test:e2e
|
||||
ci-sdk-status-check:
|
||||
if: always() && !cancelled()
|
||||
timeout-minutes: 5
|
||||
runs-on: ubuntu-latest
|
||||
needs: [changed-files-check, sdk-test]
|
||||
# TODO uncomment when syncApplication resolver is fixed
|
||||
# needs: [changed-files-check, sdk-test, sdk-e2e-integration-test]
|
||||
needs: [changed-files-check, sdk-test, sdk-e2e-test]
|
||||
steps:
|
||||
- name: Fail job if any needs failed
|
||||
if: contains(needs.*.result, 'failure')
|
||||
|
||||
+8
-2
@@ -11,7 +11,9 @@ import unicornPlugin from 'eslint-plugin-unicorn';
|
||||
import unusedImportsPlugin from 'eslint-plugin-unused-imports';
|
||||
import jsoncParser from 'jsonc-eslint-parser';
|
||||
|
||||
const twentyRules = await nxPlugin.loadWorkspaceRules('packages/twenty-eslint-rules');
|
||||
const twentyRules = await nxPlugin.loadWorkspaceRules(
|
||||
'packages/twenty-eslint-rules',
|
||||
);
|
||||
|
||||
export default [
|
||||
// Base JavaScript configuration
|
||||
@@ -65,6 +67,10 @@ export default [
|
||||
sourceTag: 'scope:sdk',
|
||||
onlyDependOnLibsWithTags: ['scope:sdk', 'scope:shared'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:create-app',
|
||||
onlyDependOnLibsWithTags: ['scope:create-app', 'scope:shared'],
|
||||
},
|
||||
{
|
||||
sourceTag: 'scope:shared',
|
||||
onlyDependOnLibsWithTags: ['scope:shared'],
|
||||
@@ -197,7 +203,7 @@ export default [
|
||||
plugins: {
|
||||
...mdxPlugin.flat.plugins,
|
||||
'@nx': nxPlugin,
|
||||
'twenty': { rules: twentyRules },
|
||||
twenty: { rules: twentyRules },
|
||||
},
|
||||
},
|
||||
mdxPlugin.flatCodeBlocks,
|
||||
|
||||
@@ -1,111 +1,20 @@
|
||||
import js from '@eslint/js';
|
||||
import typescriptEslint from '@typescript-eslint/eslint-plugin';
|
||||
import typescriptParser from '@typescript-eslint/parser';
|
||||
import prettierPlugin from 'eslint-plugin-prettier';
|
||||
import baseConfig from '../../eslint.config.mjs';
|
||||
|
||||
export default [
|
||||
js.configs.recommended,
|
||||
...baseConfig,
|
||||
{
|
||||
files: ['**/*.ts', '**/*.tsx'],
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
globals: {
|
||||
// Node.js globals
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
Buffer: 'readonly',
|
||||
__dirname: 'readonly',
|
||||
__filename: 'readonly',
|
||||
global: 'readonly',
|
||||
setTimeout: 'readonly',
|
||||
clearTimeout: 'readonly',
|
||||
setInterval: 'readonly',
|
||||
clearInterval: 'readonly',
|
||||
// Browser globals that Node.js also has
|
||||
URL: 'readonly',
|
||||
URLSearchParams: 'readonly',
|
||||
// Node.js types
|
||||
NodeJS: 'readonly',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
prettier: prettierPlugin,
|
||||
},
|
||||
ignores: ['**/dist/**'],
|
||||
},
|
||||
{
|
||||
files: ['**/*.{js,jsx,ts,tsx}'],
|
||||
rules: {
|
||||
...typescriptEslint.configs.recommended.rules,
|
||||
'prettier/prettier': 'error',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'no-useless-escape': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.js'],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
globals: {
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
Buffer: 'readonly',
|
||||
__dirname: 'readonly',
|
||||
__filename: 'readonly',
|
||||
global: 'readonly',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.test.ts', '**/*.spec.ts', '**/__tests__/**/*.ts'],
|
||||
languageOptions: {
|
||||
parser: typescriptParser,
|
||||
parserOptions: {
|
||||
ecmaVersion: 2022,
|
||||
sourceType: 'module',
|
||||
},
|
||||
globals: {
|
||||
// Node.js globals
|
||||
process: 'readonly',
|
||||
console: 'readonly',
|
||||
Buffer: 'readonly',
|
||||
__dirname: 'readonly',
|
||||
__filename: 'readonly',
|
||||
global: 'readonly',
|
||||
// Jest globals
|
||||
describe: 'readonly',
|
||||
it: 'readonly',
|
||||
test: 'readonly',
|
||||
expect: 'readonly',
|
||||
jest: 'readonly',
|
||||
beforeEach: 'readonly',
|
||||
afterEach: 'readonly',
|
||||
beforeAll: 'readonly',
|
||||
afterAll: 'readonly',
|
||||
},
|
||||
},
|
||||
plugins: {
|
||||
'@typescript-eslint': typescriptEslint,
|
||||
prettier: prettierPlugin,
|
||||
},
|
||||
rules: {
|
||||
...typescriptEslint.configs.recommended.rules,
|
||||
'prettier/prettier': 'error',
|
||||
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'no-useless-escape': 'off',
|
||||
'no-console': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ['dist/**', 'node_modules/**'],
|
||||
ignores: ['src/**/*.ts', '!src/cli/**/*.ts'],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
{
|
||||
"name": "create-twenty-app",
|
||||
"version": "0.4.0",
|
||||
"version": "0.5.0",
|
||||
"description": "Command-line interface to create Twenty application",
|
||||
"main": "dist/cli.cjs",
|
||||
"bin": "dist/cli.cjs",
|
||||
"files": [
|
||||
"dist/**/*"
|
||||
"dist",
|
||||
"README.md",
|
||||
"package.json"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "npx rimraf dist && npx vite build"
|
||||
"build": "npx rimraf dist && npx vite build",
|
||||
"prepublishOnly": "tsx ../twenty-utils/pack-scripts/pre-publish-only.ts",
|
||||
"postpublish": "tsx ../twenty-utils/pack-scripts/post-publish.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"twenty",
|
||||
@@ -34,6 +38,7 @@
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"lodash.kebabcase": "^4.1.1",
|
||||
"lodash.startcase": "^4.4.0",
|
||||
"twenty-shared": "workspace:*",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -43,6 +48,7 @@
|
||||
"@types/lodash.kebabcase": "^4.1.7",
|
||||
"@types/lodash.startcase": "^4",
|
||||
"@types/node": "^20.0.0",
|
||||
"typescript": "^5.9.2",
|
||||
"vite": "^7.0.0",
|
||||
"vite-plugin-dts": "^4.5.4",
|
||||
"vite-tsconfig-paths": "^4.2.1"
|
||||
|
||||
@@ -70,7 +70,7 @@ describe('copyBaseApplicationProject', () => {
|
||||
const packageJson = await fs.readJson(packageJsonPath);
|
||||
expect(packageJson.name).toBe('my-test-app');
|
||||
expect(packageJson.version).toBe('0.1.0');
|
||||
expect(packageJson.dependencies['twenty-sdk']).toBe('0.4.0');
|
||||
expect(packageJson.dependencies['twenty-sdk']).toBe('0.5.0');
|
||||
expect(packageJson.scripts['app:dev']).toBe('twenty app:dev');
|
||||
});
|
||||
|
||||
|
||||
@@ -189,7 +189,6 @@ const createDefaultFunction = async ({
|
||||
fileName: string;
|
||||
}) => {
|
||||
const universalIdentifier = v4();
|
||||
const triggerUniversalIdentifier = v4();
|
||||
|
||||
const content = `import { defineLogicFunction } from 'twenty-sdk';
|
||||
|
||||
@@ -204,15 +203,11 @@ export default defineLogicFunction({
|
||||
description: 'A simple logic function',
|
||||
timeoutSeconds: 5,
|
||||
handler,
|
||||
triggers: [
|
||||
{
|
||||
universalIdentifier: '${triggerUniversalIdentifier}',
|
||||
type: 'route',
|
||||
path: '/hello-world-logic-function',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
],
|
||||
httpRouteTriggerSettings: {
|
||||
path: '/hello-world-logic-function',
|
||||
httpMethod: 'GET',
|
||||
isAuthRequired: false,
|
||||
},
|
||||
});
|
||||
`;
|
||||
|
||||
@@ -282,7 +277,7 @@ const createPackageJson = async ({
|
||||
'lint:fix': 'eslint --fix',
|
||||
},
|
||||
dependencies: {
|
||||
'twenty-sdk': '0.4.0',
|
||||
'twenty-sdk': '0.5.0',
|
||||
},
|
||||
devDependencies: {
|
||||
typescript: '^5.9.3',
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"types": ["jest", "node"],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"jsx": "react"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
|
||||
@@ -17,7 +17,10 @@ setup_and_migrate_db() {
|
||||
yarn database:migrate:prod
|
||||
fi
|
||||
|
||||
yarn command:prod cache:flush
|
||||
yarn command:prod upgrade
|
||||
yarn command:prod cache:flush
|
||||
|
||||
echo "Successfully migrated DB!"
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,9 @@ test('Create workflow', async ({ page }) => {
|
||||
|
||||
await page.goto(process.env.LINK);
|
||||
|
||||
const workflowsFolder = page.getByRole('button', { name: 'Workflows' });
|
||||
await workflowsFolder.click();
|
||||
|
||||
const workflowsLink = page.getByRole('link', { name: 'Workflows' });
|
||||
await workflowsLink.click();
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "react-jsx",
|
||||
"moduleResolution": "bundler",
|
||||
"allowJs": false,
|
||||
"esModuleInterop": false,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
|
||||
@@ -18,9 +18,9 @@ module.exports = {
|
||||
|
||||
'./src/modules/billing/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/settings/**/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/logic-functions/graphql/**/*.{ts,tsx}',
|
||||
|
||||
'./src/modules/databases/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/workflow/**/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/analytics/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/object-metadata/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/navigation-menu-item/graphql/**/*.{ts,tsx}',
|
||||
@@ -31,6 +31,9 @@ module.exports = {
|
||||
|
||||
'./src/modules/page-layout/widgets/**/graphql/**/*.{ts,tsx}',
|
||||
|
||||
'./src/modules/dashboards/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/page-layout/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/marketplace/graphql/**/*.{ts,tsx}',
|
||||
'!./src/**/*.test.{ts,tsx}',
|
||||
'!./src/**/*.stories.{ts,tsx}',
|
||||
'!./src/**/__mocks__/*.ts',
|
||||
|
||||
@@ -5,22 +5,11 @@ module.exports = {
|
||||
(process.env.REACT_APP_SERVER_BASE_URL ?? 'http://localhost:3000') +
|
||||
'/graphql',
|
||||
documents: [
|
||||
'./src/modules/activities/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/companies/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/people/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/opportunities/graphql/**/*.{ts,tsx}',
|
||||
|
||||
'./src/modules/workflow/**/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/activities/emails/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/activities/calendar/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/search/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/views/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/favorites/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/spreadsheet-import/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/command-menu/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/marketplace/graphql/**/*.{ts,tsx}',
|
||||
|
||||
'./src/modules/prefetch/graphql/**/*.{ts,tsx}',
|
||||
'./src/modules/subscription/graphql/**/*.{ts,tsx}',
|
||||
|
||||
'./src/modules/page-layout/graphql/**/*.{ts,tsx}',
|
||||
|
||||
'!./src/**/*.test.{ts,tsx}',
|
||||
'!./src/**/*.stories.{ts,tsx}',
|
||||
|
||||
@@ -62,9 +62,9 @@ const jestConfig = {
|
||||
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 50,
|
||||
lines: 48.9,
|
||||
functions: 40.9,
|
||||
statements: 49.5,
|
||||
lines: 48,
|
||||
functions: 40,
|
||||
},
|
||||
},
|
||||
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { type AppPath } from 'twenty-shared/types';
|
||||
import { type AppPath, type NavigateOptions } from 'twenty-shared/types';
|
||||
import { getAppPath } from 'twenty-shared/utils';
|
||||
|
||||
export const useNavigateApp = () => {
|
||||
@@ -9,10 +9,7 @@ export const useNavigateApp = () => {
|
||||
to: T,
|
||||
params?: Parameters<typeof getAppPath<T>>[1],
|
||||
queryParams?: Record<string, any>,
|
||||
options?: {
|
||||
replace?: boolean;
|
||||
state?: any;
|
||||
},
|
||||
options?: NavigateOptions,
|
||||
) => {
|
||||
const path = getAppPath(to, params, queryParams);
|
||||
return navigate(path, options);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
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 one or more lines are too long
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 one or more lines are too long
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 one or more lines are too long
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 one or more lines are too long
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 one or more lines are too long
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 one or more lines are too long
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 one or more lines are too long
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 one or more lines are too long
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 one or more lines are too long
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 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
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
import { type ConnectionParameters } from '~/generated/graphql';
|
||||
import { type ConnectionParameters } from '~/generated-metadata/graphql';
|
||||
|
||||
export type ImapSmtpCaldavAccount = {
|
||||
IMAP?: ConnectionParameters;
|
||||
|
||||
@@ -10,15 +10,18 @@ import { downloadFile } from '@/activities/files/utils/downloadFile';
|
||||
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { isAttachmentPreviewEnabledState } from '@/client-config/states/isAttachmentPreviewEnabledState';
|
||||
import { Modal } from '@/ui/layout/modal/components/Modal';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
|
||||
import { ActivityList } from '@/activities/components/ActivityList';
|
||||
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
|
||||
import { useModal } from '@/ui/layout/modal/hooks/useModal';
|
||||
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
|
||||
import { assertIsDefinedOrThrow } from 'twenty-shared/utils';
|
||||
import { IconDownload, IconX } from 'twenty-ui/display';
|
||||
import { IconButton } from 'twenty-ui/input';
|
||||
import { PermissionFlagType } from '~/generated-metadata/graphql';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { AttachmentRow } from './AttachmentRow';
|
||||
|
||||
const DocumentViewer = lazy(() =>
|
||||
@@ -134,6 +137,10 @@ export const AttachmentList = ({
|
||||
isAttachmentPreviewEnabledState,
|
||||
);
|
||||
|
||||
const isFilesFieldMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
|
||||
);
|
||||
|
||||
const hasDownloadPermission = useHasPermissionFlag(
|
||||
PermissionFlagType.DOWNLOAD_FILE,
|
||||
);
|
||||
@@ -144,6 +151,16 @@ export const AttachmentList = ({
|
||||
|
||||
const { openModal, closeModal } = useModal();
|
||||
|
||||
const getAttachmentUrl = (attachment: Attachment) => {
|
||||
const fileUrl = isFilesFieldMigrated
|
||||
? attachment.file?.[0]?.url || attachment.fullPath
|
||||
: attachment.fullPath;
|
||||
|
||||
assertIsDefinedOrThrow(fileUrl, new Error(t`File URL is not defined`));
|
||||
|
||||
return fileUrl;
|
||||
};
|
||||
|
||||
const onUploadFile = async (file: File) => {
|
||||
await uploadAttachmentFile(file, targetableObject);
|
||||
};
|
||||
@@ -167,7 +184,10 @@ export const AttachmentList = ({
|
||||
|
||||
const handleDownload = () => {
|
||||
if (!previewedAttachment) return;
|
||||
downloadFile(previewedAttachment.fullPath, previewedAttachment.name);
|
||||
downloadFile(
|
||||
getAttachmentUrl(previewedAttachment),
|
||||
previewedAttachment.name,
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -248,7 +268,7 @@ export const AttachmentList = ({
|
||||
>
|
||||
<DocumentViewer
|
||||
documentName={previewedAttachment.name}
|
||||
documentUrl={previewedAttachment.fullPath}
|
||||
documentUrl={getAttachmentUrl(previewedAttachment)}
|
||||
/>
|
||||
</Suspense>
|
||||
</StyledModalContent>
|
||||
|
||||
@@ -9,17 +9,21 @@ import {
|
||||
FieldContext,
|
||||
type GenericFieldContextType,
|
||||
} from '@/object-record/record-field/ui/contexts/FieldContext';
|
||||
import { getFileCategoryFromExtension } from '@/object-record/record-field/ui/utils/getFileCategoryFromExtension';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { FileIcon } from '@/file/components/FileIcon';
|
||||
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconCalendar, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { isNavigationModifierPressed } from 'twenty-ui/utilities';
|
||||
import { PermissionFlagType } from '~/generated-metadata/graphql';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { formatToHumanReadableDate } from '~/utils/date-utils';
|
||||
import { getFileNameAndExtension } from '~/utils/file/getFileNameAndExtension';
|
||||
|
||||
@@ -82,6 +86,10 @@ export const AttachmentRow = ({
|
||||
const theme = useTheme();
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
||||
const isFilesFieldMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
|
||||
);
|
||||
|
||||
const hasDownloadPermission = useHasPermissionFlag(
|
||||
PermissionFlagType.DOWNLOAD_FILE,
|
||||
);
|
||||
@@ -92,6 +100,16 @@ export const AttachmentRow = ({
|
||||
const [attachmentFileName, setAttachmentFileName] =
|
||||
useState(originalFileName);
|
||||
|
||||
const fileCategory = isFilesFieldMigrated
|
||||
? getFileCategoryFromExtension(attachment.file?.[0]?.extension)
|
||||
: attachment.fileCategory;
|
||||
|
||||
const fileUrl = isFilesFieldMigrated
|
||||
? attachment.file?.[0]?.url
|
||||
: attachment.fullPath;
|
||||
|
||||
assertIsDefinedOrThrow(fileUrl, new Error(t`File URL is not defined`));
|
||||
|
||||
const { destroyOneRecord: destroyOneAttachment } = useDestroyOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Attachment,
|
||||
});
|
||||
@@ -114,7 +132,19 @@ export const AttachmentRow = ({
|
||||
updateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.Attachment,
|
||||
idToUpdate: attachment.id,
|
||||
updateOneRecordInput: { name: newFileName },
|
||||
updateOneRecordInput: {
|
||||
name: newFileName,
|
||||
...(isFilesFieldMigrated && isDefined(attachment.file?.[0]?.fileId)
|
||||
? {
|
||||
file: [
|
||||
{
|
||||
fileId: attachment.file?.[0]?.fileId,
|
||||
label: newFileName,
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -133,10 +163,7 @@ export const AttachmentRow = ({
|
||||
};
|
||||
|
||||
const handleDownload = () => {
|
||||
downloadFile(
|
||||
attachment.fullPath,
|
||||
`${attachmentFileName}${attachmentFileExtension}`,
|
||||
);
|
||||
downloadFile(fileUrl, `${attachmentFileName}${attachmentFileExtension}`);
|
||||
};
|
||||
|
||||
const handleOpenDocument = (e: React.MouseEvent) => {
|
||||
@@ -162,7 +189,7 @@ export const AttachmentRow = ({
|
||||
>
|
||||
<ActivityRow disabled>
|
||||
<StyledLeftContent>
|
||||
<FileIcon fileCategory={attachment.fileCategory} />
|
||||
<FileIcon fileCategory={fileCategory} />
|
||||
{isEditing ? (
|
||||
<SettingsTextInput
|
||||
instanceId={`attachment-${attachment.id}-name`}
|
||||
@@ -176,7 +203,7 @@ export const AttachmentRow = ({
|
||||
<StyledLinkContainer>
|
||||
<StyledLink
|
||||
onClick={handleOpenDocument}
|
||||
href={attachment.fullPath}
|
||||
href={fileUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { PREVIEWABLE_EXTENSIONS } from '@/activities/files/const/previewable-extensions.const';
|
||||
import { downloadFile } from '@/activities/files/utils/downloadFile';
|
||||
import { fetchCsvPreview } from '@/activities/files/utils/fetchCsvPreview';
|
||||
import {
|
||||
type CsvPreviewData,
|
||||
fetchCsvPreview,
|
||||
} from '@/activities/files/utils/fetchCsvPreview';
|
||||
import { getFileType } from '@/activities/files/utils/getFileType';
|
||||
import DocViewer, { DocViewerRenderers } from '@cyntler/react-doc-viewer';
|
||||
import '@cyntler/react-doc-viewer/dist/index.css';
|
||||
@@ -79,9 +82,43 @@ const StyledTitle = styled.div`
|
||||
font-weight: ${({ theme }) => theme.font.weight.semiBold};
|
||||
`;
|
||||
|
||||
const StyledCsvTable = styled.table`
|
||||
border-collapse: collapse;
|
||||
font-size: ${({ theme }) => theme.font.size.sm};
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
|
||||
th {
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||
}
|
||||
|
||||
td {
|
||||
color: ${({ theme }) => theme.font.color.secondary};
|
||||
height: ${({ theme }) => theme.spacing(8)};
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
padding: 0 ${({ theme }) => theme.spacing(2)};
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
tbody tr {
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
}
|
||||
|
||||
tbody tr:hover {
|
||||
background-color: ${({ theme }) => theme.background.transparent.light};
|
||||
}
|
||||
`;
|
||||
|
||||
type DocumentViewerProps = {
|
||||
documentName: string;
|
||||
documentUrl: string;
|
||||
documentExtension?: string;
|
||||
};
|
||||
|
||||
// MS Office Online viewer requires documents to be publicly accessible from the internet.
|
||||
@@ -160,26 +197,27 @@ const MIME_TYPE_MAPPING: Record<
|
||||
export const DocumentViewer = ({
|
||||
documentName,
|
||||
documentUrl,
|
||||
documentExtension,
|
||||
}: DocumentViewerProps) => {
|
||||
const { t } = useLingui();
|
||||
const theme = useTheme();
|
||||
const [csvPreview, setCsvPreview] = useState<string | undefined>(undefined);
|
||||
const [csvPreview, setCsvPreview] = useState<CsvPreviewData | undefined>(
|
||||
undefined,
|
||||
);
|
||||
|
||||
const { extension } = getFileNameAndExtension(documentName);
|
||||
const fileExtension = extension?.toLowerCase().replace('.', '') ?? '';
|
||||
const fileExtension = isDefined(documentExtension)
|
||||
? documentExtension.toLowerCase().replace('.', '')
|
||||
: (extension?.toLowerCase().replace('.', '') ?? '');
|
||||
const fileCategory = getFileType(documentName);
|
||||
const isPreviewable = PREVIEWABLE_EXTENSIONS.includes(fileExtension);
|
||||
const isMsOfficeFile = MS_OFFICE_EXTENSIONS.includes(fileExtension);
|
||||
|
||||
const mimeType = PREVIEWABLE_EXTENSIONS.includes(fileExtension)
|
||||
? MIME_TYPE_MAPPING[fileExtension]
|
||||
: undefined;
|
||||
const mimeType = isPreviewable ? MIME_TYPE_MAPPING[fileExtension] : undefined;
|
||||
|
||||
useEffect(() => {
|
||||
if (fileExtension === 'csv') {
|
||||
fetchCsvPreview(documentUrl).then((content) => {
|
||||
setCsvPreview(content);
|
||||
});
|
||||
fetchCsvPreview(documentUrl).then(setCsvPreview);
|
||||
}
|
||||
}, [documentUrl, fileExtension]);
|
||||
|
||||
@@ -214,12 +252,37 @@ export const DocumentViewer = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (fileExtension === 'csv' && !isDefined(csvPreview))
|
||||
if (fileExtension === 'csv') {
|
||||
if (!isDefined(csvPreview)) {
|
||||
return (
|
||||
<StyledDocumentViewerContainer>
|
||||
<Trans>Loading csv ... </Trans>
|
||||
</StyledDocumentViewerContainer>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<StyledDocumentViewerContainer>
|
||||
<Trans>Loading csv ... </Trans>
|
||||
<StyledDocumentViewerContainer style={{ background: 'transparent' }}>
|
||||
<StyledCsvTable>
|
||||
<thead>
|
||||
<tr>
|
||||
{csvPreview.headers.map((header, columnIndex) => (
|
||||
<th key={columnIndex}>{header}</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{csvPreview.rows.map((row, rowIndex) => (
|
||||
<tr key={rowIndex}>
|
||||
{row.map((cell, cellIndex) => (
|
||||
<td key={cellIndex}>{cell}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</StyledCsvTable>
|
||||
</StyledDocumentViewerContainer>
|
||||
);
|
||||
}
|
||||
|
||||
if (isMsOfficeFile && isPrivateUrl(documentUrl)) {
|
||||
return (
|
||||
@@ -247,12 +310,7 @@ export const DocumentViewer = ({
|
||||
<DocViewer
|
||||
documents={[
|
||||
{
|
||||
uri:
|
||||
fileExtension === 'csv' && isDefined(csvPreview)
|
||||
? window.URL.createObjectURL(
|
||||
new Blob([csvPreview], { type: 'text/csv' }),
|
||||
)
|
||||
: documentUrl,
|
||||
uri: documentUrl,
|
||||
fileName: documentName,
|
||||
fileType: mimeType,
|
||||
},
|
||||
|
||||
+63
-13
@@ -3,22 +3,39 @@ import { getFileType } from '@/activities/files/utils/getFileType';
|
||||
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils';
|
||||
import {
|
||||
FileFolder,
|
||||
useUploadFileMutation,
|
||||
useUploadFilesFieldFileMutation,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { FeatureFlagKey, FieldMetadataType } from '~/generated/graphql';
|
||||
|
||||
export const useUploadAttachmentFile = () => {
|
||||
const coreClient = useApolloCoreClient();
|
||||
const [uploadFile] = useUploadFileMutation({ client: coreClient });
|
||||
const [uploadFilesFieldFile] = useUploadFilesFieldFileMutation({
|
||||
client: coreClient,
|
||||
});
|
||||
const isAttachmentMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_ATTACHMENT_MIGRATED,
|
||||
);
|
||||
const isFilesFieldMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_FILES_FIELD_MIGRATED,
|
||||
);
|
||||
|
||||
const { objectMetadataItem: attachmentMetadata } = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.Attachment,
|
||||
});
|
||||
|
||||
const filesFieldMetadataId = attachmentMetadata.fields.find(
|
||||
(field) => field.type === FieldMetadataType.FILES && field.name === 'file',
|
||||
)?.id;
|
||||
|
||||
const { createOneRecord: createOneAttachment } =
|
||||
useCreateOneRecord<Attachment>({
|
||||
@@ -30,21 +47,44 @@ export const useUploadAttachmentFile = () => {
|
||||
file: File,
|
||||
targetableObject: ActivityTargetableObject,
|
||||
) => {
|
||||
const result = await uploadFile({
|
||||
variables: {
|
||||
file,
|
||||
fileFolder: FileFolder.Attachment,
|
||||
},
|
||||
});
|
||||
let attachmentPath: string;
|
||||
let fileId: string | undefined;
|
||||
|
||||
const signedFile = result?.data?.uploadFile;
|
||||
if (isFilesFieldMigrated) {
|
||||
assertIsDefinedOrThrow(
|
||||
filesFieldMetadataId,
|
||||
new Error(t`File field not found for attachment object`),
|
||||
);
|
||||
|
||||
if (!isDefined(signedFile)) {
|
||||
throw new Error("Couldn't upload the attachment.");
|
||||
const result = await uploadFilesFieldFile({
|
||||
variables: { file, fieldMetadataId: filesFieldMetadataId },
|
||||
});
|
||||
|
||||
const uploadedFile = result?.data?.uploadFilesFieldFile;
|
||||
|
||||
if (!isDefined(uploadedFile)) {
|
||||
throw new Error("Couldn't upload the attachment.");
|
||||
}
|
||||
|
||||
attachmentPath = uploadedFile.path;
|
||||
fileId = uploadedFile.id;
|
||||
} else {
|
||||
const result = await uploadFile({
|
||||
variables: {
|
||||
file,
|
||||
fileFolder: FileFolder.Attachment,
|
||||
},
|
||||
});
|
||||
|
||||
const signedFile = result?.data?.uploadFile;
|
||||
|
||||
if (!isDefined(signedFile)) {
|
||||
throw new Error("Couldn't upload the attachment.");
|
||||
}
|
||||
|
||||
attachmentPath = signedFile.path;
|
||||
}
|
||||
|
||||
const { path: attachmentPath } = signedFile;
|
||||
|
||||
const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
|
||||
nameSingular: targetableObject.targetObjectNameSingular,
|
||||
isMorphRelation: isAttachmentMigrated,
|
||||
@@ -55,6 +95,16 @@ export const useUploadAttachmentFile = () => {
|
||||
fullPath: attachmentPath,
|
||||
fileCategory: getFileType(file.name),
|
||||
[targetableObjectFieldIdName]: targetableObject.id,
|
||||
...(isFilesFieldMigrated && isDefined(fileId)
|
||||
? {
|
||||
file: [
|
||||
{
|
||||
fileId,
|
||||
label: file.name,
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
} as Partial<Attachment>;
|
||||
|
||||
const createdAttachment = await createOneAttachment(attachmentToCreate);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { type FieldFilesValue } from '@/object-record/record-field/ui/types/FieldMetadata';
|
||||
|
||||
import { type AttachmentFileCategory } from './AttachmentFileCategory';
|
||||
|
||||
export type { AttachmentFileCategory };
|
||||
@@ -7,6 +9,7 @@ export type Attachment = {
|
||||
name: string;
|
||||
fullPath: string;
|
||||
fileCategory: AttachmentFileCategory;
|
||||
file?: FieldFilesValue[] | null;
|
||||
companyId?: string | null;
|
||||
personId?: string | null;
|
||||
taskId?: string | null;
|
||||
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import { fetchCsvPreview } from '@/activities/files/utils/fetchCsvPreview';
|
||||
|
||||
const mockFetch = (text: string) => {
|
||||
global.fetch = jest.fn(() =>
|
||||
Promise.resolve({
|
||||
text: () => Promise.resolve(text),
|
||||
} as unknown as Response),
|
||||
);
|
||||
};
|
||||
|
||||
describe('fetchCsvPreview', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should parse headers and rows from CSV', async () => {
|
||||
mockFetch('Name,Age,City\nAlice,30,Paris\nBob,25,London\n');
|
||||
|
||||
const result = await fetchCsvPreview('https://example.com/file.csv');
|
||||
|
||||
expect(result.headers).toEqual(['Name', 'Age', 'City']);
|
||||
expect(result.rows).toEqual([
|
||||
['Alice', '30', 'Paris'],
|
||||
['Bob', '25', 'London'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return empty headers and rows for empty CSV', async () => {
|
||||
mockFetch('');
|
||||
|
||||
const result = await fetchCsvPreview('https://example.com/empty.csv');
|
||||
|
||||
expect(result.headers).toEqual([]);
|
||||
expect(result.rows).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return headers with no rows when CSV has only a header line', async () => {
|
||||
mockFetch('Name,Age,City\n');
|
||||
|
||||
const result = await fetchCsvPreview('https://example.com/header-only.csv');
|
||||
|
||||
expect(result.headers).toEqual(['Name', 'Age', 'City']);
|
||||
expect(result.rows).toEqual([]);
|
||||
});
|
||||
|
||||
it('should skip empty lines', async () => {
|
||||
mockFetch('Name,Age\n\nAlice,30\n\nBob,25\n');
|
||||
|
||||
const result = await fetchCsvPreview('https://example.com/file.csv');
|
||||
|
||||
expect(result.rows).toEqual([
|
||||
['Alice', '30'],
|
||||
['Bob', '25'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle rows with inconsistent column counts', async () => {
|
||||
mockFetch('Name,Age,City\nAlice,30\nBob,25,London,Extra\n');
|
||||
|
||||
const result = await fetchCsvPreview('https://example.com/malformed.csv');
|
||||
|
||||
expect(result.headers).toEqual(['Name', 'Age', 'City']);
|
||||
expect(result.rows).toEqual([
|
||||
['Alice', '30'],
|
||||
['Bob', '25', 'London', 'Extra'],
|
||||
]);
|
||||
});
|
||||
|
||||
it('should limit rows to the preview amount', async () => {
|
||||
const lines = ['Name'];
|
||||
for (let i = 0; i < 100; i++) {
|
||||
lines.push(`Person${i}`);
|
||||
}
|
||||
mockFetch(lines.join('\n'));
|
||||
|
||||
const result = await fetchCsvPreview('https://example.com/large.csv');
|
||||
|
||||
expect(result.headers).toEqual(['Name']);
|
||||
expect(result.rows).toHaveLength(50);
|
||||
});
|
||||
});
|
||||
@@ -2,21 +2,22 @@ import Papa from 'papaparse';
|
||||
|
||||
const DEFAULT_PREVIEW_ROWS = 50;
|
||||
|
||||
export const fetchCsvPreview = async (url: string): Promise<string> => {
|
||||
export type CsvPreviewData = {
|
||||
headers: string[];
|
||||
rows: string[][];
|
||||
};
|
||||
|
||||
export const fetchCsvPreview = async (url: string): Promise<CsvPreviewData> => {
|
||||
const response = await fetch(url);
|
||||
const text = await response.text();
|
||||
|
||||
const result = Papa.parse(text, {
|
||||
preview: DEFAULT_PREVIEW_ROWS,
|
||||
const result = Papa.parse<string[]>(text, {
|
||||
preview: DEFAULT_PREVIEW_ROWS + 1, // +1 for header row
|
||||
skipEmptyLines: true,
|
||||
header: true,
|
||||
header: false,
|
||||
});
|
||||
|
||||
const data = result.data as Record<string, string>[];
|
||||
const [headers = [], ...rows] = result.data;
|
||||
|
||||
const csvContent = Papa.unparse(data, {
|
||||
header: true,
|
||||
});
|
||||
|
||||
return csvContent;
|
||||
return { headers, rows };
|
||||
};
|
||||
|
||||
+12
-2
@@ -6,6 +6,7 @@ import { type RecordGqlOperationSignatureFactory } from '@/object-record/graphql
|
||||
type FindActivitiesOperationSignatureFactory = {
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
objectNameSingular: CoreObjectNameSingular;
|
||||
isMorphRelation: boolean;
|
||||
};
|
||||
|
||||
export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatureFactory<
|
||||
@@ -13,6 +14,7 @@ export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatur
|
||||
> = ({
|
||||
objectMetadataItems,
|
||||
objectNameSingular,
|
||||
isMorphRelation,
|
||||
}: FindActivitiesOperationSignatureFactory) => {
|
||||
const body = {
|
||||
bodyV2: {
|
||||
@@ -62,9 +64,13 @@ export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatur
|
||||
__typename: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
deletedAt: true,
|
||||
note: true,
|
||||
noteId: true,
|
||||
...generateActivityTargetMorphFieldKeys(objectMetadataItems),
|
||||
...generateActivityTargetMorphFieldKeys(
|
||||
objectMetadataItems,
|
||||
isMorphRelation,
|
||||
),
|
||||
},
|
||||
}
|
||||
: {
|
||||
@@ -73,9 +79,13 @@ export const findActivitiesOperationSignatureFactory: RecordGqlOperationSignatur
|
||||
__typename: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
deletedAt: true,
|
||||
task: true,
|
||||
taskId: true,
|
||||
...generateActivityTargetMorphFieldKeys(objectMetadataItems),
|
||||
...generateActivityTargetMorphFieldKeys(
|
||||
objectMetadataItems,
|
||||
isMorphRelation,
|
||||
),
|
||||
},
|
||||
}),
|
||||
},
|
||||
|
||||
+20
-3
@@ -1,4 +1,5 @@
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { type RecordGqlOperationOrderBy } from 'twenty-shared/types';
|
||||
|
||||
import { findActivityTargetsOperationSignatureFactory } from '@/activities/graphql/operation-signatures/factories/findActivityTargetsOperationSignatureFactory';
|
||||
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
@@ -6,9 +7,12 @@ import { type NoteTarget } from '@/activities/types/NoteTarget';
|
||||
import { type TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { getActivityTargetsFilter } from '@/activities/utils/getActivityTargetsFilter';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { type RecordGqlOperationOrderBy } from 'twenty-shared/types';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
export const useActivityTargetsForTargetableObjects = ({
|
||||
objectNameSingular,
|
||||
@@ -28,12 +32,25 @@ export const useActivityTargetsForTargetableObjects = ({
|
||||
activityTargetsOrderByVariables: RecordGqlOperationOrderBy;
|
||||
limit: number;
|
||||
}) => {
|
||||
const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
|
||||
const objectMetadataItems = useRecoilValue<ObjectMetadataItem[]>(
|
||||
objectMetadataItemsState,
|
||||
);
|
||||
const isNoteTargetMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
|
||||
);
|
||||
const isTaskTargetMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
|
||||
);
|
||||
const isMorphRelation =
|
||||
objectNameSingular === CoreObjectNameSingular.Task
|
||||
? isTaskTargetMigrated
|
||||
: isNoteTargetMigrated;
|
||||
|
||||
const activityTargetsFilter = getActivityTargetsFilter({
|
||||
targetableObjects: targetableObjects,
|
||||
activityObjectNameSingular: objectNameSingular,
|
||||
objectMetadataItems,
|
||||
isMorphRelation,
|
||||
});
|
||||
|
||||
const FIND_ACTIVITY_TARGETS_OPERATION_SIGNATURE =
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import { useState } from 'react';
|
||||
|
||||
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
|
||||
@@ -37,6 +38,7 @@ export const useCustomResolver = <
|
||||
fetchMoreRecords: () => Promise<void>;
|
||||
} => {
|
||||
const { enqueueErrorSnackBar } = useSnackBar();
|
||||
const apolloCoreClient = useApolloCoreClient();
|
||||
|
||||
const [page, setPage] = useState({
|
||||
pageNumber: 1,
|
||||
@@ -62,6 +64,7 @@ export const useCustomResolver = <
|
||||
loading: firstQueryLoading,
|
||||
fetchMore,
|
||||
} = useQuery<CustomResolverQueryResult<T>>(query, {
|
||||
client: apolloCoreClient,
|
||||
variables: queryVariables,
|
||||
onError: (error) => {
|
||||
enqueueErrorSnackBar({
|
||||
|
||||
@@ -15,7 +15,7 @@ import { type TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { useOpenRecordInCommandMenu } from '@/command-menu/hooks/useOpenRecordInCommandMenu';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { findTargetFieldInfo } from '@/object-record/record-field/ui/utils/junction/findTargetFieldInfo';
|
||||
|
||||
export const useOpenCreateActivityDrawer = ({
|
||||
activityObjectNameSingular,
|
||||
@@ -90,17 +90,15 @@ export const useOpenCreateActivityDrawer = ({
|
||||
item.nameSingular === targetableObjects[0].targetObjectNameSingular,
|
||||
);
|
||||
|
||||
const targetField = activityTargetObjectMetadata?.fields.find(
|
||||
(field) =>
|
||||
field.relation?.targetObjectMetadata.id ===
|
||||
targetObjectMetadataItem?.id,
|
||||
const targetFieldInfo = findTargetFieldInfo(
|
||||
activityTargetObjectMetadata?.fields ?? [],
|
||||
targetObjectMetadataItem?.id ?? '',
|
||||
objectMetadataItems,
|
||||
);
|
||||
|
||||
const joinColumnName = targetField?.settings?.joinColumnName;
|
||||
|
||||
const targetableObjectRelationIdName = isDefined(joinColumnName)
|
||||
? joinColumnName
|
||||
: `${targetableObjects[0].targetObjectNameSingular}Id`;
|
||||
const targetableObjectRelationIdName =
|
||||
targetFieldInfo?.joinColumnName ??
|
||||
`${targetableObjects[0].targetObjectNameSingular}Id`;
|
||||
|
||||
await createOneActivityTarget({
|
||||
...(activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||
|
||||
+13
-1
@@ -7,13 +7,15 @@ import { type TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
|
||||
import { type CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache';
|
||||
import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache';
|
||||
import { getRecordFromCache } from '@/object-record/cache/utils/getRecordFromCache';
|
||||
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { sortByAscString } from '~/utils/array/sortByAscString';
|
||||
|
||||
export const usePrepareFindManyActivitiesQuery = ({
|
||||
@@ -33,6 +35,12 @@ export const usePrepareFindManyActivitiesQuery = ({
|
||||
const cache = useApolloCoreClient().cache;
|
||||
const { objectMetadataItems } = useObjectMetadataItems();
|
||||
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
|
||||
const isNoteTargetMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
|
||||
);
|
||||
const isTaskTargetMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
|
||||
);
|
||||
|
||||
const { upsertFindManyRecordsQueryInCache: upsertFindManyActivitiesInCache } =
|
||||
useUpsertFindManyRecordsQueryInCache({
|
||||
@@ -116,6 +124,10 @@ export const usePrepareFindManyActivitiesQuery = ({
|
||||
findActivitiesOperationSignatureFactory({
|
||||
objectNameSingular: activityObjectNameSingular,
|
||||
objectMetadataItems,
|
||||
isMorphRelation:
|
||||
activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||
? isTaskTargetMigrated
|
||||
: isNoteTargetMigrated,
|
||||
});
|
||||
|
||||
upsertFindManyActivitiesInCache({
|
||||
|
||||
+22
-5
@@ -5,14 +5,17 @@ import { getActivityTargetFieldNameForObject } from '@/activities/utils/getActiv
|
||||
import { getJoinObjectNameSingular } from '@/activities/utils/getJoinObjectNameSingular';
|
||||
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
|
||||
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
|
||||
import { searchRecordStoreFamilyState } from '@/object-record/record-picker/multiple-record-picker/states/searchRecordStoreComponentFamilyState';
|
||||
import { type RecordPickerPickableMorphItem } from '@/object-record/record-picker/types/RecordPickerPickableMorphItem';
|
||||
import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { useRecoilCallback, useSetRecoilState } from 'recoil';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { v4 } from 'uuid';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
type UpdateActivityTargetFromCellProps = {
|
||||
recordPickerInstanceId: string;
|
||||
@@ -50,6 +53,16 @@ export const useUpdateActivityTargetFromCell = ({
|
||||
? activityObjectNameSingular
|
||||
: joinObjectNameSingular,
|
||||
});
|
||||
const isNoteTargetMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_NOTE_TARGET_MIGRATED,
|
||||
);
|
||||
const isTaskTargetMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_TASK_TARGET_MIGRATED,
|
||||
);
|
||||
const isMorphRelation =
|
||||
activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||
? isTaskTargetMigrated
|
||||
: isNoteTargetMigrated;
|
||||
|
||||
const setActivityFromStore = useSetRecoilState(
|
||||
recordStoreFamilyState(activityId),
|
||||
@@ -67,7 +80,7 @@ export const useUpdateActivityTargetFromCell = ({
|
||||
: 'note';
|
||||
|
||||
const objectMetadataItems = snapshot
|
||||
.getLoadable(objectMetadataItemsState)
|
||||
.getLoadable<ObjectMetadataItem[]>(objectMetadataItemsState)
|
||||
.getValue();
|
||||
|
||||
const pickedObjectMetadataItem = objectMetadataItems.find(
|
||||
@@ -83,6 +96,7 @@ export const useUpdateActivityTargetFromCell = ({
|
||||
activityObjectNameSingular,
|
||||
targetObjectMetadataId: morphItem.objectMetadataId,
|
||||
objectMetadataItems,
|
||||
isMorphRelation,
|
||||
});
|
||||
|
||||
if (!isDefined(targetFieldName)) {
|
||||
@@ -132,7 +146,7 @@ export const useUpdateActivityTargetFromCell = ({
|
||||
|
||||
const targetRecord = searchRecord.record;
|
||||
|
||||
const activityTarget =
|
||||
const activityTarget: NoteTarget | TaskTarget =
|
||||
activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||
? {
|
||||
id: v4(),
|
||||
@@ -167,15 +181,17 @@ export const useUpdateActivityTargetFromCell = ({
|
||||
...activityTargetWithTargetRecords.map((activityTarget) => {
|
||||
return activityTarget.activityTarget;
|
||||
}),
|
||||
activityTarget as NoteTarget | TaskTarget,
|
||||
activityTarget,
|
||||
];
|
||||
|
||||
await createOneActivityTarget({
|
||||
const activityTargetInput: Partial<NoteTarget | TaskTarget> = {
|
||||
...activityTarget,
|
||||
[targetObjectName]: undefined,
|
||||
[targetFieldName]: undefined,
|
||||
[`${targetFieldName}Id`]: morphItem.recordId,
|
||||
} as Partial<NoteTarget | TaskTarget>);
|
||||
};
|
||||
|
||||
await createOneActivityTarget(activityTargetInput);
|
||||
}
|
||||
|
||||
setActivityFromStore((currentActivity) => {
|
||||
@@ -194,6 +210,7 @@ export const useUpdateActivityTargetFromCell = ({
|
||||
activityObjectNameSingular,
|
||||
createOneActivityTarget,
|
||||
deleteOneActivityTarget,
|
||||
isMorphRelation,
|
||||
setActivityFromStore,
|
||||
],
|
||||
);
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ export const Empty: Story = {};
|
||||
export const WithTasks: Story = {
|
||||
args: {
|
||||
targetableObject: {
|
||||
id: mockedTasks[0].taskTargets?.[0].personId,
|
||||
id: mockedTasks[0].taskTargets?.[0].targetPersonId,
|
||||
targetObjectNameSingular: 'person',
|
||||
} as ActivityTargetableObject,
|
||||
},
|
||||
|
||||
+1
-12
@@ -1,28 +1,17 @@
|
||||
import { useLinkedObjectsTitle } from '@/activities/timeline-activities/hooks/useLinkedObjectsTitle';
|
||||
import { type TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity';
|
||||
import { type ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
|
||||
// do we need to test this?
|
||||
export const useTimelineActivities = (
|
||||
targetableObject: ActivityTargetableObject,
|
||||
) => {
|
||||
const isTimelineActivityMigrated = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_TIMELINE_ACTIVITY_MIGRATED,
|
||||
);
|
||||
|
||||
const targetableObjectFieldIdName = isTimelineActivityMigrated
|
||||
? `target${capitalize(targetableObject.targetObjectNameSingular)}Id`
|
||||
: getActivityTargetObjectFieldIdName({
|
||||
nameSingular: targetableObject.targetObjectNameSingular,
|
||||
});
|
||||
const targetableObjectFieldIdName = `target${capitalize(targetableObject.targetObjectNameSingular)}Id`;
|
||||
|
||||
const { objectMetadataItem: timelineActivityMetadata } =
|
||||
useObjectMetadataItem({
|
||||
|
||||
@@ -6,8 +6,13 @@ export type NoteTarget = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt?: string | null;
|
||||
companyId?: string | null;
|
||||
personId?: string | null;
|
||||
opportunityId?: string | null;
|
||||
targetCompanyId?: string | null;
|
||||
targetPersonId?: string | null;
|
||||
targetOpportunityId?: string | null;
|
||||
note: Pick<Note, 'id' | 'createdAt' | 'updatedAt' | '__typename'>;
|
||||
person?: Pick<Person, 'id' | 'name' | 'avatarUrl' | '__typename'> | null;
|
||||
company?: Pick<Company, 'id' | 'name' | 'domainName' | '__typename'> | null;
|
||||
|
||||
@@ -7,8 +7,13 @@ export type TaskTarget = {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
deletedAt?: string | null;
|
||||
companyId?: string | null;
|
||||
personId?: string | null;
|
||||
opportunityId?: string | null;
|
||||
targetCompanyId?: string | null;
|
||||
targetPersonId?: string | null;
|
||||
targetOpportunityId?: string | null;
|
||||
taskId: string | null;
|
||||
task: Pick<Task, 'id' | 'createdAt' | 'updatedAt' | '__typename'>;
|
||||
person?: Pick<Person, 'id' | 'name' | 'avatarUrl' | '__typename'> | null;
|
||||
|
||||
+23
-15
@@ -1,27 +1,35 @@
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
|
||||
export const generateActivityTargetMorphFieldKeys = (
|
||||
objectMetadataItems: ObjectMetadataItem[],
|
||||
isMorphRelation: boolean,
|
||||
) => {
|
||||
const targetableObjectMetadataItems = objectMetadataItems.filter(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.isActive && !objectMetadataItem.isSystem,
|
||||
);
|
||||
|
||||
const targetableObjects = Object.fromEntries(
|
||||
objectMetadataItems
|
||||
.filter(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.isActive && !objectMetadataItem.isSystem,
|
||||
)
|
||||
.map((objectMetadataItem) => [objectMetadataItem.nameSingular, true]),
|
||||
targetableObjectMetadataItems.map((objectMetadataItem) => {
|
||||
const targetFieldIdName = getActivityTargetObjectFieldIdName({
|
||||
nameSingular: objectMetadataItem.nameSingular,
|
||||
isMorphRelation,
|
||||
});
|
||||
|
||||
return [targetFieldIdName.replace(/Id$/, ''), true];
|
||||
}),
|
||||
);
|
||||
|
||||
const targetableObjectIds = Object.fromEntries(
|
||||
objectMetadataItems
|
||||
.filter(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.isActive && !objectMetadataItem.isSystem,
|
||||
)
|
||||
.map((objectMetadataItem) => [
|
||||
`${objectMetadataItem.nameSingular}Id`,
|
||||
true,
|
||||
]),
|
||||
targetableObjectMetadataItems.map((objectMetadataItem) => {
|
||||
const targetFieldIdName = getActivityTargetObjectFieldIdName({
|
||||
nameSingular: objectMetadataItem.nameSingular,
|
||||
isMorphRelation,
|
||||
});
|
||||
|
||||
return [targetFieldIdName, true];
|
||||
}),
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
+18
-4
@@ -1,3 +1,4 @@
|
||||
import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
@@ -8,12 +9,14 @@ type GetActivityTargetFieldNameForObjectArgs = {
|
||||
| CoreObjectNameSingular.Task;
|
||||
targetObjectMetadataId: string;
|
||||
objectMetadataItems: ObjectMetadataItem[];
|
||||
isMorphRelation?: boolean;
|
||||
};
|
||||
|
||||
export const getActivityTargetFieldNameForObject = ({
|
||||
activityObjectNameSingular,
|
||||
targetObjectMetadataId,
|
||||
objectMetadataItems,
|
||||
isMorphRelation = false,
|
||||
}: GetActivityTargetFieldNameForObjectArgs): string | undefined => {
|
||||
const activityTargetObjectNameSingular =
|
||||
activityObjectNameSingular === CoreObjectNameSingular.Task
|
||||
@@ -29,10 +32,21 @@ export const getActivityTargetFieldNameForObject = ({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const targetField = activityTargetObjectMetadata.fields.find(
|
||||
(field) =>
|
||||
field.relation?.targetObjectMetadata.id === targetObjectMetadataId,
|
||||
const targetObjectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) => objectMetadataItem.id === targetObjectMetadataId,
|
||||
);
|
||||
|
||||
return targetField?.name;
|
||||
if (isMorphRelation && isDefined(targetObjectMetadataItem)) {
|
||||
const fieldIdName = getActivityTargetObjectFieldIdName({
|
||||
nameSingular: targetObjectMetadataItem.nameSingular,
|
||||
isMorphRelation: true,
|
||||
});
|
||||
|
||||
return fieldIdName.replace(/Id$/, '');
|
||||
}
|
||||
|
||||
return activityTargetObjectMetadata.fields.find(
|
||||
(field) =>
|
||||
field.relation?.targetObjectMetadata.id === targetObjectMetadataId,
|
||||
)?.name;
|
||||
};
|
||||
|
||||
+82
-23
@@ -6,8 +6,8 @@ import { type TaskTarget } from '@/activities/types/TaskTarget';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { type ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
|
||||
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
|
||||
import { type Nullable } from 'twenty-shared/types';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FieldMetadataType, type Nullable } from 'twenty-shared/types';
|
||||
import { computeMorphRelationFieldName, isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type GetActivityTargetObjectRecordsProps = {
|
||||
activityRecord: Note | Task;
|
||||
@@ -47,41 +47,100 @@ export const getActivityTargetObjectRecords = ({
|
||||
);
|
||||
|
||||
const activityTargetRelationFields =
|
||||
activityTargetObjectMetadata?.fields.filter(
|
||||
(field) =>
|
||||
isDefined(field.relation?.targetObjectMetadata.id) &&
|
||||
![CoreObjectNameSingular.Note, CoreObjectNameSingular.Task].includes(
|
||||
field.relation?.targetObjectMetadata
|
||||
.nameSingular as CoreObjectNameSingular,
|
||||
),
|
||||
) ?? [];
|
||||
activityTargetObjectMetadata?.fields.filter((field) => {
|
||||
if (
|
||||
field.type === FieldMetadataType.MORPH_RELATION &&
|
||||
isDefined(field.morphRelations) &&
|
||||
field.morphRelations.length > 0
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const targetObjectNameSingular =
|
||||
field.relation?.targetObjectMetadata.nameSingular;
|
||||
|
||||
if (!isDefined(field.relation?.targetObjectMetadata.id)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!isDefined(targetObjectNameSingular)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
targetObjectNameSingular !== CoreObjectNameSingular.Note &&
|
||||
targetObjectNameSingular !== CoreObjectNameSingular.Task
|
||||
);
|
||||
}) ?? [];
|
||||
|
||||
const activityTargetObjectRecords = targets
|
||||
.map<ActivityTargetWithTargetRecord | undefined>((activityTarget) => {
|
||||
if (!isDefined(activityTarget)) {
|
||||
throw new Error('Cannot find activity target');
|
||||
}
|
||||
const matchingField = activityTargetRelationFields.find((field) =>
|
||||
isDefined(activityTarget[field.name]),
|
||||
);
|
||||
|
||||
if (!matchingField || !matchingField.relation) {
|
||||
if (isDefined(activityTarget.deletedAt)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const correspondingObjectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.id ===
|
||||
matchingField.relation?.targetObjectMetadata.id,
|
||||
);
|
||||
let matchingFieldName: string | undefined;
|
||||
let correspondingObjectMetadataItem: ObjectMetadataItem | undefined;
|
||||
|
||||
if (!correspondingObjectMetadataItem) {
|
||||
for (const field of activityTargetRelationFields) {
|
||||
if (
|
||||
field.type === FieldMetadataType.MORPH_RELATION &&
|
||||
isDefined(field.morphRelations)
|
||||
) {
|
||||
const matchingMorphRelation = field.morphRelations.find(
|
||||
(morphRelation) => {
|
||||
const morphFieldName = computeMorphRelationFieldName({
|
||||
fieldName: field.name,
|
||||
relationType: morphRelation.type,
|
||||
targetObjectMetadataNameSingular:
|
||||
morphRelation.targetObjectMetadata.nameSingular,
|
||||
targetObjectMetadataNamePlural:
|
||||
morphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
|
||||
return isDefined(activityTarget[morphFieldName]);
|
||||
},
|
||||
);
|
||||
|
||||
if (isDefined(matchingMorphRelation)) {
|
||||
matchingFieldName = computeMorphRelationFieldName({
|
||||
fieldName: field.name,
|
||||
relationType: matchingMorphRelation.type,
|
||||
targetObjectMetadataNameSingular:
|
||||
matchingMorphRelation.targetObjectMetadata.nameSingular,
|
||||
targetObjectMetadataNamePlural:
|
||||
matchingMorphRelation.targetObjectMetadata.namePlural,
|
||||
});
|
||||
correspondingObjectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.id ===
|
||||
matchingMorphRelation.targetObjectMetadata.id,
|
||||
);
|
||||
break;
|
||||
}
|
||||
} else if (isDefined(activityTarget[field.name])) {
|
||||
matchingFieldName = field.name;
|
||||
correspondingObjectMetadataItem = objectMetadataItems.find(
|
||||
(objectMetadataItem) =>
|
||||
objectMetadataItem.id === field.relation?.targetObjectMetadata.id,
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
!isDefined(matchingFieldName) ||
|
||||
!isDefined(correspondingObjectMetadataItem)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const targetObjectRecord = activityTarget[matchingField.name] as
|
||||
| ObjectRecord
|
||||
| undefined;
|
||||
const targetObjectRecord: ObjectRecord | undefined =
|
||||
activityTarget[matchingFieldName];
|
||||
|
||||
if (!isDefined(targetObjectRecord)) {
|
||||
throw new Error(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user