mirror of
https://github.com/twentyhq/twenty.git
synced 2025-12-23 16:09:15 -05:00
Fixes https://github.com/twentyhq/twenty/issues/16110 This PR implements Temporal to replace the legacy Date object, in all features that are time zone sensitive. (around 80% of the app) Here we define a few utils to handle Temporal primitives and obtain an easier DX for timezone manipulation, front end and back end. This PR deactivates the usage of timezone from the graph configuration, because for now it's always UTC and is not really relevant, let's handle that later. Workflows code and backend only code that don't take user input are using UTC time zone, the affected utils have not been refactored yet because this PR is big enough. # New way of filtering on date intervals As we'll progressively rollup Temporal everywhere in the codebase and remove `Date` JS object everywhere possible, we'll use the way to filter that is recommended by Temporal. This way of filtering on date intervals involves half-open intervals, and is the preferred way to avoid edge-cases with DST and smallest time increment edge-case. ## Filtering endOfX with DST edge-cases Some day-light save time shifts involve having no existing hour, or even day on certain days, for example Samoa Islands have no 30th of December 2011 : https://www.timeanddate.com/news/time/samoa-dateline.html, it jumps from 29th to 31st, so filtering on `< next period start` makes it easier to let the date library handle the strict inferior comparison, than filtering on `≤ end of period` and trying to compute manually the end of the period. For example for Samoa Islands, is end of day `2011-12-29T23:59:59.999` or is it `2011-12-30T23:59:59.999` ? If you say I don't need to know and compute it, because I want everything strictly before `2011-12-29T00:00:00 + start of next day (according to the library which knows those edge-cases)`, then you have a 100% deterministic way of computing date intervals in any timezone, for any day of any year. Of course the Samoa example is an extreme one, but more common ones involve DST shifts of 1 hour, which are still problematic on certain days of the year. ## Computing the exact _end of period_ Having an open interval filtering, with `[included - included]` instead of half-open `[included - excluded)`, forces to compute the open end of an interval, which often involves taking an arbitrary unit like minute, second, microsecond or nanosecond, which will lead to edge-case of unhandled values. For example, let's say my code computes endOfDay by setting the time to `23:59:59.999`, if another library, API, or anything else, ends up giving me a date-time with another time precision `23:59:59.999999999` (down to the nanosecond), then this date-time will be filtered out, while it should not. The good deterministic way to avoid 100% of those complex bugs is to create a half-open filter : `≥ start of period` to `< start of next period` For example : `≥ 2025-01-01T00:00:00` to `< 2025-01-02T00:00:00` instead of `≥ 2025-01-01T00:00:00` to `≤ 2025-01-01T23:59:59.999` Because, `2025-01-01T00:00:00` = `2025-01-01T00:00:00.000` = `2025-01-01T00:00:00.000000` = `2025-01-01T00:00:00.000000000` => no risk of error in computing start of period But `2025-01-01T23:59:59` ≠ `2025-01-01T23:59:59.999` ≠ `2025-01-01T23:59:59.999999` ≠ `2025-01-01T23:59:59.999999999` => existing risk of error in computing end of period This is why an half-open interval has no risk of error in computing a date-time interval filter. Here is a link to this debate : https://github.com/tc39/proposal-temporal/issues/2568 > For this reason, we recommend not calculating the exact nanosecond at the end of the day if it's not absolutely necessary. For example, if it's needed for <= comparisons, we recommend just changing the comparison code. So instead of <= zdtEndOfDay your code could be < zdtStartOfNextDay which is easier to calculate and not subject to the issue of not knowing which unit is the right one. > > [Justin Grant](https://github.com/justingrant), top contributor of Temporal ## Application to our codebase Applying this half-open filtering paradigm to our codebase means we would have to rename `IS_AFTER` to `IS_AFTER_OR_EQUAL` and to keep `IS_BEFORE` (or even `IS_STRICTLY_BEFORE`) to make this half-open interval self-explanatory everywhere in the codebase, this will avoid any confusion. See the relevant issue : https://github.com/twentyhq/core-team-issues/issues/2010 In the mean time, we'll keep this operand and add this semantic in the naming everywhere possible. ## Example with a different user timezone Example on a graph grouped by week in timezone Pacific/Samoa, on a computer running on Europe/Paris : <img width="342" height="511" alt="image" src="https://github.com/user-attachments/assets/9e7d5121-ecc4-4233-835b-f59293fbd8c8" /> Then the associated data in the table view, with our **half-open date-time filter** : <img width="804" height="262" alt="image" src="https://github.com/user-attachments/assets/28efe1d7-d2fc-4aec-b521-bada7f980447" /> And the associated SQL query result to see how DATE_TRUNC in Postgres applies its internal start of week logic : <img width="709" height="220" alt="image" src="https://github.com/user-attachments/assets/4d0542e1-eaae-4b4b-afa9-5005f48ffdca" /> The associated SQL query without parameters to test in your SQL client : ```SQL SELECT "opportunity"."closeDate" as "close_date", TO_CHAR(DATE_TRUNC('week', "opportunity"."closeDate", 'Pacific/Samoa') AT TIME ZONE 'Pacific/Samoa', 'YYYY-MM-DD') AS "DATE_TRUNC by week start in timezone Pacific/Samoa", "opportunity"."name" FROM "workspace_1wgvd1injqtife6y4rvfbu3h5"."opportunity" "opportunity" ORDER BY "opportunity"."closeDate" ASC NULLS LAST ``` # Date picker simplification (not in this PR) Our DatePicker component, which is wrapping `react-datepicker` library component, is now exposing plain dates as string instead of Date object. The Date object is still used internally to manage the library component, but since the date picker calendar is only manipulating plain dates, there is no need to add timezone management to it, and no need to expose a handleChange with Date object. The timezone management relies on date time inputs now. The modification has been made in a previous PR : https://github.com/twentyhq/twenty/issues/15377 but it's good to reference it here. # Calendar feature refactor Calendar feature has been refactored to rely on Temporal.PlainDate as much as possible, while leaving some date-fns utils to avoid re-coding them. Since the trick is to use utils to convert back and from Date object in exec env reliably, we can do it everywhere we need to interface legacy Date object utils and Temporal related code. ## TimeZone is now shown on Calendar : <img width="894" height="958" alt="image" src="https://github.com/user-attachments/assets/231f8107-fad6-4786-b532-456692c20f1d" /> ## Month picker has been refactored <img width="503" height="266" alt="image" src="https://github.com/user-attachments/assets/cb90bc34-6c4d-436d-93bc-4b6fb00de7f5" /> Since the days weren't useful, the picker has been refactored to remove the days. # Miscellaneous - Fixed a bug with drag and drop edge-case with 2 items in a list. # Improvements ## Lots of chained operations It would be nice to create small utils to avoid repeated chained operations, but that is how Temporal is designed, a very small set of primitive operations that allow to compose everything needed. Maybe we'll have wrappers on top of Temporal in the coming years. ## Creation of Temporal objects is throwing errors If the input is badly formatted Temporal will throw, we might want to adopt a global strategy to avoid that. Example : ```ts const newPlainDate = Temporal.PlainDate.from('bad-string'); // Will throw ```
235 lines
7.5 KiB
JSON
235 lines
7.5 KiB
JSON
{
|
|
"private": true,
|
|
"dependencies": {
|
|
"@apollo/client": "^3.7.17",
|
|
"@emotion/react": "^11.11.1",
|
|
"@emotion/styled": "^11.11.0",
|
|
"@floating-ui/react": "^0.24.3",
|
|
"@linaria/core": "^6.2.0",
|
|
"@linaria/react": "^6.2.1",
|
|
"@radix-ui/colors": "^3.0.0",
|
|
"@sniptt/guards": "^0.2.0",
|
|
"@tabler/icons-react": "^3.31.0",
|
|
"@wyw-in-js/vite": "^0.7.0",
|
|
"archiver": "^7.0.1",
|
|
"danger-plugin-todos": "^1.3.1",
|
|
"date-fns": "^2.30.0",
|
|
"date-fns-tz": "^2.0.0",
|
|
"deep-equal": "^2.2.2",
|
|
"file-type": "16.5.4",
|
|
"framer-motion": "^11.18.0",
|
|
"fuse.js": "^7.1.0",
|
|
"googleapis": "105",
|
|
"hex-rgb": "^5.0.0",
|
|
"immer": "^10.1.1",
|
|
"libphonenumber-js": "^1.10.26",
|
|
"lodash.camelcase": "^4.3.0",
|
|
"lodash.chunk": "^4.2.0",
|
|
"lodash.compact": "^3.0.1",
|
|
"lodash.escaperegexp": "^4.1.2",
|
|
"lodash.groupby": "^4.6.0",
|
|
"lodash.identity": "^3.0.0",
|
|
"lodash.isempty": "^4.4.0",
|
|
"lodash.isequal": "^4.5.0",
|
|
"lodash.isobject": "^3.0.2",
|
|
"lodash.kebabcase": "^4.1.1",
|
|
"lodash.mapvalues": "^4.6.0",
|
|
"lodash.merge": "^4.6.2",
|
|
"lodash.omit": "^4.5.0",
|
|
"lodash.pickby": "^4.6.0",
|
|
"lodash.snakecase": "^4.1.1",
|
|
"lodash.upperfirst": "^4.3.1",
|
|
"microdiff": "^1.3.2",
|
|
"planer": "^1.2.0",
|
|
"pluralize": "^8.0.0",
|
|
"react": "^18.2.0",
|
|
"react-dom": "^18.2.0",
|
|
"react-responsive": "^9.0.2",
|
|
"react-router-dom": "^6.4.4",
|
|
"react-tooltip": "^5.13.1",
|
|
"recoil": "^0.7.7",
|
|
"remark-gfm": "^3.0.1",
|
|
"rxjs": "^7.2.0",
|
|
"semver": "^7.5.4",
|
|
"slash": "^5.1.0",
|
|
"storybook-addon-mock-date": "^0.6.0",
|
|
"temporal-polyfill": "^0.3.0",
|
|
"ts-key-enum": "^2.0.12",
|
|
"tslib": "^2.8.1",
|
|
"type-fest": "4.10.1",
|
|
"typescript": "5.9.2",
|
|
"uuid": "^9.0.0",
|
|
"vite-tsconfig-paths": "^4.2.1",
|
|
"xlsx-ugnis": "^0.19.3",
|
|
"zod": "^4.1.11"
|
|
},
|
|
"devDependencies": {
|
|
"@babel/core": "^7.14.5",
|
|
"@babel/preset-react": "^7.14.5",
|
|
"@babel/preset-typescript": "^7.24.6",
|
|
"@chromatic-com/storybook": "^3",
|
|
"@graphql-codegen/cli": "^3.3.1",
|
|
"@graphql-codegen/client-preset": "^4.1.0",
|
|
"@graphql-codegen/typescript": "^3.0.4",
|
|
"@graphql-codegen/typescript-operations": "^3.0.4",
|
|
"@graphql-codegen/typescript-react-apollo": "^3.3.7",
|
|
"@nx/eslint": "22.0.3",
|
|
"@nx/eslint-plugin": "22.0.3",
|
|
"@nx/jest": "22.0.3",
|
|
"@nx/js": "22.0.3",
|
|
"@nx/react": "22.0.3",
|
|
"@nx/storybook": "22.0.3",
|
|
"@nx/vite": "22.0.3",
|
|
"@nx/web": "22.0.3",
|
|
"@sentry/types": "^8",
|
|
"@storybook/addon-actions": "8.6.15",
|
|
"@storybook/addon-coverage": "^1.0.0",
|
|
"@storybook/addon-essentials": "8.6.15",
|
|
"@storybook/addon-interactions": "8.6.15",
|
|
"@storybook/addon-links": "8.6.15",
|
|
"@storybook/blocks": "8.6.15",
|
|
"@storybook/core-server": "8.6.15",
|
|
"@storybook/icons": "^1.2.9",
|
|
"@storybook/preview-api": "8.6.15",
|
|
"@storybook/react": "8.6.15",
|
|
"@storybook/react-vite": "8.6.15",
|
|
"@storybook/test": "8.6.15",
|
|
"@storybook/test-runner": "^0.23.0",
|
|
"@storybook/types": "8.6.15",
|
|
"@stylistic/eslint-plugin": "^1.5.0",
|
|
"@swc-node/register": "1.8.0",
|
|
"@swc/cli": "^0.3.12",
|
|
"@swc/core": "1.13.3",
|
|
"@swc/helpers": "~0.5.2",
|
|
"@swc/jest": "^0.2.39",
|
|
"@testing-library/jest-dom": "^6.6.3",
|
|
"@testing-library/react": "^16.3.0",
|
|
"@types/addressparser": "^1.0.3",
|
|
"@types/bcrypt": "^5.0.0",
|
|
"@types/bytes": "^3.1.1",
|
|
"@types/chrome": "^0.0.267",
|
|
"@types/deep-equal": "^1.0.1",
|
|
"@types/express": "^4.17.13",
|
|
"@types/fs-extra": "^11.0.4",
|
|
"@types/graphql-fields": "^1.3.6",
|
|
"@types/inquirer": "^9.0.9",
|
|
"@types/jest": "^30.0.0",
|
|
"@types/lodash.camelcase": "^4.3.7",
|
|
"@types/lodash.compact": "^3.0.9",
|
|
"@types/lodash.escaperegexp": "^4.1.9",
|
|
"@types/lodash.groupby": "^4.6.9",
|
|
"@types/lodash.identity": "^3.0.9",
|
|
"@types/lodash.isempty": "^4.4.7",
|
|
"@types/lodash.isequal": "^4.5.7",
|
|
"@types/lodash.isobject": "^3.0.7",
|
|
"@types/lodash.kebabcase": "^4.1.7",
|
|
"@types/lodash.mapvalues": "^4.6.9",
|
|
"@types/lodash.omit": "^4.5.9",
|
|
"@types/lodash.pickby": "^4.6.9",
|
|
"@types/lodash.snakecase": "^4.1.7",
|
|
"@types/lodash.upperfirst": "^4.3.7",
|
|
"@types/ms": "^0.7.31",
|
|
"@types/node": "^24.0.0",
|
|
"@types/passport-google-oauth20": "^2.0.11",
|
|
"@types/passport-jwt": "^3.0.8",
|
|
"@types/passport-microsoft": "^2.1.0",
|
|
"@types/pluralize": "^0.0.33",
|
|
"@types/react": "^18.2.39",
|
|
"@types/react-datepicker": "^6.2.0",
|
|
"@types/react-dom": "^18.2.15",
|
|
"@types/supertest": "^2.0.11",
|
|
"@types/uuid": "^9.0.2",
|
|
"@typescript-eslint/eslint-plugin": "^8.39.0",
|
|
"@typescript-eslint/parser": "^8.39.0",
|
|
"@typescript-eslint/utils": "^8.39.0",
|
|
"@vitejs/plugin-react-swc": "3.11.0",
|
|
"@yarnpkg/types": "^4.0.0",
|
|
"chromatic": "^6.18.0",
|
|
"concurrently": "^8.2.2",
|
|
"danger": "^13.0.4",
|
|
"dotenv-cli": "^7.4.4",
|
|
"esbuild": "^0.25.10",
|
|
"eslint": "^9.32.0",
|
|
"eslint-config-prettier": "^9.1.0",
|
|
"eslint-plugin-import": "^2.31.0",
|
|
"eslint-plugin-jsx-a11y": "^6.10.2",
|
|
"eslint-plugin-lingui": "^0.9.0",
|
|
"eslint-plugin-mdx": "^3.6.2",
|
|
"eslint-plugin-prefer-arrow": "^1.2.3",
|
|
"eslint-plugin-prettier": "^5.1.2",
|
|
"eslint-plugin-project-structure": "^3.9.1",
|
|
"eslint-plugin-react": "^7.37.2",
|
|
"eslint-plugin-react-hooks": "^5.0.0",
|
|
"eslint-plugin-react-refresh": "^0.4.4",
|
|
"eslint-plugin-simple-import-sort": "^10.0.0",
|
|
"eslint-plugin-storybook": "^0.9.0",
|
|
"eslint-plugin-unicorn": "^56.0.1",
|
|
"eslint-plugin-unused-imports": "^3.0.0",
|
|
"http-server": "^14.1.1",
|
|
"jest": "29.7.0",
|
|
"jest-environment-jsdom": "30.0.0-beta.3",
|
|
"jest-environment-node": "^29.4.1",
|
|
"jest-fetch-mock": "^3.0.3",
|
|
"jsdom": "~22.1.0",
|
|
"msw": "^2.0.11",
|
|
"msw-storybook-addon": "^2.0.5",
|
|
"nx": "22.0.3",
|
|
"prettier": "^3.1.1",
|
|
"raw-loader": "^4.0.2",
|
|
"rimraf": "^5.0.5",
|
|
"source-map-support": "^0.5.20",
|
|
"storybook": "8.6.15",
|
|
"storybook-addon-cookie": "^3.2.0",
|
|
"storybook-addon-pseudo-states": "^2.1.2",
|
|
"supertest": "^6.1.3",
|
|
"ts-jest": "^29.1.1",
|
|
"ts-loader": "^9.2.3",
|
|
"ts-node": "10.9.1",
|
|
"tsconfig-paths": "^4.2.0",
|
|
"tsx": "^4.17.0",
|
|
"vite": "^7.0.0"
|
|
},
|
|
"engines": {
|
|
"node": "^24.5.0",
|
|
"npm": "please-use-yarn",
|
|
"yarn": ">=4.0.2"
|
|
},
|
|
"license": "AGPL-3.0",
|
|
"name": "twenty",
|
|
"packageManager": "yarn@4.9.2",
|
|
"resolutions": {
|
|
"graphql": "16.8.1",
|
|
"type-fest": "4.10.1",
|
|
"typescript": "5.9.2",
|
|
"graphql-redis-subscriptions/ioredis": "^5.6.0",
|
|
"prosemirror-view": "1.40.0",
|
|
"prosemirror-transform": "1.10.4"
|
|
},
|
|
"version": "0.2.1",
|
|
"nx": {},
|
|
"scripts": {
|
|
"docs:generate": "tsx packages/twenty-docs/scripts/generate-docs-json.ts",
|
|
"docs:generate-navigation-template": "tsx packages/twenty-docs/scripts/generate-navigation-template.ts",
|
|
"start": "npx concurrently --kill-others 'npx nx run-many -t start -p twenty-server twenty-front' 'npx wait-on tcp:3000 && npx nx run twenty-server:worker'"
|
|
},
|
|
"workspaces": {
|
|
"packages": [
|
|
"packages/twenty-front",
|
|
"packages/twenty-server",
|
|
"packages/twenty-emails",
|
|
"packages/twenty-ui",
|
|
"packages/twenty-utils",
|
|
"packages/twenty-zapier",
|
|
"packages/twenty-website",
|
|
"packages/twenty-docs",
|
|
"packages/twenty-e2e-testing",
|
|
"packages/twenty-shared",
|
|
"packages/twenty-sdk",
|
|
"packages/twenty-apps",
|
|
"packages/twenty-cli",
|
|
"packages/create-twenty-app",
|
|
"tools/eslint-rules"
|
|
]
|
|
}
|
|
}
|