Compare commits

...

97 Commits

Author SHA1 Message Date
sriram veeraghanta db18c3555c Merge branch 'canary' of github.com:makeplane/plane into preview 2025-09-10 14:45:14 +05:30
sriram veeraghanta b696ae91ed Potential fix for code scanning alert no. 646: Server-side request forgery (#7758)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-09-10 14:44:34 +05:30
sriramveeraghanta 61e91bd09c chore (deps): django and vite version bump 2025-09-10 14:44:34 +05:30
sriram veeraghanta 20d773042b Potential fix for code scanning alert no. 646: Server-side request forgery (#7758)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-09-10 14:42:46 +05:30
sriramveeraghanta 30b175108b chore (deps): django and vite version bump 2025-09-10 13:38:30 +05:30
Anmol Singh Bhatia 6d116beea3 [WEB-4320] dev: propel emoji reaction component (#7741)
* dev: animated counter added to propel

* chore: animated counter story added

* chore: propel config updated

* chore: code refactor

* dev: emoji reaction and renderer component added to propel

* dev: emoji reaction story added

* chore: propel config updated

* chore: code refactor

* fix: format error

* chore: lint error resolved

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2025-09-10 00:54:47 +05:30
Anmol Singh Bhatia b0db4fcf10 [WEB-4736] dev: propel button (#7746)
* dev: button added to propel

* dev: button story added

* chore: propel config updated

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2025-09-10 00:34:17 +05:30
Anmol Singh Bhatia 7e03264758 [WEB-4730] dev: propel context menu component (#7745)
* dev: context menu component added

* dev: context menu story added

* chore: propel config updated
2025-09-10 00:15:55 +05:30
Anmol Singh Bhatia 1c8ac3d247 [WEB-4737] dev: propel pill component (#7743)
* dev: pill component added to propel

* dev: pill story added

* chore: propel config updated

* chore: code refactor
2025-09-10 00:15:37 +05:30
Anmol Singh Bhatia 3b8bb1effc [WEB-4733] dev: propel toolbar component (#7742)
* dev: toolbar component added to propel

* dev: toolbar story added

* chore: propel config updated

* chore: code refactor

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2025-09-10 00:15:10 +05:30
Anmol Singh Bhatia 5a63e6dad2 [WEB-4858] chore: error page ui revamp #7747 2025-09-10 00:03:39 +05:30
Anmol Singh Bhatia 45688bdc72 [WEB-4860] dev: propel animated counter component (#7740)
* dev: animated counter added to propel

* chore: animated counter story added

* chore: propel config updated

* chore: code refactor

* chore: code refactor

* fix: format error
2025-09-09 23:51:45 +05:30
Jayash Tripathy 43b7a6ad0a [WEB-4855] refactor: chart tick improvements (#7732)
* 🚧 WIP: Introduced customTicks prop in BarChart for flexible tick rendering.

*  feat: added customTicks to axis charts for flexible tick rendering

* 🔧 fix: update default bar fill color to black and ensure consistent color usage in BarChart

*  feat: add customTooltipContent prop to LineChart for enhanced tooltip flexibility

* 🔧 fix: update bar fill color handling to support dynamic colors based on data and removed DEFAULT_BAR_FILL_COLOR

* 🔧 fix: correct bar fill color handling in BarChart to ensure proper color assignment for tooltips

* 🔧 fix: update customTicks prop types in TAxisChartProps to use unknown type for better type safety

* 📝 chore: updated translations and cleaned up insight card

* 🚨 fix: lint

* 🔧 fix: remove unused translation key "no_of" from Russian translations
2025-09-09 23:51:13 +05:30
Anmol Singh Bhatia 498613284e [WEB-4841] chore: calendar component migration UI to propel (#7730)
* chore: move calendar components and dependencies

* chore: update package configurations

* chore: calendar import updated

* chore: propel config updated

* chore: propel calendar code refactor

* chore: code refactor

* fix: build error
2025-09-09 23:50:51 +05:30
Anmol Singh Bhatia 9ab3143a73 [WEB-4816] chore: add label flow (#7716)
* chore: remove create label modal

* fix: label spinner

* chore: add label flow improvements

* chore: code refactor

* chore: code refactor

* chore: code refactor
2025-09-09 23:50:39 +05:30
Anmol Singh Bhatia 56cd0fc445 [WEB-4025] fix: external user comment and reaction (#7692)
* chore: reactions types updated

* fix: external user comments

* fix: external user reactions

* chore: added display name for actor

* chore: merge conflicts

* chore: updated the created_by and updated_by

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2025-09-09 23:50:11 +05:30
Vamsi Krishna 260d9a053d [WEB-4802] fix: module sort order (#7674)
* fix: module sort order

* chore: removed secondary sort for progress

* chore: sort refactor
2025-09-09 23:49:30 +05:30
Vipin Chaudhary 34bdc2ad76 [WIKI-636] fix: replace project loader with page loader #7752 2025-09-09 19:27:00 +05:30
M. Palanikannan 99bc4262c5 [WIKI-654] fix: version history to load from html (#7754)
* fix: versions history to load from html

* chore: added json and stripped to store in version history (#7755)

---------

Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
2025-09-09 19:26:19 +05:30
Aaryan Khandelwal 49127ebeea [WIKI-653] fix: page content loader #7753 2025-09-09 18:20:31 +05:30
Vamsi Krishna a8a6536379 [WEB-4688]feat: added collapsible to propel (#7643)
* chore: added collapsibl to propel

* fix: export path

* chore: made collapsible a compound component

* fix: lint and format errors

* chore: updated propel exports order and added collapsible to tsdown config
2025-09-09 13:59:00 +05:30
Vamsi Krishna 11b83cf4f2 [WEB-4692]feat: added toast to propel package (#7640)
* * chore: migrated toast to base ui toast
* chore: added toast to propel package

* fix: lint errors

* fix: export path

* fix: lint errors

* chore: imporoved props handling

* fix: lint errors

* chore: updated import paths

* chore: replaced clsx with cn

* * chore: updated tsdown config
* chore: updated exports order
2025-09-09 13:58:11 +05:30
Anmol Singh Bhatia fbd48c33f5 [WEB-4729] dev: propel scrollarea (#7748)
* dev: scrollarea added to propel

* dev: scrollarea story added

* chore: propel config updated

* fix: format error
2025-09-09 13:56:28 +05:30
Vamsi Krishna 37ce8a9fe6 [WEB-4800]chore: settings header component refactor #7669 2025-09-09 13:42:40 +05:30
Bavisetti Narayan 291101a8e5 chore: added attributes in ol tag (#7736) 2025-09-08 17:17:38 +05:30
Vipin Chaudhary 0cb4976e38 [WIKI-647] chore: update image path (#7734)
* chore: update image path

* chore: update asset path
2025-09-08 15:44:56 +05:30
Anmol Singh Bhatia 20d139cc9e [WEB-4839] chore: maintenance mode empty state updated (#7728)
* chore: maintenance mode empty state updated

* chore: code refactor

* chore: code refactor

* chore: code refactor
2025-09-05 20:16:39 +05:30
Jayash Tripathy ff181e566f [WEB-4738] feat: add Skeleton component with respective stories in propel (#7704)
*  feat: add Skeleton component with respective stories in propel

* 🚨 fix: lint issues

* ♻️ refactor : enhance Skeleton component with improved props and structure

*  feat: add Skeleton component entry to package.json

* 🚨 fix: lint

* ♻️ refactor: rename Skeleton to skeleton

* fix: rename fixes

* ♻️ refactor: rename skeleton

* 🚨 fix: lint

* ♻️ refactor: add skeleton entry to tsdown config and package.json

* ♻️  refactor: sorted propel exports

---------

Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
2025-09-05 15:51:05 +05:30
Jayash Tripathy a696b6039c [WEB-4835] feat: storybook enhancements (#7702)
*  feat: integrate Storybook addons and enhance Tabs component

- Added `@storybook/addon-designs` and `@storybook/addon-docs` to the project dependencies.
- Enhanced the Tabs component stories to support dynamic tab options.
- Introduced a custom theme for Storybook with a new manager configuration.
- Added a new SVG logo for branding in Storybook.
- Pinned the storybook and related packages version

* ♻️  refactor: update Storybook manager configuration by removing unused import

* ♻️  refactor: enhance Tabs story with size labels and layout adjustments

* 🚨 fix: lint issues

* 🚨 fix: lock file

* ✏️  fix: update brand image path in Storybook manager configuration

* ♻️ refactor: improve Tabs story by ensuring safe default value and enhancing content rendering
2025-09-05 14:55:33 +05:30
Vipin Chaudhary af1dcd335e [WIKI-640] chore: refactor link view #7727 2025-09-05 14:14:30 +05:30
Aaryan Khandelwal a1500c2206 [WIKI-644] fix: remove unnecessary props from the editor package #7726 2025-09-05 13:50:44 +05:30
Aaryan Khandelwal 88f194ca8e [WIKI-637] fix: work item description version history (#7725)
* fix: work item description version history

* chore: add key to the editor
2025-09-04 18:57:50 +05:30
Aaryan Khandelwal b68d2ca921 [WIKI-642] fix: editor bubble menu overflow issues #7724 2025-09-04 18:16:03 +05:30
Aaryan Khandelwal 78c6aede52 [WIKI-641] fix: overwrite tailwind typography plugin styles #7723 2025-09-04 18:14:48 +05:30
sriramveeraghanta c6c46f9aab Merge branch 'canary' of github.com:makeplane/plane into preview 2025-09-04 14:48:27 +05:30
Anmol Singh Bhatia 7d7e37439d [WEB-4840] chore: icon migration from @plane/ui to @plane/propel/icons (#7721)
* chore: move icons from ui package to propel package

* chore: package and tsdown config updated

* chore: migrate all icon imports from @plane/ui to @plane/propel/icons

* chore: remove icon components from @plane/ui package (migrated to
  @plane/propel/icons)

* chore: code refactoring

* chore: migrate remaining icon components from @Plane/ui to @Plane/propel/icons

* fix: lint error

* chore: code refactor
2025-09-04 14:44:53 +05:30
Akshat Jain d258080ee7 [INFRA-235] fix: docker compose file restart_policy changed to any (#7689)
* chore: update docker-compose.yml to change restart policy condition from 'on-failure' to 'any' and remove SSL variable from variables.env

* fix: update docker-compose.yml to change restart policy condition from 'any' to 'on-failure'
2025-09-04 14:18:45 +05:30
Bavisetti Narayan c40c7804e7 [WEB-4806] chore: updated html attribute validation #7719 2025-09-04 13:53:21 +05:30
Bavisetti Narayan 36c735bede [WEB-4806] chore: updated html attribute validation #7719 2025-09-04 13:52:43 +05:30
Bavisetti Narayan cab5d20217 [WEB-4806] chore: improved the html validation (#7676)
* chore: improved the html validation

* chore: removed the models changes

* chore: removed extra filters

* chore: changed the protocols
2025-09-03 19:57:37 +05:30
Bavisetti Narayan 9965fc75a6 [WEB-4806] chore: improved the html validation (#7676)
* chore: improved the html validation

* chore: removed the models changes

* chore: removed extra filters

* chore: changed the protocols
2025-09-03 19:56:20 +05:30
Dheeraj Kumar Ketireddy cb1e93f83d [WEB-4831] schema: add rich_filters field to multiple models for enhanced filtering capabilities (#7700)
* chore: add rich_filters field to multiple models for enhanced filtering capabilities

* Set the default  value to empty dict
2025-09-03 18:37:56 +05:30
Sangeetha 2e673026d4 [WEB-4129] fix: work item filter assignees missing fields #7698 2025-09-03 18:36:04 +05:30
Nikhil 49a40031a4 [WEB-4795] chore: remove caching decorator from WorkspaceStatesEndpoint #7714 2025-09-03 18:28:11 +05:30
sriram veeraghanta 54e4a0f6dd fix: build error due to exports in propel package (#7715)
* fix: build error due to exports in propel package

* fix: editor css exports

* fix: editor css build

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2025-09-03 18:11:31 +05:30
sriram veeraghanta 473535fcfa [WEB-4844] fix: export prople package dist (#7712)
* fix: export prople package dist

* fix: code refactor
2025-09-03 17:25:31 +05:30
Akshita Goyal 59ace4f411 [WEB-4787] fix: changed issue to work item in cycles dropdown (#7706) 2025-09-03 14:11:30 +05:30
sriram veeraghanta b99ddc24e7 [WEB-4810] feat: migrate to tsdown from tsup (#7679)
* feat: migrat to tsdown to tsup

* fix: build scripts

* fix: lock file fixes

* fix: adding build process to i18n and propel packages

* fix: lint warning

* chore: update services module entry points

* fix: lock file

* fix: lock file

* fix: remove tsc from build

* fix: tsdown configs

* fix: remove tsc step from build process

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
Co-authored-by: Aaron Reisman <aaron.reisman@plane.so>
2025-09-03 14:01:57 +05:30
Anmol Singh Bhatia 91f0228b5f [WEB-4838] fix: github logo theme #7709 2025-09-02 22:39:42 +05:30
sriram veeraghanta 4b74751e4e [WEB-4837] feat: adding more packages to catalog (#7693)
* feat: adding more packages to catalog

* fix: lock file

* fix: sharp pacakge catalog
2025-09-02 21:03:03 +05:30
Anmol Singh Bhatia 1c28bd5887 [WEB-4724] fix: editor tooltip (#7708)
* fix: editor tooltip code refactoring

* chore: code refactor
2025-09-02 20:40:14 +05:30
Aaryan Khandelwal 292264ba96 [WIKI-574] fix: disable copy comment link option for intake work item #7707 2025-09-02 20:39:09 +05:30
Prateek Shourya 28d368d502 [WEB-4836] fix: work item retrieval in peek overview (#7705) 2025-09-02 19:26:35 +05:30
Anmol Singh Bhatia 652a6cc885 [WEB-4734] feat: replace emoji picker with frimousse (#7639) 2025-09-02 19:00:15 +05:30
Anmol Singh Bhatia 26b48bfcf0 [WEB-4724] feat: migrate tooltips from blueprintjs to base-ui (#7646)
* feat: add card component to propel package and update tooltip imports

* refactor: remove @plane/ui dependency and update tooltip imports to use local card component

* fix: lint

* refactor: update import from @plane/ui to @plane/utils in command component

* chore: removed blueprintjs/core and blueprintjs/popover2 dependencies

* chore: updated tooltip instances across platform and performed related code refactoring

* chore: code refactor

* chore: code refactor

* fix: lint and build error

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor

* fix: format issue

* fix: build fix

---------

Co-authored-by: Jayash Tripathy <76092296+JayashTripathy@users.noreply.github.com>
2025-09-02 18:19:56 +05:30
Vamsi Krishna 569b2fba83 [WEB-4196]fix: toast message for sub work item quick actions (#7688)
* fix: toast message for sub work item quick actions

* chore: updated translation files
2025-09-02 18:02:30 +05:30
Anmol Singh Bhatia 779a96acb8 [WEB-4834] fix: range date picker weekStartsOn (#7699)
* fix: range date picker weekStartsOn

* chore: code refactor
2025-09-02 18:02:09 +05:30
guru_sainath 2d31b562d8 [WEB-4823] chore: Add compound indexing for notification fields to improve query performance (#7691)
* dev: handled indexing for the notification fields

* dev: removing indexing related to workspaces

* dev: handled indexing for file asset, user favorite, and page log

* dev: indexing concurrently
2025-09-02 14:18:13 +05:30
Prateek Shourya 50b4aca726 [WEB-4829] refactor: star us on GitHub link component (#7697)
* [WEB-4829] refactor: star us on github link

* fix: review comment

---------

Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
2025-09-02 13:54:13 +05:30
Prateek Shourya d960d7ce88 [WEB-4828] refactor: remove legacy project-level draft work items components (#7694) 2025-09-02 12:29:00 +05:30
M. Palanikannan fd5ba6c7b8 [WIKI-511] [WIKI-572] fix: extended page root and editor body (#7661)
* fix: better refactoring of page root and editor body

* fix: editor sideeffects

* fix: add comments json field

* fix: props name

* fix: review changes

* fix: extra checks

* fix: type changes

* fix: remove editor ref current

* fix: renaming

* fix: link check
2025-09-01 22:17:53 +05:30
Jayash Tripathy f42eeec2c0 [WEB-4686] feat: propel tabs (#7620)
* chore: global css file added to tailwind config package

* chore: tailwind config updated

* chore: cn utility function added to propel package

* chore: storybook init

* fix: format error

* feat: added base ui tabs

* fix: add missing newline at end of package.json in propel package

* fix: reorder import statement for Tabs component in propel package

* feat: refactor Tabs component to support compound structure with forward refs

* fix: lint

* chore: code refactor

* chore: code refactor

* fix: lock file

* chore: added stories for tabs

* refactor: clean up

* fix: lint

* fix: lint

* fix: Remove duplicate storybook ESLint config

* fix: lint

* fix: update classname import path in Tabs component

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
2025-09-01 19:58:40 +05:30
Vamsi Krishna e679dc3d12 [WEB-4814] chore: enabled reordering in list when group is none #7684 2025-09-01 19:46:17 +05:30
Jayash Tripathy f2edf637de [WEB-4809] chore: added common libs to pnpm workspace catalog (#7611)
* chore: added common libs to pnpm workspace catalog

* chore: update pnpm lockfile to use exact versions for React and TypeScript dependencies

* refactor: removed string from the versions

* fix: lint

* refactor: cleanup

* fix: lint

* chore: updated lock file
2025-09-01 19:42:56 +05:30
Vamsi Krishna 7437deaa86 [WEB-4689]chore: added accordion to propel (#7641)
* chore: added accordion to propel

* fix: lint errors

* fix: updated export path

* fix: lint errors

* chore: made accordion into compound component

* fix: coderabbit suggestions
2025-09-01 19:42:12 +05:30
Jayash Tripathy 64b95daff4 [WEB-4740] feat: add propel seperator (#7637)
* chore: global css file added to tailwind config package

* chore: tailwind config updated

* chore: cn utility function added to propel package

* chore: storybook init

* fix: format error

* chore: code refactor

* chore: code refactor

* fix: format error

* feat: add propel seperator component

* 🔒 chore: updated lock file

* ✏️ fix: typo in separator filename and some linting issues

* ♻️ refactor: replace clsx with cn utility in Separator component for class name management

* 🐛 fix: re-added twMerge

* 🧹 cleanup: remove unnecessary blank line in Separator component

---------

Co-authored-by: Anmol Singh Bhatia <anmolsinghbhatia@plane.so>
2025-09-01 19:40:01 +05:30
sriramveeraghanta 34181fba80 chore: version bump 2025-09-01 19:38:21 +05:30
Vipin Chaudhary 0358e9b965 [WIKI-589] fix: update project page creation flow (#7685)
* fix : update project page creation flow

* chore: update translations

* chore: remove unused changes
2025-09-01 16:43:07 +05:30
Vipin Chaudhary ee471c772a [WIKI-622] fix: update space work item description styles #7686 2025-09-01 15:13:52 +05:30
Nikhil e2c0d0f23c [WEB-4813] fix: ensure all identifiers in log transformations are converted to strings (#7682)
* fix: ensure all identifiers in log transformations are converted to strings

* Update apps/api/plane/bgtasks/cleanup_task.py

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-09-01 14:12:34 +05:30
Anmol Singh Bhatia 3ecebc02ae [WEB-4811] chore: plane logo asset updated #7683 2025-09-01 13:52:05 +05:30
Anmol Singh Bhatia 16d531cc7a [WEB-4808] fix: joinUrlPath utility fn #7678 2025-09-01 13:51:26 +05:30
dependabot[bot] ab283c7c78 chore(deps): bump the npm_and_yarn group across 4 directories with 1 update (#7681)
Bumps the npm_and_yarn group with 1 update in the / directory: [next](https://github.com/vercel/next.js).
Bumps the npm_and_yarn group with 1 update in the /apps/admin directory: [next](https://github.com/vercel/next.js).
Bumps the npm_and_yarn group with 1 update in the /apps/space directory: [next](https://github.com/vercel/next.js).
Bumps the npm_and_yarn group with 1 update in the /apps/web directory: [next](https://github.com/vercel/next.js).


Updates `next` from 14.2.30 to 14.2.32
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.30...v14.2.32)

Updates `next` from 14.2.30 to 14.2.32
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.30...v14.2.32)

Updates `next` from 14.2.30 to 14.2.32
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.30...v14.2.32)

Updates `next` from 14.2.30 to 14.2.32
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v14.2.30...v14.2.32)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 14.2.32
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: next
  dependency-version: 14.2.32
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: next
  dependency-version: 14.2.32
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: next
  dependency-version: 14.2.32
  dependency-type: direct:production
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-09-01 13:05:18 +05:30
Anmol Singh Bhatia 7c1bbf4a6f [WEB-4807] fix: workspace menu item highlight #7677 2025-08-29 20:23:31 +05:30
sriram veeraghanta 4293892178 [WEB-4803] fix: eslint errors in code editors (#7675)
* fix: eslint errors in code editors

* fix: removed workspaces from package.json
2025-08-29 19:44:46 +05:30
Bavisetti Narayan ebd517bb7d [WIKI-619] chore: added sort order migration for page model #7673 2025-08-29 19:44:29 +05:30
Henit Chobisa 4042af9f32 feat: added support for expanding updated_by in work item (#7667)
* feat: added support for expanding `updated_by` and `type` in work item

* fix: moved type to dictionary for expansion

* fix: refactored unnecessary fields
2025-08-29 16:41:54 +05:30
Surya Prashanth 258d24bf06 [SILO-454] chore: refactor decorator, logger packages (#7618)
* [SILO-454] chore: refactor decorator, logger packages

- add registerControllers function abstracting both rest, ws controllers
- update logger to a simple json based logger

* fix: logger instance and middleware

* fix: type and module resolutions

* fix: lodash type package update

* fix: bypass lint errors in decorators

* chore: format changes

---------

Co-authored-by: sriramveeraghanta <veeraghanta.sriram@gmail.com>
2025-08-29 14:29:16 +05:30
Nikhil 489a6e1e94 [WEB-4796] fix: update MongoDB collection check to use 'is not None' for better clarity #7671 2025-08-29 14:25:46 +05:30
sriram veeraghanta 4f349807be chore: docker image builds by removing node version in workspace.yml file (#7672)
* fix: docker image builts by removing nodeversion in workspace.yml file

* chore: remve .nvmrc file
2025-08-29 14:25:07 +05:30
Vipin Chaudhary 7a43137620 [WIKI-556] fix : invert tracking logic #7668 2025-08-28 20:37:20 +05:30
Bavisetti Narayan e144ce8cf2 [WIKI-556] chore: disable tracking of page hover (#7650)
* chore: disable tracking of page hover

* chore: add track check for page feth

* chore: make track check mandatory

* chore: update track format

---------

Co-authored-by: VipinDevelops <vipinchaudhary1809@gmail.com>
2025-08-28 20:02:44 +05:30
Anmol Singh Bhatia ba7303b7af [WEB-4559] fix: workspace menu item active state (#7666)
* fix: highlight active workspace menu item correctly

* chore: code refactor
2025-08-28 20:01:28 +05:30
Prateek Shourya e0912ccefc [WEB-4040] fix: minor changes in base plan names (#7656) 2025-08-28 18:51:10 +05:30
Anmol Singh Bhatia aef465415b [WEB-4748] chore: propel utils (#7662)
* chore: tailwind merge added to propel

* chore: classname utils updated

* chore: utils import alias added

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor

* chore: code refactor
2025-08-28 18:39:27 +05:30
Vamsi Krishna 3b3bd3e54e [WEB-4722] fix: fixed draft state update #7663 2025-08-28 18:38:30 +05:30
Sangeetha f2fabff10a [WEB-4521] fix: attachments #7665 2025-08-28 18:36:50 +05:30
Nikhil ddeabeeeb1 [WEB-4720] fix: mongo connection class to initialize mongo db #7652 2025-08-28 13:57:16 +05:30
sriram veeraghanta 0e6fbaee3a [WEB-4790] fix: moved typescript parser to the base eslint config (#7658)
* fix: moved typescript parser to the base eslint config

* fix: eslint warning

* fix: type config setting

* fix: convert live eslint to cjs
2025-08-27 21:03:20 +05:30
Anmol Singh Bhatia cfe710d492 [WEB-4779] chore: app sidebar wrapper component for consistency and reusability (#7655)
* chore: app sidebar ui enhancements and code refactor

* chore: code refactor

* chore: code refactor
2025-08-27 20:46:43 +05:30
Aaron 4a3c172992 feat(repo): add diff checks in CI flows (#7657) 2025-08-27 16:31:23 +05:30
Vipin Chaudhary 5d1ad8a183 [WIKI-550] fix: emoji modal for touch device (#7651)
* fix: emoji modal for touch device

* refactor: editor from props

* fix : update is touch device plugin
2025-08-27 00:39:47 +05:30
Vipin Chaudhary f95a07d8c8 [WIKI-436] fix: check for code in alignment #7649 2025-08-27 00:39:26 +05:30
Bavisetti Narayan 0af75897f5 [WEB-4780] chore: changed the html validation (#7648)
* chore: changed the html validation

* chore: added requirements for nh3

* chore: removed the json validations
2025-08-27 00:38:25 +05:30
Vamsi Krishna 3602ff6930 [WEB-4781] fix: add peek view get to store #7654 2025-08-26 18:24:08 +05:30
Prateek Shourya 6d1275d58c [WEB-4761] fix: resolve circular import between IssueService and local db (#7653)
- Move local db import to dynamic import to break circular dependency
2025-08-26 16:48:32 +05:30
Vipin Chaudhary cf7b288f93 [WIKI-345] regression: update block menu options (#7647)
* chore: update flagged and disabled extension

* chore :build fix
2025-08-26 14:18:26 +05:30
744 changed files with 8497 additions and 4518 deletions
+21 -12
View File
@@ -35,6 +35,10 @@ on:
- preview
- canary
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
TARGET_BRANCH: ${{ github.ref_name }}
ARM64_BUILD: ${{ github.event.inputs.arm64 }}
@@ -268,15 +272,14 @@ jobs:
if: ${{ needs.branch_build_setup.outputs.aio_build == 'true' }}
name: Build-Push AIO Docker Image
runs-on: ubuntu-22.04
needs: [
branch_build_setup,
branch_build_push_admin,
branch_build_push_web,
branch_build_push_space,
branch_build_push_live,
branch_build_push_api,
branch_build_push_proxy
]
needs:
- branch_build_setup
- branch_build_push_admin
- branch_build_push_web
- branch_build_push_space
- branch_build_push_live
- branch_build_push_api
- branch_build_push_proxy
steps:
- name: Checkout Files
uses: actions/checkout@v4
@@ -285,7 +288,7 @@ jobs:
id: prepare_aio_assets
run: |
cd deployments/aio/community
if [ "${{ needs.branch_build_setup.outputs.build_type }}" == "Release" ]; then
aio_version=${{ needs.branch_build_setup.outputs.release_version }}
else
@@ -324,7 +327,14 @@ jobs:
upload_build_assets:
name: Upload Build Assets
runs-on: ubuntu-22.04
needs: [branch_build_setup, branch_build_push_admin, branch_build_push_web, branch_build_push_space, branch_build_push_live, branch_build_push_api, branch_build_push_proxy]
needs:
- branch_build_setup
- branch_build_push_admin
- branch_build_push_web
- branch_build_push_space
- branch_build_push_live
- branch_build_push_api
- branch_build_push_proxy
steps:
- name: Checkout Files
uses: actions/checkout@v4
@@ -397,4 +407,3 @@ jobs:
${{ github.workspace }}/deployments/cli/community/docker-compose.yml
${{ github.workspace }}/deployments/cli/community/variables.env
${{ github.workspace }}/deployments/swarm/community/swarm.sh
-2
View File
@@ -17,8 +17,6 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
- name: Get PR Branch version
run: echo "PR_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
@@ -3,11 +3,21 @@ name: Build and lint API
on:
workflow_dispatch:
pull_request:
branches: ["preview"]
types: ["opened", "synchronize", "ready_for_review", "review_requested", "reopened"]
branches:
- "preview"
types:
- "opened"
- "synchronize"
- "ready_for_review"
- "review_requested"
- "reopened"
paths:
- "apps/api/**"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint-api:
name: Lint API
@@ -3,21 +3,18 @@ name: Build and lint web apps
on:
workflow_dispatch:
pull_request:
branches: ["preview"]
branches:
- "preview"
types:
[
"opened",
"synchronize",
"ready_for_review",
"review_requested",
"reopened",
]
paths:
- "**.tsx?"
- "**.jsx?"
- "**.css"
- "**.json"
- "!apps/api/**"
- "opened"
- "synchronize"
- "ready_for_review"
- "review_requested"
- "reopened"
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build-and-lint:
@@ -27,16 +24,18 @@ jobs:
if: |
github.event.pull_request.draft == false &&
github.event.pull_request.requested_reviewers != null
env:
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
TURBO_SCM_HEAD: ${{ github.sha }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 2
fetch-depth: 50
filter: blob:none
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version-file: ".nvmrc"
- name: Enable Corepack and pnpm
run: corepack enable pnpm
@@ -44,11 +43,11 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Lint web apps
run: pnpm run check:lint
- name: Lint Affected
run: pnpm turbo run check:lint --affected
- name: Check format
run: pnpm run check:format
- name: Check Affected format
run: pnpm turbo run check:format --affected
- name: Build apps
run: pnpm run build
- name: Build Affected
run: pnpm turbo run build --affected
+1 -1
View File
@@ -14,7 +14,7 @@ strict-peer-dependencies=false
# Turbo occasionally performs postinstall tasks for optimal performance
# moved to pnpm-workspace.yaml: onlyBuiltDependencies (e.g., allow turbo)
public-hoist-pattern[]=eslint
public-hoist-pattern[]=*eslint*
public-hoist-pattern[]=prettier
public-hoist-pattern[]=typescript
-1
View File
@@ -1 +0,0 @@
lts/jod
+12
View File
@@ -0,0 +1,12 @@
.next/*
out/*
public/*
dist/*
node_modules/*
.turbo/*
.env*
.env
.env.local
.env.development
.env.production
.env.test
-1
View File
@@ -1,5 +1,4 @@
module.exports = {
root: true,
extends: ["@plane/eslint-config/next.js"],
parser: "@typescript-eslint/parser",
};
@@ -9,7 +9,7 @@ import { useInstance } from "@/hooks/store";
// components
import { InstanceEmailForm } from "./email-config-form";
const InstanceEmailPage = observer(() => {
const InstanceEmailPage: React.FC = observer(() => {
// store
const { fetchInstanceConfigurations, formattedConfig, disableEmail } = useInstance();
@@ -29,7 +29,7 @@ const InstanceEmailPage = observer(() => {
message: "Email feature has been disabled",
type: TOAST_TYPE.SUCCESS,
});
} catch (error) {
} catch (_error) {
setToast({
title: "Error disabling email",
message: "Failed to disable email feature. Please try again.",
@@ -7,7 +7,8 @@ import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
import { Transition } from "@headlessui/react";
// plane internal packages
import { WEB_BASE_URL } from "@plane/constants";
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
import { DiscordIcon, GithubIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { cn } from "@plane/utils";
// hooks
import { useTheme } from "@/hooks/store";
@@ -5,7 +5,8 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
// plane internal packages
import { Tooltip, WorkspaceIcon } from "@plane/ui";
import { WorkspaceIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { cn } from "@plane/utils";
// hooks
import { useTheme } from "@/hooks/store";
+1 -1
View File
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { PlaneLockup } from "@plane/ui";
import { PlaneLockup } from "@plane/propel/icons";
export const AuthHeader = () => (
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0">
@@ -25,9 +25,8 @@ export const EmailCodesConfiguration: React.FC<Props> = observer((props) => {
<ToggleSwitch
value={Boolean(parseInt(enableMagicLogin))}
onChange={() => {
Boolean(parseInt(enableMagicLogin)) === true
? updateConfig("ENABLE_MAGIC_LINK_LOGIN", "0")
: updateConfig("ENABLE_MAGIC_LINK_LOGIN", "1");
const newEnableMagicLogin = Boolean(parseInt(enableMagicLogin)) === true ? "0" : "1";
updateConfig("ENABLE_MAGIC_LINK_LOGIN", newEnableMagicLogin);
}}
size="sm"
disabled={disabled}
@@ -35,9 +35,8 @@ export const GithubConfiguration: React.FC<Props> = observer((props) => {
<ToggleSwitch
value={Boolean(parseInt(enableGithubConfig))}
onChange={() => {
Boolean(parseInt(enableGithubConfig)) === true
? updateConfig("IS_GITHUB_ENABLED", "0")
: updateConfig("IS_GITHUB_ENABLED", "1");
const newEnableGithubConfig = Boolean(parseInt(enableGithubConfig)) === true ? "0" : "1";
updateConfig("IS_GITHUB_ENABLED", newEnableGithubConfig);
}}
size="sm"
disabled={disabled}
@@ -35,9 +35,8 @@ export const GitlabConfiguration: React.FC<Props> = observer((props) => {
<ToggleSwitch
value={Boolean(parseInt(enableGitlabConfig))}
onChange={() => {
Boolean(parseInt(enableGitlabConfig)) === true
? updateConfig("IS_GITLAB_ENABLED", "0")
: updateConfig("IS_GITLAB_ENABLED", "1");
const newEnableGitlabConfig = Boolean(parseInt(enableGitlabConfig)) === true ? "0" : "1";
updateConfig("IS_GITLAB_ENABLED", newEnableGitlabConfig);
}}
size="sm"
disabled={disabled}
@@ -35,9 +35,8 @@ export const GoogleConfiguration: React.FC<Props> = observer((props) => {
<ToggleSwitch
value={Boolean(parseInt(enableGoogleConfig))}
onChange={() => {
Boolean(parseInt(enableGoogleConfig)) === true
? updateConfig("IS_GOOGLE_ENABLED", "0")
: updateConfig("IS_GOOGLE_ENABLED", "1");
const newEnableGoogleConfig = Boolean(parseInt(enableGoogleConfig)) === true ? "0" : "1";
updateConfig("IS_GOOGLE_ENABLED", newEnableGoogleConfig);
}}
size="sm"
disabled={disabled}
@@ -25,9 +25,8 @@ export const PasswordLoginConfiguration: React.FC<Props> = observer((props) => {
<ToggleSwitch
value={Boolean(parseInt(enableEmailPassword))}
onChange={() => {
Boolean(parseInt(enableEmailPassword)) === true
? updateConfig("ENABLE_EMAIL_PASSWORD", "0")
: updateConfig("ENABLE_EMAIL_PASSWORD", "1");
const newEnableEmailPassword = Boolean(parseInt(enableEmailPassword)) === true ? "0" : "1";
updateConfig("ENABLE_EMAIL_PASSWORD", newEnableEmailPassword);
}}
size="sm"
disabled={disabled}
@@ -1,7 +1,7 @@
"use client";
import Link from "next/link";
import { Tooltip } from "@plane/ui";
import { Tooltip } from "@plane/propel/tooltip";
type Props = {
label?: string;
@@ -2,7 +2,7 @@ import { observer } from "mobx-react";
import { ExternalLink } from "lucide-react";
// plane internal packages
import { WEB_BASE_URL } from "@plane/constants";
import { Tooltip } from "@plane/ui";
import { Tooltip } from "@plane/propel/tooltip";
import { getFileURL } from "@plane/utils";
// hooks
import { useWorkspace } from "@/hooks/store";
+1 -1
View File
@@ -209,7 +209,7 @@ export class InstanceStore implements IInstanceStore {
});
});
await this.instanceService.disableEmail();
} catch (error) {
} catch (_error) {
console.error("Error disabling the email");
this.instanceConfigurations = instanceConfigurations;
}
+16 -16
View File
@@ -1,7 +1,7 @@
{
"name": "admin",
"description": "Admin UI for Plane",
"version": "0.28.0",
"version": "1.0.0",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@@ -26,30 +26,30 @@
"@plane/ui": "workspace:*",
"@plane/utils": "workspace:*",
"autoprefixer": "10.4.14",
"axios": "1.11.0",
"lodash": "^4.17.21",
"lucide-react": "^0.469.0",
"mobx": "^6.12.0",
"mobx-react": "^9.1.1",
"next": "14.2.30",
"axios": "catalog:",
"lodash": "catalog:",
"lucide-react": "catalog:",
"mobx": "catalog:",
"mobx-react": "catalog:",
"next": "catalog:",
"next-themes": "^0.2.1",
"postcss": "^8.4.49",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "catalog:",
"react-dom": "catalog:",
"react-hook-form": "7.51.5",
"sharp": "^0.33.5",
"swr": "^2.2.4",
"uuid": "^9.0.1"
"sharp": "catalog:",
"swr": "catalog:",
"uuid": "catalog:"
},
"devDependencies": {
"@plane/eslint-config": "workspace:*",
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@types/lodash": "^4.17.6",
"@types/lodash": "catalog:",
"@types/node": "18.16.1",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.2.18",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@types/uuid": "^9.0.8",
"typescript": "5.8.3"
"typescript": "catalog:"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "plane-api",
"version": "0.28.0",
"version": "1.0.0",
"license": "AGPL-3.0",
"private": true,
"description": "API server powering Plane's backend"
+1
View File
@@ -91,6 +91,7 @@ class BaseSerializer(serializers.ModelSerializer):
"project_lead": UserLiteSerializer,
"state": StateLiteSerializer,
"created_by": UserLiteSerializer,
"updated_by": UserLiteSerializer,
"issue": IssueSerializer,
"actor": UserLiteSerializer,
"owned_by": UserLiteSerializer,
+12 -9
View File
@@ -24,7 +24,6 @@ from plane.db.models import (
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)
@@ -89,20 +88,24 @@ class IssueSerializer(BaseSerializer):
raise serializers.ValidationError("Invalid HTML passed")
# Validate description content for security
if data.get("description"):
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if data.get("description_html"):
is_valid, error_msg = validate_html_content(data["description_html"])
is_valid, error_msg, sanitized_html = validate_html_content(
data["description_html"]
)
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
# Update the data with sanitized HTML if available
if sanitized_html is not None:
data["description_html"] = sanitized_html
if data.get("description_binary"):
is_valid, error_msg = validate_binary_data(data["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})
raise serializers.ValidationError(
{"description_binary": "Invalid binary data"}
)
# Validate assignees are from project
if data.get("assignees", []):
+7 -17
View File
@@ -12,7 +12,6 @@ from plane.db.models import (
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
)
from .base import BaseSerializer
@@ -200,27 +199,18 @@ class ProjectSerializer(BaseSerializer):
)
# Validate description content for security
if "description" in data and data["description"]:
# For Project, description might be text field, not JSON
if isinstance(data["description"], dict):
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if "description_text" in data and data["description_text"]:
is_valid, error_msg = validate_json_content(data["description_text"])
if not is_valid:
raise serializers.ValidationError({"description_text": error_msg})
if "description_html" in data and data["description_html"]:
if isinstance(data["description_html"], dict):
is_valid, error_msg = validate_json_content(data["description_html"])
else:
is_valid, error_msg = validate_html_content(
is_valid, error_msg, sanitized_html = validate_html_content(
str(data["description_html"])
)
# Update the data with sanitized HTML if available
if sanitized_html is not None:
data["description_html"] = sanitized_html
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
return data
+12 -9
View File
@@ -23,7 +23,6 @@ from plane.db.models import (
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)
from plane.app.permissions import ROLE
@@ -76,20 +75,24 @@ class DraftIssueCreateSerializer(BaseSerializer):
raise serializers.ValidationError("Start date cannot exceed target date")
# Validate description content for security
if "description" in attrs and attrs["description"]:
is_valid, error_msg = validate_json_content(attrs["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if "description_html" in attrs and attrs["description_html"]:
is_valid, error_msg = validate_html_content(attrs["description_html"])
is_valid, error_msg, sanitized_html = validate_html_content(
attrs["description_html"]
)
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
# Update the attrs with sanitized HTML if available
if sanitized_html is not None:
attrs["description_html"] = sanitized_html
if "description_binary" in attrs and attrs["description_binary"]:
is_valid, error_msg = validate_binary_data(attrs["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})
raise serializers.ValidationError(
{"description_binary": "Invalid binary data"}
)
# Validate assignees are from project
if attrs.get("assignee_ids", []):
+32 -12
View File
@@ -43,7 +43,6 @@ from plane.db.models import (
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)
@@ -128,20 +127,24 @@ class IssueCreateSerializer(BaseSerializer):
raise serializers.ValidationError("Start date cannot exceed target date")
# Validate description content for security
if "description" in attrs and attrs["description"]:
is_valid, error_msg = validate_json_content(attrs["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if "description_html" in attrs and attrs["description_html"]:
is_valid, error_msg = validate_html_content(attrs["description_html"])
is_valid, error_msg, sanitized_html = validate_html_content(
attrs["description_html"]
)
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
# Update the attrs with sanitized HTML if available
if sanitized_html is not None:
attrs["description_html"] = sanitized_html
if "description_binary" in attrs and attrs["description_binary"]:
is_valid, error_msg = validate_binary_data(attrs["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})
raise serializers.ValidationError(
{"description_binary": "Invalid binary data"}
)
# Validate assignees are from project
if attrs.get("assignee_ids", []):
@@ -664,16 +667,33 @@ class IssueReactionSerializer(BaseSerializer):
class IssueReactionLiteSerializer(DynamicBaseSerializer):
display_name = serializers.CharField(source="actor.display_name", read_only=True)
class Meta:
model = IssueReaction
fields = ["id", "actor", "issue", "reaction"]
fields = ["id", "actor", "issue", "reaction", "display_name"]
class CommentReactionSerializer(BaseSerializer):
display_name = serializers.CharField(source="actor.display_name", read_only=True)
class Meta:
model = CommentReaction
fields = "__all__"
read_only_fields = ["workspace", "project", "comment", "actor", "deleted_at"]
fields = [
"id",
"actor",
"comment",
"reaction",
"display_name",
"deleted_at",
"workspace",
"project",
"created_at",
"updated_at",
"created_by",
"updated_by",
]
read_only_fields = ["workspace", "project", "comment", "actor", "deleted_at", "created_by", "updated_by"]
class IssueVoteSerializer(BaseSerializer):
+3 -14
View File
@@ -7,7 +7,6 @@ from .base import BaseSerializer
from plane.utils.content_validator import (
validate_binary_data,
validate_html_content,
validate_json_content,
)
from plane.db.models import (
Page,
@@ -229,23 +228,13 @@ class PageBinaryUpdateSerializer(serializers.Serializer):
return value
# Use the validation function from utils
is_valid, error_message = validate_html_content(value)
is_valid, error_message, sanitized_html = validate_html_content(value)
if not is_valid:
raise serializers.ValidationError(error_message)
return value
# Return sanitized HTML if available, otherwise return original
return sanitized_html if sanitized_html is not None else value
def validate_description(self, value):
"""Validate the JSON description"""
if not value:
return value
# Use the validation function from utils
is_valid, error_message = validate_json_content(value)
if not is_valid:
raise serializers.ValidationError(error_message)
return value
def update(self, instance, validated_data):
"""Update the page instance with validated data"""
+10 -20
View File
@@ -15,7 +15,6 @@ from plane.db.models import (
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)
@@ -65,27 +64,18 @@ class ProjectSerializer(BaseSerializer):
def validate(self, data):
# Validate description content for security
if "description" in data and data["description"]:
# For Project, description might be text field, not JSON
if isinstance(data["description"], dict):
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if "description_text" in data and data["description_text"]:
is_valid, error_msg = validate_json_content(data["description_text"])
if not is_valid:
raise serializers.ValidationError({"description_text": error_msg})
if "description_html" in data and data["description_html"]:
if isinstance(data["description_html"], dict):
is_valid, error_msg = validate_json_content(data["description_html"])
else:
is_valid, error_msg = validate_html_content(
str(data["description_html"])
)
is_valid, error_msg, sanitized_html = validate_html_content(
str(data["description_html"])
)
# Update the data with sanitized HTML if available
if sanitized_html is not None:
data["description_html"] = sanitized_html
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
return data
+12 -9
View File
@@ -26,7 +26,6 @@ from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
from plane.utils.url import contains_url
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)
@@ -319,20 +318,24 @@ class StickySerializer(BaseSerializer):
def validate(self, data):
# Validate description content for security
if "description" in data and data["description"]:
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if "description_html" in data and data["description_html"]:
is_valid, error_msg = validate_html_content(data["description_html"])
is_valid, error_msg, sanitized_html = validate_html_content(
data["description_html"]
)
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
# Update the data with sanitized HTML if available
if sanitized_html is not None:
data["description_html"] = sanitized_html
if "description_binary" in data and data["description_binary"]:
is_valid, error_msg = validate_binary_data(data["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})
raise serializers.ValidationError(
{"description_binary": "Invalid binary data"}
)
return data
+10 -2
View File
@@ -441,7 +441,11 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
# Get the presigned URL
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
signed_url = storage.generate_presigned_url(object_name=asset.asset.name)
signed_url = storage.generate_presigned_url(
object_name=asset.asset.name,
disposition="attachment",
filename=asset.attributes.get("name"),
)
# Redirect to the signed URL
return HttpResponseRedirect(signed_url)
@@ -641,7 +645,11 @@ class ProjectAssetEndpoint(BaseAPIView):
# Get the presigned URL
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
signed_url = storage.generate_presigned_url(object_name=asset.asset.name)
signed_url = storage.generate_presigned_url(
object_name=asset.asset.name,
disposition="attachment",
filename=asset.attributes.get("name"),
)
# Redirect to the signed URL
return HttpResponseRedirect(signed_url)
+9 -7
View File
@@ -198,6 +198,7 @@ class PageViewSet(BaseViewSet):
def retrieve(self, request, slug, project_id, pk=None):
page = self.get_queryset().filter(pk=pk).first()
project = Project.objects.get(pk=project_id)
track_visit = request.query_params.get("track_visit", "true").lower() == "true"
"""
if the role is guest and guest_view_all_features is false and owned by is not
@@ -230,13 +231,14 @@ class PageViewSet(BaseViewSet):
).values_list("entity_identifier", flat=True)
data = PageDetailSerializer(page).data
data["issue_ids"] = issue_ids
recent_visited_task.delay(
slug=slug,
entity_name="page",
entity_identifier=pk,
user_id=request.user.id,
project_id=project_id,
)
if track_visit:
recent_visited_task.delay(
slug=slug,
entity_name="page",
entity_identifier=pk,
user_id=request.user.id,
project_id=project_id,
)
return Response(data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN], model=Page, creator=True)
+3 -1
View File
@@ -172,12 +172,14 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
{"error": "Issue not found"}, status=status.HTTP_404_NOT_FOUND
)
project_id = request.data.get("project_id", issue.project_id)
serializer = DraftIssueCreateSerializer(
issue,
data=request.data,
partial=True,
context={
"project_id": request.data.get("project_id", None),
"project_id": project_id,
"cycle_id": request.data.get("cycle_id", "not_provided"),
},
)
@@ -7,7 +7,6 @@ from plane.app.serializers import StateSerializer
from plane.app.views.base import BaseAPIView
from plane.db.models import State
from plane.app.permissions import WorkspaceEntityPermission
from plane.utils.cache import cache_response
from collections import defaultdict
@@ -15,7 +14,6 @@ class WorkspaceStatesEndpoint(BaseAPIView):
permission_classes = [WorkspaceEntityPermission]
use_read_replica = True
@cache_response(60 * 60 * 2)
def get(self, request, slug):
states = State.objects.filter(
workspace__slug=slug,
+24 -24
View File
@@ -67,7 +67,7 @@ def flush_to_mongo_and_delete(
mongo_archival_failed = False
# Try to insert into MongoDB if available
if mongo_collection and mongo_available:
if mongo_collection is not None and mongo_available:
try:
mongo_collection.bulk_write([InsertOne(doc) for doc in buffer])
except BulkWriteError as bwe:
@@ -166,9 +166,9 @@ def process_cleanup_task(
def transform_api_log(record: Dict) -> Dict:
"""Transform API activity log record."""
return {
"id": record["id"],
"id": str(record["id"]),
"created_at": str(record["created_at"]) if record.get("created_at") else None,
"token_identifier": record["token_identifier"],
"token_identifier": str(record["token_identifier"]),
"path": record["path"],
"method": record["method"],
"query_params": record.get("query_params"),
@@ -178,18 +178,18 @@ def transform_api_log(record: Dict) -> Dict:
"response_body": record["response_body"],
"ip_address": record["ip_address"],
"user_agent": record["user_agent"],
"created_by_id": record["created_by_id"],
"created_by_id": str(record["created_by_id"]),
}
def transform_email_log(record: Dict) -> Dict:
"""Transform email notification log record."""
return {
"id": record["id"],
"id": str(record["id"]),
"created_at": str(record["created_at"]) if record.get("created_at") else None,
"receiver_id": record["receiver_id"],
"triggered_by_id": record["triggered_by_id"],
"entity_identifier": record["entity_identifier"],
"receiver_id": str(record["receiver_id"]),
"triggered_by_id": str(record["triggered_by_id"]),
"entity_identifier": str(record["entity_identifier"]),
"entity_name": record["entity_name"],
"data": record["data"],
"processed_at": (
@@ -197,27 +197,27 @@ def transform_email_log(record: Dict) -> Dict:
),
"sent_at": str(record["sent_at"]) if record.get("sent_at") else None,
"entity": record["entity"],
"old_value": record["old_value"],
"new_value": record["new_value"],
"created_by_id": record["created_by_id"],
"old_value": str(record["old_value"]),
"new_value": str(record["new_value"]),
"created_by_id": str(record["created_by_id"]),
}
def transform_page_version(record: Dict) -> Dict:
"""Transform page version record."""
return {
"id": record["id"],
"id": str(record["id"]),
"created_at": str(record["created_at"]) if record.get("created_at") else None,
"page_id": record["page_id"],
"workspace_id": record["workspace_id"],
"owned_by_id": record["owned_by_id"],
"page_id": str(record["page_id"]),
"workspace_id": str(record["workspace_id"]),
"owned_by_id": str(record["owned_by_id"]),
"description_html": record["description_html"],
"description_binary": record["description_binary"],
"description_stripped": record["description_stripped"],
"description_json": record["description_json"],
"sub_pages_data": record["sub_pages_data"],
"created_by_id": record["created_by_id"],
"updated_by_id": record["updated_by_id"],
"created_by_id": str(record["created_by_id"]),
"updated_by_id": str(record["updated_by_id"]),
"deleted_at": str(record["deleted_at"]) if record.get("deleted_at") else None,
"last_saved_at": (
str(record["last_saved_at"]) if record.get("last_saved_at") else None
@@ -228,14 +228,14 @@ def transform_page_version(record: Dict) -> Dict:
def transform_issue_description_version(record: Dict) -> Dict:
"""Transform issue description version record."""
return {
"id": record["id"],
"id": str(record["id"]),
"created_at": str(record["created_at"]) if record.get("created_at") else None,
"issue_id": record["issue_id"],
"workspace_id": record["workspace_id"],
"project_id": record["project_id"],
"created_by_id": record["created_by_id"],
"updated_by_id": record["updated_by_id"],
"owned_by_id": record["owned_by_id"],
"issue_id": str(record["issue_id"]),
"workspace_id": str(record["workspace_id"]),
"project_id": str(record["project_id"]),
"created_by_id": str(record["created_by_id"]),
"updated_by_id": str(record["updated_by_id"]),
"owned_by_id": str(record["owned_by_id"]),
"last_saved_at": (
str(record["last_saved_at"]) if record.get("last_saved_at") else None
),
@@ -30,6 +30,8 @@ def page_version(page_id, existing_instance, user_id):
description_binary=page.description_binary,
owned_by_id=user_id,
last_saved_at=page.updated_at,
description_json=page.description,
description_stripped=page.description_stripped,
)
# If page versions are greater than 20 delete the oldest one
@@ -0,0 +1,30 @@
# Generated by Django 4.2.22 on 2025-08-29 11:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("db", "0101_description_descriptionversion"),
]
operations = [
migrations.AddField(
model_name="page",
name="sort_order",
field=models.FloatField(default=65535),
),
migrations.AddField(
model_name="pagelog",
name="entity_type",
field=models.CharField(
blank=True, max_length=30, null=True, verbose_name="Entity Type"
),
),
migrations.AlterField(
model_name="pagelog",
name="entity_identifier",
field=models.UUIDField(blank=True, null=True),
),
]
@@ -0,0 +1,75 @@
# Generated by Django 4.2.22 on 2025-09-01 14:33
from django.db import migrations, models
from django.contrib.postgres.operations import AddIndexConcurrently
class Migration(migrations.Migration):
atomic = False
dependencies = [
('db', '0102_page_sort_order_pagelog_entity_type_and_more'),
]
operations = [
AddIndexConcurrently(
model_name='fileasset',
index=models.Index(fields=['entity_type'], name='asset_entity_type_idx'),
),
AddIndexConcurrently(
model_name='fileasset',
index=models.Index(fields=['entity_identifier'], name='asset_entity_identifier_idx'),
),
AddIndexConcurrently(
model_name='fileasset',
index=models.Index(fields=['entity_type', 'entity_identifier'], name='asset_entity_idx'),
),
AddIndexConcurrently(
model_name='notification',
index=models.Index(fields=['entity_identifier'], name='notif_entity_identifier_idx'),
),
AddIndexConcurrently(
model_name='notification',
index=models.Index(fields=['entity_name'], name='notif_entity_name_idx'),
),
AddIndexConcurrently(
model_name='notification',
index=models.Index(fields=['read_at'], name='notif_read_at_idx'),
),
AddIndexConcurrently(
model_name='notification',
index=models.Index(fields=['receiver', 'read_at'], name='notif_entity_idx'),
),
AddIndexConcurrently(
model_name='pagelog',
index=models.Index(fields=['entity_type'], name='pagelog_entity_type_idx'),
),
AddIndexConcurrently(
model_name='pagelog',
index=models.Index(fields=['entity_identifier'], name='pagelog_entity_id_idx'),
),
AddIndexConcurrently(
model_name='pagelog',
index=models.Index(fields=['entity_name'], name='pagelog_entity_name_idx'),
),
AddIndexConcurrently(
model_name='pagelog',
index=models.Index(fields=['entity_type', 'entity_identifier'], name='pagelog_type_id_idx'),
),
AddIndexConcurrently(
model_name='pagelog',
index=models.Index(fields=['entity_name', 'entity_identifier'], name='pagelog_name_id_idx'),
),
AddIndexConcurrently(
model_name='userfavorite',
index=models.Index(fields=['entity_type'], name='fav_entity_type_idx'),
),
AddIndexConcurrently(
model_name='userfavorite',
index=models.Index(fields=['entity_identifier'], name='fav_entity_identifier_idx'),
),
AddIndexConcurrently(
model_name='userfavorite',
index=models.Index(fields=['entity_type', 'entity_identifier'], name='fav_entity_idx'),
),
]
@@ -0,0 +1,43 @@
# Generated by Django 4.2.22 on 2025-09-03 05:18
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('db', '0103_fileasset_asset_entity_type_idx_and_more'),
]
operations = [
migrations.AddField(
model_name='cycleuserproperties',
name='rich_filters',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='exporterhistory',
name='rich_filters',
field=models.JSONField(blank=True, default=dict, null=True),
),
migrations.AddField(
model_name='issueuserproperty',
name='rich_filters',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='issueview',
name='rich_filters',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='moduleuserproperties',
name='rich_filters',
field=models.JSONField(default=dict),
),
migrations.AddField(
model_name='workspaceuserproperties',
name='rich_filters',
field=models.JSONField(default=dict),
),
]
+9
View File
@@ -76,6 +76,15 @@ class FileAsset(BaseModel):
verbose_name_plural = "File Assets"
db_table = "file_assets"
ordering = ("-created_at",)
indexes = [
models.Index(fields=["entity_type"], name="asset_entity_type_idx"),
models.Index(
fields=["entity_identifier"], name="asset_entity_identifier_idx"
),
models.Index(
fields=["entity_type", "entity_identifier"], name="asset_entity_idx"
),
]
def __str__(self):
return str(self.asset)
+1
View File
@@ -139,6 +139,7 @@ class CycleUserProperties(ProjectBaseModel):
filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
rich_filters = models.JSONField(default=dict)
class Meta:
unique_together = ["cycle", "user", "deleted_at"]
+1
View File
@@ -56,6 +56,7 @@ class ExporterHistory(BaseModel):
related_name="workspace_exporters",
)
filters = models.JSONField(blank=True, null=True)
rich_filters = models.JSONField(default=dict, blank=True, null=True)
class Meta:
verbose_name = "Exporter"
+9
View File
@@ -41,6 +41,15 @@ class UserFavorite(WorkspaceBaseModel):
verbose_name_plural = "User Favorites"
db_table = "user_favorites"
ordering = ("-created_at",)
indexes = [
models.Index(fields=["entity_type"], name="fav_entity_type_idx"),
models.Index(
fields=["entity_identifier"], name="fav_entity_identifier_idx"
),
models.Index(
fields=["entity_type", "entity_identifier"], name="fav_entity_idx"
),
]
def save(self, *args, **kwargs):
if self._state.adding:
+1
View File
@@ -509,6 +509,7 @@ class IssueUserProperty(ProjectBaseModel):
filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
rich_filters = models.JSONField(default=dict)
class Meta:
verbose_name = "Issue User Property"
+1
View File
@@ -207,6 +207,7 @@ class ModuleUserProperties(ProjectBaseModel):
filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
rich_filters = models.JSONField(default=dict)
class Meta:
unique_together = ["module", "user", "deleted_at"]
+8
View File
@@ -39,6 +39,14 @@ class Notification(BaseModel):
verbose_name_plural = "Notifications"
db_table = "notifications"
ordering = ("-created_at",)
indexes = [
models.Index(
fields=["entity_identifier"], name="notif_entity_identifier_idx"
),
models.Index(fields=["entity_name"], name="notif_entity_name_idx"),
models.Index(fields=["read_at"], name="notif_read_at_idx"),
models.Index(fields=["receiver", "read_at"], name="notif_entity_idx"),
]
def __str__(self):
"""Return name of the notifications"""
+16 -1
View File
@@ -57,6 +57,7 @@ class Page(BaseModel):
)
moved_to_page = models.UUIDField(null=True, blank=True)
moved_to_project = models.UUIDField(null=True, blank=True)
sort_order = models.FloatField(default=65535)
external_id = models.CharField(max_length=255, null=True, blank=True)
external_source = models.CharField(max_length=255, null=True, blank=True)
@@ -98,8 +99,11 @@ class PageLog(BaseModel):
)
transaction = models.UUIDField(default=uuid.uuid4)
page = models.ForeignKey(Page, related_name="page_log", on_delete=models.CASCADE)
entity_identifier = models.UUIDField(null=True)
entity_identifier = models.UUIDField(null=True, blank=True)
entity_name = models.CharField(max_length=30, verbose_name="Transaction Type")
entity_type = models.CharField(
max_length=30, verbose_name="Entity Type", null=True, blank=True
)
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_page_log"
)
@@ -110,6 +114,17 @@ class PageLog(BaseModel):
verbose_name_plural = "Page Logs"
db_table = "page_logs"
ordering = ("-created_at",)
indexes = [
models.Index(fields=["entity_type"], name="pagelog_entity_type_idx"),
models.Index(fields=["entity_identifier"], name="pagelog_entity_id_idx"),
models.Index(fields=["entity_name"], name="pagelog_entity_name_idx"),
models.Index(
fields=["entity_type", "entity_identifier"], name="pagelog_type_id_idx"
),
models.Index(
fields=["entity_name", "entity_identifier"], name="pagelog_name_id_idx"
),
]
def __str__(self):
return f"{self.page.name} {self.entity_name}"
+1
View File
@@ -58,6 +58,7 @@ class IssueView(WorkspaceBaseModel):
filters = models.JSONField(default=dict)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
rich_filters = models.JSONField(default=dict)
access = models.PositiveSmallIntegerField(
default=1, choices=((0, "Private"), (1, "Public"))
)
+1
View File
@@ -332,6 +332,7 @@ class WorkspaceUserProperties(BaseModel):
filters = models.JSONField(default=get_default_filters)
display_filters = models.JSONField(default=get_default_display_filters)
display_properties = models.JSONField(default=get_default_display_properties)
rich_filters = models.JSONField(default=dict)
class Meta:
unique_together = ["workspace", "user", "deleted_at"]
+4
View File
@@ -465,3 +465,7 @@ if ENABLE_DRF_SPECTACULAR:
REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] = "drf_spectacular.openapi.AutoSchema"
INSTALLED_APPS.append("drf_spectacular")
from .openapi import SPECTACULAR_SETTINGS # noqa: F401
# MongoDB Settings
MONGO_DB_URL = os.environ.get("MONGO_DB_URL", False)
MONGO_DB_DATABASE = os.environ.get("MONGO_DB_DATABASE", False)
+3
View File
@@ -118,4 +118,7 @@ class MongoConnection:
Returns:
bool: True if MongoDB is configured and connected, False otherwise
"""
if cls._client is None:
cls._instance = cls()
return cls._client is not None and cls._db is not None
+10 -9
View File
@@ -30,7 +30,6 @@ from plane.db.models import (
)
from plane.utils.content_validator import (
validate_html_content,
validate_json_content,
validate_binary_data,
)
@@ -290,20 +289,22 @@ class IssueCreateSerializer(BaseSerializer):
raise serializers.ValidationError("Start date cannot exceed target date")
# Validate description content for security
if "description" in data and data["description"]:
is_valid, error_msg = validate_json_content(data["description"])
if not is_valid:
raise serializers.ValidationError({"description": error_msg})
if "description_html" in data and data["description_html"]:
is_valid, error_msg = validate_html_content(data["description_html"])
is_valid, error_msg, sanitized_html = validate_html_content(
data["description_html"]
)
if not is_valid:
raise serializers.ValidationError({"description_html": error_msg})
raise serializers.ValidationError(
{"error": "html content is not valid"}
)
# Update the data with sanitized HTML if available
if sanitized_html is not None:
data["description_html"] = sanitized_html
if "description_binary" in data and data["description_binary"]:
is_valid, error_msg = validate_binary_data(data["description_binary"])
if not is_valid:
raise serializers.ValidationError({"description_binary": error_msg})
raise serializers.ValidationError({"description_binary": "Invalid binary data"})
return data
+171 -267
View File
@@ -1,36 +1,14 @@
# Python imports
import base64
import json
import re
import nh3
from plane.utils.exception_logger import log_exception
from bs4 import BeautifulSoup
from collections import defaultdict
# Maximum allowed size for binary data (10MB)
MAX_SIZE = 10 * 1024 * 1024
# Maximum recursion depth to prevent stack overflow
MAX_RECURSION_DEPTH = 20
# Dangerous text patterns that could indicate XSS or script injection
DANGEROUS_TEXT_PATTERNS = [
r"<script[^>]*>.*?</script>",
r"javascript\s*:",
r"data\s*:\s*text/html",
r"eval\s*\(",
r"document\s*\.",
r"window\s*\.",
r"location\s*\.",
]
# Dangerous attribute patterns for HTML attributes
DANGEROUS_ATTR_PATTERNS = [
r"javascript\s*:",
r"data\s*:\s*text/html",
r"eval\s*\(",
r"alert\s*\(",
r"document\s*\.",
r"window\s*\.",
]
# Suspicious patterns for binary data content
SUSPICIOUS_BINARY_PATTERNS = [
"<html",
@@ -41,74 +19,11 @@ SUSPICIOUS_BINARY_PATTERNS = [
"<iframe",
]
# Malicious HTML patterns for content validation
MALICIOUS_HTML_PATTERNS = [
# Script tags with any content
r"<script[^>]*>",
r"</script>",
# JavaScript URLs in various attributes
r'(?:href|src|action)\s*=\s*["\']?\s*javascript:',
# Data URLs with text/html (potential XSS)
r'(?:href|src|action)\s*=\s*["\']?\s*data:text/html',
# Dangerous event handlers with JavaScript-like content
r'on(?:load|error|click|focus|blur|change|submit|reset|select|resize|scroll|unload|beforeunload|hashchange|popstate|storage|message|offline|online)\s*=\s*["\']?[^"\']*(?:javascript|alert|eval|document\.|window\.|location\.|history\.)[^"\']*["\']?',
# Object and embed tags that could load external content
r"<(?:object|embed)[^>]*(?:data|src)\s*=",
# Base tag that could change relative URL resolution
r"<base[^>]*href\s*=",
# Dangerous iframe sources
r'<iframe[^>]*src\s*=\s*["\']?(?:javascript:|data:text/html)',
# Meta refresh redirects
r'<meta[^>]*http-equiv\s*=\s*["\']?refresh["\']?',
# Link tags - simplified patterns
r'<link[^>]*rel\s*=\s*["\']?stylesheet["\']?',
r'<link[^>]*href\s*=\s*["\']?https?://',
r'<link[^>]*href\s*=\s*["\']?//',
r'<link[^>]*href\s*=\s*["\']?(?:data:|javascript:)',
# Style tags with external imports
r"<style[^>]*>.*?@import.*?(?:https?://|//)",
# Link tags with dangerous rel types
r'<link[^>]*rel\s*=\s*["\']?(?:import|preload|prefetch|dns-prefetch|preconnect)["\']?',
# Forms with action attributes
r"<form[^>]*action\s*=",
]
# Dangerous JavaScript patterns for event handlers
DANGEROUS_JS_PATTERNS = [
r"alert\s*\(",
r"eval\s*\(",
r"document\s*\.",
r"window\s*\.",
r"location\s*\.",
r"fetch\s*\(",
r"XMLHttpRequest",
r"innerHTML\s*=",
r"outerHTML\s*=",
r"document\.write",
r"script\s*>",
]
# HTML self-closing tags that don't need closing tags
SELF_CLOSING_TAGS = {
"img",
"br",
"hr",
"input",
"meta",
"link",
"area",
"base",
"col",
"embed",
"source",
"track",
"wbr",
}
def validate_binary_data(data):
"""
Validate that binary data appears to be valid document format and doesn't contain malicious content.
Validate that binary data appears to be a valid document format
and doesn't contain malicious content.
Args:
data (bytes or str): The binary data to validate, or base64-encoded string
@@ -149,191 +64,180 @@ def validate_binary_data(data):
return True, None
def validate_html_content(html_content):
# Combine custom components and editor-specific nodes into a single set of tags
CUSTOM_TAGS = {
# editor node/tag names
"mention-component",
"label",
"input",
"image-component",
}
ALLOWED_TAGS = nh3.ALLOWED_TAGS | CUSTOM_TAGS
# Merge nh3 defaults with all attributes used across our custom components
ATTRIBUTES = {
"*": {
"class",
"id",
"title",
"role",
"aria-label",
"aria-hidden",
"style",
"start",
"type",
# common editor data-* attributes seen in stored HTML
# (wildcards like data-* are NOT supported by nh3; we add known keys
# here and dynamically include all data-* seen in the input below)
"data-tight",
"data-node-type",
"data-type",
"data-checked",
"data-background-color",
"data-text-color",
"data-name",
# callout attributes
"data-icon-name",
"data-icon-color",
"data-background",
"data-emoji-unicode",
"data-emoji-url",
"data-logo-in-use",
"data-block-type",
},
"a": {"href", "target"},
# editor node/tag attributes
"image-component": {
"id",
"width",
"height",
"aspectRatio",
"aspectratio",
"src",
"alignment",
},
"img": {
"width",
"height",
"aspectRatio",
"aspectratio",
"alignment",
"src",
"alt",
"title",
},
"mention-component": {"id", "entity_identifier", "entity_name"},
"th": {
"colspan",
"rowspan",
"colwidth",
"background",
"hideContent",
"hidecontent",
"style",
},
"td": {
"colspan",
"rowspan",
"colwidth",
"background",
"textColor",
"textcolor",
"hideContent",
"hidecontent",
"style",
},
"tr": {"background", "textColor", "textcolor", "style"},
"pre": {"language"},
"code": {"language", "spellcheck"},
"input": {"type", "checked"},
}
SAFE_PROTOCOLS = {"http", "https", "mailto", "tel"}
def _compute_html_sanitization_diff(before_html: str, after_html: str):
"""
Validate that HTML content is safe and doesn't contain malicious patterns.
Compute a coarse diff between original and sanitized HTML.
Args:
html_content (str): The HTML content to validate
Returns a dict with:
- removed_tags: mapping[tag] -> removed_count
- removed_attributes: mapping[tag] -> sorted list of attribute names removed
"""
try:
Returns:
tuple: (is_valid: bool, error_message: str or None)
def collect(soup):
tag_counts = defaultdict(int)
attrs_by_tag = defaultdict(set)
for el in soup.find_all(True):
tag_name = (el.name or "").lower()
if not tag_name:
continue
tag_counts[tag_name] += 1
for attr_name in list(el.attrs.keys()):
if isinstance(attr_name, str) and attr_name:
attrs_by_tag[tag_name].add(attr_name.lower())
return tag_counts, attrs_by_tag
soup_before = BeautifulSoup(before_html or "", "html.parser")
soup_after = BeautifulSoup(after_html or "", "html.parser")
counts_before, attrs_before = collect(soup_before)
counts_after, attrs_after = collect(soup_after)
removed_tags = {}
for tag, cnt_before in counts_before.items():
cnt_after = counts_after.get(tag, 0)
if cnt_after < cnt_before:
removed = cnt_before - cnt_after
removed_tags[tag] = removed
removed_attributes = {}
for tag, before_set in attrs_before.items():
after_set = attrs_after.get(tag, set())
removed = before_set - after_set
if removed:
removed_attributes[tag] = sorted(list(removed))
return {"removed_tags": removed_tags, "removed_attributes": removed_attributes}
except Exception:
# Best-effort only; if diffing fails we don't block the request
return {"removed_tags": {}, "removed_attributes": {}}
def validate_html_content(html_content: str):
"""
Sanitize HTML content using nh3.
Returns a tuple: (is_valid, error_message, clean_html)
"""
if not html_content:
return True, None # Empty is OK
return True, None, None
# Size check - 10MB limit (consistent with binary validation)
if len(html_content.encode("utf-8")) > MAX_SIZE:
return False, "HTML content exceeds maximum size limit (10MB)"
# Check for specific malicious patterns (simplified and more reliable)
for pattern in MALICIOUS_HTML_PATTERNS:
if re.search(pattern, html_content, re.IGNORECASE | re.DOTALL):
return (
False,
f"HTML content contains potentially malicious patterns: {pattern}",
)
# Additional check for inline event handlers that contain suspicious content
# This is more permissive - only blocks if the event handler contains actual dangerous code
event_handler_pattern = r'on\w+\s*=\s*["\']([^"\']*)["\']'
event_matches = re.findall(event_handler_pattern, html_content, re.IGNORECASE)
for handler_content in event_matches:
for js_pattern in DANGEROUS_JS_PATTERNS:
if re.search(js_pattern, handler_content, re.IGNORECASE):
return (
False,
f"HTML content contains dangerous JavaScript in event handler: {handler_content[:100]}",
)
return True, None
def validate_json_content(json_content):
"""
Validate that JSON content is safe and doesn't contain malicious patterns.
Args:
json_content (dict): The JSON content to validate
Returns:
tuple: (is_valid: bool, error_message: str or None)
"""
if not json_content:
return True, None # Empty is OK
return False, "HTML content exceeds maximum size limit (10MB)", None
try:
# Size check - 10MB limit (consistent with other validations)
json_str = json.dumps(json_content)
if len(json_str.encode("utf-8")) > MAX_SIZE:
return False, "JSON content exceeds maximum size limit (10MB)"
clean_html = nh3.clean(
html_content,
tags=ALLOWED_TAGS,
attributes=ATTRIBUTES,
url_schemes=SAFE_PROTOCOLS,
)
# Report removals to logger (Sentry) if anything was stripped
diff = _compute_html_sanitization_diff(html_content, clean_html)
if diff.get("removed_tags") or diff.get("removed_attributes"):
try:
import json
# Basic structure validation for page description JSON
if isinstance(json_content, dict):
# Check for expected page description structure
# This is based on ProseMirror/Tiptap JSON structure
if "type" in json_content and json_content.get("type") == "doc":
# Valid document structure
if "content" in json_content and isinstance(
json_content["content"], list
):
# Recursively check content for suspicious patterns
is_valid, error_msg = _validate_json_content_array(
json_content["content"]
)
if not is_valid:
return False, error_msg
elif "type" not in json_content and "content" not in json_content:
# Allow other JSON structures but validate for suspicious content
is_valid, error_msg = _validate_json_content_recursive(json_content)
if not is_valid:
return False, error_msg
else:
return False, "JSON description must be a valid object"
except (TypeError, ValueError) as e:
return False, "Invalid JSON structure"
summary = json.dumps(diff)
except Exception:
summary = str(diff)
log_exception(
f"HTML sanitization removals: {summary}",
warning=True,
)
return True, None, clean_html
except Exception as e:
return False, "Failed to validate JSON content"
return True, None
def _validate_json_content_array(content, depth=0):
"""
Validate JSON content array for suspicious patterns.
Args:
content (list): Array of content nodes to validate
depth (int): Current recursion depth (default: 0)
Returns:
tuple: (is_valid: bool, error_message: str or None)
"""
# Check recursion depth to prevent stack overflow
if depth > MAX_RECURSION_DEPTH:
return False, f"Maximum recursion depth ({MAX_RECURSION_DEPTH}) exceeded"
if not isinstance(content, list):
return True, None
for node in content:
if isinstance(node, dict):
# Check text content for suspicious patterns (more targeted)
if node.get("type") == "text" and "text" in node:
text_content = node["text"]
for pattern in DANGEROUS_TEXT_PATTERNS:
if re.search(pattern, text_content, re.IGNORECASE):
return (
False,
"JSON content contains suspicious script patterns in text",
)
# Check attributes for suspicious content (more targeted)
if "attrs" in node and isinstance(node["attrs"], dict):
for attr_name, attr_value in node["attrs"].items():
if isinstance(attr_value, str):
# Only check specific attributes that could be dangerous
if attr_name.lower() in [
"href",
"src",
"action",
"onclick",
"onload",
"onerror",
]:
for pattern in DANGEROUS_ATTR_PATTERNS:
if re.search(pattern, attr_value, re.IGNORECASE):
return (
False,
f"JSON content contains dangerous pattern in {attr_name} attribute",
)
# Recursively check nested content
if "content" in node and isinstance(node["content"], list):
is_valid, error_msg = _validate_json_content_array(
node["content"], depth + 1
)
if not is_valid:
return False, error_msg
return True, None
def _validate_json_content_recursive(obj, depth=0):
"""
Recursively validate JSON object for suspicious content.
Args:
obj: JSON object (dict, list, or primitive) to validate
depth (int): Current recursion depth (default: 0)
Returns:
tuple: (is_valid: bool, error_message: str or None)
"""
# Check recursion depth to prevent stack overflow
if depth > MAX_RECURSION_DEPTH:
return False, f"Maximum recursion depth ({MAX_RECURSION_DEPTH}) exceeded"
if isinstance(obj, dict):
for key, value in obj.items():
if isinstance(value, str):
# Check for dangerous patterns using module constants
for pattern in DANGEROUS_TEXT_PATTERNS:
if re.search(pattern, value, re.IGNORECASE):
return (
False,
"JSON content contains suspicious script patterns",
)
elif isinstance(value, (dict, list)):
is_valid, error_msg = _validate_json_content_recursive(value, depth + 1)
if not is_valid:
return False, error_msg
elif isinstance(obj, list):
for item in obj:
is_valid, error_msg = _validate_json_content_recursive(item, depth + 1)
if not is_valid:
return False, error_msg
return True, None
log_exception(e)
return False, "Failed to sanitize HTML", None
+44 -22
View File
@@ -1,7 +1,7 @@
# Django imports
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db.models import Q, UUIDField, Value, QuerySet
from django.db.models import Q, UUIDField, Value, QuerySet, OuterRef, Subquery
from django.db.models.functions import Coalesce
# Module imports
@@ -14,6 +14,9 @@ from plane.db.models import (
ProjectMember,
State,
WorkspaceMember,
IssueAssignee,
ModuleIssue,
IssueLabel,
)
from typing import Optional, Dict, Tuple, Any, Union, List
@@ -39,33 +42,52 @@ def issue_queryset_grouper(
if group_key in GROUP_FILTER_MAPPER:
queryset = queryset.filter(GROUP_FILTER_MAPPER[group_key])
issue_assignee_subquery = Subquery(
IssueAssignee.objects.filter(
issue_id=OuterRef("pk"),
deleted_at__isnull=True,
)
.values("issue_id")
.annotate(arr=ArrayAgg("assignee_id", distinct=True))
.values("arr")
)
issue_module_subquery = Subquery(
ModuleIssue.objects.filter(
issue_id=OuterRef("pk"),
deleted_at__isnull=True,
module__archived_at__isnull=True,
)
.values("issue_id")
.annotate(arr=ArrayAgg("module_id", distinct=True))
.values("arr")
)
issue_label_subquery = Subquery(
IssueLabel.objects.filter(issue_id=OuterRef("pk"), deleted_at__isnull=True)
.values("issue_id")
.annotate(arr=ArrayAgg("label_id", distinct=True))
.values("arr")
)
annotations_map: Dict[str, Tuple[str, Q]] = {
"assignee_ids": (
"assignees__id",
~Q(assignees__id__isnull=True) & Q(issue_assignee__deleted_at__isnull=True),
"assignee_ids": Coalesce(
issue_assignee_subquery, Value([], output_field=ArrayField(UUIDField()))
),
"label_ids": (
"labels__id",
~Q(labels__id__isnull=True) & Q(label_issue__deleted_at__isnull=True),
"label_ids": Coalesce(
issue_label_subquery, Value([], output_field=ArrayField(UUIDField()))
),
"module_ids": (
"issue_module__module_id",
(
~Q(issue_module__module_id__isnull=True)
& Q(issue_module__module__archived_at__isnull=True)
& Q(issue_module__deleted_at__isnull=True)
),
"module_ids": Coalesce(
issue_module_subquery, Value([], output_field=ArrayField(UUIDField()))
),
}
default_annotations: Dict[str, Any] = {
key: Coalesce(
ArrayAgg(field, distinct=True, filter=condition),
Value([], output_field=ArrayField(UUIDField())),
)
for key, (field, condition) in annotations_map.items()
if FIELD_MAPPER.get(key) != group_by or FIELD_MAPPER.get(key) != sub_group_by
}
default_annotations: Dict[str, Any] = {}
for key, expression in annotations_map.items():
if FIELD_MAPPER.get(key) in {group_by, sub_group_by}:
continue
default_annotations[key] = expression
return queryset.annotate(**default_annotations)
+3 -1
View File
@@ -1,7 +1,7 @@
# base requirements
# django
Django==4.2.22
Django==4.2.24
# rest framework
djangorestframework==3.15.2
# postgres
@@ -69,3 +69,5 @@ opentelemetry-instrumentation-django==0.49b1
opentelemetry-exporter-otlp==1.28.1
# OpenAPI Specification
drf-spectacular==0.28.0
# html sanitizer
nh3==0.2.18
+4
View File
@@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ["@plane/eslint-config/server.js"],
};
-5
View File
@@ -1,5 +0,0 @@
{
"root": true,
"extends": ["@plane/eslint-config/server.js"],
"parser": "@typescript-eslint/parser"
}
+8 -8
View File
@@ -1,14 +1,14 @@
{
"name": "live",
"version": "0.28.0",
"version": "1.0.0",
"license": "AGPL-3.0",
"description": "A realtime collaborative server powers Plane's rich text editor",
"main": "./src/server.ts",
"private": true,
"type": "module",
"scripts": {
"dev": "tsup --watch --onSuccess 'node --env-file=.env dist/server.js'",
"build": "tsc --noEmit && tsup",
"build": "tsdown",
"dev": "tsdown --watch",
"start": "node --env-file=.env dist/server.js",
"check:lint": "eslint . --max-warnings 10",
"check:types": "tsc --noEmit",
@@ -28,7 +28,7 @@
"@plane/types": "workspace:*",
"@tiptap/core": "^2.22.3",
"@tiptap/html": "^2.22.3",
"axios": "1.11.0",
"axios": "catalog:",
"compression": "1.8.1",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
@@ -36,11 +36,11 @@
"express-ws": "^5.0.2",
"helmet": "^7.1.0",
"ioredis": "^5.4.1",
"lodash": "^4.17.21",
"lodash": "catalog:",
"morgan": "1.10.1",
"pino-http": "^10.3.0",
"pino-pretty": "^11.2.2",
"uuid": "^10.0.0",
"uuid": "catalog:",
"y-prosemirror": "^1.2.15",
"y-protocols": "^1.0.6",
"yjs": "^13.6.20"
@@ -58,8 +58,8 @@
"concurrently": "^9.0.1",
"nodemon": "^3.1.7",
"ts-node": "^10.9.2",
"tsup": "8.4.0",
"typescript": "5.8.3",
"tsdown": "catalog:",
"typescript": "catalog:",
"ws": "^8.18.3"
}
}
+4 -7
View File
@@ -1,20 +1,17 @@
// Third-party libraries
import { Redis } from "ioredis";
// Hocuspocus extensions and core
import { Database } from "@hocuspocus/extension-database";
import { Extension } from "@hocuspocus/server";
import { Logger } from "@hocuspocus/extension-logger";
import { Redis as HocusPocusRedis } from "@hocuspocus/extension-redis";
import { Extension } from "@hocuspocus/server";
import { Redis } from "ioredis";
// core helpers and utilities
import { manualLogger } from "@/core/helpers/logger.js";
import { getRedisUrl } from "@/core/lib/utils/redis-url.js";
// core libraries
import { fetchPageDescriptionBinary, updatePageDescription } from "@/core/lib/page.js";
import { getRedisUrl } from "@/core/lib/utils/redis-url.js";
import { type HocusPocusServerContext, type TDocumentTypes } from "@/core/types/common.js";
// plane live libraries
import { fetchDocument } from "@/plane-live/lib/fetch-document.js";
import { updateDocument } from "@/plane-live/lib/update-document.js";
// types
import { type HocusPocusServerContext, type TDocumentTypes } from "@/core/types/common.js";
export const getExtensions: () => Promise<Extension[]> = async () => {
const extensions: Extension[] = [
+5 -5
View File
@@ -1,12 +1,12 @@
import { Server } from "@hocuspocus/server";
import { v4 as uuidv4 } from "uuid";
// lib
import { handleAuthentication } from "@/core/lib/authentication.js";
// extensions
import { getExtensions } from "@/core/extensions/index.js";
import { DocumentCollaborativeEvents, TDocumentEventsServer } from "@plane/editor/lib";
// editor types
import { TUserDetails } from "@plane/editor";
import { DocumentCollaborativeEvents, TDocumentEventsServer } from "@plane/editor/lib";
// extensions
import { getExtensions } from "@/core/extensions/index.js";
// lib
import { handleAuthentication } from "@/core/lib/authentication.js";
// types
import { type HocusPocusServerContext } from "@/core/types/common.js";
+2 -2
View File
@@ -1,7 +1,7 @@
// services
import { UserService } from "@/core/services/user.service.js";
// core helpers
import { manualLogger } from "@/core/helpers/logger.js";
// services
import { UserService } from "@/core/services/user.service.js";
const userService = new UserService();
+2 -2
View File
@@ -1,13 +1,13 @@
import compression from "compression";
import cors from "cors";
import expressWs from "express-ws";
import express, { Request, Response } from "express";
import expressWs from "express-ws";
import helmet from "helmet";
// hocuspocus server
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
// helpers
import { convertHTMLDocumentToAllFormats } from "@/core/helpers/convert-document.js";
import { logger, manualLogger } from "@/core/helpers/logger.js";
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
// types
import { TConvertDocumentRequestBody } from "@/core/types/common.js";
+1 -1
View File
@@ -21,6 +21,6 @@
"emitDecoratorMetadata": true,
"sourceRoot": "/"
},
"include": ["src/**/*.ts", "tsup.config.ts"],
"include": ["src/**/*.ts", "tsdown.config.ts"],
"exclude": ["./dist", "./build", "./node_modules"]
}
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from "tsdown";
export default defineConfig({
entry: ["src/server.ts"],
outDir: "dist",
format: ["esm", "cjs"],
});
-15
View File
@@ -1,15 +0,0 @@
import { defineConfig } from "tsup";
export default defineConfig({
entry: ["src/server.ts"],
format: ["esm", "cjs"],
dts: true,
splitting: false,
sourcemap: true,
minify: false,
target: "node18",
outDir: "dist",
env: {
NODE_ENV: process.env.NODE_ENV || "development",
},
});
+12
View File
@@ -0,0 +1,12 @@
.next/*
out/*
public/*
dist/*
node_modules/*
.turbo/*
.env*
.env
.env.local
.env.development
.env.production
.env.test
-2
View File
@@ -1,6 +1,4 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/next.js"],
parser: "@typescript-eslint/parser",
};
@@ -13,6 +13,11 @@ export async function generateMetadata({ params }: Props) {
const { anchor } = params;
const DEFAULT_TITLE = "Plane";
const DEFAULT_DESCRIPTION = "Made with Plane, an AI-powered work management platform with publishing capabilities.";
// Validate anchor before using in request (only allow alphanumeric, -, _)
const ANCHOR_REGEX = /^[a-zA-Z0-9_-]+$/;
if (!ANCHOR_REGEX.test(anchor)) {
return { title: DEFAULT_TITLE, description: DEFAULT_DESCRIPTION };
}
try {
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/public/anchor/${anchor}/meta/`);
const data = await response.json();
@@ -172,7 +172,7 @@ export const AuthRoot: FC = observer(() => {
text: `${content} with GitHub`,
icon: (
<Image
src={resolvedTheme === "dark" ? GithubDarkLogo : GithubLightLogo}
src={resolvedTheme === "dark" ? GithubLightLogo : GithubDarkLogo}
height={18}
width={18}
alt="GitHub Logo"
@@ -2,7 +2,7 @@
import { observer } from "mobx-react";
import Image from "next/image";
import { PlaneLockup } from "@plane/ui";
import { PlaneLockup } from "@plane/propel/icons";
// components
import { PoweredBy } from "@/components/common/powered-by";
import { UserAvatar } from "@/components/issues/navbar/user-avatar";
@@ -3,7 +3,7 @@
import { FC } from "react";
import { WEBSITE_URL } from "@plane/constants";
// assets
import { PlaneLogo } from "@plane/ui";
import { PlaneLogo } from "@plane/propel/icons";
type TPoweredBy = {
disabled?: boolean;
@@ -2,17 +2,16 @@ import React from "react";
// plane imports
import { type EditorRefApi, type ILiteTextEditorProps, LiteTextEditorWithRef, type TFileHandler } from "@plane/editor";
import type { MakeOptional } from "@plane/types";
import { cn } from "@plane/utils";
import { cn, isCommentEmpty } from "@plane/utils";
// helpers
import { getEditorFileHandlers } from "@/helpers/editor.helper";
import { isCommentEmpty } from "@/helpers/string.helper";
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
// local imports
import { EditorMentionsRoot } from "./embeds/mentions";
import { IssueCommentToolbar } from "./toolbar";
type LiteTextEditorWrapperProps = MakeOptional<
Omit<ILiteTextEditorProps, "fileHandler" | "mentionHandler">,
Omit<ILiteTextEditorProps, "fileHandler" | "mentionHandler" | "extendedEditorProps">,
"disabledExtensions" | "flaggedExtensions"
> & {
anchor: string;
@@ -63,6 +62,7 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
mentionHandler={{
renderComponent: (props) => <EditorMentionsRoot {...props} />,
}}
extendedEditorProps={{}}
{...rest}
// overriding the containerClassName to add relative class passed
containerClassName={cn(containerClassName, "relative")}
@@ -12,7 +12,7 @@ import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
import { EditorMentionsRoot } from "./embeds/mentions";
type RichTextEditorWrapperProps = MakeOptional<
Omit<IRichTextEditorProps, "editable" | "fileHandler" | "mentionHandler">,
Omit<IRichTextEditorProps, "editable" | "fileHandler" | "mentionHandler" | "extendedEditorProps">,
"disabledExtensions" | "flaggedExtensions"
> & {
anchor: string;
@@ -56,9 +56,10 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
workspaceId,
})}
flaggedExtensions={richTextEditorExtensions.flagged}
extendedEditorProps={{}}
{...rest}
containerClassName={containerClassName}
editorClassName="min-h-[100px] max-h-[200px] border-[0.5px] border-custom-border-300 rounded-md pl-3 py-2 overflow-hidden"
editorClassName="min-h-[100px] py-2 overflow-hidden"
displayConfig={{ fontSize: "large-font" }}
/>
);
@@ -3,7 +3,8 @@
import React, { useEffect, useState, useCallback } from "react";
// plane imports
import { TOOLBAR_ITEMS, type ToolbarMenuItem, type EditorRefApi } from "@plane/editor";
import { Button, Tooltip } from "@plane/ui";
import { Tooltip } from "@plane/propel/tooltip";
import { Button } from "@plane/ui";
import { cn } from "@plane/utils";
type Props = {
@@ -1,7 +1,7 @@
"use client";
import { X } from "lucide-react";
import { PriorityIcon, type TIssuePriorities } from "@plane/ui";
import { PriorityIcon, type TIssuePriorities } from "@plane/propel/icons";
type Props = {
handleRemove: (val: string) => void;
@@ -4,7 +4,7 @@ import { observer } from "mobx-react";
import { X } from "lucide-react";
// plane imports
import { EIconSize } from "@plane/constants";
import { StateGroupIcon } from "@plane/ui";
import { StateGroupIcon } from "@plane/propel/icons";
// hooks
import { useStates } from "@/hooks/store/use-state";
@@ -5,7 +5,7 @@ import { observer } from "mobx-react";
// plane imports
import { ISSUE_PRIORITY_FILTERS } from "@plane/constants";
import { useTranslation } from "@plane/i18n";
import { PriorityIcon } from "@plane/ui";
import { PriorityIcon } from "@plane/propel/icons";
// local imports
import { FilterHeader } from "./helpers/filter-header";
import { FilterOption } from "./helpers/filter-option";
@@ -4,7 +4,8 @@ import React, { useState } from "react";
import { observer } from "mobx-react";
// ui
import { EIconSize } from "@plane/constants";
import { Loader, StateGroupIcon } from "@plane/ui";
import { StateGroupIcon } from "@plane/propel/icons";
import { Loader } from "@plane/ui";
// hooks
import { useStates } from "@/hooks/store/use-state";
// local imports
@@ -5,9 +5,9 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, useSearchParams } from "next/navigation";
// plane types
import { Tooltip } from "@plane/propel/tooltip";
import { IIssueDisplayProperties } from "@plane/types";
// plane ui
import { Tooltip } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// components
@@ -5,9 +5,9 @@ import { observer } from "mobx-react";
import Link from "next/link";
import { useParams, useSearchParams } from "next/navigation";
// plane types
import { Tooltip } from "@plane/propel/tooltip";
import { IIssueDisplayProperties } from "@plane/types";
// plane ui
import { Tooltip } from "@plane/ui";
// plane utils
import { cn } from "@plane/utils";
// helpers
@@ -75,7 +75,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
onClick={handleIssuePeekOverview}
className="w-full truncate cursor-pointer text-sm text-custom-text-100"
>
<Tooltip tooltipContent={issue.name} position="top-left">
<Tooltip tooltipContent={issue.name} position="top-start">
<p className="truncate">{issue.name}</p>
</Tooltip>
</Link>
@@ -3,8 +3,8 @@
import { observer } from "mobx-react";
import { Layers, Link, Paperclip } from "lucide-react";
// plane imports
import { Tooltip } from "@plane/propel/tooltip";
import type { IIssueDisplayProperties } from "@plane/types";
import { Tooltip } from "@plane/ui";
import { cn } from "@plane/utils";
// components
import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/with-display-properties-HOC";
@@ -2,7 +2,8 @@
import { observer } from "mobx-react";
// plane ui
import { ContrastIcon, Tooltip } from "@plane/ui";
import { ContrastIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
// plane utils
import { cn } from "@plane/utils";
//hooks
@@ -2,7 +2,7 @@
import { observer } from "mobx-react";
import { CalendarCheck2 } from "lucide-react";
import { Tooltip } from "@plane/ui";
import { Tooltip } from "@plane/propel/tooltip";
import { cn } from "@plane/utils";
// helpers
import { renderFormattedDate } from "@/helpers/date-time.helper";
@@ -3,7 +3,7 @@
import { observer } from "mobx-react";
import { Tags } from "lucide-react";
// plane imports
import { Tooltip } from "@plane/ui";
import { Tooltip } from "@plane/propel/tooltip";
// hooks
import { useLabel } from "@/hooks/store/use-label";
@@ -2,7 +2,8 @@
import { observer } from "mobx-react";
// plane ui
import { DiceIcon, Tooltip } from "@plane/ui";
import { DiceIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
// plane utils
import { cn } from "@plane/utils";
// hooks
@@ -3,8 +3,9 @@
import { SignalHigh } from "lucide-react";
import { useTranslation } from "@plane/i18n";
// types
import { PriorityIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
import { TIssuePriorities } from "@plane/types";
import { PriorityIcon, Tooltip } from "@plane/ui";
// constants
import { cn, getIssuePriorityFilters } from "@plane/utils";
@@ -2,7 +2,8 @@
import { observer } from "mobx-react";
// plane ui
import { StateGroupIcon, Tooltip } from "@plane/ui";
import { StateGroupIcon } from "@plane/propel/icons";
import { Tooltip } from "@plane/propel/tooltip";
// plane utils
import { cn } from "@plane/utils";
//hooks
@@ -4,6 +4,7 @@ import isNil from "lodash/isNil";
import { ContrastIcon } from "lucide-react";
// types
import { EIconSize, ISSUE_PRIORITIES } from "@plane/constants";
import { CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/propel/icons";
import {
GroupByColumnTypes,
IGroupByColumn,
@@ -12,7 +13,7 @@ import {
TGroupedIssues,
} from "@plane/types";
// ui
import { Avatar, CycleGroupIcon, DiceIcon, PriorityIcon, StateGroupIcon } from "@plane/ui";
import { Avatar } from "@plane/ui";
// components
// constants
// stores
@@ -7,7 +7,7 @@ import { useRouter, useSearchParams } from "next/navigation";
import { SITES_ISSUE_LAYOUTS } from "@plane/constants";
// plane i18n
import { useTranslation } from "@plane/i18n";
import { Tooltip } from "@plane/ui";
import { Tooltip } from "@plane/propel/tooltip";
// helpers
import { queryParamGenerator } from "@/helpers/query-param-generator";
// hooks
@@ -3,7 +3,7 @@
import React from "react";
import { observer } from "mobx-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { Tooltip } from "@plane/ui";
import { Tooltip } from "@plane/propel/tooltip";
// plane imports
import { cn } from "@plane/utils";
// ui
@@ -5,7 +5,8 @@ import { observer } from "mobx-react";
import { Link2, MoveRight } from "lucide-react";
import { Listbox, Transition } from "@headlessui/react";
// ui
import { CenterPanelIcon, FullScreenPanelIcon, setToast, SidePanelIcon, TOAST_TYPE } from "@plane/ui";
import { CenterPanelIcon, FullScreenPanelIcon, SidePanelIcon } from "@plane/propel/icons";
import { setToast, TOAST_TYPE } from "@plane/ui";
// helpers
import { copyTextToClipboard } from "@/helpers/string.helper";
// hooks
@@ -5,7 +5,8 @@ import { useParams } from "next/navigation";
import { CalendarCheck2, Signal } from "lucide-react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { DoubleCircleIcon, StateGroupIcon, TOAST_TYPE, setToast } from "@plane/ui";
import { DoubleCircleIcon, StateGroupIcon } from "@plane/propel/icons";
import { TOAST_TYPE, setToast } from "@plane/ui";
import { cn, getIssuePriorityFilters } from "@plane/utils";
// components
import { Icon } from "@/components/ui";
@@ -3,7 +3,7 @@
import { observer } from "mobx-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
// lib
import { Tooltip } from "@plane/ui";
import { Tooltip } from "@plane/propel/tooltip";
import { ReactionSelector } from "@/components/ui";
// helpers
import { groupReactions, renderEmoji } from "@/helpers/emoji.helper";
@@ -4,7 +4,7 @@ import { useState } from "react";
import { observer } from "mobx-react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
// plane imports
import { Tooltip } from "@plane/ui";
import { Tooltip } from "@plane/propel/tooltip";
import { cn } from "@plane/utils";
// helpers
import { queryParamGenerator } from "@/helpers/query-param-generator";
+1 -1
View File
@@ -2,7 +2,7 @@
import React from "react";
import Link from "next/link";
import { PlaneLockup } from "@plane/ui";
import { PlaneLockup } from "@plane/propel/icons";
export const AuthHeader = () => (
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0">
-18
View File
@@ -57,24 +57,6 @@ export const isEmptyHtmlString = (htmlString: string, allowedHTMLTags: string[]
return cleanText.trim() === "";
};
/**
* @description this function returns whether a comment is empty or not by checking for the following conditions-
* 1. If comment is undefined
* 2. If comment is an empty string
* 3. If comment is "<p></p>"
* @param {string | undefined} comment
* @returns {boolean}
*/
export const isCommentEmpty = (comment: string | undefined): boolean => {
// return true if comment is undefined
if (!comment) return true;
return (
comment?.trim() === "" ||
comment === "<p></p>" ||
isEmptyHtmlString(comment ?? "", ["img", "mention-component", "image-component", "embed-component"])
);
};
export const replaceUnderscoreIfSnakeCase = (str: string) => str.replace(/_/g, " ");
export const capitalizeFirstLetter = (str: string) => str.charAt(0).toUpperCase() + str.slice(1);
+17 -20
View File
@@ -1,6 +1,6 @@
{
"name": "space",
"version": "0.28.0",
"version": "1.0.0",
"private": true,
"license": "AGPL-3.0",
"scripts": {
@@ -15,8 +15,6 @@
"fix:format": "prettier --write \"**/*.{ts,tsx,md,json,css,scss}\""
},
"dependencies": {
"@blueprintjs/core": "^4.16.3",
"@blueprintjs/popover2": "^1.13.3",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@headlessui/react": "^1.7.13",
@@ -30,41 +28,40 @@
"@plane/ui": "workspace:*",
"@plane/utils": "workspace:*",
"@popperjs/core": "^2.11.8",
"axios": "1.11.0",
"axios": "catalog:",
"clsx": "^2.0.0",
"date-fns": "^4.1.0",
"dompurify": "^3.0.11",
"dotenv": "^16.3.1",
"lodash": "^4.17.21",
"lodash": "catalog:",
"lowlight": "^2.9.0",
"lucide-react": "^0.469.0",
"mobx": "^6.10.0",
"mobx-react": "^9.1.1",
"mobx-utils": "^6.0.8",
"next": "14.2.30",
"lucide-react": "catalog:",
"mobx": "catalog:",
"mobx-react": "catalog:",
"mobx-utils": "catalog:",
"next": "catalog:",
"next-themes": "^0.2.1",
"nprogress": "^0.2.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "catalog:",
"react-dom": "catalog:",
"react-dropzone": "^14.2.3",
"react-hook-form": "7.51.5",
"react-popper": "^2.3.0",
"sharp": "^0.33.5",
"swr": "^2.2.2",
"sharp": "catalog:",
"swr": "catalog:",
"tailwind-merge": "^2.0.0",
"uuid": "^9.0.0"
"uuid": "catalog:"
},
"devDependencies": {
"@plane/eslint-config": "workspace:*",
"@plane/tailwind-config": "workspace:*",
"@plane/typescript-config": "workspace:*",
"@types/lodash": "^4.17.6",
"@types/lodash": "catalog:",
"@types/node": "18.14.1",
"@types/nprogress": "^0.2.0",
"@types/react": "^18.3.11",
"@types/react-dom": "^18.2.18",
"@types/react": "catalog:",
"@types/react-dom": "catalog:",
"@types/uuid": "^9.0.1",
"@typescript-eslint/eslint-plugin": "^8.36.0",
"typescript": "5.8.3"
"typescript": "catalog:"
}
}
+1
View File
@@ -1,4 +1,5 @@
@import "@plane/propel/styles/fonts";
@import "@plane/editor/styles";
@tailwind base;
@tailwind components;
+10 -1
View File
@@ -1,4 +1,13 @@
.next/*
out/*
public/*
core/local-db/worker/wa-sqlite/src/*
core/local-db/worker/wa-sqlite/src/*
dist/*
node_modules/*
.turbo/*
.env*
.env
.env.local
.env.development
.env.production
.env.test
-2
View File
@@ -1,6 +1,4 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/next.js"],
parser: "@typescript-eslint/parser",
};

Some files were not shown because too many files have changed in this diff Show More