Compare commits

...
Author SHA1 Message Date
vamsikrishnamathala e6d04d3b0d feat: allowing cycles to accept back dates till created date 2025-05-08 14:19:24 +05:30
NarayanBavisetti 1cff830c3c chore: added for the create request 2025-05-08 12:54:30 +05:30
NarayanBavisetti 30b4ccbcd5 chore: passed created at of cycle 2025-05-08 12:50:24 +05:30
14ebaf0799 [WEB-3942] chore: intake url pattern (#7006)
* chore: intake url pattern updated

* chore: code refactor

* chore: removed unused components

---------

Co-authored-by: vamsikrishnamathala <matalav55@gmail.com>
2025-05-07 21:19:24 +05:30
SangeethaandGitHub 7cdb622663 [WEB-3930] chore: change source in-app to IN_APP #7008 2025-05-07 18:46:10 +05:30
JayashTripathyGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
855e4a3218 [WEB-4016] updated project and workitem form (#7019)
* updated project and workitem form

* added translation for other languages also

* Update packages/i18n/src/locales/zh-CN/translations.json

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-05-07 18:44:04 +05:30
Anmol Singh BhatiaandGitHub d456767492 [WEB-3955] chore: work item parent select modal params #7021 2025-05-07 18:41:28 +05:30
Bavisetti NarayanandGitHub 6faff1d556 [WEB-3877] fix: changed logic to calculate cycle duration (#7024)
* chore: cycle running days

* chore: removed the module filter
2025-05-07 18:40:37 +05:30
Aaryan KhandelwalandGitHub bc2936dcd3 [WEB-3906] fix: page table of content overlap with the page content #7018 2025-05-07 00:51:51 +05:30
Aaryan KhandelwalandGitHub d366ac1581 [WEB-2508] fix: page favorite item title mutation (#7020)
* fix: remove page favorite item title fallback value

* refactor: use nullish coalescing operator
2025-05-07 00:28:43 +05:30
NikhilandGitHub 0a01e0eb41 [WEB-4013] chore: correct live url #7014 2025-05-06 01:21:53 +05:30
NikhilandGitHub b4cc2d83fe [WEB-4014] fix: check access when duplicating pages #7015 2025-05-06 01:20:33 +05:30
42e2b787f0 [WEB-4013]chore: publish login and standardize urls in common settings (#7013)
* chore: handling base path and urls

* chore: uniformize urls in common settings

* correct live url

* chore: use url join to correctly join urls

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2025-05-05 18:58:24 +05:30
fbca9d9a7a [WEB-3996] fix: attachment icon rendering and added support for rar and zip icons (#7007)
* chore: zip and rar file icon

* chore: zip and rar file icon

* fix: attachment icon

* chore: application/x-rar type added

* fix: compressed file extensions

* chore: updated file upload extensions

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2025-05-02 16:53:06 +05:30
SangeethaandGitHub dbc00e4add [WEB-3992] chore: support for x-zip-compressed type #7001 2025-05-01 19:22:00 +05:30
28f9733d1b [WEB-3991] chore: local dev improvements (#6991)
* chore: local dev improvements

* chore: pr feedback

* chore: fix setup

* fix: env variables updated in .env.example files

* fix(local): sign in to admin and web

* chore: update minio deployment to create an bucket automatically on startup.

* chore: resolve merge conflict

* chore: updated api env with live base path

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2025-04-30 21:46:59 +05:30
SangeethaandGitHub 1e46290727 [WEB-3958] chore: allow members and admins to create api tokens (#6979)
* chore: allow members and admins to create api tokens

* chore: change permission for service api token
2025-04-30 19:51:04 +05:30
Anmol Singh BhatiaandGitHub 5a1df8b496 [WEB-3560] chore: work item modal code refactor #6996 2025-04-30 14:56:38 +05:30
Anmol Singh BhatiaandGitHub f23a2f0780 [WEB-3973] chore: space app state icon size #6995 2025-04-29 20:13:55 +05:30
sriram veeraghanta d10bb0b638 chore: yarn lock updates 2025-04-29 15:49:14 +05:30
sriram veeraghanta c4ddff5419 chore: nextjs dependencies upgrade 2025-04-29 15:48:52 +05:30
sriram veeraghanta 10f5b4e9b8 fix: turbo repo upgrade 2025-04-29 15:34:12 +05:30
sriram veeraghanta cdca5a4126 chore: build fixes 2025-04-29 15:33:03 +05:30
14dc6a56bc [WEB-3838]feat:sub work items sorting (#6967)
* refactor: sub-work items components, hooks and types

* feat: added orderby and display properties toggle for sub work items

* fix: build errors

* chore: removed issue type from filters

* chore: added null check

* fix: added null check

---------

Co-authored-by: Aaryan Khandelwal <aaryankhandu123@gmail.com>
2025-04-29 15:23:10 +05:30
SangeethaandGitHub 55340f9f48 [WEB-3957] chore: IntakeIssues with iexact 'in-app' changed to 'IN_APP' (#6977)
* migration: data with iexact 'in-app' changed to 'IN_APP'

* chore: add start_of_week field in profile

* chore: define variables for choices

* chore: merge migration files
2025-04-29 15:22:42 +05:30
Prateek ShouryaandGitHub efa64fc4b8 [WEB-3968] improvement: added few missing translation keys #6993 2025-04-29 15:14:31 +05:30
f5449c8f93 [WEB-3751] chore: work item state icon improvement (#6960)
* chore: return order based on group

* chore: order for workspace stats endpoint

* chore: state response updated

* chore: state icon types updated

* chore: state icon updated

* chore: state settings new icon implementation

* chore: icon implementation

* chore: code refactor

* chore: code refactor

* chore: code refactor

* fix: order field type

---------

Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
2025-04-29 14:33:53 +05:30
baabb82669 [WEB-3926] chore: removed the duplicated webhook task and updated the webhook task to handle exceptions correctly (#6951)
* chore: removed the duplicated webhook function

* chore: update webhook send task to handle errors

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2025-04-29 14:04:00 +05:30
NikhilandGitHub 298e3dc9ca [WEB-3945] chore: update workspace onboarding to add default project (#6964)
* chore: add json files and initial job to push data to workspace

* chore: update seed data location

* chore: update seed data to use assets from static urls

* chore: update seed data to use updated labels

* chore: add logging and update label name

* chore: add created_by for project member

* chore: add created_by_id for issue user property

* chore: add workspace seed task logs

* chore: update log message to return task name

* chore: add warning log for workspace seed task

* chore: add validation for issue seed data
2025-04-29 14:01:22 +05:30
Bavisetti NarayanandGitHub 190300bc6c [WEB-3877] chore: changed the logic to end cycle (#6971)
* chore: changed the logic to end cycle

* chore: added issue deleted filter

* chore: added check for progress snapshot
2025-04-29 14:00:54 +05:30
Dheeraj Kumar KetireddyandGitHub 550fe547e2 [WEB-3967] feat: Optimized module patch endpoint to reduce duplicate db calls (#6983) 2025-04-29 13:51:46 +05:30
Akshat JainandGitHub f278a284c4 chore: comment out APP_RELEASE variable update in build-branch workflow (#6989) 2025-04-28 17:45:44 +05:30
sriram veeraghanta 2bcf6c76cd chore: remove dockerhub user varible from compose 2025-04-28 16:28:48 +05:30
Akshat JainandGitHub fb3e022042 [INFRA-134] fix: Setup and Swarm scripts for DOCKERHUB_USERNAME #6988 2025-04-28 14:37:51 +05:30
Akshat JainandGitHub e3fbb7b073 [INFRA-134]: Update Docker images to use new artifact repository path #6978 2025-04-25 18:09:43 +05:30
Anmol Singh BhatiaandGitHub cce6dd581c [WEB-3948] chore: recent work item improvement (#6976)
* chore: issue entity data type updated

* chore: HomePeekOverviewsRoot component added

* chore: recent work item improvement and code refactor
2025-04-25 15:08:10 +05:30
Akshita GoyalandGitHub d86ac368a4 [WEB-3863] fix: handled error handling for link editing #6968 2025-04-25 14:31:35 +05:30
101994840a [WEB-3944] fix: Error Toast message content update while uploading images (#6969)
* fix: handled svg uploads

* chore: proper error message with all allowed types

---------

Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
2025-04-25 14:30:12 +05:30
Anmol Singh BhatiaandGitHub f60f57ef11 [WEB-3494] chore: platform ux copy changes (#6970)
* chore: project quick action option ux copy updated

* chore: project tour copy updated
2025-04-25 14:29:09 +05:30
Prateek ShouryaandGitHub 546217f09b [WEB-3953] fix: issue description assets upload when project id is switched (#6975) 2025-04-25 14:27:40 +05:30
sriram veeraghanta 6df8323665 fix: add gzip upload support 2025-04-24 17:50:37 +05:30
SangeethaandGitHub 77d022df71 [WEB-3919] chore: support .sql file attachment #6966 2025-04-24 17:39:16 +05:30
M. PalanikannanandGitHub 797f150ec4 [WIKI-331] fix: editor ref issues while locking/unlocking page #6965 2025-04-24 17:38:41 +05:30
sriram veeraghanta b54f54999e chore: bump up the package version 2025-04-24 17:37:50 +05:30
SangeethaandGitHub dff176be8f [WEB-3930] chore: set IN_APP as default source value for intake issue (#6963)
* chore: chore: only allow intake issues with source IN_APP to be created

* chore: set IN_APP as default intake issue
2025-04-24 16:25:15 +05:30
2bbaaed3ea [WEB-3918] fix: api tokens is_active (#6941)
* fix: is_active always returning true
chore: formate expired_at to iso date

* Display exact expiration timestamp for API tokens

* chore: remove conversion to iso

* chore: remove unwanted imports

* fix: added timestamp for api token expiry

* fix: handle none value in expired_at

* fix: fix: handle none value in expired_at

* chore: add type hints

* fix: refactor

---------

Co-authored-by: Alaaeddine bousselmi <alaaeddine.bousselmi@medtech.tn>
Co-authored-by: gakshita <akshitagoyal1516@gmail.com>
Co-authored-by: Akshita Goyal <36129505+gakshita@users.noreply.github.com>
2025-04-24 01:28:29 +05:30
Prateek ShouryaandGitHub b5ceb94fb2 [WEB-3930] fix: application crash on accessing intake work items (#6958) 2025-04-23 15:12:54 +05:30
alaabousselmiandGitHub feb6243065 docs: document minimum RAM requirement and issue naming conventions (#6954) 2025-04-22 18:00:19 +05:30
Anmol Singh BhatiaandGitHub 5dacba74c9 [WEB-3923] fix: applied filters list #6957 2025-04-22 17:58:16 +05:30
bIaOandGitHub 0efb0c239c feat: improve setup.sh script with better error handling and user feedback (#6758) 2025-04-22 17:56:34 +05:30
Vamsi KrishnaandGitHub c8be836d6c [WEB-3920]fix: estimate activity #6950 2025-04-22 17:45:15 +05:30
NikhilandGitHub 833b82e247 [WEB-3927] chore: add logging to support json logging (#6955)
* chore: update logging to json based logging

* chore: add logging to file
2025-04-22 17:41:58 +05:30
Akshita GoyalandGitHub 280aa7f671 [WEB-3399] fix: progress data for cycle list item #6956 2025-04-22 17:41:06 +05:30
Aaryan KhandelwalandGitHub eac1115566 [WIKI-320] refactor: page header actions (#6946)
* refactor: page header actions

* chore: update toolbar component

* chore: update archived and lock badge colors

* chore: added observer to favorite control
2025-04-17 20:52:33 +05:30
sriram veeraghanta 8166a757a7 fix: removed @plane alias from ui package 2025-04-17 20:51:52 +05:30
Anmol Singh BhatiaandGitHub be5d77d978 [WEB-3892] chore: link item improvements (#6944)
* chore: code refactor

* chore: global link block component added

* chore: link item improvement and code refactor
2025-04-17 20:08:53 +05:30
Anmol Singh BhatiaandGitHub 18fb3b8450 [WEB-3904] fix: sub work item fetching #6945 2025-04-17 20:07:13 +05:30
sriram veeraghanta ef5616905e chore: upgrade turbo repo version 2025-04-17 17:51:59 +05:30
SangeethaandGitHub aeb41e603c [WEB-3826] feat: estimate activitites #6937 2025-04-17 17:16:57 +05:30
Vamsi KrishnaandGitHub 55eea1a8b7 [WEB-3872]chore: header switcher enhancements (#6935)
* * chore: alignment and size for header
* fix: switcher close on click

* chore: moved acces icon component to components
2025-04-17 17:15:53 +05:30
Aaryan KhandelwalandGitHub fa87ff14b7 [WIKI-319] chore: remove bottom border when toolbar is hidden (#6943)
* chore: remove border when toolbar is hidden

* chore: add stricter conditions
2025-04-17 17:13:21 +05:30
khalilzitouni2058andGitHub 7d91b5f8df [WEB-3892] feat: add icon to Quicklinks (#6927)
* [feature]: add icon to Quicklinks

* fix: moving  getIconForLink to utils packages
2025-04-17 17:11:57 +05:30
Anmol Singh BhatiaandGitHub 3ce40dfa2f [WIKI-316] fix: list item overflow #6942 2025-04-17 17:08:13 +05:30
Anmol Singh BhatiaandGitHub f65253c994 [WEB-2561] chore: favicon icon updated #6938 (#6940)
* chore: favicon icon updated

* chore: code refactor
2025-04-17 15:38:42 +05:30
Anmol Singh BhatiaandGitHub 97fcfaa653 [WEB-2561] chore: favicon icon updated #6938 2025-04-16 20:34:12 +05:30
Anmol Singh BhatiaandGitHub 0e1ebff978 [WEB-3871] fix: sidebar label property #6934 2025-04-15 19:42:02 +05:30
Anmol Singh BhatiaandGitHub 642dabfe35 [WEB-3870] fix: sidebar comment scroll #6932 2025-04-15 17:47:22 +05:30
Aaryan KhandelwalandGitHub 48557cb670 [WEB-3868] fix: issue detail widget modals #6933 2025-04-15 17:46:45 +05:30
Bavisetti NarayanandGitHub 608da1465c [WEB-3860] chore: added deleted filter in the grouper (#6931)
* chore: added deleted filter in the grouper

* chore: added type hints for the function
2025-04-15 17:42:45 +05:30
dbcc7bedb4 [WEB-3855] feat: Turkish language support (#6922)
* add Turkish language support (#6874)

* add turkish language support

* fix indentation

* chore: extended core translation added

* chore: code refactor

---------

Co-authored-by: Farahat Abdrabouh <88924701+fasdjkherig@users.noreply.github.com>
2025-04-15 16:36:02 +05:30
Vamsi KrishnaandGitHub c401b26dd4 [WEB-3856]chore: refactor work item activity (#6923)
* chore: refactor work item activity

* chore: added estimate render for notifications
2025-04-15 16:35:28 +05:30
Aaryan KhandelwalandGitHub a4bca0c39c [WEB-3859] fix: work item links #6930 2025-04-15 13:46:29 +05:30
Saurabh KumarandGitHub 24899887b2 chore: Add workspace slug to should render setting link method (#6886)
* add workspace slug to setting link function

* add params in the function
2025-04-14 17:41:47 +05:30
sriram veeraghanta c6953ff878 fix: db modeling changes in pages 2025-04-12 16:22:13 +05:30
Prateek ShouryaandGitHub 06be9ab81b [WEB-3854] feat: billing and plans new design (#6920)
* [WEB-3854] feat: billing and plans new design

* chore: add missing styles
2025-04-11 20:37:25 +05:30
Akshita GoyalandGitHub ed8d00acb1 [WEB-3849] chore: added intake source in the list (#6919)
* chore: added intake source in the list

* fix: refactor
2025-04-11 19:49:35 +05:30
Aaryan KhandelwalandGitHub 915e374485 [WIKI-307]chore: update page icon placement #6916 2025-04-11 18:07:03 +05:30
Vamsi KrishnaandGitHub 1d5b93cebd [WEB-3853] fix: untitled page name issue #6918 2025-04-11 18:06:26 +05:30
sriram veeraghanta df65b8c34a fix: adding request logger middleware 2025-04-11 17:59:19 +05:30
Akshita GoyalandGitHub 4c688b1d25 [WEB-3529] fix: fixed the comment create box position in common comments component (#6915) 2025-04-11 14:00:54 +05:30
NikhilandGitHub bfc6ed839f fix: uuid validation, status and webhook errors (#6896)
* fix: uuid validation and function parameter handling for external apis

* chore: update status 410 Gone to 409 conflicts

* chore: add webhook trigger for issue created through apis

* chore: remove pks from post

* chore: remove issue id from module post
2025-04-11 01:47:00 +05:30
Surya PrashanthandGitHub b68396a4b2 [WEB-3831] chore: add validation for project_id in cycle serializer #6908 2025-04-11 01:42:53 +05:30
Vamsi KrishnaandGitHub b4fc715aba [WEB-3826] fix: estimate dropdown formatting (#6906)
* * fix: time conversion for estimate dropdown in browse
* chore: updated puncutations for estimates.

* chore: estimate activiy formatting

* chore: estimate activity refactor
2025-04-11 01:41:43 +05:30
Anmol Singh BhatiaandGitHub 33a1b916cb [WEB-3837] fix: mutation of child work item added via Cmd+K with parent context #6910 2025-04-11 01:40:29 +05:30
Akshita GoyalandGitHub 2818310619 [WEB-3529] fix: comment reset + edit comment font size + comment box position (#6909)
* fix: comment reset + edit comment font size

* fix: dynamically setting the position of the comment box

* fix: refactor

* fix: nomenclature
2025-04-11 01:40:05 +05:30
Anmol Singh BhatiaandGitHub 882520b3c7 [WEB-3841] fix: create issue modal now correctly uses current project context #6911 2025-04-11 01:35:27 +05:30
20132e7544 [WEB-3839] fix: peek overview description version history (#6912)
* fix: handle undefined created_at

* chore: add created_by, updated_by updated_at and created_at fields in
relation apis

* chore: handle undefined date

* fix: project typo

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
2025-04-10 16:22:26 +05:30
Aaryan KhandelwalandGitHub 0ae57b49d2 [WEB-3829]fix: update workspace store action (#6905) 2025-04-09 20:31:52 +05:30
M. PalanikannanandGitHub d347269afb [WEB-3819] fix: images now restore in read only mode as well (#6904) 2025-04-09 20:06:15 +05:30
Aaryan KhandelwalandGitHub a3fd616ec4 [WEB-3827] refactor: work item widget components (#6902) 2025-04-09 19:58:16 +05:30
Akshita GoyalandGitHub 9eeff158d5 [WEB-3811] fix: cycle charts issues (#6901) 2025-04-09 19:57:47 +05:30
Aaryan KhandelwalandGitHub ef20b5814e [WEB-3792, 3823] fix: intake form version history (#6898)
* chore: intake form version history

* fix: remove autofocus from the copy markdown button

* chore: add logic to display deactivated user
2025-04-09 19:56:59 +05:30
Vamsi KrishnaandGitHub 14914e8716 [WEB-3759]chore: updated module and pages detail header (#6903)
* chore: added panel collapse and quick action menu for module detail header

* fix: updated pages header swithcer
2025-04-09 19:36:15 +05:30
Vamsi KrishnaandGitHub b738e39a4a [WEB-3798]chore: updated language support to estimates (#6900) 2025-04-09 19:34:01 +05:30
368 changed files with 11095 additions and 4066 deletions
+1 -1
View File
@@ -273,7 +273,7 @@ jobs:
run: |
cp ./deploy/selfhost/install.sh deploy/selfhost/setup.sh
sed -i 's/${APP_RELEASE:-stable}/${APP_RELEASE:-'${REL_VERSION}'}/g' deploy/selfhost/docker-compose.yml
sed -i 's/APP_RELEASE=stable/APP_RELEASE='${REL_VERSION}'/g' deploy/selfhost/variables.env
# sed -i 's/APP_RELEASE=stable/APP_RELEASE='${REL_VERSION}'/g' deploy/selfhost/variables.env
- name: Create Release
id: create_release
+1
View File
@@ -1,5 +1,6 @@
node_modules
.next
.yarn
### NextJS ###
# Dependencies
+1
View File
@@ -0,0 +1 @@
nodeLinker: node-modules
+40 -10
View File
@@ -15,14 +15,33 @@ Without said minimal reproduction, we won't be able to investigate all [issues](
You can open a new issue with this [issue form](https://github.com/makeplane/plane/issues/new).
### Naming conventions for issues
When opening a new issue, please use a clear and concise title that follows this format:
- For bugs: `🐛 Bug: [short description]`
- For features: `🚀 Feature: [short description]`
- For improvements: `🛠️ Improvement: [short description]`
- For documentation: `📘 Docs: [short description]`
**Examples:**
- `🐛 Bug: API token expiry time not saving correctly`
- `📘 Docs: Clarify RAM requirement for local setup`
- `🚀 Feature: Allow custom time selection for token expiration`
This helps us triage and manage issues more efficiently.
## Projects setup and Architecture
### Requirements
- Node.js version v16.18.0
- Docker Engine installed and running
- Node.js version 20+ [LTS version](https://nodejs.org/en/about/previous-releases)
- Python version 3.8+
- Postgres version v14
- Redis version v6.2.7
- **Memory**: Minimum **12 GB RAM** recommended
> ⚠️ Running the project on a system with only 8 GB RAM may lead to setup failures or memory crashes (especially during Docker container build/start or dependency install). Use cloud environments like GitHub Codespaces or upgrade local RAM if possible.
### Setup the project
@@ -50,6 +69,17 @@ chmod +x setup.sh
docker compose -f docker-compose-local.yml up
```
5. Start web apps:
```bash
yarn dev
```
6. Open your browser to http://localhost:3001/god-mode/ and register yourself as instance admin
7. Open up your browser to http://localhost:3000 then log in using the same credentials from the previous step
Thats it! Youre all set to begin coding. Remember to refresh your browser if changes dont auto-reload. Happy contributing! 🎉
## Missing a Feature?
If a feature is missing, you can directly _request_ a new one [here](https://github.com/makeplane/plane/issues/new?assignees=&labels=feature&template=feature_request.yml&title=%F0%9F%9A%80+Feature%3A+). You also can do the same by choosing "🚀 Feature" when raising a [New Issue](https://github.com/makeplane/plane/issues/new/choose) on our GitHub Repository.
@@ -75,7 +105,7 @@ To ensure consistency throughout the source code, please keep these rules in min
- **Improve documentation** - fix incomplete or missing [docs](https://docs.plane.so/), bad wording, examples or explanations.
## Contributing to language support
This guide is designed to help contributors understand how to add or update translations in the application.
This guide is designed to help contributors understand how to add or update translations in the application.
### Understanding translation structure
@@ -90,7 +120,7 @@ packages/i18n/src/locales/
├── fr/
│ └── translations.json
└── [language]/
└── translations.json
└── translations.json
```
#### Nested structure
To keep translations organized, we use a nested structure for keys. This makes it easier to manage and locate specific translations. For example:
@@ -110,14 +140,14 @@ To keep translations organized, we use a nested structure for keys. This makes i
We use [IntlMessageFormat](https://formatjs.github.io/docs/intl-messageformat/) to handle dynamic content, such as variables and pluralization. Here's how to format your translations:
#### Examples
- **Simple variables**
- **Simple variables**
```json
{
"greeting": "Hello, {name}!"
}
```
- **Pluralization**
- **Pluralization**
```json
{
"items": "{count, plural, one {Work item} other {Work items}}"
@@ -142,15 +172,15 @@ We use [IntlMessageFormat](https://formatjs.github.io/docs/intl-messageformat/)
### Adding new languages
Adding a new language involves several steps to ensure it integrates seamlessly with the project. Follow these instructions carefully:
1. **Update type definitions**
1. **Update type definitions**
Add the new language to the TLanguage type in the language definitions file:
```typescript
// types/language.ts
export type TLanguage = "en" | "fr" | "your-lang";
```
```
2. **Add language configuration**
2. **Add language configuration**
Include the new language in the list of supported languages:
```typescript
@@ -161,14 +191,14 @@ Include the new language in the list of supported languages:
];
```
3. **Create translation files**
3. **Create translation files**
1. Create a new folder for your language under locales (e.g., `locales/your-lang/`).
2. Add a `translations.json` file inside the folder.
3. Copy the structure from an existing translation file and translate all keys.
4. **Update import logic**
4. **Update import logic**
Modify the language import logic to include your new language:
```typescript
+12 -43
View File
@@ -47,10 +47,10 @@ Meet [Plane](https://plane.so/), an open-source project management tool to track
Getting started with Plane is simple. Choose the setup that works best for you:
- **Plane Cloud**
- **Plane Cloud**
Sign up for a free account on [Plane Cloud](https://app.plane.so)—it's the fastest way to get up and running without worrying about infrastructure.
- **Self-host Plane**
- **Self-host Plane**
Prefer full control over your data and infrastructure? Install and run Plane on your own servers. Follow our detailed [deployment guides](https://developers.plane.so/self-hosting/overview) to get started.
| Installation methods | Docs link |
@@ -62,22 +62,22 @@ Prefer full control over your data and infrastructure? Install and run Plane on
## 🌟 Features
- **Issues**
- **Issues**
Efficiently create and manage tasks with a robust rich text editor that supports file uploads. Enhance organization and tracking by adding sub-properties and referencing related issues.
- **Cycles**
- **Cycles**
Maintain your teams momentum with Cycles. Track progress effortlessly using burn-down charts and other insightful tools.
- **Modules**
Simplify complex projects by dividing them into smaller, manageable modules.
- **Modules**
Simplify complex projects by dividing them into smaller, manageable modules.
- **Views**
- **Views**
Customize your workflow by creating filters to display only the most relevant issues. Save and share these views with ease.
- **Pages**
- **Pages**
Capture and organize ideas using Plane Pages, complete with AI capabilities and a rich text editor. Format text, insert images, add hyperlinks, or convert your notes into actionable items.
- **Analytics**
- **Analytics**
Access real-time insights across all your Plane data. Visualize trends, remove blockers, and keep your projects moving forward.
- **Drive** (_coming soon_): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution.
@@ -85,38 +85,7 @@ Access real-time insights across all your Plane data. Visualize trends, remove b
## 🛠️ Local development
### Pre-requisites
- Ensure Docker Engine is installed and running.
### Development setup
Setting up your local environment is simple and straightforward. Follow these steps to get started:
1. Clone the repository:
```
git clone https://github.com/makeplane/plane.git
```
2. Navigate to the project folder:
```
cd plane
```
3. Create a new branch for your feature or fix:
```
git checkout -b <feature-branch-name>
```
4. Run the setup script in the terminal:
```
./setup.sh
```
5. Open the project in an IDE such as VS Code.
6. Review the `.env` files in the relevant folders. Refer to [Environment Setup](./ENV_SETUP.md) for details on the environment variables used.
7. Start the services using Docker:
```
docker compose -f docker-compose-local.yml up -d
```
Thats it! Youre all set to begin coding. Remember to refresh your browser if changes dont auto-reload. Happy contributing! 🎉
See [CONTRIBUTING](./CONTRIBUTING.md)
## ⚙️ Built with
[![Next JS](https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white)](https://nextjs.org/)
@@ -194,7 +163,7 @@ Feel free to ask questions, report bugs, participate in discussions, share ideas
If you discover a security vulnerability in Plane, please report it responsibly instead of opening a public issue. We take all legitimate reports seriously and will investigate them promptly. See [Security policy](https://github.com/makeplane/plane/blob/master/SECURITY.md) for more info.
To disclose any security issues, please email us at security@plane.so.
To disclose any security issues, please email us at security@plane.so.
## 🤝 Contributing
@@ -219,4 +188,4 @@ Please read [CONTRIBUTING.md](https://github.com/makeplane/plane/blob/master/CON
## License
This project is licensed under the [GNU Affero General Public License v3.0](https://github.com/makeplane/plane/blob/master/LICENSE.txt).
This project is licensed under the [GNU Affero General Public License v3.0](https://github.com/makeplane/plane/blob/master/LICENSE.txt).
+11 -2
View File
@@ -1,3 +1,12 @@
NEXT_PUBLIC_API_BASE_URL=""
NEXT_PUBLIC_API_BASE_URL="http://localhost:8000"
NEXT_PUBLIC_WEB_BASE_URL="http://localhost:3000"
NEXT_PUBLIC_ADMIN_BASE_URL="http://localhost:3001"
NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
NEXT_PUBLIC_WEB_BASE_URL=""
NEXT_PUBLIC_SPACE_BASE_URL="http://localhost:3002"
NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
NEXT_PUBLIC_LIVE_BASE_URL="http://localhost:3100"
NEXT_PUBLIC_LIVE_BASE_PATH="/live"
+3 -3
View File
@@ -1,7 +1,7 @@
{
"name": "admin",
"description": "Admin UI for Plane",
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"private": true,
"scripts": {
@@ -17,10 +17,10 @@
"@headlessui/react": "^1.7.19",
"@plane/constants": "*",
"@plane/hooks": "*",
"@plane/services": "*",
"@plane/types": "*",
"@plane/ui": "*",
"@plane/utils": "*",
"@plane/services": "*",
"@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.17.0",
"autoprefixer": "10.4.14",
@@ -29,7 +29,7 @@
"lucide-react": "^0.469.0",
"mobx": "^6.12.0",
"mobx-react": "^9.1.1",
"next": "^14.2.26",
"next": "^14.2.28",
"next-themes": "^0.2.1",
"postcss": "^8.4.38",
"react": "^18.3.1",
+16 -7
View File
@@ -1,7 +1,7 @@
# Backend
# Debug value for api server use it as 0 for production use
DEBUG=0
CORS_ALLOWED_ORIGINS="http://localhost"
CORS_ALLOWED_ORIGINS="http://localhost:3000,http://localhost:3001,http://localhost:3002,http://localhost:3100"
# Database Settings
POSTGRES_USER="plane"
@@ -27,7 +27,7 @@ RABBITMQ_VHOST="plane"
AWS_REGION=""
AWS_ACCESS_KEY_ID="access-key"
AWS_SECRET_ACCESS_KEY="secret-key"
AWS_S3_ENDPOINT_URL="http://plane-minio:9000"
AWS_S3_ENDPOINT_URL="http://localhost:9000"
# Changing this requires change in the nginx.conf for uploads if using minio setup
AWS_S3_BUCKET_NAME="uploads"
# Maximum file upload limit
@@ -37,22 +37,31 @@ FILE_SIZE_LIMIT=5242880
DOCKERIZED=1 # deprecated
# set to 1 If using the pre-configured minio setup
USE_MINIO=1
USE_MINIO=0
# Nginx Configuration
NGINX_PORT=80
# Email redirections and minio domain settings
WEB_URL="http://localhost"
WEB_URL="http://localhost:8000"
# Gunicorn Workers
GUNICORN_WORKERS=2
# Base URLs
ADMIN_BASE_URL=
SPACE_BASE_URL=
APP_BASE_URL=
ADMIN_BASE_URL="http://localhost:3001"
ADMIN_BASE_PATH="/god-mode"
SPACE_BASE_URL="http://localhost:3002"
SPACE_BASE_PATH="/spaces"
APP_BASE_URL="http://localhost:3000"
APP_BASE_PATH=""
LIVE_BASE_URL="http://localhost:3100"
LIVE_BASE_PATH="/live"
LIVE_SERVER_SECRET_KEY="secret-key"
# Hard delete files after days
HARD_DELETE_AFTER_DAYS=60
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "plane-api",
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"private": true,
"description": "API server powering Plane's backend"
+8 -6
View File
@@ -39,12 +39,15 @@ class CycleSerializer(BaseSerializer):
data.get("start_date", None) is not None
and data.get("end_date", None) is not None
):
project_id = self.initial_data.get("project_id") or self.instance.project_id
is_start_date_end_date_equal = (
True
if str(data.get("start_date")) == str(data.get("end_date"))
else False
project_id = self.initial_data.get("project_id") or (
self.instance.project_id
if self.instance and hasattr(self.instance, "project_id")
else None
)
if not project_id:
raise serializers.ValidationError("Project ID is required")
data["start_date"] = convert_to_utc(
date=str(data.get("start_date").date()),
project_id=project_id,
@@ -53,7 +56,6 @@ class CycleSerializer(BaseSerializer):
data["end_date"] = convert_to_utc(
date=str(data.get("end_date", None).date()),
project_id=project_id,
is_start_date_end_date_equal=is_start_date_end_date_equal,
)
return data
+45 -22
View File
@@ -141,8 +141,10 @@ class CycleAPIEndpoint(BaseAPIView):
if pk:
queryset = self.get_queryset().filter(archived_at__isnull=True).get(pk=pk)
data = CycleSerializer(
queryset, fields=self.fields,
expand=self.expand, context={"project": project}
queryset,
fields=self.fields,
expand=self.expand,
context={"project": project},
).data
return Response(data, status=status.HTTP_200_OK)
queryset = self.get_queryset().filter(archived_at__isnull=True)
@@ -154,8 +156,11 @@ class CycleAPIEndpoint(BaseAPIView):
start_date__lte=timezone.now(), end_date__gte=timezone.now()
)
data = CycleSerializer(
queryset, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
queryset,
many=True,
fields=self.fields,
expand=self.expand,
context={"project": project},
).data
return Response(data, status=status.HTTP_200_OK)
@@ -166,8 +171,11 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
cycles,
many=True,
fields=self.fields,
expand=self.expand,
context={"project": project},
).data,
)
@@ -178,8 +186,11 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
cycles,
many=True,
fields=self.fields,
expand=self.expand,
context={"project": project},
).data,
)
@@ -190,8 +201,11 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
cycles,
many=True,
fields=self.fields,
expand=self.expand,
context={"project": project},
).data,
)
@@ -204,16 +218,22 @@ class CycleAPIEndpoint(BaseAPIView):
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
cycles,
many=True,
fields=self.fields,
expand=self.expand,
context={"project": project},
).data,
)
return self.paginate(
request=request,
queryset=(queryset),
on_results=lambda cycles: CycleSerializer(
cycles, many=True, fields=self.fields,
expand=self.expand, context={"project": project}
cycles,
many=True,
fields=self.fields,
expand=self.expand,
context={"project": project},
).data,
)
@@ -768,6 +788,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -779,6 +800,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -827,6 +849,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
)
)
)
old_cycle = old_cycle.first()
estimate_type = Project.objects.filter(
workspace__slug=slug,
@@ -946,7 +969,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
)
estimate_completion_chart = burndown_plot(
queryset=old_cycle.first(),
queryset=old_cycle,
slug=slug,
project_id=project_id,
plot_type="points",
@@ -1094,7 +1117,7 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
# Pass the new_cycle queryset to burndown_plot
completion_chart = burndown_plot(
queryset=old_cycle.first(),
queryset=old_cycle,
slug=slug,
project_id=project_id,
plot_type="issues",
@@ -1106,12 +1129,12 @@ class TransferCycleIssueAPIEndpoint(BaseAPIView):
).first()
current_cycle.progress_snapshot = {
"total_issues": old_cycle.first().total_issues,
"completed_issues": old_cycle.first().completed_issues,
"cancelled_issues": old_cycle.first().cancelled_issues,
"started_issues": old_cycle.first().started_issues,
"unstarted_issues": old_cycle.first().unstarted_issues,
"backlog_issues": old_cycle.first().backlog_issues,
"total_issues": old_cycle.total_issues,
"completed_issues": old_cycle.completed_issues,
"cancelled_issues": old_cycle.cancelled_issues,
"started_issues": old_cycle.started_issues,
"unstarted_issues": old_cycle.unstarted_issues,
"backlog_issues": old_cycle.backlog_issues,
"distribution": {
"labels": label_distribution_data,
"assignees": assignee_distribution_data,
+2 -1
View File
@@ -20,6 +20,7 @@ from plane.bgtasks.issue_activities_task import issue_activity
from plane.db.models import Intake, IntakeIssue, Issue, Project, ProjectMember, State
from plane.utils.host import base_host
from .base import BaseAPIView
from plane.db.models.intake import SourceType
class IntakeIssueAPIEndpoint(BaseAPIView):
@@ -125,7 +126,7 @@ class IntakeIssueAPIEndpoint(BaseAPIView):
intake_id=intake.id,
project_id=project_id,
issue=issue,
source=request.data.get("source", "IN-APP"),
source=SourceType.IN_APP,
)
# Create an Issue Activity
issue_activity.delay(
+12
View File
@@ -57,6 +57,7 @@ from plane.settings.storage import S3Storage
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
from .base import BaseAPIView
from plane.utils.host import base_host
from plane.bgtasks.webhook_task import model_activity
class WorkspaceIssueAPIEndpoint(BaseAPIView):
@@ -322,6 +323,17 @@ class IssueAPIEndpoint(BaseAPIView):
current_instance=None,
epoch=int(timezone.now().timestamp()),
)
# Send the model activity
model_activity.delay(
model_name="issue",
model_id=str(serializer.data["id"]),
requested_data=request.data,
current_instance=None,
actor_id=request.user.id,
slug=slug,
origin=base_host(request=request, is_app=True),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+9 -8
View File
@@ -32,6 +32,7 @@ from plane.bgtasks.webhook_task import model_activity, webhook_activity
from .base import BaseAPIView
from plane.utils.host import base_host
class ProjectAPIEndpoint(BaseAPIView):
"""Project Endpoints to create, update, list, retrieve and delete endpoint"""
@@ -171,14 +172,14 @@ class ProjectAPIEndpoint(BaseAPIView):
states = [
{
"name": "Backlog",
"color": "#A3A3A3",
"color": "#60646C",
"sequence": 15000,
"group": "backlog",
"default": True,
},
{
"name": "Todo",
"color": "#3A3A3A",
"color": "#60646C",
"sequence": 25000,
"group": "unstarted",
},
@@ -190,13 +191,13 @@ class ProjectAPIEndpoint(BaseAPIView):
},
{
"name": "Done",
"color": "#16A34A",
"color": "#46A758",
"sequence": 45000,
"group": "completed",
},
{
"name": "Cancelled",
"color": "#EF4444",
"color": "#9AA4BC",
"sequence": 55000,
"group": "cancelled",
},
@@ -238,7 +239,7 @@ class ProjectAPIEndpoint(BaseAPIView):
if "already exists" in str(e):
return Response(
{"name": "The project name is already taken"},
status=status.HTTP_410_GONE,
status=status.HTTP_409_CONFLICT,
)
except Workspace.DoesNotExist:
return Response(
@@ -247,7 +248,7 @@ class ProjectAPIEndpoint(BaseAPIView):
except ValidationError:
return Response(
{"identifier": "The project identifier is already taken"},
status=status.HTTP_410_GONE,
status=status.HTTP_409_CONFLICT,
)
def patch(self, request, slug, pk):
@@ -305,7 +306,7 @@ class ProjectAPIEndpoint(BaseAPIView):
if "already exists" in str(e):
return Response(
{"name": "The project name is already taken"},
status=status.HTTP_410_GONE,
status=status.HTTP_409_CONFLICT,
)
except (Project.DoesNotExist, Workspace.DoesNotExist):
return Response(
@@ -314,7 +315,7 @@ class ProjectAPIEndpoint(BaseAPIView):
except ValidationError:
return Response(
{"identifier": "The project identifier is already taken"},
status=status.HTTP_410_GONE,
status=status.HTTP_409_CONFLICT,
)
def delete(self, request, slug, pk):
+9
View File
@@ -1,5 +1,7 @@
from .base import BaseSerializer
from plane.db.models import APIToken, APIActivityLog
from rest_framework import serializers
from django.utils import timezone
class APITokenSerializer(BaseSerializer):
@@ -17,10 +19,17 @@ class APITokenSerializer(BaseSerializer):
class APITokenReadSerializer(BaseSerializer):
is_active = serializers.SerializerMethodField()
class Meta:
model = APIToken
exclude = ("token",)
def get_is_active(self, obj: APIToken) -> bool:
if obj.expired_at is None:
return True
return timezone.now() < obj.expired_at
class APIActivityLogSerializer(BaseSerializer):
class Meta:
-6
View File
@@ -25,11 +25,6 @@ class CycleWriteSerializer(BaseSerializer):
or (self.instance and self.instance.project_id)
or self.context.get("project_id", None)
)
is_start_date_end_date_equal = (
True
if str(data.get("start_date")) == str(data.get("end_date"))
else False
)
data["start_date"] = convert_to_utc(
date=str(data.get("start_date").date()),
project_id=project_id,
@@ -38,7 +33,6 @@ class CycleWriteSerializer(BaseSerializer):
data["end_date"] = convert_to_utc(
date=str(data.get("end_date", None).date()),
project_id=project_id,
is_start_date_end_date_equal=is_start_date_end_date_equal,
)
return data
+24 -2
View File
@@ -352,8 +352,19 @@ class IssueRelationSerializer(BaseSerializer):
"state_id",
"priority",
"assignee_ids",
"created_by",
"created_at",
"updated_at",
"updated_by",
]
read_only_fields = [
"workspace",
"project",
"created_by",
"created_at",
"updated_by",
"updated_at",
]
read_only_fields = ["workspace", "project"]
class RelatedIssueSerializer(BaseSerializer):
@@ -383,8 +394,19 @@ class RelatedIssueSerializer(BaseSerializer):
"state_id",
"priority",
"assignee_ids",
"created_by",
"created_at",
"updated_by",
"updated_at",
]
read_only_fields = [
"workspace",
"project",
"created_by",
"created_at",
"updated_by",
"updated_at",
]
read_only_fields = ["workspace", "project"]
class IssueAssigneeSerializer(BaseSerializer):
+4 -1
View File
@@ -1,11 +1,13 @@
# Module imports
from .base import BaseSerializer
from rest_framework import serializers
from plane.db.models import State
class StateSerializer(BaseSerializer):
order = serializers.FloatField(required=False)
class Meta:
model = State
fields = [
@@ -18,6 +20,7 @@ class StateSerializer(BaseSerializer):
"default",
"description",
"sequence",
"order",
]
read_only_fields = ["workspace", "project"]
+3 -3
View File
@@ -9,11 +9,11 @@ from rest_framework import status
from .base import BaseAPIView
from plane.db.models import APIToken, Workspace
from plane.app.serializers import APITokenSerializer, APITokenReadSerializer
from plane.app.permissions import WorkspaceOwnerPermission
from plane.app.permissions import WorkspaceEntityPermission
class ApiTokenEndpoint(BaseAPIView):
permission_classes = [WorkspaceOwnerPermission]
permission_classes = [WorkspaceEntityPermission]
def post(self, request, slug):
label = request.data.get("label", str(uuid4().hex))
@@ -68,7 +68,7 @@ class ApiTokenEndpoint(BaseAPIView):
class ServiceApiTokenEndpoint(BaseAPIView):
permission_classes = [WorkspaceOwnerPermission]
permission_classes = [WorkspaceEntityPermission]
def post(self, request, slug):
workspace = Workspace.objects.get(slug=slug)
+4 -4
View File
@@ -137,7 +137,7 @@ class UserAssetsV2Endpoint(BaseAPIView):
if type not in allowed_types:
return Response(
{
"error": "Invalid file type. Only JPEG and PNG files are allowed.",
"error": "Invalid file type. Only JPEG, PNG, WebP, JPG and GIF files are allowed.",
"status": False,
},
status=status.HTTP_400_BAD_REQUEST,
@@ -351,7 +351,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
if type not in allowed_types:
return Response(
{
"error": "Invalid file type. Only JPEG and PNG files are allowed.",
"error": "Invalid file type. Only JPEG, PNG, WebP, JPG and GIF files are allowed.",
"status": False,
},
status=status.HTTP_400_BAD_REQUEST,
@@ -552,7 +552,7 @@ class ProjectAssetEndpoint(BaseAPIView):
if type not in allowed_types:
return Response(
{
"error": "Invalid file type. Only JPEG and PNG files are allowed.",
"error": "Invalid file type. Only JPEG, PNG, WebP, JPG and GIF files are allowed.",
"status": False,
},
status=status.HTTP_400_BAD_REQUEST,
@@ -683,7 +683,7 @@ class ProjectBulkAssetEndpoint(BaseAPIView):
# For some cases, the bulk api is called after the issue is deleted creating
# an integrity error
try:
assets.update(issue_id=entity_id)
assets.update(issue_id=entity_id, project_id=project_id)
except IntegrityError:
pass
+95 -60
View File
@@ -117,6 +117,7 @@ class CycleViewSet(BaseViewSet):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -129,6 +130,7 @@ class CycleViewSet(BaseViewSet):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -141,6 +143,7 @@ class CycleViewSet(BaseViewSet):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -231,6 +234,7 @@ class CycleViewSet(BaseViewSet):
"status",
"version",
"created_by",
"created_at",
)
datetime_fields = ["start_date", "end_date"]
data = user_timezone_converter(data, datetime_fields, project_timezone)
@@ -264,11 +268,10 @@ class CycleViewSet(BaseViewSet):
"status",
"version",
"created_by",
"created_at",
)
datetime_fields = ["start_date", "end_date"]
data = user_timezone_converter(
data, datetime_fields, project_timezone
)
data = user_timezone_converter(data, datetime_fields, project_timezone)
return Response(data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
@@ -313,6 +316,7 @@ class CycleViewSet(BaseViewSet):
"assignee_ids",
"status",
"created_by",
"created_at",
)
.first()
)
@@ -415,9 +419,7 @@ class CycleViewSet(BaseViewSet):
project_timezone = project.timezone
datetime_fields = ["start_date", "end_date"]
cycle = user_timezone_converter(
cycle, datetime_fields, project_timezone
)
cycle = user_timezone_converter(cycle, datetime_fields, project_timezone)
# Send the model activity
model_activity.delay(
@@ -477,6 +479,7 @@ class CycleViewSet(BaseViewSet):
"assignee_ids",
"status",
"created_by",
"created_at",
)
.first()
)
@@ -574,16 +577,12 @@ class CycleDateCheckEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
is_start_date_end_date_equal = (
True if str(start_date) == str(end_date) else False
)
start_date = convert_to_utc(
date=str(start_date), project_id=project_id, is_start_date=True
)
end_date = convert_to_utc(
date=str(end_date),
project_id=project_id,
is_start_date_end_date_equal=is_start_date_end_date_equal,
)
# Check if any cycle intersects in the given interval
@@ -668,6 +667,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -732,6 +732,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
)
)
)
old_cycle = old_cycle.first()
estimate_type = Project.objects.filter(
workspace__slug=slug,
@@ -850,7 +851,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
)
estimate_completion_chart = burndown_plot(
queryset=old_cycle.first(),
queryset=old_cycle,
slug=slug,
project_id=project_id,
plot_type="points",
@@ -997,7 +998,7 @@ class TransferCycleIssueEndpoint(BaseAPIView):
# Pass the new_cycle queryset to burndown_plot
completion_chart = burndown_plot(
queryset=old_cycle.first(),
queryset=old_cycle,
slug=slug,
project_id=project_id,
plot_type="issues",
@@ -1009,12 +1010,12 @@ class TransferCycleIssueEndpoint(BaseAPIView):
).first()
current_cycle.progress_snapshot = {
"total_issues": old_cycle.first().total_issues,
"completed_issues": old_cycle.first().completed_issues,
"cancelled_issues": old_cycle.first().cancelled_issues,
"started_issues": old_cycle.first().started_issues,
"unstarted_issues": old_cycle.first().unstarted_issues,
"backlog_issues": old_cycle.first().backlog_issues,
"total_issues": old_cycle.total_issues,
"completed_issues": old_cycle.completed_issues,
"cancelled_issues": old_cycle.cancelled_issues,
"started_issues": old_cycle.started_issues,
"unstarted_issues": old_cycle.unstarted_issues,
"backlog_issues": old_cycle.backlog_issues,
"distribution": {
"labels": label_distribution_data,
"assignees": assignee_distribution_data,
@@ -1122,6 +1123,14 @@ class CycleUserPropertiesEndpoint(BaseAPIView):
class CycleProgressEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def get(self, request, slug, project_id, cycle_id):
cycle = Cycle.objects.filter(
workspace__slug=slug, project_id=project_id, id=cycle_id
).first()
if not cycle:
return Response(
{"error": "Cycle not found"}, status=status.HTTP_404_NOT_FOUND
)
aggregate_estimates = (
Issue.issue_objects.filter(
estimate_point__estimate__type="points",
@@ -1172,53 +1181,60 @@ class CycleProgressEndpoint(BaseAPIView):
),
)
)
if cycle.progress_snapshot:
backlog_issues = cycle.progress_snapshot.get("backlog_issues", 0)
unstarted_issues = cycle.progress_snapshot.get("unstarted_issues", 0)
started_issues = cycle.progress_snapshot.get("started_issues", 0)
cancelled_issues = cycle.progress_snapshot.get("cancelled_issues", 0)
completed_issues = cycle.progress_snapshot.get("completed_issues", 0)
total_issues = cycle.progress_snapshot.get("total_issues", 0)
else:
backlog_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="backlog",
).count()
backlog_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="backlog",
).count()
unstarted_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="unstarted",
).count()
unstarted_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="unstarted",
).count()
started_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="started",
).count()
started_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="started",
).count()
cancelled_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="cancelled",
).count()
cancelled_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="cancelled",
).count()
completed_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="completed",
).count()
completed_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
state__group="completed",
).count()
total_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
).count()
total_issues = Issue.issue_objects.filter(
issue_cycle__cycle_id=cycle_id,
issue_cycle__deleted_at__isnull=True,
workspace__slug=slug,
project_id=project_id,
).count()
return Response(
{
@@ -1279,6 +1295,25 @@ class CycleAnalyticsEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
# this will tell whether the issues were transferred to the new cycle
"""
if the issues were transferred to the new cycle, then the progress_snapshot will be present
return the progress_snapshot data in the analytics for each date
else issues were not transferred to the new cycle then generate the stats from the cycle isssue bridge tables
"""
if cycle.progress_snapshot:
distribution = cycle.progress_snapshot.get("distribution", {})
return Response(
{
"labels": distribution.get("labels", []),
"assignees": distribution.get("assignees", []),
"completion_chart": distribution.get("completion_chart", {}),
},
status=status.HTTP_200_OK,
)
estimate_type = Project.objects.filter(
workspace__slug=slug,
pk=project_id,
+2 -3
View File
@@ -44,6 +44,7 @@ from plane.app.views.base import BaseAPIView
from plane.utils.timezone_converter import user_timezone_converter
from plane.utils.global_paginator import paginate
from plane.utils.host import base_host
from plane.db.models.intake import SourceType
class IntakeViewSet(BaseViewSet):
@@ -278,7 +279,7 @@ class IntakeIssueViewSet(BaseViewSet):
intake_id=intake_id.id,
project_id=project_id,
issue_id=serializer.data["id"],
source=request.data.get("source", "IN-APP"),
source=SourceType.IN_APP,
)
# Create an Issue Activity
issue_activity.delay(
@@ -408,7 +409,6 @@ class IntakeIssueViewSet(BaseViewSet):
)
if issue_serializer.is_valid():
# Log all the updates
requested_data = json.dumps(issue_data, cls=DjangoJSONEncoder)
if issue is not None:
@@ -607,7 +607,6 @@ class IntakeIssueViewSet(BaseViewSet):
class IntakeWorkItemDescriptionVersionEndpoint(BaseAPIView):
def process_paginated_result(self, fields, results, timezone):
paginated_data = results.values(*fields)
@@ -23,6 +23,7 @@ from plane.bgtasks.issue_activities_task import issue_activity
from plane.utils.timezone_converter import user_timezone_converter
from collections import defaultdict
from plane.utils.host import base_host
from plane.utils.order_queryset import order_issue_queryset
class SubIssuesEndpoint(BaseAPIView):
permission_classes = [ProjectEntityPermission]
@@ -102,6 +103,15 @@ class SubIssuesEndpoint(BaseAPIView):
.order_by("-created_at")
)
# Ordering
order_by_param = request.GET.get("order_by", "-created_at")
group_by = request.GET.get("group_by", False)
if order_by_param:
sub_issues, order_by_param = order_issue_queryset(
sub_issues, order_by_param
)
# create's a dict with state group name with their respective issue id's
result = defaultdict(list)
for sub_issue in sub_issues:
@@ -138,6 +148,26 @@ class SubIssuesEndpoint(BaseAPIView):
sub_issues = user_timezone_converter(
sub_issues, datetime_fields, request.user.user_timezone
)
# Grouping
if group_by:
result_dict = defaultdict(list)
for issue in sub_issues:
if group_by == "assignees__ids":
if issue["assignee_ids"]:
assignee_ids = issue["assignee_ids"]
for assignee_id in assignee_ids:
result_dict[str(assignee_id)].append(issue)
elif issue["assignee_ids"] == []:
result_dict["None"].append(issue)
elif group_by:
result_dict[str(issue[group_by])].append(issue)
return Response(
{"sub_issues": result_dict, "state_distribution": result},
status=status.HTTP_200_OK,
)
return Response(
{"sub_issues": sub_issues, "state_distribution": result},
status=status.HTTP_200_OK,
+13 -5
View File
@@ -710,23 +710,31 @@ class ModuleViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def partial_update(self, request, slug, project_id, pk):
module = self.get_queryset().filter(pk=pk)
module_queryset = self.get_queryset().filter(pk=pk)
if module.first().archived_at:
current_module = module_queryset.first()
if not current_module:
return Response(
{"error": "Module not found"},
status=status.HTTP_404_NOT_FOUND,
)
if current_module.archived_at:
return Response(
{"error": "Archived module cannot be updated"},
status=status.HTTP_400_BAD_REQUEST,
)
current_instance = json.dumps(
ModuleSerializer(module.first()).data, cls=DjangoJSONEncoder
ModuleSerializer(current_module).data, cls=DjangoJSONEncoder
)
serializer = ModuleWriteSerializer(
module.first(), data=request.data, partial=True
current_module, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
module = module.values(
module = module_queryset.values(
# Required fields
"id",
"workspace_id",
+8 -1
View File
@@ -42,6 +42,7 @@ from plane.bgtasks.page_version_task import page_version
from plane.bgtasks.recent_visited_task import recent_visited_task
from plane.bgtasks.copy_s3_object import copy_s3_objects
def unarchive_archive_page_and_descendants(page_id, archived_at):
# Your SQL query
sql = """
@@ -198,7 +199,7 @@ class PageViewSet(BaseViewSet):
project = Project.objects.get(pk=project_id)
"""
if the role is guest and guest_view_all_features is false and owned by is not
if the role is guest and guest_view_all_features is false and owned by is not
the requesting user then dont show the page
"""
@@ -572,6 +573,12 @@ class PageDuplicateEndpoint(BaseAPIView):
pk=page_id, workspace__slug=slug, projects__id=project_id
).first()
# check for permission
if page.access == Page.PRIVATE_ACCESS and page.owned_by_id != request.user.id:
return Response(
{"error": "Permission denied"}, status=status.HTTP_403_FORBIDDEN
)
# get all the project ids where page is present
project_ids = ProjectPage.objects.filter(page_id=page_id).values_list(
"project_id", flat=True
+9 -8
View File
@@ -41,6 +41,7 @@ from plane.bgtasks.recent_visited_task import recent_visited_task
from plane.utils.exception_logger import log_exception
from plane.utils.host import base_host
class ProjectViewSet(BaseViewSet):
serializer_class = ProjectListSerializer
model = Project
@@ -274,14 +275,14 @@ class ProjectViewSet(BaseViewSet):
states = [
{
"name": "Backlog",
"color": "#A3A3A3",
"color": "#60646C",
"sequence": 15000,
"group": "backlog",
"default": True,
},
{
"name": "Todo",
"color": "#3A3A3A",
"color": "#60646C",
"sequence": 25000,
"group": "unstarted",
},
@@ -293,13 +294,13 @@ class ProjectViewSet(BaseViewSet):
},
{
"name": "Done",
"color": "#16A34A",
"color": "#46A758",
"sequence": 45000,
"group": "completed",
},
{
"name": "Cancelled",
"color": "#EF4444",
"color": "#9AA4BC",
"sequence": 55000,
"group": "cancelled",
},
@@ -341,7 +342,7 @@ class ProjectViewSet(BaseViewSet):
if "already exists" in str(e):
return Response(
{"name": "The project name is already taken"},
status=status.HTTP_410_GONE,
status=status.HTTP_409_CONFLICT,
)
except Workspace.DoesNotExist:
return Response(
@@ -350,7 +351,7 @@ class ProjectViewSet(BaseViewSet):
except serializers.ValidationError:
return Response(
{"identifier": "The project identifier is already taken"},
status=status.HTTP_410_GONE,
status=status.HTTP_409_CONFLICT,
)
def partial_update(self, request, slug, pk=None):
@@ -419,7 +420,7 @@ class ProjectViewSet(BaseViewSet):
if "already exists" in str(e):
return Response(
{"name": "The project name is already taken"},
status=status.HTTP_410_GONE,
status=status.HTTP_409_CONFLICT,
)
except (Project.DoesNotExist, Workspace.DoesNotExist):
return Response(
@@ -428,7 +429,7 @@ class ProjectViewSet(BaseViewSet):
except serializers.ValidationError:
return Response(
{"identifier": "The project identifier is already taken"},
status=status.HTTP_410_GONE,
status=status.HTTP_409_CONFLICT,
)
def destroy(self, request, slug, pk):
+14
View File
@@ -1,5 +1,6 @@
# Python imports
from itertools import groupby
from collections import defaultdict
# Django imports
from django.db.utils import IntegrityError
@@ -74,7 +75,19 @@ class StateViewSet(BaseViewSet):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def list(self, request, slug, project_id):
states = StateSerializer(self.get_queryset(), many=True).data
grouped_states = defaultdict(list)
for state in states:
grouped_states[state["group"]].append(state)
for group, group_states in grouped_states.items():
count = len(group_states)
for index, state in enumerate(group_states, start=1):
state["order"] = index / count
grouped = request.GET.get("grouped", False)
if grouped == "true":
state_dict = {}
for key, value in groupby(
@@ -83,6 +96,7 @@ class StateViewSet(BaseViewSet):
):
state_dict[str(key)] = list(value)
return Response(state_dict, status=status.HTTP_200_OK)
return Response(states, status=status.HTTP_200_OK)
@invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False)
+1 -1
View File
@@ -29,7 +29,7 @@ class WebhookEndpoint(BaseAPIView):
if "already exists" in str(e):
return Response(
{"error": "URL already exists for the workspace"},
status=status.HTTP_410_GONE,
status=status.HTTP_409_CONFLICT,
)
raise IntegrityError
+10 -6
View File
@@ -42,6 +42,7 @@ from django.views.decorators.cache import cache_control
from django.views.decorators.vary import vary_on_cookie
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
from plane.license.utils.instance_value import get_configuration_value
from plane.bgtasks.workspace_seed_task import workspace_seed
class WorkSpaceViewSet(BaseViewSet):
@@ -119,11 +120,15 @@ class WorkSpaceViewSet(BaseViewSet):
)
# Get total members and role
total_members=WorkspaceMember.objects.filter(workspace_id=serializer.data["id"]).count()
total_members = WorkspaceMember.objects.filter(
workspace_id=serializer.data["id"]
).count()
data = serializer.data
data["total_members"] = total_members
data["role"] = 20
workspace_seed.delay(serializer.data["id"])
return Response(data, status=status.HTTP_201_CREATED)
return Response(
[serializer.errors[error][0] for error in serializer.errors],
@@ -134,7 +139,7 @@ class WorkSpaceViewSet(BaseViewSet):
if "already exists" in str(e):
return Response(
{"slug": "The workspace with the slug already exists"},
status=status.HTTP_410_GONE,
status=status.HTTP_409_CONFLICT,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
@@ -167,10 +172,9 @@ class UserWorkSpacesEndpoint(BaseAPIView):
.values("count")
)
role = (
WorkspaceMember.objects.filter(workspace=OuterRef("id"), member=request.user, is_active=True)
.values("role")
)
role = WorkspaceMember.objects.filter(
workspace=OuterRef("id"), member=request.user, is_active=True
).values("role")
workspace = (
Workspace.objects.prefetch_related(
@@ -29,6 +29,7 @@ class WorkspaceCyclesEndpoint(BaseAPIView):
issue_cycle__issue__archived_at__isnull=True,
issue_cycle__issue__is_draft=False,
issue_cycle__deleted_at__isnull=True,
issue_cycle__issue__deleted_at__isnull=True,
),
)
)
@@ -8,6 +8,7 @@ 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
class WorkspaceStatesEndpoint(BaseAPIView):
@@ -22,5 +23,16 @@ class WorkspaceStatesEndpoint(BaseAPIView):
project__archived_at__isnull=True,
is_triage=False,
)
grouped_states = defaultdict(list)
for state in states:
grouped_states[state.group].append(state)
for group, group_states in grouped_states.items():
count = len(group_states)
for index, state in enumerate(group_states, start=1):
state.order = index / count
serializer = StateSerializer(states, many=True).data
return Response(serializer, status=status.HTTP_200_OK)
+30 -11
View File
@@ -1,30 +1,49 @@
# Django imports
from django.conf import settings
from django.http import HttpRequest
# Third party imports
from rest_framework.request import Request
# Module imports
from plane.utils.ip_address import get_client_ip
def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space: bool = False, is_app: bool = False) -> str:
def base_host(
request: Request | HttpRequest,
is_admin: bool = False,
is_space: bool = False,
is_app: bool = False,
) -> str:
"""Utility function to return host / origin from the request"""
# Calculate the base origin from request
base_origin = settings.WEB_URL or settings.APP_BASE_URL
# Admin redirections
# Admin redirection
if is_admin:
if settings.ADMIN_BASE_URL:
return settings.ADMIN_BASE_URL
else:
return base_origin + "/god-mode/"
admin_base_path = getattr(settings, "ADMIN_BASE_PATH", "/god-mode/")
if not admin_base_path.startswith("/"):
admin_base_path = "/" + admin_base_path
if not admin_base_path.endswith("/"):
admin_base_path += "/"
# Space redirections
if is_space:
if settings.SPACE_BASE_URL:
return settings.SPACE_BASE_URL
if settings.ADMIN_BASE_URL:
return settings.ADMIN_BASE_URL + admin_base_path
else:
return base_origin + "/spaces/"
return base_origin + admin_base_path
# Space redirection
if is_space:
space_base_path = getattr(settings, "SPACE_BASE_PATH", "/spaces/")
if not space_base_path.startswith("/"):
space_base_path = "/" + space_base_path
if not space_base_path.endswith("/"):
space_base_path += "/"
if settings.SPACE_BASE_URL:
return settings.SPACE_BASE_URL + space_base_path
else:
return base_origin + space_base_path
# App Redirection
if is_app:
+9 -5
View File
@@ -12,6 +12,7 @@ from plane.db.models import FileAsset, Page, Issue
from plane.utils.exception_logger import log_exception
from plane.settings.storage import S3Storage
from celery import shared_task
from plane.utils.url import normalize_url_path
def get_entity_id_field(entity_type, entity_id):
@@ -67,11 +68,14 @@ def sync_with_external_service(entity_name, description_html):
"description_html": description_html,
"variant": "rich" if entity_name == "PAGE" else "document",
}
response = requests.post(
f"{settings.LIVE_BASE_URL}/convert-document/",
json=data,
headers=None,
)
live_url = settings.LIVE_URL
if not live_url:
return {}
url = normalize_url_path(f"{live_url}/convert-document/")
response = requests.post(url, json=data, headers=None)
if response.status_code == 200:
return response.json()
except requests.RequestException as e:
+2 -1
View File
@@ -33,6 +33,7 @@ from plane.db.models import (
Intake,
IntakeIssue,
)
from plane.db.models.intake import SourceType
def create_project(workspace, user_id):
@@ -388,7 +389,7 @@ def create_intake_issues(workspace, project, user_id, intake_issue_count):
if status == 0
else None
),
source="in-app",
source=SourceType.IN_APP,
workspace=workspace,
project=project,
)
@@ -307,6 +307,10 @@ def track_labels(
# Set of newly added labels
for added_label in added_labels:
# validate uuids
if not is_valid_uuid(added_label):
continue
label = Label.objects.get(pk=added_label)
issue_activities.append(
IssueActivity(
@@ -327,6 +331,10 @@ def track_labels(
# Set of dropped labels
for dropped_label in dropped_labels:
# validate uuids
if not is_valid_uuid(dropped_label):
continue
label = Label.objects.get(pk=dropped_label)
issue_activities.append(
IssueActivity(
@@ -373,6 +381,10 @@ def track_assignees(
bulk_subscribers = []
for added_asignee in added_assignees:
# validate uuids
if not is_valid_uuid(added_asignee):
continue
assignee = User.objects.get(pk=added_asignee)
issue_activities.append(
IssueActivity(
@@ -406,6 +418,10 @@ def track_assignees(
)
for dropped_assignee in dropped_assginees:
# validate uuids
if not is_valid_uuid(dropped_assignee):
continue
assignee = User.objects.get(pk=dropped_assignee)
issue_activities.append(
IssueActivity(
@@ -466,7 +482,7 @@ def track_estimate_points(
),
old_value=old_estimate.value if old_estimate else None,
new_value=new_estimate.value if new_estimate else None,
field="estimate_point",
field="estimate_" + new_estimate.estimate.type,
project_id=project_id,
workspace_id=workspace_id,
comment="updated the estimate point to ",
@@ -1634,40 +1650,6 @@ def issue_activity(
# Save all the values to database
issue_activities_created = IssueActivity.objects.bulk_create(issue_activities)
# Post the updates to segway for integrations and webhooks
if len(issue_activities_created):
for activity in issue_activities_created:
webhook_activity.delay(
event=(
"issue_comment"
if activity.field == "comment"
else "intake_issue"
if intake
else "issue"
),
event_id=(
activity.issue_comment_id
if activity.field == "comment"
else intake
if intake
else activity.issue_id
),
verb=activity.verb,
field=(
"description" if activity.field == "comment" else activity.field
),
old_value=(
activity.old_value if activity.old_value != "" else None
),
new_value=(
activity.new_value if activity.new_value != "" else None
),
actor_id=activity.actor_id,
current_site=origin,
slug=activity.workspace.slug,
old_identifier=activity.old_identifier,
new_identifier=activity.new_identifier,
)
if notification:
notifications.delay(
+146 -157
View File
@@ -5,6 +5,7 @@ import logging
import uuid
import requests
from typing import Any, Dict, List, Optional, Union
# Third party imports
from celery import shared_task
@@ -70,150 +71,89 @@ MODEL_MAPPER = {
}
def get_model_data(event, event_id, many=False):
logger = logging.getLogger("plane.worker")
def get_model_data(
event: str, event_id: Union[str, List[str]], many: bool = False
) -> Dict[str, Any]:
"""
Retrieve and serialize model data based on the event type.
Args:
event (str): The type of event/model to retrieve data for
event_id (Union[str, List[str]]): The ID or list of IDs of the model instance(s)
many (bool): Whether to retrieve multiple instances
Returns:
Dict[str, Any]: Serialized model data
Raises:
ValueError: If serializer is not found for the event
ObjectDoesNotExist: If model instance is not found
"""
model = MODEL_MAPPER.get(event)
if many:
queryset = model.objects.filter(pk__in=event_id)
else:
queryset = model.objects.get(pk=event_id)
serializer = SERIALIZER_MAPPER.get(event)
return serializer(queryset, many=many).data
if model is None:
raise ValueError(f"Model not found for event: {event}")
@shared_task(
bind=True,
autoretry_for=(requests.RequestException,),
retry_backoff=600,
max_retries=5,
retry_jitter=True,
)
def webhook_task(self, webhook, slug, event, event_data, action, current_site):
try:
webhook = Webhook.objects.get(id=webhook, workspace__slug=slug)
if many:
queryset = model.objects.filter(pk__in=event_id)
else:
queryset = model.objects.get(pk=event_id)
headers = {
"Content-Type": "application/json",
"User-Agent": "Autopilot",
"X-Plane-Delivery": str(uuid.uuid4()),
"X-Plane-Event": event,
}
serializer = SERIALIZER_MAPPER.get(event)
if serializer is None:
raise ValueError(f"Serializer not found for event: {event}")
# # Your secret key
event_data = (
json.loads(json.dumps(event_data, cls=DjangoJSONEncoder))
if event_data is not None
else None
)
action = {
"POST": "create",
"PATCH": "update",
"PUT": "update",
"DELETE": "delete",
}.get(action, action)
payload = {
"event": event,
"action": action,
"webhook_id": str(webhook.id),
"workspace_id": str(webhook.workspace_id),
"data": event_data,
}
# Use HMAC for generating signature
if webhook.secret_key:
hmac_signature = hmac.new(
webhook.secret_key.encode("utf-8"),
json.dumps(payload).encode("utf-8"),
hashlib.sha256,
)
signature = hmac_signature.hexdigest()
headers["X-Plane-Signature"] = signature
# Send the webhook event
response = requests.post(webhook.url, headers=headers, json=payload, timeout=30)
# Log the webhook request
WebhookLog.objects.create(
workspace_id=str(webhook.workspace_id),
webhook=str(webhook.id),
event_type=str(event),
request_method=str(action),
request_headers=str(headers),
request_body=str(payload),
response_status=str(response.status_code),
response_headers=str(response.headers),
response_body=str(response.text),
retry_count=str(self.request.retries),
)
except Webhook.DoesNotExist:
return
except requests.RequestException as e:
# Log the failed webhook request
WebhookLog.objects.create(
workspace_id=str(webhook.workspace_id),
webhook=str(webhook.id),
event_type=str(event),
request_method=str(action),
request_headers=str(headers),
request_body=str(payload),
response_status=500,
response_headers="",
response_body=str(e),
retry_count=str(self.request.retries),
)
# Retry logic
if self.request.retries >= self.max_retries:
Webhook.objects.filter(pk=webhook.id).update(is_active=False)
if webhook:
# send email for the deactivation of the webhook
send_webhook_deactivation_email(
webhook_id=webhook.id,
receiver_id=webhook.created_by_id,
reason=str(e),
current_site=current_site,
)
return
raise requests.RequestException()
except Exception as e:
if settings.DEBUG:
print(e)
log_exception(e)
return
return serializer(queryset, many=many).data
except ObjectDoesNotExist:
raise ObjectDoesNotExist(f"No {event} found with id: {event_id}")
@shared_task
def send_webhook_deactivation_email(webhook_id, receiver_id, current_site, reason):
# Get email configurations
(
EMAIL_HOST,
EMAIL_HOST_USER,
EMAIL_HOST_PASSWORD,
EMAIL_PORT,
EMAIL_USE_TLS,
EMAIL_USE_SSL,
EMAIL_FROM,
) = get_email_configuration()
receiver = User.objects.get(pk=receiver_id)
webhook = Webhook.objects.get(pk=webhook_id)
subject = "Webhook Deactivated"
message = f"Webhook {webhook.url} has been deactivated due to failed requests."
# Send the mail
context = {
"email": receiver.email,
"message": message,
"webhook_url": f"{current_site}/{str(webhook.workspace.slug)}/settings/webhooks/{str(webhook.id)}",
}
html_content = render_to_string(
"emails/notifications/webhook-deactivate.html", context
)
text_content = strip_tags(html_content)
def send_webhook_deactivation_email(
webhook_id: str, receiver_id: str, current_site: str, reason: str
) -> None:
"""
Send an email notification when a webhook is deactivated.
Args:
webhook_id (str): ID of the deactivated webhook
receiver_id (str): ID of the user to receive the notification
current_site (str): Current site URL
reason (str): Reason for webhook deactivation
"""
try:
(
EMAIL_HOST,
EMAIL_HOST_USER,
EMAIL_HOST_PASSWORD,
EMAIL_PORT,
EMAIL_USE_TLS,
EMAIL_USE_SSL,
EMAIL_FROM,
) = get_email_configuration()
receiver = User.objects.get(pk=receiver_id)
webhook = Webhook.objects.get(pk=webhook_id)
# Get the webhook payload
subject = "Webhook Deactivated"
message = f"Webhook {webhook.url} has been deactivated due to failed requests."
# Send the mail
context = {
"email": receiver.email,
"message": message,
"webhook_url": f"{current_site}/{str(webhook.workspace.slug)}/settings/webhooks/{str(webhook.id)}",
}
html_content = render_to_string(
"emails/notifications/webhook-deactivate.html", context
)
text_content = strip_tags(html_content)
# Set the email connection
connection = get_connection(
host=EMAIL_HOST,
port=int(EMAIL_PORT),
@@ -223,6 +163,7 @@ def send_webhook_deactivation_email(webhook_id, receiver_id, current_site, reaso
use_ssl=EMAIL_USE_SSL == "1",
)
# Create the email message
msg = EmailMultiAlternatives(
subject=subject,
body=text_content,
@@ -232,11 +173,10 @@ def send_webhook_deactivation_email(webhook_id, receiver_id, current_site, reaso
)
msg.attach_alternative(html_content, "text/html")
msg.send()
logging.getLogger("plane").info("Email sent successfully.")
return
logger.info("Email sent successfully.")
except Exception as e:
log_exception(e)
return
logger.error(f"Failed to send email: {e}")
@shared_task(
@@ -247,10 +187,29 @@ def send_webhook_deactivation_email(webhook_id, receiver_id, current_site, reaso
retry_jitter=True,
)
def webhook_send_task(
self, webhook, slug, event, event_data, action, current_site, activity
):
self,
webhook_id: str,
slug: str,
event: str,
event_data: Optional[Dict[str, Any]],
action: str,
current_site: str,
activity: Optional[Dict[str, Any]],
) -> None:
"""
Send webhook notifications to configured endpoints.
Args:
webhook (str): Webhook ID
slug (str): Workspace slug
event (str): Event type
event_data (Optional[Dict[str, Any]]): Event data to be sent
action (str): HTTP method/action
current_site (str): Current site URL
activity (Optional[Dict[str, Any]]): Activity data
"""
try:
webhook = Webhook.objects.get(id=webhook, workspace__slug=slug)
webhook = Webhook.objects.get(id=webhook_id, workspace__slug=slug)
headers = {
"Content-Type": "application/json",
@@ -297,7 +256,12 @@ def webhook_send_task(
)
signature = hmac_signature.hexdigest()
headers["X-Plane-Signature"] = signature
except Exception as e:
log_exception(e)
logger.error(f"Failed to send webhook: {e}")
return
try:
# Send the webhook event
response = requests.post(webhook.url, headers=headers, json=payload, timeout=30)
@@ -314,7 +278,7 @@ def webhook_send_task(
response_body=str(response.text),
retry_count=str(self.request.retries),
)
logger.info(f"Webhook {webhook.id} sent successfully")
except requests.RequestException as e:
# Log the failed webhook request
WebhookLog.objects.create(
@@ -329,12 +293,13 @@ def webhook_send_task(
response_body=str(e),
retry_count=str(self.request.retries),
)
logger.error(f"Webhook {webhook.id} failed with error: {e}")
# Retry logic
if self.request.retries >= self.max_retries:
Webhook.objects.filter(pk=webhook.id).update(is_active=False)
if webhook:
# send email for the deactivation of the webhook
send_webhook_deactivation_email(
send_webhook_deactivation_email.delay(
webhook_id=webhook.id,
receiver_id=webhook.created_by_id,
reason=str(e),
@@ -344,26 +309,50 @@ def webhook_send_task(
raise requests.RequestException()
except Exception as e:
if settings.DEBUG:
print(e)
log_exception(e)
return
@shared_task
def webhook_activity(
event,
verb,
field,
old_value,
new_value,
actor_id,
slug,
current_site,
event_id,
old_identifier,
new_identifier,
):
event: str,
verb: str,
field: Optional[str],
old_value: Any,
new_value: Any,
actor_id: str | uuid.UUID,
slug: str,
current_site: str,
event_id: str | uuid.UUID,
old_identifier: Optional[str],
new_identifier: Optional[str],
) -> None:
"""
Process and send webhook notifications for various activities in the system.
This task filters relevant webhooks based on the event type and sends notifications
to all active webhooks for the workspace.
Args:
event (str): Type of event (project, issue, module, cycle, issue_comment)
verb (str): Action performed (created, updated, deleted)
field (Optional[str]): Name of the field that was changed
old_value (Any): Previous value of the field
new_value (Any): New value of the field
actor_id (str | uuid.UUID): ID of the user who performed the action
slug (str): Workspace slug
current_site (str): Current site URL
event_id (str | uuid.UUID): ID of the event object
old_identifier (Optional[str]): Previous identifier if any
new_identifier (Optional[str]): New identifier if any
Returns:
None
Note:
The function silently returns on ObjectDoesNotExist exceptions to handle
race conditions where objects might have been deleted.
"""
try:
webhooks = Webhook.objects.filter(workspace__slug=slug, is_active=True)
@@ -384,7 +373,7 @@ def webhook_activity(
for webhook in webhooks:
webhook_send_task.delay(
webhook=webhook.id,
webhook_id=webhook.id,
slug=slug,
event=event,
event_data=(
@@ -0,0 +1,319 @@
# Python imports
import os
import json
import time
import uuid
from typing import Dict
import logging
# Django imports
from django.conf import settings
# Third party imports
from celery import shared_task
# Module imports
from plane.db.models import (
Workspace,
WorkspaceMember,
Project,
ProjectMember,
IssueUserProperty,
State,
Label,
Issue,
IssueLabel,
IssueSequence,
IssueActivity,
)
logger = logging.getLogger("plane.worker")
def read_seed_file(filename):
"""
Read a JSON file from the seed directory.
Args:
filename (str): Name of the JSON file to read
Returns:
dict: Contents of the JSON file
"""
file_path = os.path.join(settings.SEED_DIR, "data", filename)
try:
with open(file_path, "r") as file:
return json.load(file)
except FileNotFoundError:
logger.error(f"Seed file {filename} not found in {settings.SEED_DIR}/data")
return None
except json.JSONDecodeError:
logger.error(f"Error decoding JSON from {filename}")
return None
def create_project_and_member(workspace: Workspace) -> Dict[int, uuid.UUID]:
"""Creates a project and associated members for a workspace.
Creates a new project using the workspace name and sets up all necessary
member associations and user properties.
Args:
workspace: The workspace to create the project in
Returns:
A mapping of seed project IDs to actual project IDs
"""
project_seeds = read_seed_file("projects.json")
project_identifier = "".join(ch for ch in workspace.name if ch.isalnum())[:5]
# Create members
workspace_members = WorkspaceMember.objects.filter(workspace=workspace).values(
"member_id", "role"
)
projects_map: Dict[int, uuid.UUID] = {}
if not project_seeds:
logger.warning(
"Task: workspace_seed_task -> No project seeds found. Skipping project creation."
)
return projects_map
for project_seed in project_seeds:
project_id = project_seed.pop("id")
# Remove the name from seed data since we want to use workspace name
project_seed.pop("name", None)
project_seed.pop("identifier", None)
project = Project.objects.create(
**project_seed,
workspace=workspace,
name=workspace.name, # Use workspace name
identifier=project_identifier,
created_by_id=workspace.created_by_id,
)
# Create project members
ProjectMember.objects.bulk_create(
[
ProjectMember(
project=project,
member_id=workspace_member["member_id"],
role=workspace_member["role"],
workspace_id=workspace.id,
created_by_id=workspace.created_by_id,
)
for workspace_member in workspace_members
]
)
# Create issue user properties
IssueUserProperty.objects.bulk_create(
[
IssueUserProperty(
project=project,
user_id=workspace_member["member_id"],
workspace_id=workspace.id,
display_filters={
"group_by": None,
"order_by": "sort_order",
"type": None,
"sub_issue": True,
"show_empty_groups": True,
"layout": "list",
"calendar_date_range": "",
},
created_by_id=workspace.created_by_id,
)
for workspace_member in workspace_members
]
)
# update map
projects_map[project_id] = project.id
logger.info(f"Task: workspace_seed_task -> Project {project_id} created")
return projects_map
def create_project_states(
workspace: Workspace, project_map: Dict[int, uuid.UUID]
) -> Dict[int, uuid.UUID]:
"""Creates states for each project in the workspace.
Args:
workspace: The workspace containing the projects
project_map: Mapping of seed project IDs to actual project IDs
Returns:
A mapping of seed state IDs to actual state IDs
"""
state_seeds = read_seed_file("states.json")
state_map: Dict[int, uuid.UUID] = {}
if not state_seeds:
return state_map
for state_seed in state_seeds:
state_id = state_seed.pop("id")
project_id = state_seed.pop("project_id")
state = State.objects.create(
**state_seed,
project_id=project_map[project_id],
workspace=workspace,
created_by_id=workspace.created_by_id,
)
state_map[state_id] = state.id
logger.info(f"Task: workspace_seed_task -> State {state_id} created")
return state_map
def create_project_labels(
workspace: Workspace, project_map: Dict[int, uuid.UUID]
) -> Dict[int, uuid.UUID]:
"""Creates labels for each project in the workspace.
Args:
workspace: The workspace containing the projects
project_map: Mapping of seed project IDs to actual project IDs
Returns:
A mapping of seed label IDs to actual label IDs
"""
label_seeds = read_seed_file("labels.json")
label_map: Dict[int, uuid.UUID] = {}
if not label_seeds:
return label_map
for label_seed in label_seeds:
label_id = label_seed.pop("id")
project_id = label_seed.pop("project_id")
label = Label.objects.create(
**label_seed,
project_id=project_map[project_id],
workspace=workspace,
created_by_id=workspace.created_by_id,
)
label_map[label_id] = label.id
logger.info(f"Task: workspace_seed_task -> Label {label_id} created")
return label_map
def create_project_issues(
workspace: Workspace,
project_map: Dict[int, uuid.UUID],
states_map: Dict[int, uuid.UUID],
labels_map: Dict[int, uuid.UUID],
) -> None:
"""Creates issues and their associated records for each project.
Creates issues along with their sequences, activities, and label associations.
Args:
workspace: The workspace containing the projects
project_map: Mapping of seed project IDs to actual project IDs
states_map: Mapping of seed state IDs to actual state IDs
labels_map: Mapping of seed label IDs to actual label IDs
"""
issue_seeds = read_seed_file("issues.json")
if not issue_seeds:
return
for issue_seed in issue_seeds:
required_fields = ["id", "labels", "project_id", "state_id"]
# get the values
for field in required_fields:
if field not in issue_seed:
logger.error(
f"Task: workspace_seed_task -> Required field '{field}' missing in issue seed"
)
continue
# get the values
issue_id = issue_seed.pop("id")
labels = issue_seed.pop("labels")
project_id = issue_seed.pop("project_id")
state_id = issue_seed.pop("state_id")
issue = Issue.objects.create(
**issue_seed,
state_id=states_map[state_id],
project_id=project_map[project_id],
workspace=workspace,
created_by_id=workspace.created_by_id,
)
IssueSequence.objects.create(
issue=issue,
project_id=project_map[project_id],
workspace_id=workspace.id,
created_by_id=workspace.created_by_id,
)
IssueActivity.objects.create(
issue=issue,
project_id=project_map[project_id],
workspace_id=workspace.id,
comment="created the issue",
verb="created",
actor_id=workspace.created_by_id,
epoch=time.time(),
)
for label_id in labels:
IssueLabel.objects.create(
issue=issue,
label_id=labels_map[label_id],
project_id=project_map[project_id],
workspace_id=workspace.id,
created_by_id=workspace.created_by_id,
)
logger.info(f"Task: workspace_seed_task -> Issue {issue_id} created")
return
@shared_task
def workspace_seed(workspace_id: uuid.UUID) -> None:
"""Seeds a new workspace with initial project data.
Creates a complete workspace setup including:
- Projects and project members
- Project states
- Project labels
- Issues and their associations
Args:
workspace_id: ID of the workspace to seed
"""
try:
logger.info(f"Task: workspace_seed_task -> Seeding workspace {workspace_id}")
# Get the workspace
workspace = Workspace.objects.get(id=workspace_id)
# Create a project with the same name as workspace
project_map = create_project_and_member(workspace)
# Create project states
state_map = create_project_states(workspace, project_map)
# Create project labels
label_map = create_project_labels(workspace, project_map)
# create project issues
create_project_issues(workspace, project_map, state_map, label_map)
logger.info(
f"Task: workspace_seed_task -> Workspace {workspace_id} seeded successfully"
)
return
except Exception as e:
logger.error(
f"Task: workspace_seed_task -> Failed to seed workspace {workspace_id}: {str(e)}"
)
raise e
+31 -1
View File
@@ -1,8 +1,16 @@
# Python imports
import os
import logging
# Third party imports
from celery import Celery
from plane.settings.redis import redis_instance
from pythonjsonlogger.jsonlogger import JsonFormatter
from celery.signals import after_setup_logger, after_setup_task_logger
from celery.schedules import crontab
# Module imports
from plane.settings.redis import redis_instance
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "plane.settings.production")
@@ -47,6 +55,28 @@ app.conf.beat_schedule = {
},
}
# Setup logging
@after_setup_logger.connect
def setup_loggers(logger, *args, **kwargs):
formatter = JsonFormatter(
'"%(levelname)s %(asctime)s %(module)s %(name)s %(message)s'
)
handler = logging.StreamHandler()
handler.setFormatter(fmt=formatter)
logger.addHandler(handler)
@after_setup_task_logger.connect
def setup_task_loggers(logger, *args, **kwargs):
formatter = JsonFormatter(
'"%(levelname)s %(asctime)s %(module)s %(name)s %(message)s'
)
handler = logging.StreamHandler()
handler.setFormatter(fmt=formatter)
logger.addHandler(handler)
# Load task modules from all registered Django app configs.
app.autodiscover_tasks()
@@ -0,0 +1,31 @@
# Generated by Django 4.2.17 on 2025-03-04 19:29
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
("db", "0092_alter_deprecateddashboardwidget_unique_together_and_more"),
]
operations = [
migrations.AddField(
model_name="page",
name="moved_to_page",
field=models.UUIDField(blank=True, null=True),
),
migrations.AddField(
model_name="page",
name="moved_to_project",
field=models.UUIDField(blank=True, null=True),
),
migrations.AddField(
model_name="pageversion",
name="sub_pages_data",
field=models.JSONField(blank=True, default=dict),
),
]
@@ -0,0 +1,25 @@
# Generated by Django 4.2.17 on 2025-04-25 09:02
from django.db import migrations, models
from plane.db.models.intake import SourceType
def set_default_source_type(apps, schema_editor):
IntakeIssue = apps.get_model("db", "IntakeIssue")
IntakeIssue.objects.filter(source__iexact="in-app").update(source=SourceType.IN_APP)
class Migration(migrations.Migration):
dependencies = [
('db', '0093_page_moved_to_page_page_moved_to_project_and_more'),
]
operations = [
migrations.RunPython(
set_default_source_type,
migrations.RunPython.noop,
),
migrations.AddField(
model_name='profile',
name='start_of_the_week',
field=models.PositiveSmallIntegerField(choices=[(0, 'Sunday'), (1, 'Monday'), (2, 'Tuesday'), (3, 'Wednesday'), (4, 'Thursday'), (5, 'Friday'), (6, 'Saturday')], default=0),
),
]
+1 -1
View File
@@ -82,4 +82,4 @@ from .label import Label
from .device import Device, DeviceSession
from .sticky import Sticky
from .sticky import Sticky
+4
View File
@@ -31,6 +31,10 @@ class Intake(ProjectBaseModel):
ordering = ("name",)
class SourceType(models.TextChoices):
IN_APP = "IN_APP"
class IntakeIssue(ProjectBaseModel):
intake = models.ForeignKey(
"db.Intake", related_name="issue_intake", on_delete=models.CASCADE
+9 -3
View File
@@ -17,6 +17,11 @@ def get_view_props():
class Page(BaseModel):
PRIVATE_ACCESS = 1
PUBLIC_ACCESS = 0
ACCESS_CHOICES = ((PRIVATE_ACCESS, "Private"), (PUBLIC_ACCESS, "Public"))
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="pages"
)
@@ -50,6 +55,8 @@ class Page(BaseModel):
projects = models.ManyToManyField(
"db.Project", related_name="pages", through="db.ProjectPage"
)
moved_to_page = models.UUIDField(null=True, blank=True)
moved_to_project = models.UUIDField(null=True, blank=True)
class Meta:
verbose_name = "Page"
@@ -89,9 +96,7 @@ 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_name = models.CharField(
max_length=30, verbose_name="Transaction Type"
)
entity_name = models.CharField(max_length=30, verbose_name="Transaction Type")
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_page_log"
)
@@ -172,6 +177,7 @@ class PageVersion(BaseModel):
description_html = models.TextField(blank=True, default="<p></p>")
description_stripped = models.TextField(blank=True, null=True)
description_json = models.JSONField(default=dict, blank=True)
sub_pages_data = models.JSONField(default=dict, blank=True)
class Meta:
verbose_name = "Page Version"
+21
View File
@@ -164,6 +164,24 @@ class User(AbstractBaseUser, PermissionsMixin):
class Profile(TimeAuditModel):
SUNDAY = 0
MONDAY = 1
TUESDAY = 2
WEDNESDAY = 3
THURSDAY = 4
FRIDAY = 5
SATURDAY = 6
START_OF_THE_WEEK_CHOICES = (
(SUNDAY, "Sunday"),
(MONDAY, "Monday"),
(TUESDAY, "Tuesday"),
(WEDNESDAY, "Wednesday"),
(THURSDAY, "Thursday"),
(FRIDAY, "Friday"),
(SATURDAY, "Saturday"),
)
id = models.UUIDField(
default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True
)
@@ -194,6 +212,9 @@ class Profile(TimeAuditModel):
mobile_timezone_auto_set = models.BooleanField(default=False)
# language
language = models.CharField(max_length=255, default="en")
start_of_the_week = models.PositiveSmallIntegerField(
choices=START_OF_THE_WEEK_CHOICES, default=SUNDAY
)
class Meta:
verbose_name = "Profile"
@@ -109,5 +109,5 @@ class InstanceWorkSpaceEndpoint(BaseAPIView):
if "already exists" in str(e):
return Response(
{"slug": "The workspace with the slug already exists"},
status=status.HTTP_410_GONE,
status=status.HTTP_409_CONFLICT,
)
@@ -1,40 +0,0 @@
# Module imports
from plane.db.models import APIActivityLog
from plane.utils.ip_address import get_client_ip
class APITokenLogMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request_body = request.body
response = self.get_response(request)
self.process_request(request, response, request_body)
return response
def process_request(self, request, response, request_body):
api_key_header = "X-Api-Key"
api_key = request.headers.get(api_key_header)
# If the API key is present, log the request
if api_key:
try:
APIActivityLog.objects.create(
token_identifier=api_key,
path=request.path,
method=request.method,
query_params=request.META.get("QUERY_STRING", ""),
headers=str(request.headers),
body=(request_body.decode("utf-8") if request_body else None),
response_body=(
response.content.decode("utf-8") if response.content else None
),
response_code=response.status_code,
ip_address=get_client_ip(request=request),
user_agent=request.META.get("HTTP_USER_AGENT", None),
)
except Exception as e:
print(e)
# If the token does not exist, you can decide whether to log this as an invalid attempt
return None
+111
View File
@@ -0,0 +1,111 @@
# Python imports
import logging
import time
# Django imports
from django.http import HttpRequest
# Third party imports
from rest_framework.request import Request
# Module imports
from plane.utils.ip_address import get_client_ip
from plane.db.models import APIActivityLog
api_logger = logging.getLogger("plane.api.request")
class RequestLoggerMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def _should_log_route(self, request: Request | HttpRequest) -> bool:
"""
Determines whether a route should be logged based on the request and status code.
"""
# Don't log health checks
if request.path == "/" and request.method == "GET":
return False
return True
def __call__(self, request):
# get the start time
start_time = time.time()
# Get the response
response = self.get_response(request)
# calculate the duration
duration = time.time() - start_time
# Check if logging is required
log_true = self._should_log_route(request=request)
# If logging is not required, return the response
if not log_true:
return response
user_id = (
request.user.id
if getattr(request, "user")
and getattr(request.user, "is_authenticated", False)
else None
)
user_agent = request.META.get("HTTP_USER_AGENT", "")
# Log the request information
api_logger.info(
f"{request.method} {request.get_full_path()} {response.status_code}",
extra={
"path": request.path,
"method": request.method,
"status_code": response.status_code,
"duration_ms": int(duration * 1000),
"remote_addr": get_client_ip(request),
"user_agent": user_agent,
"user_id": user_id,
},
)
# return the response
return response
class APITokenLogMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request_body = request.body
response = self.get_response(request)
self.process_request(request, response, request_body)
return response
def process_request(self, request, response, request_body):
api_key_header = "X-Api-Key"
api_key = request.headers.get(api_key_header)
# If the API key is present, log the request
if api_key:
try:
APIActivityLog.objects.create(
token_identifier=api_key,
path=request.path,
method=request.method,
query_params=request.META.get("QUERY_STRING", ""),
headers=str(request.headers),
body=(request_body.decode("utf-8") if request_body else None),
response_body=(
response.content.decode("utf-8") if response.content else None
),
response_code=response.status_code,
ip_address=get_client_ip(request=request),
user_agent=request.META.get("HTTP_USER_AGENT", None),
)
except Exception as e:
api_logger.exception(e)
# If the token does not exist, you can decide whether to log this as an invalid attempt
return None
+85
View File
@@ -0,0 +1,85 @@
[
{
"id": 1,
"name": "Welcome to Plane 👋",
"sequence_id": 1,
"description_html": "<p class=\"editor-paragraph-block\">Hey there! This demo project is your playground to get hands-on with Plane. We've set this up so you can click around and see how everything works without worrying about breaking anything.</p><p class=\"editor-paragraph-block\">Each work item is designed to make you familiar with the basics of using Plane. Just follow along card by card at your own pace.</p><p class=\"editor-paragraph-block\">First thing to try</p><ol class=\"list-decimal pl-7 space-y-[--list-spacing-y] tight\" data-tight=\"true\"><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Look in the <strong>Properties</strong> section below where it says <strong>State: Todo</strong>.</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Click on it and change it to <strong>Done</strong> from the dropdown. Alternatively, you can drag and drop the card to the Done column.</p></li></ol>",
"description_stripped": "Hey there! This demo project is your playground to get hands-on with Plane. We've set this up so you can click around and see how everything works without worrying about breaking anything.Each work item is designed to make you familiar with the basics of using Plane. Just follow along card by card at your own pace.First thing to tryLook in the Properties section below where it says State: Todo.Click on it and change it to Done from the dropdown. Alternatively, you can drag and drop the card to the Done column.",
"sort_order": 1000,
"state_id": 3,
"labels": [],
"priority": "none",
"project_id": 1
},
{
"id": 2,
"name": "1. Create Projects 🎯",
"sequence_id": 2,
"description_html": "<p class=\"editor-paragraph-block\"><br>A Project in Plane is where all your work comes together. Think of it as a base that organizes your work items and everything else your team needs to get things done.</p><div data-emoji-unicode=\"128204\" data-emoji-url=\"https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f4cc.png\" data-logo-in-use=\"emoji\" data-background=\"light-blue\" data-block-type=\"callout-component\"><p class=\"editor-paragraph-block\"><strong>Note: </strong>This tutorial is already set up as a Project, and these cards you're reading are work items within it!</p><p class=\"editor-paragraph-block\">We're showing you how to create a new project just so you'll know exactly what to do when you're ready to start your own real one.</p></div><ol class=\"list-decimal pl-7 space-y-[--list-spacing-y] tight\" data-tight=\"true\"><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Look over at the left sidebar and find where it says <strong>Projects.</strong></p><image-component src=\"https://media.docs.plane.so/seed_assets/21.png\" width=\"395px\" height=\"362px\" id=\"7cb0d276-8686-4c8e-9f00-06a18140964d\" aspectratio=\"1.0900243309002433\"></image-component></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Hover your mouse there and you'll see a little <strong>+</strong> icon pop up - go ahead and click it!</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">A modal opens where you can give your project a name and other details.</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Notice the Access type<strong> </strong>options? <strong>Public</strong> means anyone (except Guest users) can see and join it, while <strong>Private</strong> keeps it just for those you invite.</p><div data-icon-color=\"#6d7b8a\" data-icon-name=\"Info\" data-emoji-unicode=\"128161\" data-emoji-url=\"https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f4a1.png\" data-logo-in-use=\"emoji\" data-background=\"green\" data-block-type=\"callout-component\"><p class=\"editor-paragraph-block\"><strong>Tip:</strong> You can also quickly create a new project by using the keyboard shortcut <strong>P</strong> from anywhere in Plane!</p></div></li></ol>",
"sort_order": 2000,
"state_id": 2,
"labels": [2],
"priority": "none",
"project_id": 1
},
{
"id": 3,
"name": "2. Invite your team 🤜🤛",
"sequence_id": 3,
"description_html": "<p class=\"editor-paragraph-block\">Let's get your teammates on board!</p><p class=\"editor-paragraph-block\">First, you'll need to invite them to your workspace before they can join specific projects:</p><ol class=\"list-decimal pl-7 space-y-[--list-spacing-y] tight\" data-tight=\"true\"><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Click on your workspace name in the top-left corner, then select <strong>Settings</strong> from the dropdown.<br></p><image-component src=\"https://media.docs.plane.so/seed_assets/31.png\" width=\"395px\" height=\"367px\" id=\"26b0f613-b9d8-48b8-a10d-1a75501f19e0\" aspectratio=\"1.074766355140187\"></image-component></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Head over to the <strong>Members</strong> tab - this is your user management hub. Click <strong>Add member</strong> on the top right.<br></p><image-component src=\"https://media.docs.plane.so/seed_assets/32.png\" width=\"1144.380859375px\" height=\"206.3244316692872px\" id=\"7c64e9b0-4f6d-4958-917d-f77119cd48bd\" aspectratio=\"5.546511627906977\"></image-component></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Enter your teammate's email address. Select a role for them (Admin, Member or Guest) that determines what they can do in the workspace.</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Your team member will get an email invite. Once they've joined your workspace, you can add them to specific projects.</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">To do this, go to your project's <strong>Settings</strong> page.</p><image-component src=\"https://media.docs.plane.so/seed_assets/33.png\" width=\"1119.380859375px\" height=\"329.9601265352615px\" id=\"3029c693-19fc-458e-9f5c-fdf3511dd2b6\" aspectratio=\"3.39247311827957\"></image-component></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Find the <strong>Members</strong> section, select your teammate, and assign them a project role - this controls what they can do within just this project.</p></li></ol><p class=\"editor-paragraph-block\"><br>That's it!</p><div class=\"py-4 border-custom-border-400\" data-type=\"horizontalRule\"><div></div></div><p class=\"editor-paragraph-block\">To learn more about user management, see <a target=\"_blank\" rel=\"noopener noreferrer nofollow\" class=\"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer\" href=\"https://docs.plane.so/core-concepts/workspaces/members\">Manage users and roles</a>.</p>",
"description_stripped": "Let's get your teammates on board!First, you'll need to invite them to your workspace before they can join specific projects:Click on your workspace name in the top-left corner, then select Settings from the dropdown.Head over to the Members tab - this is your user management hub. Click Add member on the top right.Enter your teammate's email address. Select a role for them (Admin, Member or Guest) that determines what they can do in the workspace.Your team member will get an email invite. Once they've joined your workspace, you can add them to specific projects.To do this, go to your project's Settings page.Find the Members section, select your teammate, and assign them a project role - this controls what they can do within just this project.That's it!To learn more about user management, see Manage users and roles.",
"sort_order": 3000,
"state_id": 1,
"labels": [],
"priority": "none",
"project_id": 1
},
{
"id": 4,
"name": "3. Create and assign Work Items ✏️",
"sequence_id": 4,
"description_html": "<p class=\"editor-paragraph-block\">A work item is the fundamental building block of your project. Think of these as the actionable tasks that move your project forward.</p><p class=\"editor-paragraph-block\">Ready to add something to your project's to-do list? Here's how:</p><ol class=\"list-decimal pl-7 space-y-[--list-spacing-y] tight\" data-tight=\"true\"><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Click the <strong>Add work item</strong> button in the top-right corner of the Work Items page.</p><image-component src=\"https://media.docs.plane.so/seed_assets/41.png\" width=\"1085.380859375px\" height=\"482.53758375605696px\" id=\"ba055bc3-4162-4750-9ad4-9434fc0e7121\" aspectratio=\"2.249318801089918\"></image-component></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Give your task a clear title and add any details in the description.</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Set up the essentials:</p><ul class=\"list-disc pl-7 space-y-[--list-spacing-y] tight\" data-tight=\"true\"><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Assign it to a team member (or yourself!)</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Choose a priority level</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Add start and due dates if there's a timeline</p></li></ul></li></ol><div data-emoji-unicode=\"128161\" data-emoji-url=\"https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f4a1.png\" data-logo-in-use=\"emoji\" data-background=\"green\" data-block-type=\"callout-component\"><p class=\"editor-paragraph-block\"><strong>Tip:</strong> Save time by using the keyboard shortcut <strong>C</strong> from anywhere in your project to quickly create a new work item!</p></div><div class=\"py-4 border-custom-border-400\" data-type=\"horizontalRule\"><div></div></div><p class=\"editor-paragraph-block\">Want to dive deeper into all the things you can do with work items? Check out our <a target=\"_blank\" rel=\"noopener noreferrer nofollow\" class=\"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer\" href=\"https://docs.plane.so/core-concepts/issues/overview\">documentation</a>.</p>",
"description_stripped": "A work item is the fundamental building block of your project. Think of these as the actionable tasks that move your project forward.Ready to add something to your project's to-do list? Here's how:Click the Add work item button in the top-right corner of the Work Items page.Give your task a clear title and add any details in the description.Set up the essentials:Assign it to a team member (or yourself!)Choose a priority levelAdd start and due dates if there's a timelineTip: Save time by using the keyboard shortcut C from anywhere in your project to quickly create a new work item!Want to dive deeper into all the things you can do with work items? Check out our documentation.",
"sort_order": 4000,
"state_id": 1,
"labels": [2],
"priority": "none",
"project_id": 1
},
{
"id": 5,
"name": "4. Visualize your work 🔮",
"sequence_id": 5,
"description_html": "<p class=\"editor-paragraph-block\">Plane offers multiple ways to look at your work items depending on what you need to see. Let's explore how to change views and customize them!</p><image-component src=\"https://media.docs.plane.so/seed_assets/51.png\" aspectratio=\"4.489130434782608\"></image-component><h2 class=\"editor-heading-block\">Switch between layouts</h2><ol class=\"list-decimal pl-7 space-y-[--list-spacing-y] tight\" data-tight=\"true\"><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Look at the top toolbar in your project. You'll see several layout icons.</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Click any of these icons to instantly switch between layouts.</p></li></ol><div data-emoji-unicode=\"128161\" data-emoji-url=\"https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f4a1.png\" data-logo-in-use=\"emoji\" data-background=\"green\" data-block-type=\"callout-component\"><p class=\"editor-paragraph-block\"><strong>Tip:</strong> Different layouts work best for different needs. Try Board view for tracking progress, Calendar for deadline management, and Gantt for timeline planning! See <a target=\"_blank\" rel=\"noopener noreferrer nofollow\" class=\"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer\" href=\"https://docs.plane.so/core-concepts/issues/layouts\"><strong>Layouts</strong></a> for more info.</p></div><h2 class=\"editor-heading-block\">Filter and display options</h2><p class=\"editor-paragraph-block\">Need to focus on specific work?</p><ol class=\"list-decimal pl-7 space-y-[--list-spacing-y] tight\" data-tight=\"true\"><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Click the <strong>Filters</strong> dropdown in the toolbar. Select criteria and choose which items to show.</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Click the <strong>Display</strong> dropdown to tailor how the information appears in your layout</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Created the perfect setup? Save it for later by clicking the the <strong>Save View</strong> button.</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Access saved views anytime from the <strong>Views</strong> section in your sidebar.</p></li></ol>",
"description_stripped": "Plane offers multiple ways to look at your work items depending on what you need to see. Let's explore how to change views and customize them!Switch between layoutsLook at the top toolbar in your project. You'll see several layout icons.Click any of these icons to instantly switch between layouts.Tip: Different layouts work best for different needs. Try Board view for tracking progress, Calendar for deadline management, and Gantt for timeline planning! See Layouts for more info.Filter and display optionsNeed to focus on specific work?Click the Filters dropdown in the toolbar. Select criteria and choose which items to show.Click the Display dropdown to tailor how the information appears in your layoutCreated the perfect setup? Save it for later by clicking the the Save View button.Access saved views anytime from the Views section in your sidebar.",
"sort_order": 5000,
"state_id": 1,
"labels": [],
"priority": "none",
"project_id": 1
},
{
"id": 6,
"name": "5. Use Cycles to time box tasks 🗓️",
"sequence_id": 6,
"description_html": "<p class=\"editor-paragraph-block\">A Cycle in Plane is like a sprint - a dedicated timeframe where your team focuses on completing specific work items. It helps you break down your project into manageable chunks with clear start and end dates so everyone knows what to work on and when it needs to be done.</p><h2 class=\"editor-heading-block\"><strong>Setup Cycles</strong></h2><ol class=\"list-decimal pl-7 space-y-[--list-spacing-y] tight\" data-tight=\"true\"><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Go to the <strong>Cycles</strong> section in your project (you can find it in the left sidebar)</p><image-component src=\"https://media.docs.plane.so/seed_assets/61.png\" width=\"1144.380859375px\" height=\"341.8747850334119px\" id=\"9c3aea94-703a-4d4c-8c39-4201e994711d\" aspectratio=\"3.3473684210526318\"></image-component></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Click the <strong>Add cycle </strong>button in the top-right corner</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Enter details and set the start and end dates for your cycle.</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Click <strong>Create cycle</strong> and you're ready to go!</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Add existing work items to the Cycle or create new ones.</p></li></ol><div data-emoji-unicode=\"128161\" data-emoji-url=\"https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f4a1.png\" data-logo-in-use=\"emoji\" data-background=\"green\" data-block-type=\"callout-component\"><p class=\"editor-paragraph-block\"><strong>Tip:</strong> To create a new Cycle quickly, just press <code class=\"rounded bg-custom-background-80 px-[6px] py-[1.5px] font-mono font-medium text-orange-500 border-[0.5px] border-custom-border-200\" spellcheck=\"false\">Q</code> from anywhere in your project!</p></div><div class=\"py-4 border-custom-border-400\" data-type=\"horizontalRule\"><div></div></div><p class=\"editor-paragraph-block\">Want to learn more?</p><ul class=\"list-disc pl-7 space-y-[--list-spacing-y] tight\" data-tight=\"true\"><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Starting and stopping cycles</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Transferring work items between cycles</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Tracking progress with charts</p></li></ul><p class=\"editor-paragraph-block\">Check out our <a target=\"_blank\" rel=\"noopener noreferrer nofollow\" class=\"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer\" href=\"https://docs.plane.so/core-concepts/cycles\">detailed documentation</a> for everything you need to know!</p>",
"description_stripped": "A Cycle in Plane is like a sprint - a dedicated timeframe where your team focuses on completing specific work items. It helps you break down your project into manageable chunks with clear start and end dates so everyone knows what to work on and when it needs to be done.Setup CyclesGo to the Cycles section in your project (you can find it in the left sidebar)Click the Add cycle button in the top-right cornerEnter details and set the start and end dates for your cycle.Click Create cycle and you're ready to go!Add existing work items to the Cycle or create new ones.Tip: To create a new Cycle quickly, just press Q from anywhere in your project!Want to learn more?Starting and stopping cyclesTransferring work items between cyclesTracking progress with chartsCheck out our detailed documentation for everything you need to know!",
"sort_order": 6000,
"state_id": 1,
"labels": [2],
"priority": "none",
"project_id": 1
},
{
"id": 7,
"name": "6. Customize your settings ⚙️",
"sequence_id": 7,
"description_html": "<p class=\"editor-paragraph-block\">Now that you're getting familiar with Plane, let's explore how you can customize settings to make it work just right for you and your team!</p><h2 class=\"editor-heading-block\">Workspace settings</h2><p class=\"editor-paragraph-block\">Remember those workspace settings we mentioned when inviting team members? There's a lot more you can do there:</p><ul class=\"list-disc pl-7 space-y-[--list-spacing-y] tight\" data-tight=\"true\"><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Invite and manage workspace members</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Upgrade plans and manage billing</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Import data from other tools</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Export your data</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Manage integrations</p></li></ul><h2 class=\"editor-heading-block\">Project Settings</h2><p class=\"editor-paragraph-block\">Each project has its own settings where you can:</p><ul class=\"list-disc pl-7 space-y-[--list-spacing-y] tight\" data-tight=\"true\"><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Change project details and visibility</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Invite specific members to just this project</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Customize your workflow States (like adding a \"Testing\" state)</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Create and organize Labels</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Enable or disable features you need (or don't need)</p></li></ul><h2 class=\"editor-heading-block\">Your Profile Settings</h2><p class=\"editor-paragraph-block\">You can also customize your own personal experience! Click on your profile icon in the top-right corner to find:</p><ul class=\"list-disc pl-7 space-y-[--list-spacing-y] tight\" data-tight=\"true\"><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Profile settings (update your name, photo, etc.)</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Choose your timezone and preferred language for the interface</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Email notification preferences (what you want to be alerted about)</p></li><li class=\"not-prose space-y-2\"><p class=\"editor-paragraph-block\">Appearance settings (light/dark mode)<br></p></li></ul><p class=\"editor-paragraph-block\">Taking a few minutes to set things up just the way you like will make your everyday Plane experience much smoother!</p><div data-emoji-unicode=\"128278\" data-emoji-url=\"https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f516.png\" data-logo-in-use=\"emoji\" data-background=\"green\" data-block-type=\"callout-component\"><p class=\"editor-paragraph-block\"><strong>Note:</strong> Some settings are only available to workspace or project admins. If you don't see certain options, you might need admin access.</p></div><p class=\"editor-paragraph-block\"></p><div class=\"py-4 border-custom-border-400\" data-type=\"horizontalRule\"><div></div></div><p class=\"editor-paragraph-block\"></p>",
"description_stripped": "Now that you're getting familiar with Plane, let's explore how you can customize settings to make it work just right for you and your team!Workspace settingsRemember those workspace settings we mentioned when inviting team members? There's a lot more you can do there:Invite and manage workspace membersUpgrade plans and manage billingImport data from other toolsExport your dataManage integrationsProject SettingsEach project has its own settings where you can:Change project details and visibilityInvite specific members to just this projectCustomize your workflow States (like adding a \"Testing\" state)Create and organize LabelsEnable or disable features you need (or don't need)Your Profile SettingsYou can also customize your own personal experience! Click on your profile icon in the top-right corner to find:Profile settings (update your name, photo, etc.)Choose your timezone and preferred language for the interfaceEmail notification preferences (what you want to be alerted about)Appearance settings (light/dark mode)Taking a few minutes to set things up just the way you like will make your everyday Plane experience much smoother!Note: Some settings are only available to workspace or project admins. If you don't see certain options, you might need admin access.",
"sort_order": 7000,
"state_id": 1,
"labels": [],
"priority": "none",
"project_id": 1
}
]
+16
View File
@@ -0,0 +1,16 @@
[
{
"id": 1,
"name": "admin",
"color": "#0693e3",
"sort_order": 85535,
"project_id": 1
},
{
"id": 2,
"name": "concepts",
"color": "#9900ef",
"sort_order": 95535,
"project_id": 1
}
]
+17
View File
@@ -0,0 +1,17 @@
[
{
"id": 1,
"name": "Plane Demo Project",
"identifier": "PDP",
"description": "Welcome to the Plane Demo Project! This project throws you into the drivers seat of Plane, work management software. Through curated work items, youll uncover key features, pick up best practices, and see how Plane can streamline your teams workflow. Whether youre a startup hungry to scale or an enterprise sharpening efficiency, this demo is your launchpad to mastering Plane. Jump in and see what it can do!",
"network": 2,
"cover_image": "https://images.unsplash.com/photo-1691230995681-480d86cbc135?auto=format&fit=crop&q=80&ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&w=870&q=80",
"logo_props": {
"emoji": {
"url": "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/1f447.png",
"value": "128071"
},
"in_use": "emoji"
}
}
]
+47
View File
@@ -0,0 +1,47 @@
[
{
"id": 1,
"name": "Backlog",
"color": "#A3A3A3",
"sequence": 15000,
"group": "backlog",
"default": true,
"project_id": 1
},
{
"id": 2,
"name": "Todo",
"color": "#3A3A3A",
"sequence": 25000,
"group": "unstarted",
"default": false,
"project_id": 1
},
{
"id": 3,
"name": "In Progress",
"color": "#F59E0B",
"sequence": 35000,
"group": "started",
"default": false,
"project_id": 1
},
{
"id": 4,
"name": "Done",
"color": "#16A34A",
"sequence": 45000,
"group": "completed",
"default": false,
"project_id": 1
},
{
"id": 5,
"name": "Cancelled",
"color": "#EF4444",
"sequence": 55000,
"group": "cancelled",
"default": false,
"project_id": 1
}
]
+53 -5
View File
@@ -3,7 +3,7 @@
# Python imports
import os
from urllib.parse import urlparse
from urllib.parse import urljoin
# Third party imports
import dj_database_url
@@ -13,6 +13,10 @@ from django.core.management.utils import get_random_secret_key
from corsheaders.defaults import default_headers
# Module imports
from plane.utils.url import is_valid_url
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Secret Key
@@ -58,7 +62,8 @@ MIDDLEWARE = [
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"crum.CurrentRequestUserMiddleware",
"django.middleware.gzip.GZipMiddleware",
"plane.middleware.api_log_middleware.APITokenLogMiddleware",
"plane.middleware.logger.APITokenLogMiddleware",
"plane.middleware.logger.RequestLoggerMiddleware",
]
# Rest Framework settings
@@ -309,11 +314,35 @@ CSRF_TRUSTED_ORIGINS = cors_allowed_origins
CSRF_COOKIE_DOMAIN = os.environ.get("COOKIE_DOMAIN", None)
CSRF_FAILURE_VIEW = "plane.authentication.views.common.csrf_failure"
# Base URLs
###### Base URLs ######
# Admin Base URL
ADMIN_BASE_URL = os.environ.get("ADMIN_BASE_URL", None)
if ADMIN_BASE_URL and not is_valid_url(ADMIN_BASE_URL):
ADMIN_BASE_URL = None
ADMIN_BASE_PATH = os.environ.get("ADMIN_BASE_PATH", "/god-mode/")
# Space Base URL
SPACE_BASE_URL = os.environ.get("SPACE_BASE_URL", None)
APP_BASE_URL = os.environ.get("APP_BASE_URL")
LIVE_BASE_URL = os.environ.get("LIVE_BASE_URL")
if SPACE_BASE_URL and not is_valid_url(SPACE_BASE_URL):
SPACE_BASE_URL = None
SPACE_BASE_PATH = os.environ.get("SPACE_BASE_PATH", "/spaces/")
# App Base URL
APP_BASE_URL = os.environ.get("APP_BASE_URL", None)
if APP_BASE_URL and not is_valid_url(APP_BASE_URL):
APP_BASE_URL = None
APP_BASE_PATH = os.environ.get("APP_BASE_PATH", "/")
# Live Base URL
LIVE_BASE_URL = os.environ.get("LIVE_BASE_URL", None)
if LIVE_BASE_URL and not is_valid_url(LIVE_BASE_URL):
LIVE_BASE_URL = None
LIVE_BASE_PATH = os.environ.get("LIVE_BASE_PATH", "/live/")
LIVE_URL = urljoin(LIVE_BASE_URL, LIVE_BASE_PATH) if LIVE_BASE_URL else None
# WEB URL
WEB_URL = os.environ.get("WEB_URL")
HARD_DELETE_AFTER_DAYS = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 60))
@@ -371,9 +400,21 @@ ATTACHMENT_MIME_TYPES = [
"video/x-ms-wmv",
# Archives
"application/zip",
"application/x-rar",
"application/x-rar-compressed",
"application/x-tar",
"application/gzip",
"application/x-zip",
"application/x-zip-compressed",
"application/x-7z-compressed",
"application/x-compressed",
"application/x-compressed-tar",
"application/x-compressed-tar-gz",
"application/x-compressed-tar-bz2",
"application/x-compressed-tar-zip",
"application/x-compressed-tar-7z",
"application/x-compressed-tar-rar",
"application/x-compressed-tar-zip",
# 3D Models
"model/gltf-binary",
"model/gltf+json",
@@ -390,4 +431,11 @@ ATTACHMENT_MIME_TYPES = [
"text/xml",
"text/csv",
"application/xml",
# SQL
"application/x-sql",
# Gzip
"application/x-gzip",
]
# Seed directory path
SEED_DIR = os.path.join(BASE_DIR, "seeds")
+21 -6
View File
@@ -37,26 +37,41 @@ if not os.path.exists(LOG_DIR):
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"disable_existing_loggers": True,
"formatters": {
"verbose": {
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
"style": "{",
}
},
"json": {
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
"fmt": "%(levelname)s %(asctime)s %(module)s %(name)s %(message)s",
},
},
"handlers": {
"console": {
"level": "DEBUG",
"class": "logging.StreamHandler",
"formatter": "verbose",
"formatter": "json",
}
},
"loggers": {
"django.request": {
"plane.api.request": {
"level": "INFO",
"handlers": ["console"],
"propagate": False,
},
"plane.api": {"level": "INFO", "handlers": ["console"], "propagate": False},
"plane.worker": {"level": "INFO", "handlers": ["console"], "propagate": False},
"plane.exception": {
"level": "ERROR",
"handlers": ["console"],
"propagate": False,
},
"plane.external": {
"level": "INFO",
"handlers": ["console"],
"level": "DEBUG",
"propagate": False,
},
"plane": {"handlers": ["console"], "level": "DEBUG", "propagate": False},
},
}
+22 -9
View File
@@ -26,11 +26,10 @@ if not os.path.exists(LOG_DIR):
# Logging configuration
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"disable_existing_loggers": True,
"formatters": {
"verbose": {
"format": "{levelname} {asctime} {module} {process:d} {thread:d} {message}",
"style": "{",
"format": "%(asctime)s [%(process)d] %(levelname)s %(name)s: %(message)s"
},
"json": {
"()": "pythonjsonlogger.jsonlogger.JsonFormatter",
@@ -40,7 +39,7 @@ LOGGING = {
"handlers": {
"console": {
"class": "logging.StreamHandler",
"formatter": "verbose",
"formatter": "json",
"level": "INFO",
},
"file": {
@@ -59,16 +58,30 @@ LOGGING = {
},
},
"loggers": {
"django": {"handlers": ["console", "file"], "level": "INFO", "propagate": True},
"django.request": {
"handlers": ["console", "file"],
"level": "INFO",
"plane.api.request": {
"level": "DEBUG" if DEBUG else "INFO",
"handlers": ["console"],
"propagate": False,
},
"plane": {
"plane.api": {
"level": "DEBUG" if DEBUG else "INFO",
"handlers": ["console"],
"propagate": False,
},
"plane.worker": {
"level": "DEBUG" if DEBUG else "INFO",
"handlers": ["console"],
"propagate": False,
},
"plane.exception": {
"level": "DEBUG" if DEBUG else "ERROR",
"handlers": ["console", "file"],
"propagate": False,
},
"plane.external": {
"level": "INFO",
"handlers": ["console"],
"propagate": False,
},
},
}
+25 -3
View File
@@ -3,6 +3,9 @@ from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db.models import Q, UUIDField, Value, F, Case, When, JSONField, CharField
from django.db.models.functions import Coalesce, JSONObject, Concat
from django.db.models import QuerySet
from typing import List, Optional, Dict, Any, Union
# Module imports
from plane.db.models import (
@@ -17,13 +20,25 @@ from plane.db.models import (
)
def issue_queryset_grouper(queryset, group_by, sub_group_by):
def issue_queryset_grouper(
queryset: QuerySet[Issue], group_by: Optional[str], sub_group_by: Optional[str]
) -> QuerySet[Issue]:
FIELD_MAPPER = {
"label_ids": "labels__id",
"assignee_ids": "assignees__id",
"module_ids": "issue_module__module_id",
}
GROUP_FILTER_MAPPER = {
"assignees__id": Q(issue_assignee__deleted_at__isnull=True),
"labels__id": Q(label_issue__deleted_at__isnull=True),
"issue_module__module_id": Q(issue_module__deleted_at__isnull=True),
}
for group_key in [group_by, sub_group_by]:
if group_key in GROUP_FILTER_MAPPER:
queryset = queryset.filter(GROUP_FILTER_MAPPER[group_key])
annotations_map = {
"assignee_ids": (
"assignees__id",
@@ -50,7 +65,9 @@ def issue_queryset_grouper(queryset, group_by, sub_group_by):
return queryset.annotate(**default_annotations)
def issue_on_results(issues, group_by, sub_group_by):
def issue_on_results(
issues: QuerySet[Issue], group_by: Optional[str], sub_group_by: Optional[str]
) -> List[Dict[str, Any]]:
FIELD_MAPPER = {
"labels__id": "label_ids",
"assignees__id": "assignee_ids",
@@ -160,7 +177,12 @@ def issue_on_results(issues, group_by, sub_group_by):
return issues
def issue_group_values(field, slug, project_id=None, filters=dict):
def issue_group_values(
field: str,
slug: str,
project_id: Optional[str] = None,
filters: Dict[str, Any] = {},
) -> List[Union[str, Any]]:
if field == "state_id":
queryset = State.objects.filter(
is_triage=False, workspace__slug=slug
+1 -1
View File
@@ -96,7 +96,7 @@ class EntityAssetEndpoint(BaseAPIView):
if type not in allowed_types:
return Response(
{
"error": "Invalid file type. Only JPEG and PNG files are allowed.",
"error": "Invalid file type. Only JPEG, PNG, WebP, JPG and GIF files are allowed.",
"status": False,
},
status=status.HTTP_400_BAD_REQUEST,
+2 -1
View File
@@ -21,6 +21,7 @@ from plane.app.serializers import (
)
from plane.utils.issue_filters import issue_filters
from plane.bgtasks.issue_activities_task import issue_activity
from plane.db.models.intake import SourceType
class IntakeIssuePublicViewSet(BaseViewSet):
@@ -156,7 +157,7 @@ class IntakeIssuePublicViewSet(BaseViewSet):
intake_id=intake_id,
project_id=project_deploy_board.project_id,
issue=issue,
source=request.data.get("source", "IN-APP"),
source=SourceType.IN_APP,
)
serializer = IssueStateIntakeSerializer(issue)
+1 -1
View File
@@ -41,4 +41,4 @@ class WorkSpaceCreateReadUpdateDelete(AuthenticatedAPITest):
response = self.client.post(
url, {"name": "Plane", "slug": "pla-ne"}, format="json"
)
self.assertEqual(response.status_code, status.HTTP_410_GONE)
self.assertEqual(response.status_code, status.HTTP_409_CONFLICT)
+6 -2
View File
@@ -140,7 +140,9 @@ def burndown_plot(queryset, slug, project_id, plot_type, cycle_id=None, module_i
# Get all dates between the two dates
date_range = [
(queryset.start_date + timedelta(days=x)).date()
for x in range((queryset.end_date - queryset.start_date).days + 1)
for x in range(
(queryset.end_date.date() - queryset.start_date.date()).days + 1
)
]
else:
date_range = []
@@ -180,7 +182,9 @@ def burndown_plot(queryset, slug, project_id, plot_type, cycle_id=None, module_i
# Get all dates between the two dates
date_range = [
(queryset.start_date + timedelta(days=x))
for x in range((queryset.target_date - queryset.start_date).days + 1)
for x in range(
(queryset.target_date - queryset.start_date).days + 1
)
]
chart_data = {str(date): 0 for date in date_range}
+2 -2
View File
@@ -8,8 +8,8 @@ from django.conf import settings
def log_exception(e):
# Log the error
logger = logging.getLogger("plane")
logger.error(e)
logger = logging.getLogger("plane.exception")
logger.exception(e)
if settings.DEBUG:
# Print the traceback if in debug mode
+59 -31
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
from django.db.models import Q, UUIDField, Value, QuerySet
from django.db.models.functions import Coalesce
# Module imports
@@ -15,16 +15,31 @@ from plane.db.models import (
State,
WorkspaceMember,
)
from typing import Optional, Dict, Tuple, Any, Union, List
def issue_queryset_grouper(queryset, group_by, sub_group_by):
FIELD_MAPPER = {
def issue_queryset_grouper(
queryset: QuerySet[Issue],
group_by: Optional[str],
sub_group_by: Optional[str],
) -> QuerySet[Issue]:
FIELD_MAPPER: Dict[str, str] = {
"label_ids": "labels__id",
"assignee_ids": "assignees__id",
"module_ids": "issue_module__module_id",
}
annotations_map = {
GROUP_FILTER_MAPPER: Dict[str, Q] = {
"assignees__id": Q(issue_assignee__deleted_at__isnull=True),
"labels__id": Q(label_issue__deleted_at__isnull=True),
"issue_module__module_id": Q(issue_module__deleted_at__isnull=True),
}
for group_key in [group_by, sub_group_by]:
if group_key in GROUP_FILTER_MAPPER:
queryset = queryset.filter(GROUP_FILTER_MAPPER[group_key])
annotations_map: Dict[str, Tuple[str, Q]] = {
"assignee_ids": (
"assignees__id",
~Q(assignees__id__isnull=True) & Q(issue_assignee__deleted_at__isnull=True),
@@ -42,7 +57,8 @@ def issue_queryset_grouper(queryset, group_by, sub_group_by):
),
),
}
default_annotations = {
default_annotations: Dict[str, Any] = {
key: Coalesce(
ArrayAgg(field, distinct=True, filter=condition),
Value([], output_field=ArrayField(UUIDField())),
@@ -54,16 +70,20 @@ def issue_queryset_grouper(queryset, group_by, sub_group_by):
return queryset.annotate(**default_annotations)
def issue_on_results(issues, group_by, sub_group_by):
FIELD_MAPPER = {
def issue_on_results(
issues: QuerySet[Issue],
group_by: Optional[str],
sub_group_by: Optional[str],
) -> List[Dict[str, Any]]:
FIELD_MAPPER: Dict[str, str] = {
"labels__id": "label_ids",
"assignees__id": "assignee_ids",
"issue_module__module_id": "module_ids",
}
original_list = ["assignee_ids", "label_ids", "module_ids"]
original_list: List[str] = ["assignee_ids", "label_ids", "module_ids"]
required_fields = [
required_fields: List[str] = [
"id",
"name",
"state_id",
@@ -98,62 +118,72 @@ def issue_on_results(issues, group_by, sub_group_by):
original_list.append(sub_group_by)
required_fields.extend(original_list)
return issues.values(*required_fields)
return list(issues.values(*required_fields))
def issue_group_values(field, slug, project_id=None, filters=dict):
def issue_group_values(
field: str,
slug: str,
project_id: Optional[str] = None,
filters: Dict[str, Any] = {},
) -> List[Union[str, Any]]:
if field == "state_id":
queryset = State.objects.filter(
is_triage=False, workspace__slug=slug
).values_list("id", flat=True)
if project_id:
return list(queryset.filter(project_id=project_id))
else:
return list(queryset)
return list(queryset)
if field == "labels__id":
queryset = Label.objects.filter(workspace__slug=slug).values_list(
"id", flat=True
)
if project_id:
return list(queryset.filter(project_id=project_id)) + ["None"]
else:
return list(queryset) + ["None"]
return list(queryset) + ["None"]
if field == "assignees__id":
if project_id:
return ProjectMember.objects.filter(
workspace__slug=slug, project_id=project_id, is_active=True
).values_list("member_id", flat=True)
else:
return list(
WorkspaceMember.objects.filter(
workspace__slug=slug, is_active=True
ProjectMember.objects.filter(
workspace__slug=slug, project_id=project_id, is_active=True
).values_list("member_id", flat=True)
)
return list(
WorkspaceMember.objects.filter(
workspace__slug=slug, is_active=True
).values_list("member_id", flat=True)
)
if field == "issue_module__module_id":
queryset = Module.objects.filter(workspace__slug=slug).values_list(
"id", flat=True
)
if project_id:
return list(queryset.filter(project_id=project_id)) + ["None"]
else:
return list(queryset) + ["None"]
return list(queryset) + ["None"]
if field == "cycle_id":
queryset = Cycle.objects.filter(workspace__slug=slug).values_list(
"id", flat=True
)
if project_id:
return list(queryset.filter(project_id=project_id)) + ["None"]
else:
return list(queryset) + ["None"]
return list(queryset) + ["None"]
if field == "project_id":
queryset = Project.objects.filter(workspace__slug=slug).values_list(
"id", flat=True
)
return list(queryset)
if field == "priority":
return ["low", "medium", "high", "urgent", "none"]
if field == "state__group":
return ["backlog", "unstarted", "started", "completed", "cancelled"]
if field == "target_date":
queryset = (
Issue.issue_objects.filter(workspace__slug=slug)
@@ -163,8 +193,8 @@ def issue_group_values(field, slug, project_id=None, filters=dict):
)
if project_id:
return list(queryset.filter(project_id=project_id))
else:
return list(queryset)
return list(queryset)
if field == "start_date":
queryset = (
Issue.issue_objects.filter(workspace__slug=slug)
@@ -174,8 +204,7 @@ def issue_group_values(field, slug, project_id=None, filters=dict):
)
if project_id:
return list(queryset.filter(project_id=project_id))
else:
return list(queryset)
return list(queryset)
if field == "created_by":
queryset = (
@@ -186,7 +215,6 @@ def issue_group_values(field, slug, project_id=None, filters=dict):
)
if project_id:
return list(queryset.filter(project_id=project_id))
else:
return list(queryset)
return list(queryset)
return []
+29 -11
View File
@@ -9,7 +9,13 @@ from rest_framework.request import Request
# Module imports
from plane.utils.ip_address import get_client_ip
def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space: bool = False, is_app: bool = False) -> str:
def base_host(
request: Request | HttpRequest,
is_admin: bool = False,
is_space: bool = False,
is_app: bool = False,
) -> str:
"""Utility function to return host / origin from the request"""
# Calculate the base origin from request
base_origin = settings.WEB_URL or settings.APP_BASE_URL
@@ -17,19 +23,31 @@ def base_host(request: Request | HttpRequest, is_admin: bool = False, is_space:
if not base_origin:
raise ImproperlyConfigured("APP_BASE_URL or WEB_URL is not set")
# Admin redirections
# Admin redirection
if is_admin:
if settings.ADMIN_BASE_URL:
return settings.ADMIN_BASE_URL
else:
return base_origin + "/god-mode/"
admin_base_path = getattr(settings, "ADMIN_BASE_PATH", "/god-mode/")
if not admin_base_path.startswith("/"):
admin_base_path = "/" + admin_base_path
if not admin_base_path.endswith("/"):
admin_base_path += "/"
# Space redirections
if is_space:
if settings.SPACE_BASE_URL:
return settings.SPACE_BASE_URL
if settings.ADMIN_BASE_URL:
return settings.ADMIN_BASE_URL + admin_base_path
else:
return base_origin + "/spaces/"
return base_origin + admin_base_path
# Space redirection
if is_space:
space_base_path = getattr(settings, "SPACE_BASE_PATH", "/spaces/")
if not space_base_path.startswith("/"):
space_base_path = "/" + space_base_path
if not space_base_path.endswith("/"):
space_base_path += "/"
if settings.SPACE_BASE_URL:
return settings.SPACE_BASE_URL + space_base_path
else:
return base_origin + space_base_path
# App Redirection
if is_app:
+3 -5
View File
@@ -36,7 +36,7 @@ def user_timezone_converter(queryset, datetime_fields, user_timezone):
def convert_to_utc(
date, project_id, is_start_date=False, is_start_date_end_date_equal=False
date, project_id, is_start_date=False
):
"""
Converts a start date string to the project's local timezone at 12:00 AM
@@ -82,10 +82,8 @@ def convert_to_utc(
return utc_datetime
else:
# If it's start an end date are equal, add 23 hours, 59 minutes, and 59 seconds
# to make it the end of the day
if is_start_date_end_date_equal:
localized_datetime += timedelta(hours=23, minutes=59, seconds=59)
# the cycle end date is the last minute of the day
localized_datetime += timedelta(hours=23, minutes=59, seconds=0)
# Convert the localized datetime to UTC
utc_datetime = localized_datetime.astimezone(pytz.utc)
+79
View File
@@ -0,0 +1,79 @@
# Python imports
import re
from typing import Optional
from urllib.parse import urlparse, urlunparse
def is_valid_url(url: str) -> bool:
"""
Validates whether the given string is a well-formed URL.
Args:
url (str): The URL string to validate.
Returns:
bool: True if the URL is valid, False otherwise.
Example:
>>> is_valid_url("https://example.com")
True
>>> is_valid_url("not a url")
False
"""
try:
result = urlparse(url)
# A valid URL should have at least scheme and netloc
return all([result.scheme, result.netloc])
except TypeError:
return False
def get_url_components(url: str) -> Optional[dict]:
"""
Parses the URL and returns its components if valid.
Args:
url (str): The URL string to parse.
Returns:
Optional[dict]: A dictionary with URL components if valid, None otherwise.
Example:
>>> get_url_components("https://example.com/path?query=1")
{'scheme': 'https', 'netloc': 'example.com', 'path': '/path', 'params': '', 'query': 'query=1', 'fragment': ''}
"""
if not is_valid_url(url):
return None
result = urlparse(url)
return {
"scheme": result.scheme,
"netloc": result.netloc,
"path": result.path,
"params": result.params,
"query": result.query,
"fragment": result.fragment,
}
def normalize_url_path(url: str) -> str:
"""
Normalize the path component of a URL by replacing multiple consecutive slashes with a single slash.
This function preserves the protocol, domain, query parameters, and fragments of the URL,
only modifying the path portion to ensure there are no duplicate slashes.
Args:
url (str): The input URL string to normalize.
Returns:
str: The normalized URL with redundant slashes in the path removed.
Example:
>>> normalize_url_path('https://example.com//foo///bar//baz?x=1#frag')
'https://example.com/foo/bar/baz?x=1#frag'
"""
parts = urlparse(url)
# Normalize the path
normalized_path = re.sub(r"/+", "/", parts.path)
# Reconstruct the URL
return urlunparse(parts._replace(path=normalized_path))
+1 -1
View File
@@ -43,7 +43,7 @@ scout-apm==3.1.0
# xlsx generation
openpyxl==3.1.2
# logging
python-json-logger==2.0.7
python-json-logger==3.3.0
# html parser
beautifulsoup4==4.12.3
# analytics
+9 -10
View File
@@ -51,10 +51,9 @@ x-app-env: &app-env
API_KEY_RATE_LIMIT: ${API_KEY_RATE_LIMIT:-60/minute}
MINIO_ENDPOINT_SSL: ${MINIO_ENDPOINT_SSL:-0}
services:
web:
image: ${DOCKERHUB_USER:-makeplane}/plane-frontend:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-frontend:${APP_RELEASE:-stable}
command: node web/server.js web
deploy:
replicas: ${WEB_REPLICAS:-1}
@@ -65,7 +64,7 @@ services:
- worker
space:
image: ${DOCKERHUB_USER:-makeplane}/plane-space:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-space:${APP_RELEASE:-stable}
command: node space/server.js space
deploy:
replicas: ${SPACE_REPLICAS:-1}
@@ -77,7 +76,7 @@ services:
- web
admin:
image: ${DOCKERHUB_USER:-makeplane}/plane-admin:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-admin:${APP_RELEASE:-stable}
command: node admin/server.js admin
deploy:
replicas: ${ADMIN_REPLICAS:-1}
@@ -88,7 +87,7 @@ services:
- web
live:
image: ${DOCKERHUB_USER:-makeplane}/plane-live:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-live:${APP_RELEASE:-stable}
command: node live/dist/server.js live
environment:
<<: [*live-env]
@@ -101,7 +100,7 @@ services:
- web
api:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-api.sh
deploy:
replicas: ${API_REPLICAS:-1}
@@ -117,7 +116,7 @@ services:
- plane-mq
worker:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-worker.sh
deploy:
replicas: ${WORKER_REPLICAS:-1}
@@ -134,7 +133,7 @@ services:
- plane-mq
beat-worker:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-beat.sh
deploy:
replicas: ${BEAT_WORKER_REPLICAS:-1}
@@ -151,7 +150,7 @@ services:
- plane-mq
migrator:
image: ${DOCKERHUB_USER:-makeplane}/plane-backend:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-backend:${APP_RELEASE:-stable}
command: ./bin/docker-entrypoint-migrator.sh
deploy:
replicas: 1
@@ -213,7 +212,7 @@ services:
# Comment this if you already have a reverse proxy running
proxy:
image: ${DOCKERHUB_USER:-makeplane}/plane-proxy:${APP_RELEASE:-stable}
image: artifacts.plane.so/makeplane/plane-proxy:${APP_RELEASE:-stable}
ports:
- target: 80
published: ${NGINX_PORT:-80}
+2 -2
View File
@@ -5,7 +5,7 @@ SCRIPT_DIR=$PWD
SERVICE_FOLDER=plane-app
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
export APP_RELEASE=stable
export DOCKERHUB_USER=makeplane
export DOCKERHUB_USER=artifacts.plane.so/makeplane
export PULL_POLICY=${PULL_POLICY:-if_not_present}
export GH_REPO=makeplane/plane
export RELEASE_DOWNLOAD_URL="https://github.com/$GH_REPO/releases/download"
@@ -631,7 +631,7 @@ if [ -f "$DOCKER_ENV_PATH" ]; then
CUSTOM_BUILD=$(getEnvValue "CUSTOM_BUILD" "$DOCKER_ENV_PATH")
if [ -z "$DOCKERHUB_USER" ]; then
DOCKERHUB_USER=makeplane
DOCKERHUB_USER=artifacts.plane.so/makeplane
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
fi
+2 -2
View File
@@ -5,7 +5,7 @@ SERVICE_FOLDER=plane-app
SCRIPT_DIR=$PWD
PLANE_INSTALL_DIR=$PWD/$SERVICE_FOLDER
export APP_RELEASE="stable"
export DOCKERHUB_USER=makeplane
export DOCKERHUB_USER=artifacts.plane.so/makeplane
export GH_REPO=makeplane/plane
export RELEASE_DOWNLOAD_URL="https://github.com/$GH_REPO/releases/download"
@@ -596,7 +596,7 @@ if [ -f "$DOCKER_ENV_PATH" ]; then
APP_RELEASE=$(getEnvValue "APP_RELEASE" "$DOCKER_ENV_PATH")
if [ -z "$DOCKERHUB_USER" ]; then
DOCKERHUB_USER=makeplane
DOCKERHUB_USER=artifacts.plane.so/makeplane
updateEnvFile "DOCKERHUB_USER" "$DOCKERHUB_USER" "$DOCKER_ENV_PATH"
fi
+1 -1
View File
@@ -60,4 +60,4 @@ GUNICORN_WORKERS=1
MINIO_ENDPOINT_SSL=0
# API key rate limit
API_KEY_RATE_LIMIT="60/minute"
API_KEY_RATE_LIMIT=60/minute
+91 -73
View File
@@ -6,6 +6,8 @@ services:
- dev_env
volumes:
- redisdata:/data
ports:
- "6379:6379"
plane-mq:
image: rabbitmq:3.13.6-management-alpine
@@ -26,7 +28,15 @@ services:
restart: unless-stopped
networks:
- dev_env
command: server /export --console-address ":9090"
entrypoint: >
/bin/sh -c "
mkdir -p /export/${AWS_S3_BUCKET_NAME} &&
minio server /export --console-address ':9090' &
sleep 5 &&
mc alias set myminio http://localhost:9000 ${AWS_ACCESS_KEY_ID} ${AWS_SECRET_ACCESS_KEY} &&
mc mb myminio/${AWS_S3_BUCKET_NAME} -p || true
&& tail -f /dev/null
"
volumes:
- uploads:/export
env_file:
@@ -34,6 +44,9 @@ services:
environment:
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
ports:
- "9000:9000"
- "9090:9090"
plane-db:
image: postgres:15.7-alpine
@@ -47,63 +60,65 @@ services:
- .env
environment:
PGDATA: /var/lib/postgresql/data
ports:
- "5432:5432"
web:
build:
context: .
dockerfile: ./web/Dockerfile.dev
restart: unless-stopped
networks:
- dev_env
volumes:
- ./web:/app/web
env_file:
- ./web/.env
depends_on:
- api
- worker
# web:
# build:
# context: .
# dockerfile: ./web/Dockerfile.dev
# restart: unless-stopped
# networks:
# - dev_env
# volumes:
# - ./web:/app/web
# env_file:
# - ./web/.env
# depends_on:
# - api
# - worker
space:
build:
context: .
dockerfile: ./space/Dockerfile.dev
restart: unless-stopped
networks:
- dev_env
volumes:
- ./space:/app/space
depends_on:
- api
- worker
- web
# space:
# build:
# context: .
# dockerfile: ./space/Dockerfile.dev
# restart: unless-stopped
# networks:
# - dev_env
# volumes:
# - ./space:/app/space
# depends_on:
# - api
# - worker
# - web
admin:
build:
context: .
dockerfile: ./admin/Dockerfile.dev
restart: unless-stopped
networks:
- dev_env
volumes:
- ./admin:/app/admin
depends_on:
- api
- worker
- web
# admin:
# build:
# context: .
# dockerfile: ./admin/Dockerfile.dev
# restart: unless-stopped
# networks:
# - dev_env
# volumes:
# - ./admin:/app/admin
# depends_on:
# - api
# - worker
# - web
live:
build:
context: .
dockerfile: ./live/Dockerfile.dev
restart: unless-stopped
networks:
- dev_env
volumes:
- ./live:/app/live
depends_on:
- api
- worker
- web
# live:
# build:
# context: .
# dockerfile: ./live/Dockerfile.dev
# restart: unless-stopped
# networks:
# - dev_env
# volumes:
# - ./live:/app/live
# depends_on:
# - api
# - worker
# - web
api:
build:
@@ -122,6 +137,9 @@ services:
depends_on:
- plane-db
- plane-redis
- plane-mq
ports:
- "8000:8000"
worker:
build:
@@ -179,25 +197,25 @@ services:
- plane-db
- plane-redis
proxy:
build:
context: ./nginx
dockerfile: Dockerfile.dev
restart: unless-stopped
networks:
- dev_env
ports:
- ${NGINX_PORT}:80
env_file:
- .env
environment:
FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
depends_on:
- web
- api
- space
- admin
# proxy:
# build:
# context: ./nginx
# dockerfile: Dockerfile.dev
# restart: unless-stopped
# networks:
# - dev_env
# ports:
# - ${NGINX_PORT}:80
# env_file:
# - .env
# environment:
# FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
# BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
# depends_on:
# - api
# - web
# - space
# - admin
volumes:
redisdata:
+8 -3
View File
@@ -1,8 +1,13 @@
API_BASE_URL="http://api:8000"
API_BASE_URL="http://localhost:8000"
WEB_BASE_URL="http://localhost:3000"
LIVE_BASE_URL="http://localhost:3100"
LIVE_BASE_PATH="/live"
REDIS_URL="redis://plane-redis:6379/"
LIVE_SERVER_SECRET_KEY="secret-key"
# If you prefer not to provide a Redis URL, you can set the REDIS_HOST and REDIS_PORT environment variables instead.
REDIS_PORT=6379
REDIS_HOST=plane-redis
REDIS_HOST=localhost
REDIS_URL="redis://localhost:6379/"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "live",
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"description": "A realtime collaborative server powers Plane's rich text editor",
"main": "./src/server.ts",
+2 -2
View File
@@ -2,7 +2,7 @@
"name": "plane",
"description": "Open-source project management that unlocks customer value",
"repository": "https://github.com/makeplane/plane.git",
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"private": true,
"workspaces": [
@@ -24,7 +24,7 @@
"devDependencies": {
"prettier": "latest",
"prettier-plugin-tailwindcss": "^0.5.4",
"turbo": "^2.4.2"
"turbo": "^2.5.2"
},
"resolutions": {
"nanoid": "3.3.8",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/constants",
"version": "0.25.3",
"version": "0.26.0",
"private": true,
"main": "./src/index.ts",
"license": "AGPL-3.0"
+7
View File
@@ -0,0 +1,7 @@
export enum EIconSize {
XS = "xs",
SM = "sm",
MD = "md",
LG = "lg",
XL = "xl",
}
+75 -69
View File
@@ -1,91 +1,97 @@
import { TInboxDuplicateIssueDetails, TIssue } from "@plane/types";
export enum EInboxIssueCurrentTab {
OPEN = "open",
CLOSED = "closed",
OPEN = "open",
CLOSED = "closed",
}
export enum EInboxIssueStatus {
PENDING = -2,
DECLINED = -1,
SNOOZED = 0,
ACCEPTED = 1,
DUPLICATE = 2,
PENDING = -2,
DECLINED = -1,
SNOOZED = 0,
ACCEPTED = 1,
DUPLICATE = 2,
}
export enum EInboxIssueSource {
IN_APP = "IN_APP",
FORMS = "FORMS",
EMAIL = "EMAIL",
}
export type TInboxIssueCurrentTab = EInboxIssueCurrentTab;
export type TInboxIssueStatus = EInboxIssueStatus;
export type TInboxIssue = {
id: string;
status: TInboxIssueStatus;
snoozed_till: Date | null;
duplicate_to: string | undefined;
source: string;
issue: TIssue;
created_by: string;
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined;
id: string;
status: TInboxIssueStatus;
snoozed_till: Date | null;
duplicate_to: string | undefined;
source: EInboxIssueSource | undefined;
issue: TIssue;
created_by: string;
duplicate_issue_detail: TInboxDuplicateIssueDetails | undefined;
};
export const INBOX_STATUS: {
key: string;
status: TInboxIssueStatus;
i18n_title: string;
i18n_description: () => string;
key: string;
status: TInboxIssueStatus;
i18n_title: string;
i18n_description: () => string;
}[] = [
{
key: "pending",
i18n_title: "inbox_issue.status.pending.title",
status: EInboxIssueStatus.PENDING,
i18n_description: () => `inbox_issue.status.pending.description`,
},
{
key: "declined",
i18n_title: "inbox_issue.status.declined.title",
status: EInboxIssueStatus.DECLINED,
i18n_description: () => `inbox_issue.status.declined.description`,
},
{
key: "snoozed",
i18n_title: "inbox_issue.status.snoozed.title",
status: EInboxIssueStatus.SNOOZED,
i18n_description: () => `inbox_issue.status.snoozed.description`,
},
{
key: "accepted",
i18n_title: "inbox_issue.status.accepted.title",
status: EInboxIssueStatus.ACCEPTED,
i18n_description: () => `inbox_issue.status.accepted.description`,
},
{
key: "duplicate",
i18n_title: "inbox_issue.status.duplicate.title",
status: EInboxIssueStatus.DUPLICATE,
i18n_description: () => `inbox_issue.status.duplicate.description`,
},
{
key: "pending",
i18n_title: "inbox_issue.status.pending.title",
status: EInboxIssueStatus.PENDING,
i18n_description: () => `inbox_issue.status.pending.description`,
},
{
key: "declined",
i18n_title: "inbox_issue.status.declined.title",
status: EInboxIssueStatus.DECLINED,
i18n_description: () => `inbox_issue.status.declined.description`,
},
{
key: "snoozed",
i18n_title: "inbox_issue.status.snoozed.title",
status: EInboxIssueStatus.SNOOZED,
i18n_description: () => `inbox_issue.status.snoozed.description`,
},
{
key: "accepted",
i18n_title: "inbox_issue.status.accepted.title",
status: EInboxIssueStatus.ACCEPTED,
i18n_description: () => `inbox_issue.status.accepted.description`,
},
{
key: "duplicate",
i18n_title: "inbox_issue.status.duplicate.title",
status: EInboxIssueStatus.DUPLICATE,
i18n_description: () => `inbox_issue.status.duplicate.description`,
},
];
export const INBOX_ISSUE_ORDER_BY_OPTIONS = [
{
key: "issue__created_at",
i18n_label: "inbox_issue.order_by.created_at",
},
{
key: "issue__updated_at",
i18n_label: "inbox_issue.order_by.updated_at",
},
{
key: "issue__sequence_id",
i18n_label: "inbox_issue.order_by.id",
},
{
key: "issue__created_at",
i18n_label: "inbox_issue.order_by.created_at",
},
{
key: "issue__updated_at",
i18n_label: "inbox_issue.order_by.updated_at",
},
{
key: "issue__sequence_id",
i18n_label: "inbox_issue.order_by.id",
},
];
export const INBOX_ISSUE_SORT_BY_OPTIONS = [
{
key: "asc",
i18n_label: "common.sort.asc",
},
{
key: "desc",
i18n_label: "common.sort.desc",
},
{
key: "asc",
i18n_label: "common.sort.asc",
},
{
key: "desc",
i18n_label: "common.sort.desc",
},
];
+3
View File
@@ -14,6 +14,7 @@ export * from "./state";
export * from "./swr";
export * from "./tab-indices";
export * from "./user";
export * from "./payment";
export * from "./workspace";
export * from "./stickies";
export * from "./cycle";
@@ -30,3 +31,5 @@ export * from "./spreadsheet";
export * from "./dashboard";
export * from "./page";
export * from "./emoji";
export * from "./subscription";
export * from "./icon";
+9
View File
@@ -165,6 +165,15 @@ export const ISSUE_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] =
"issue_type",
];
export const SUB_ISSUES_DISPLAY_PROPERTIES_KEYS: (keyof IIssueDisplayProperties)[] = [
"key",
"assignee",
"start_date",
"due_date",
"priority",
"state",
];
export const ISSUE_DISPLAY_PROPERTIES: {
key: keyof IIssueDisplayProperties;
titleTranslationKey: string;
+32 -145
View File
@@ -6,6 +6,7 @@ import {
TIssueFilterPriorityObject,
ISSUE_DISPLAY_PROPERTIES_KEYS,
EIssuesStoreType,
SUB_ISSUES_DISPLAY_PROPERTIES_KEYS,
} from "./common";
import { TIssueLayout } from "./layout";
@@ -96,23 +97,11 @@ export type TIssueFiltersToDisplayByPageType = {
export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
profile_issues: {
list: {
filters: [
"priority",
"state_group",
"labels",
"start_date",
"target_date",
],
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state_detail.group", "priority", "project", "labels", null],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: [null, "active", "backlog"],
},
extra_options: {
@@ -121,23 +110,11 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
},
},
kanban: {
filters: [
"priority",
"state_group",
"labels",
"start_date",
"target_date",
],
filters: ["priority", "state_group", "labels", "start_date", "target_date"],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: ["state_detail.group", "priority", "project", "labels"],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: [null, "active", "backlog"],
},
extra_options: {
@@ -173,13 +150,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
"created_by",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: [null, "active", "backlog"],
},
extra_options: {
@@ -190,34 +161,11 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
},
draft_issues: {
list: {
filters: [
"priority",
"state_group",
"cycle",
"module",
"labels",
"start_date",
"target_date",
"issue_type",
],
filters: ["priority", "state_group", "cycle", "module", "labels", "start_date", "target_date", "issue_type"],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state_detail.group",
"cycle",
"module",
"priority",
"project",
"labels",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
group_by: ["state_detail.group", "cycle", "module", "priority", "project", "labels", null],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: [null, "active", "backlog"],
},
extra_options: {
@@ -226,33 +174,11 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
},
},
kanban: {
filters: [
"priority",
"state_group",
"cycle",
"module",
"labels",
"start_date",
"target_date",
"issue_type",
],
filters: ["priority", "state_group", "cycle", "module", "labels", "start_date", "target_date", "issue_type"],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state_detail.group",
"cycle",
"module",
"priority",
"project",
"labels",
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
group_by: ["state_detail.group", "cycle", "module", "priority", "project", "labels"],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: [null, "active", "backlog"],
},
extra_options: {
@@ -323,24 +249,8 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state",
"priority",
"cycle",
"module",
"labels",
"assignees",
"created_by",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
"target_date",
],
group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by", null],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority", "target_date"],
type: [null, "active", "backlog"],
},
extra_options: {
@@ -364,33 +274,9 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
group_by: [
"state",
"priority",
"cycle",
"module",
"labels",
"assignees",
"created_by",
],
sub_group_by: [
"state",
"priority",
"cycle",
"module",
"labels",
"assignees",
"created_by",
null,
],
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
"target_date",
],
group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by"],
sub_group_by: ["state", "priority", "cycle", "module", "labels", "assignees", "created_by", null],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority", "target_date"],
type: [null, "active", "backlog"],
},
extra_options: {
@@ -436,13 +322,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
],
display_properties: ISSUE_DISPLAY_PROPERTIES_KEYS,
display_filters: {
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: [null, "active", "backlog"],
},
extra_options: {
@@ -466,13 +346,7 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
],
display_properties: ["key", "issue_type"],
display_filters: {
order_by: [
"sort_order",
"-created_at",
"-updated_at",
"start_date",
"-priority",
],
order_by: ["sort_order", "-created_at", "-updated_at", "start_date", "-priority"],
type: [null, "active", "backlog"],
},
extra_options: {
@@ -481,6 +355,19 @@ export const ISSUE_DISPLAY_FILTERS_BY_PAGE: TIssueFiltersToDisplayByPageType = {
},
},
},
sub_work_items: {
list: {
display_properties: SUB_ISSUES_DISPLAY_PROPERTIES_KEYS,
filters: [],
display_filters: {
order_by: ["-created_at", "-updated_at", "start_date", "-priority"],
},
extra_options: {
access: true,
values: ["sub_issue"],
},
},
},
};
export const ISSUE_STORE_TO_FILTERS_MAP: Partial<
+163
View File
@@ -0,0 +1,163 @@
import { IPaymentProduct, TBillingFrequency, TProductBillingFrequency } from "@plane/types";
/**
* Enum representing different product subscription types
*/
export enum EProductSubscriptionEnum {
FREE = "FREE",
ONE = "ONE",
PRO = "PRO",
BUSINESS = "BUSINESS",
ENTERPRISE = "ENTERPRISE",
}
/**
* Default billing frequency for each product subscription type
*/
export const DEFAULT_PRODUCT_BILLING_FREQUENCY: TProductBillingFrequency = {
[EProductSubscriptionEnum.FREE]: undefined,
[EProductSubscriptionEnum.ONE]: undefined,
[EProductSubscriptionEnum.PRO]: "month",
[EProductSubscriptionEnum.BUSINESS]: "month",
[EProductSubscriptionEnum.ENTERPRISE]: "month",
};
/**
* Subscription types that support billing frequency toggle (monthly/yearly)
*/
export const SUBSCRIPTION_WITH_BILLING_FREQUENCY = [
EProductSubscriptionEnum.PRO,
EProductSubscriptionEnum.BUSINESS,
EProductSubscriptionEnum.ENTERPRISE,
];
/**
* Mapping of product subscription types to their respective payment product details
* Used to provide information about each product's pricing and features
*/
export const PLANE_COMMUNITY_PRODUCTS: Record<string, IPaymentProduct> = {
[EProductSubscriptionEnum.PRO]: {
id: EProductSubscriptionEnum.PRO,
name: "Plane Pro",
description:
"More views, more cycles powers, more pages features, new reports, and better dashboards are waiting to be unlocked.",
type: "PRO",
prices: [
{
id: `price_monthly_${EProductSubscriptionEnum.PRO}`,
unit_amount: 800,
recurring: "month",
currency: "usd",
workspace_amount: 800,
product: EProductSubscriptionEnum.PRO,
},
{
id: `price_yearly_${EProductSubscriptionEnum.PRO}`,
unit_amount: 7200,
recurring: "year",
currency: "usd",
workspace_amount: 7200,
product: EProductSubscriptionEnum.PRO,
},
],
payment_quantity: 1,
is_active: true,
},
[EProductSubscriptionEnum.BUSINESS]: {
id: EProductSubscriptionEnum.BUSINESS,
name: "Plane Business",
description:
"The earliest packaging of Business at $10 a seat a month billed annually, $12 a seat a month billed monthly for Plane Cloud",
type: "BUSINESS",
prices: [
{
id: `price_yearly_${EProductSubscriptionEnum.BUSINESS}`,
unit_amount: 0,
recurring: "year",
currency: "usd",
workspace_amount: 0,
product: EProductSubscriptionEnum.BUSINESS,
},
{
id: `price_monthly_${EProductSubscriptionEnum.BUSINESS}`,
unit_amount: 0,
recurring: "month",
currency: "usd",
workspace_amount: 0,
product: EProductSubscriptionEnum.BUSINESS,
},
],
payment_quantity: 1,
is_active: false,
},
[EProductSubscriptionEnum.ENTERPRISE]: {
id: EProductSubscriptionEnum.ENTERPRISE,
name: "Plane Enterprise",
description: "",
type: "ENTERPRISE",
prices: [
{
id: `price_yearly_${EProductSubscriptionEnum.ENTERPRISE}`,
unit_amount: 0,
recurring: "year",
currency: "usd",
workspace_amount: 0,
product: EProductSubscriptionEnum.ENTERPRISE,
},
{
id: `price_monthly_${EProductSubscriptionEnum.ENTERPRISE}`,
unit_amount: 0,
recurring: "month",
currency: "usd",
workspace_amount: 0,
product: EProductSubscriptionEnum.ENTERPRISE,
},
],
payment_quantity: 1,
is_active: false,
},
};
/**
* URL for the "Talk to Sales" page where users can contact sales team
*/
export const TALK_TO_SALES_URL = "https://plane.so/talk-to-sales";
/**
* Mapping of subscription types to their respective upgrade/redirection URLs based on billing frequency
* Used for self-hosted installations to redirect users to appropriate upgrade pages
*/
export const SUBSCRIPTION_REDIRECTION_URLS: Record<EProductSubscriptionEnum, Record<TBillingFrequency, string>> = {
[EProductSubscriptionEnum.FREE]: {
month: TALK_TO_SALES_URL,
year: TALK_TO_SALES_URL,
},
[EProductSubscriptionEnum.ONE]: {
month: TALK_TO_SALES_URL,
year: TALK_TO_SALES_URL,
},
[EProductSubscriptionEnum.PRO]: {
month: "https://app.plane.so/upgrade/pro/self-hosted?plan=month",
year: "https://app.plane.so/upgrade/pro/self-hosted?plan=year",
},
[EProductSubscriptionEnum.BUSINESS]: {
month: TALK_TO_SALES_URL,
year: TALK_TO_SALES_URL,
},
[EProductSubscriptionEnum.ENTERPRISE]: {
month: TALK_TO_SALES_URL,
year: TALK_TO_SALES_URL,
},
};
/**
* Mapping of subscription types to their respective marketing webpage URLs
* Used to direct users to learn more about each plan's features and pricing
*/
export const SUBSCRIPTION_WEBPAGE_URLS: Record<EProductSubscriptionEnum, string> = {
[EProductSubscriptionEnum.FREE]: TALK_TO_SALES_URL,
[EProductSubscriptionEnum.ONE]: TALK_TO_SALES_URL,
[EProductSubscriptionEnum.PRO]: "https://plane.so/pro",
[EProductSubscriptionEnum.BUSINESS]: "https://plane.so/business",
[EProductSubscriptionEnum.ENTERPRISE]: "https://plane.so/business",
};
+42
View File
@@ -0,0 +1,42 @@
export const ENTERPRISE_PLAN_FEATURES = [
"Private + managed deployments",
"GAC",
"LDAP support",
"Databases + Formulas",
"Unlimited and full Automation Flows",
"Full-suite professional services",
];
export const BUSINESS_PLAN_FEATURES = [
"Project Templates",
"Workflows + Approvals",
"Decision + Loops Automation",
"Custom Reports",
"Nested Pages",
"Intake Forms",
];
export const PRO_PLAN_FEATURES = [
"Dashboards + Reports",
"Full Time Tracking + Bulk Ops",
"Teamspaces",
"Trigger And Action",
"Wikis",
"Popular integrations",
];
export const ONE_PLAN_FEATURES = [
"OIDC + SAML for SSO",
"Active Cycles",
"Real-time collab + public views and page",
"Link pages in issues and vice-versa",
"Time-tracking + limited bulk ops",
"Docker, Kubernetes and more",
];
export const FREE_PLAN_UPGRADE_FEATURES = [
"OIDC + SAML for SSO",
"Time Tracking and Bulk Ops",
"Integrations",
"Public Views and Pages",
];
+4 -4
View File
@@ -17,15 +17,15 @@
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"dependencies": {
"reflect-metadata": "^0.2.2",
"express": "^4.21.2"
"express": "^4.21.2",
"reflect-metadata": "^0.2.2"
},
"devDependencies": {
"@plane/eslint-config": "*",
"@types/express": "^4.17.21",
"@types/reflect-metadata": "^0.1.0",
"@plane/typescript-config": "*",
"@types/express": "^4.17.21",
"@types/node": "^20.14.9",
"@types/reflect-metadata": "^0.1.0",
"@types/ws": "^8.5.10",
"tsup": "8.4.0",
"typescript": "^5.3.3"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/editor",
"version": "0.25.3",
"version": "0.26.0",
"description": "Core Editor that powers Plane",
"license": "AGPL-3.0",
"private": true,
@@ -7,7 +7,7 @@ import { CustomImageNode, UploadImageExtensionStorage } from "@/extensions/custo
import { TReadOnlyFileHandler } from "@/types";
export const CustomReadOnlyImageExtension = (props: TReadOnlyFileHandler) => {
const { getAssetSrc } = props;
const { getAssetSrc, restore: restoreImageFn } = props;
return Image.extend<Record<string, unknown>, UploadImageExtensionStorage>({
name: "imageComponent",
@@ -66,6 +66,9 @@ export const CustomReadOnlyImageExtension = (props: TReadOnlyFileHandler) => {
addCommands() {
return {
getImageSource: (path: string) => async () => await getAssetSrc(path),
restoreImage: (src) => async () => {
await restoreImageFn(src);
},
};
},
+14 -12
View File
@@ -151,11 +151,11 @@
/* end font size and style */
/* layout config */
#page-header-container {
container-name: page-header-container;
#page-toolbar-container {
container-name: page-toolbar-container;
container-type: inline-size;
.page-header-content {
.page-toolbar-content {
--header-width: var(--normal-content-width);
&.wide-layout {
@@ -186,23 +186,23 @@
}
/* keep a static padding of 96px for wide layouts for container width >912px and <1344px */
@container page-header-container (min-width: 912px) and (max-width: 1344px) {
.page-header-content.wide-layout {
@container page-toolbar-container (min-width: 912px) and (max-width: 1344px) {
.page-toolbar-content.wide-layout {
padding-left: var(--wide-content-margin) !important;
}
}
/* keep a static padding of 96px for wide layouts for container width <912px */
@container page-header-container (max-width: 912) {
.page-header-content.wide-layout {
@container page-toolbar-container (max-width: 912) {
.page-toolbar-content.wide-layout {
padding-left: var(--wide-content-margin) !important;
}
}
/* end layout config */
/* keep a static padding of 20px for wide layouts for container width <760px */
@container page-header-container (max-width: 760px) {
.page-header-content {
@container page-toolbar-container (max-width: 760px) {
.page-toolbar-content {
padding-left: var(--normal-content-margin) !important;
}
}
@@ -211,7 +211,7 @@
/* keep a static padding of 96px for wide layouts for container width >912px and <1344px */
@container page-content-container (min-width: 912px) and (max-width: 1344px) {
.editor-container.wide-layout,
.page-title-container {
.page-header-container {
padding-left: var(--wide-content-margin);
padding-right: var(--wide-content-margin);
}
@@ -220,7 +220,7 @@
/* keep a static padding of 20px for wide layouts for container width <912px */
@container page-content-container (max-width: 912px) {
.editor-container.wide-layout,
.page-title-container {
.page-header-container {
padding-left: var(--normal-content-margin);
padding-right: var(--normal-content-margin);
}
@@ -229,11 +229,13 @@
/* keep a static padding of 20px for normal layouts for container width <760px */
@container page-content-container (max-width: 760px) {
.editor-container:not(.wide-layout),
.page-title-container {
.page-header-container {
padding-left: var(--normal-content-margin);
padding-right: var(--normal-content-margin);
}
}
@container page-content-container (max-width: 930px) {
.page-summary-container {
display: none;
}
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "@plane/eslint-config",
"private": true,
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"files": [
"library.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/hooks",
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"description": "React hooks that are shared across multiple apps internally",
"private": true,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@plane/i18n",
"version": "0.25.3",
"version": "0.26.0",
"license": "AGPL-3.0",
"description": "I18n shared across multiple apps internally",
"private": true,
+1
View File
@@ -21,6 +21,7 @@ export const SUPPORTED_LANGUAGES: ILanguageOption[] = [
{ label: "Indonesian", value: "id" },
{ label: "Română", value: "ro" },
{ label: "Tiếng việt", value: "vi-VN" },
{ label: "Türkçe", value: "tr-TR" },
];
export const LANGUAGE_STORAGE_KEY = "userLanguage";
+35 -55
View File
@@ -348,7 +348,7 @@
"couldnt_remove_the_project_from_favorites": "Nepodařilo se odstranit projekt z oblíbených. Zkuste to prosím znovu.",
"add_to_favorites": "Přidat do oblíbených",
"remove_from_favorites": "Odebrat z oblíbených",
"publish_settings": "Nastavení publikování",
"publish_project": "Publikovat projekt",
"publish": "Publikovat",
"copy_link": "Kopírovat odkaz",
"leave_project": "Opustit projekt",
@@ -365,12 +365,12 @@
"work_management": "Správa práce",
"projects_and_issues": "Projekty a pracovní položky",
"projects_and_issues_description": "Aktivujte nebo deaktivujte tyto funkce v projektu.",
"cycles_description": "Časově ohraničte práci podle potřeby a měňte frekvenci mezi obdobími.",
"modules_description": "Seskupujte práci do podobných podprojektů s vlastními vedoucími a přiřazenými osobami.",
"views_description": "Uložte řazení, filtry a zobrazovací možnosti pro pozdější použití nebo sdíle.",
"pages_description": "Pište cokoli, jako obvykle.",
"intake_description": "Zůstaňte v obraze u pracovních položek, které odebíráte. Aktivujte toto pro zasílání oznámení.",
"time_tracking_description": "Sledujte čas strávený na pracovních položkách a projektech.",
"cycles_description": "Časově vymezte práci podle projektu a podle potřeby upravte období. Jeden cyklus může trvat 2 týdny, další jen 1 týden.",
"modules_description": "Organizujte práci do podprojektů s vyhrazenými vedoucími a přiřazenými osobami.",
"views_description": "Uložte vlastní řazení, filtry a možnosti zobrazení nebo je sdílejte se svým týmem.",
"pages_description": "Vytvářejte a upravujte volně strukturovaný obsah poznámky, dokumenty, cokoli.",
"intake_description": "Umožněte nečlenům sdílet chyby, zpětnou vazbu a návrhy, aniž by narušili váš pracovní postup.",
"time_tracking_description": "Zaznamenávejte čas strávený na pracovních položkách a projektech.",
"work_management_description": "Spravujte svou práci a projekty snadno.",
"documentation": "Dokumentace",
"message_support": "Kontaktovat podporu",
@@ -500,7 +500,6 @@
"export": "Exportovat",
"member": "{count, plural, one{# člen} few{# členové} other{# členů}}",
"new_password_must_be_different_from_old_password": "Nové heslo musí být odlišné od starého hesla",
"project_view": {
"sort_by": {
"created_at": "Vytvořeno dne",
@@ -508,12 +507,10 @@
"name": "Název"
}
},
"toast": {
"success": "Úspěch!",
"error": "Chyba!"
},
"links": {
"toasts": {
"created": {
@@ -542,7 +539,6 @@
}
}
},
"home": {
"empty": {
"quickstart_guide": "Váš průvodce rychlým startem",
@@ -610,7 +606,6 @@
"title": "Domů",
"star_us_on_github": "Ohodnoťte nás na GitHubu"
},
"link": {
"modal": {
"url": {
@@ -624,7 +619,6 @@
}
}
},
"common": {
"all": "Vše",
"states": "Stavy",
@@ -867,22 +861,22 @@
"deleting": "Mazání",
"pending": "Čekající",
"invite": "Pozvat",
"view": "Pohled"
"view": "Pohled",
"deactivated_user": "Deaktivovaný uživatel",
"apply": "Použít",
"applying": "Používání"
},
"chart": {
"x_axis": "Osa X",
"y_axis": "Osa Y",
"metric": "Metrika"
},
"form": {
"title": {
"required": "Název je povinný",
"max_length": "Název by měl být kratší než {length} znaků"
}
},
"entity": {
"grouping_title": "Seskupení {entity}",
"priority": "Priorita {entity}",
@@ -906,7 +900,6 @@
"failed": "Chyba při přidávání {entity}"
}
},
"epic": {
"all": "Všechny epiky",
"label": "{count, plural, one {Epik} other {Epiky}}",
@@ -924,7 +917,6 @@
"required": "Název epiku je povinný."
}
},
"issue": {
"label": "{count, plural, one {Pracovní položka} few {Pracovní položky} other {Pracovních položek}}",
"all": "Všechny pracovní položky",
@@ -1091,7 +1083,6 @@
},
"open_in_full_screen": "Otevřít pracovní položku na celou obrazovku"
},
"attachment": {
"error": "Soubor nelze připojit. Zkuste to prosím znovu.",
"only_one_file_allowed": "Je možné nahrát pouze jeden soubor najednou.",
@@ -1099,7 +1090,6 @@
"drag_and_drop": "Přetáhněte soubor kamkoli pro nahrání",
"delete": "Smazat přílohu"
},
"label": {
"select": "Vybrat štítek",
"create": {
@@ -1109,7 +1099,6 @@
"type": "Zadejte pro vytvoření nového štítku"
}
},
"sub_work_item": {
"update": {
"success": "Podřízená pracovní položka úspěšně aktualizována",
@@ -1120,7 +1109,6 @@
"error": "Chyba při odebírání podřízené položky"
}
},
"view": {
"label": "{count, plural, one {Pohled} few {Pohledy} other {Pohledů}}",
"create": {
@@ -1130,7 +1118,6 @@
"label": "Aktualizovat pohled"
}
},
"inbox_issue": {
"status": {
"pending": {
@@ -1216,7 +1203,6 @@
}
}
},
"workspace_creation": {
"heading": "Vytvořte si pracovní prostor",
"subheading": "Pro používání Plane musíte vytvořit nebo se připojit k pracovnímu prostoru.",
@@ -1268,7 +1254,6 @@
}
}
},
"workspace_dashboard": {
"empty_state": {
"general": {
@@ -1284,7 +1269,6 @@
}
}
},
"workspace_analytics": {
"label": "Analytika",
"page_label": "{workspace} - Analytika",
@@ -1329,7 +1313,6 @@
}
}
},
"workspace_projects": {
"label": "{count, plural, one {Projekt} few {Projekty} other {Projektů}}",
"create": {
@@ -1404,7 +1387,6 @@
}
}
},
"workspace_views": {
"add_view": "Přidat pohled",
"empty_state": {
@@ -1439,7 +1421,6 @@
}
}
},
"workspace_settings": {
"label": "Nastavení pracovního prostoru",
"page_label": "{workspace} - Obecná nastavení",
@@ -1621,7 +1602,6 @@
}
}
},
"profile": {
"label": "Profil",
"page_label": "Vaše práce",
@@ -1684,7 +1664,6 @@
}
}
},
"project_settings": {
"general": {
"enter_project_id": "Zadejte ID projektu",
@@ -1738,12 +1717,15 @@
"title": "Povolit odhady pro můj projekt",
"description": "Pomáhají vám komunikovat složitost a pracovní zátěž týmu.",
"no_estimate": "Bez odhadu",
"new": "Nový systém odhadů",
"create": {
"custom": "Vlastní",
"start_from_scratch": "Začít od nuly",
"choose_template": "Vybrat šablonu",
"choose_estimate_system": "Vybrat systém odhadů",
"enter_estimate_point": "Zadat odhad"
"enter_estimate_point": "Zadat odhad",
"step": "Krok {step} z {total}",
"label": "Vytvořit odhad"
},
"toasts": {
"created": {
@@ -1792,6 +1774,25 @@
"already_exists": "Hodnota odhadu již existuje.",
"unsaved_changes": "Máte neuložené změny. Před kliknutím na hotovo je prosím uložte",
"remove_empty": "Odhad nemůže být prázdný. Zadejte hodnotu do každého pole nebo odstraňte ta, pro která nemáte hodnoty."
},
"systems": {
"points": {
"label": "Body",
"fibonacci": "Fibonacci",
"linear": "Lineární",
"squares": "Čtverce",
"custom": "Vlastní"
},
"categories": {
"label": "Kategorie",
"t_shirt_sizes": "Velikosti triček",
"easy_to_hard": "Od snadného po těžké",
"custom": "Vlastní"
},
"time": {
"label": "Čas",
"hours": "Hodiny"
}
}
},
"automations": {
@@ -1808,7 +1809,6 @@
"auto_close_status": "Stav automatického uzavření"
}
},
"empty_state": {
"labels": {
"title": "Zatím žádné štítky",
@@ -1821,7 +1821,6 @@
}
}
},
"project_cycles": {
"add_cycle": "Přidat cyklus",
"more_details": "Více detailů",
@@ -1947,7 +1946,6 @@
}
}
},
"project_issues": {
"empty_state": {
"no_issues": {
@@ -1976,7 +1974,6 @@
}
}
},
"project_module": {
"add_module": "Přidat modul",
"update_module": "Aktualizovat modul",
@@ -2030,7 +2027,6 @@
}
}
},
"project_views": {
"empty_state": {
"general": {
@@ -2050,7 +2046,6 @@
}
}
},
"project_page": {
"empty_state": {
"general": {
@@ -2080,7 +2075,6 @@
}
}
},
"command_k": {
"empty_state": {
"search": {
@@ -2088,7 +2082,6 @@
}
}
},
"issue_relation": {
"empty_state": {
"search": {
@@ -2099,7 +2092,6 @@
}
}
},
"issue_comment": {
"empty_state": {
"general": {
@@ -2108,7 +2100,6 @@
}
}
},
"notification": {
"label": "Schránka",
"page_label": "{workspace} - Schránka",
@@ -2165,7 +2156,6 @@
"custom": "Vlastní"
}
},
"active_cycle": {
"empty_state": {
"progress": {
@@ -2185,7 +2175,6 @@
}
}
},
"disabled_project": {
"empty_state": {
"inbox": {
@@ -2248,7 +2237,6 @@
}
}
},
"stickies": {
"title": "Vaše poznámky",
"placeholder": "kliknutím začněte psát",
@@ -2306,7 +2294,6 @@
}
}
},
"role_details": {
"guest": {
"title": "Host",
@@ -2321,7 +2308,6 @@
"description": "Má všechna oprávnění v prostoru."
}
},
"user_roles": {
"product_or_project_manager": "Produktový/Projektový manažer",
"development_or_engineering": "Vývoj/Inženýrství",
@@ -2334,7 +2320,6 @@
"human_resources": "Lidské zdroje",
"other": "Jiné"
},
"importer": {
"github": {
"title": "GitHub",
@@ -2345,7 +2330,6 @@
"description": "Importujte položky a epiky z Jira."
}
},
"exporter": {
"csv": {
"title": "CSV",
@@ -2374,7 +2358,6 @@
"created": "Vytvořeno",
"subscribed": "Odebíráno"
},
"themes": {
"theme_options": {
"system_preference": {
@@ -2420,20 +2403,17 @@
"manual": "Ručně"
}
},
"cycle": {
"label": "{count, plural, one {Cyklus} few {Cykly} other {Cyklů}}",
"no_cycle": "Žádný cyklus"
},
"module": {
"label": "{count, plural, one {Modul} few {Moduly} other {Modulů}}",
"no_module": "Žádný modul"
},
"description_versions": {
"last_edited_by": "Naposledy upraveno uživatelem",
"previously_edited_by": "Dříve upraveno uživatelem",
"edited_by": "Upraveno uživatelem"
}
}
}
+34 -13
View File
@@ -348,7 +348,7 @@
"couldnt_remove_the_project_from_favorites": "Projekt konnte nicht aus den Favoriten entfernt werden. Bitte versuchen Sie es erneut.",
"add_to_favorites": "Zu Favoriten hinzufügen",
"remove_from_favorites": "Aus Favoriten entfernen",
"publish_settings": "Veröffentlichungseinstellungen",
"publish_project": "Projekt veröffentlichen",
"publish": "Veröffentlichen",
"copy_link": "Link kopieren",
"leave_project": "Projekt verlassen",
@@ -365,11 +365,11 @@
"work_management": "Arbeitsverwaltung",
"projects_and_issues": "Projekte und Arbeitselemente",
"projects_and_issues_description": "Aktivieren oder deaktivieren Sie diese Funktionen im Projekt.",
"cycles_description": "Begrenzen Sie die Arbeit zeitlich bei Bedarf und ändern Sie die Häufigkeit zwischen Zeiträumen.",
"modules_description": "Gruppieren Sie die Arbeit in ähnliche Unterprojekte mit eigenen Leitern und zugewiesenen Personen.",
"views_description": "Speichern Sie Sortierungen, Filter und Anzeigeoptionen für die spätere Verwendung oder zum Teilen.",
"pages_description": "Schreiben Sie alles Mögliche, wie gewohnt.",
"intake_description": "Bleiben Sie über abonnierte Arbeitselemente informiert. Aktivieren Sie diese Option, um Benachrichtigungen zu erhalten.",
"cycles_description": "Zeitlich begrenzen Sie die Arbeit pro Projekt und passen Sie den Zeitraum bei Bedarf an. Ein Zyklus kann 2 Wochen dauern, der nächste nur 1 Woche.",
"modules_description": "Organisieren Sie die Arbeit in Unterprojekte mit eigenen Leitern und Zuständigen.",
"views_description": "Speichern Sie benutzerdefinierte Sortierungen, Filter und Anzeigeoptionen oder teilen Sie sie mit Ihrem Team.",
"pages_description": "Erstellen und bearbeiten Sie frei formulierte Inhalte Notizen, Dokumente, alles Mögliche.",
"intake_description": "Erlauben Sie Nicht-Mitgliedern, Bugs, Feedback und Vorschläge zu teilen ohne Ihren Arbeitsablauf zu stören.",
"time_tracking_description": "Erfassen Sie die auf Arbeitselemente und Projekte verwendete Zeit.",
"work_management_description": "Verwalten Sie Ihre Arbeit und Projekte mühelos.",
"documentation": "Dokumentation",
@@ -500,7 +500,6 @@
"export": "Exportieren",
"member": "{count, plural, one{# Mitglied} few{# Mitglieder} other{# Mitglieder}}",
"new_password_must_be_different_from_old_password": "Das neue Passwort muss von dem alten Passwort abweichen",
"project_view": {
"sort_by": {
"created_at": "Erstellt am",
@@ -862,7 +861,10 @@
"deleting": "Wird gelöscht",
"pending": "Ausstehend",
"invite": "Einladen",
"view": "Ansicht"
"view": "Ansicht",
"deactivated_user": "Deaktivierter Benutzer",
"apply": "Anwenden",
"applying": "Wird angewendet"
},
"chart": {
"x_axis": "X-Achse",
@@ -1714,12 +1716,15 @@
"title": "Schätzungen für mein Projekt aktivieren",
"description": "Sie helfen dir, die Komplexität und Arbeitsbelastung des Teams zu kommunizieren.",
"no_estimate": "Keine Schätzung",
"new": "Neues Schätzungssystem",
"create": {
"custom": "Benutzerdefiniert",
"start_from_scratch": "Von Grund auf neu",
"choose_template": "Vorlage wählen",
"choose_estimate_system": "Schätzungssystem wählen",
"enter_estimate_point": "Schätzung eingeben"
"enter_estimate_point": "Schätzung eingeben",
"step": "Schritt {step} von {total}",
"label": "Schätzung erstellen"
},
"toasts": {
"created": {
@@ -1768,6 +1773,25 @@
"already_exists": "Der Schätzungswert existiert bereits.",
"unsaved_changes": "Du hast ungespeicherte Änderungen. Bitte speichere sie, bevor du auf Fertig klickst",
"remove_empty": "Die Schätzung darf nicht leer sein. Gib einen Wert in jedes Feld ein oder entferne die Felder, für die du keine Werte hast."
},
"systems": {
"points": {
"label": "Punkte",
"fibonacci": "Fibonacci",
"linear": "Linear",
"squares": "Quadrate",
"custom": "Benutzerdefiniert"
},
"categories": {
"label": "Kategorien",
"t_shirt_sizes": "T-Shirt-Größen",
"easy_to_hard": "Einfach bis schwer",
"custom": "Benutzerdefiniert"
},
"time": {
"label": "Zeit",
"hours": "Stunden"
}
}
},
"automations": {
@@ -2378,20 +2402,17 @@
"manual": "Manuell"
}
},
"cycle": {
"label": "{count, plural, one {Zyklus} few {Zyklen} other {Zyklen}}",
"no_cycle": "Kein Zyklus"
},
"module": {
"label": "{count, plural, one {Modul} few {Module} other {Module}}",
"no_module": "Kein Modul"
},
"description_versions": {
"last_edited_by": "Zuletzt bearbeitet von",
"previously_edited_by": "Zuvor bearbeitet von",
"edited_by": "Bearbeitet von"
}
}
}
+35 -55
View File
@@ -180,7 +180,7 @@
"couldnt_remove_the_project_from_favorites": "Couldn't remove the project from favorites. Please try again.",
"add_to_favorites": "Add to favorites",
"remove_from_favorites": "Remove from favorites",
"publish_settings": "Publish settings",
"publish_project": "Publish project",
"publish": "Publish",
"copy_link": "Copy link",
"leave_project": "Leave project",
@@ -197,12 +197,12 @@
"work_management": "Work management",
"projects_and_issues": "Projects and work items",
"projects_and_issues_description": "Toggle these on or off this project.",
"cycles_description": "Timebox work as you see fit per project and change frequency from one period to the next.",
"modules_description": "Group work into sub-project-like set-ups with their own leads and assignees.",
"views_description": "Save sorts, filters, and display options for later or share them.",
"pages_description": "Write anything like you write anything.",
"intake_description": "Stay in the loop on Work items you are subscribed to. Enable this to get notified.",
"time_tracking_description": "Track time spent on work items and projects.",
"cycles_description": "Timebox work per project and adjust the time period as needed. One cycle can be 2 weeks, the next 1 week.",
"modules_description": "Organize work into sub-projects with dedicated leads and assignees.",
"views_description": "Save custom sorts, filters, and display options or share them with your team.",
"pages_description": "Create and edit free-form content; notes, docs, anything.",
"intake_description": "Let non-members share bugs, feedback, and suggestions; without disrupting your workflow.",
"time_tracking_description": "Log time spent on work items and projects.",
"work_management_description": "Manage your work and projects with ease.",
"documentation": "Documentation",
"message_support": "Message support",
@@ -334,7 +334,6 @@
"new_password_must_be_different_from_old_password": "New password must be different from old password",
"edited": "edited",
"bot": "Bot",
"project_view": {
"sort_by": {
"created_at": "Created at",
@@ -342,12 +341,10 @@
"name": "Name"
}
},
"toast": {
"success": "Success!",
"error": "Error!"
},
"links": {
"toasts": {
"created": {
@@ -376,7 +373,6 @@
}
}
},
"home": {
"empty": {
"quickstart_guide": "Your quickstart guide",
@@ -444,7 +440,6 @@
"title": "Home",
"star_us_on_github": "Star us on GitHub"
},
"link": {
"modal": {
"url": {
@@ -458,7 +453,6 @@
}
}
},
"common": {
"all": "All",
"states": "States",
@@ -702,22 +696,22 @@
"deleting": "Deleting",
"pending": "Pending",
"invite": "Invite",
"view": "View"
"view": "View",
"deactivated_user": "Deactivated user",
"apply": "Apply",
"applying": "Applying"
},
"chart": {
"x_axis": "X-axis",
"y_axis": "Y-axis",
"metric": "Metric"
},
"form": {
"title": {
"required": "Title is required",
"max_length": "Title should be less than {length} characters"
}
},
"entity": {
"grouping_title": "{entity} Grouping",
"priority": "{entity} Priority",
@@ -741,7 +735,6 @@
"failed": "Error adding {entity}"
}
},
"epic": {
"all": "All Epics",
"label": "{count, plural, one {Epic} other {Epics}}",
@@ -759,7 +752,6 @@
"required": "Epic title is required."
}
},
"issue": {
"label": "{count, plural, one {Work item} other {Work items}}",
"all": "All Work items",
@@ -926,7 +918,6 @@
},
"open_in_full_screen": "Open work item in full screen"
},
"attachment": {
"error": "File could not be attached. Try uploading again.",
"only_one_file_allowed": "Only one file can be uploaded at a time.",
@@ -934,7 +925,6 @@
"drag_and_drop": "Drag and drop anywhere to upload",
"delete": "Delete attachment"
},
"label": {
"select": "Select label",
"create": {
@@ -944,7 +934,6 @@
"type": "Type to add a new label"
}
},
"sub_work_item": {
"update": {
"success": "Sub-work item updated successfully",
@@ -955,7 +944,6 @@
"error": "Error removing sub-work item"
}
},
"view": {
"label": "{count, plural, one {View} other {Views}}",
"create": {
@@ -965,7 +953,6 @@
"label": "Update View"
}
},
"inbox_issue": {
"status": {
"pending": {
@@ -1051,7 +1038,6 @@
}
}
},
"workspace_creation": {
"heading": "Create your workspace",
"subheading": "To start using Plane, you need to create or join a workspace.",
@@ -1103,7 +1089,6 @@
}
}
},
"workspace_dashboard": {
"empty_state": {
"general": {
@@ -1119,7 +1104,6 @@
}
}
},
"workspace_analytics": {
"label": "Analytics",
"page_label": "{workspace} - Analytics",
@@ -1164,7 +1148,6 @@
}
}
},
"workspace_projects": {
"label": "{count, plural, one {Project} other {Projects}}",
"create": {
@@ -1239,7 +1222,6 @@
}
}
},
"workspace_views": {
"add_view": "Add view",
"empty_state": {
@@ -1274,7 +1256,6 @@
}
}
},
"workspace_settings": {
"label": "Workspace settings",
"page_label": "{workspace} - General settings",
@@ -1456,7 +1437,6 @@
}
}
},
"profile": {
"label": "Profile",
"page_label": "Your work",
@@ -1519,7 +1499,6 @@
}
}
},
"project_settings": {
"general": {
"enter_project_id": "Enter project ID",
@@ -1573,12 +1552,15 @@
"title": "Enable estimates for my project",
"description": "They help you in communicating complexity and workload of the team.",
"no_estimate": "No estimate",
"new": "New estimate system",
"create": {
"custom": "Custom",
"start_from_scratch": "Start from scratch",
"choose_template": "Choose a template",
"choose_estimate_system": "Choose an estimate system",
"enter_estimate_point": "Enter estimate"
"enter_estimate_point": "Enter estimate",
"step": "Step {step} of {total}",
"label": "Create estimate"
},
"toasts": {
"created": {
@@ -1627,6 +1609,25 @@
"already_exists": "Estimate value already exists.",
"unsaved_changes": "You have some unsaved changes, Please save them before clicking on done",
"remove_empty": "Estimate can't be empty. Enter a value in each field or remove those you don't have values for."
},
"systems": {
"points": {
"label": "Points",
"fibonacci": "Fibonacci",
"linear": "Linear",
"squares": "Squares",
"custom": "Custom"
},
"categories": {
"label": "Categories",
"t_shirt_sizes": "T-Shirt Sizes",
"easy_to_hard": "Easy to hard",
"custom": "Custom"
},
"time": {
"label": "Time",
"hours": "Hours"
}
}
},
"automations": {
@@ -1643,7 +1644,6 @@
"auto_close_status": "Auto-close status"
}
},
"empty_state": {
"labels": {
"title": "No labels yet",
@@ -1656,7 +1656,6 @@
}
}
},
"project_cycles": {
"add_cycle": "Add cycle",
"more_details": "More details",
@@ -1782,7 +1781,6 @@
}
}
},
"project_issues": {
"empty_state": {
"no_issues": {
@@ -1811,7 +1809,6 @@
}
}
},
"project_module": {
"add_module": "Add Module",
"update_module": "Update Module",
@@ -1865,7 +1862,6 @@
}
}
},
"project_views": {
"empty_state": {
"general": {
@@ -1885,7 +1881,6 @@
}
}
},
"project_page": {
"empty_state": {
"general": {
@@ -1915,7 +1910,6 @@
}
}
},
"command_k": {
"empty_state": {
"search": {
@@ -1923,7 +1917,6 @@
}
}
},
"issue_relation": {
"empty_state": {
"search": {
@@ -1934,7 +1927,6 @@
}
}
},
"issue_comment": {
"empty_state": {
"general": {
@@ -1943,7 +1935,6 @@
}
}
},
"notification": {
"label": "Inbox",
"page_label": "{workspace} - Inbox",
@@ -2000,7 +1991,6 @@
"custom": "Custom"
}
},
"active_cycle": {
"empty_state": {
"progress": {
@@ -2020,7 +2010,6 @@
}
}
},
"disabled_project": {
"empty_state": {
"inbox": {
@@ -2083,7 +2072,6 @@
}
}
},
"stickies": {
"title": "Your stickies",
"placeholder": "click to type here",
@@ -2141,7 +2129,6 @@
}
}
},
"role_details": {
"guest": {
"title": "Guest",
@@ -2156,7 +2143,6 @@
"description": "All permissions set to true within the workspace."
}
},
"user_roles": {
"product_or_project_manager": "Product / Project Manager",
"development_or_engineering": "Development / Engineering",
@@ -2169,7 +2155,6 @@
"human_resources": "Human / Resources",
"other": "Other"
},
"importer": {
"github": {
"title": "Github",
@@ -2180,7 +2165,6 @@
"description": "Import work items and epics from Jira projects and epics."
}
},
"exporter": {
"csv": {
"title": "CSV",
@@ -2209,7 +2193,6 @@
"created": "Created",
"subscribed": "Subscribed"
},
"themes": {
"theme_options": {
"system_preference": {
@@ -2255,20 +2238,17 @@
"manual": "Manual"
}
},
"cycle": {
"label": "{count, plural, one {Cycle} other {Cycles}}",
"no_cycle": "No cycle"
},
"module": {
"label": "{count, plural, one {Module} other {Modules}}",
"no_module": "No module"
},
"description_versions": {
"last_edited_by": "Last edited by",
"previously_edited_by": "Previously edited by",
"edited_by": "Edited by"
}
}
}
+35 -57
View File
@@ -18,7 +18,6 @@
"pro": "Pro",
"upgrade": "Mejorar"
},
"auth": {
"common": {
"email": {
@@ -168,7 +167,6 @@
}
}
},
"submit": "Enviar",
"cancel": "Cancelar",
"loading": "Cargando",
@@ -352,7 +350,7 @@
"couldnt_remove_the_project_from_favorites": "No se pudo eliminar el proyecto de favoritos. Por favor, inténtalo de nuevo.",
"add_to_favorites": "Agregar a favoritos",
"remove_from_favorites": "Eliminar de favoritos",
"publish_settings": "Configuración de publicación",
"publish_project": "Publicar proyecto",
"publish": "Publicar",
"copy_link": "Copiar enlace",
"leave_project": "Abandonar proyecto",
@@ -369,12 +367,12 @@
"work_management": "Gestión del trabajo",
"projects_and_issues": "Proyectos y elementos de trabajo",
"projects_and_issues_description": "Activa o desactiva estos en este proyecto.",
"cycles_description": "Organiza el trabajo en períodos de tiempo según lo consideres conveniente por proyecto y cambia la frecuencia de un período a otro.",
"modules_description": "Agrupa el trabajo en configuraciones similares a subproyectos con sus propios líderes y asignados.",
"views_description": "Guarda ordenamientos, filtros y opciones de visualización para más tarde o compártelos.",
"pages_description": "Escribe cualquier cosa como escribirías cualquier cosa.",
"intake_description": "Mantente al tanto de los elementos de trabajo a los que estás suscrito. Activa esto para recibir notificaciones.",
"time_tracking_description": "Rastrea el tiempo dedicado a elementos de trabajo y proyectos.",
"cycles_description": "Organiza el trabajo por proyecto en períodos de tiempo y ajusta la duración según sea necesario. Un ciclo puede ser de 2 semanas y el siguiente de 1 semana.",
"modules_description": "Organiza el trabajo en subproyectos con líderes y responsables dedicados.",
"views_description": "Guarda ordenamientos, filtros y opciones de visualización personalizadas o compártelos con tu equipo.",
"pages_description": "Crea y edita contenido libre; notas, documentos, lo que sea.",
"intake_description": "Permite que personas ajenas al equipo compartan errores, comentarios y sugerencias sin interrumpir tu flujo de trabajo.",
"time_tracking_description": "Registra el tiempo dedicado a elementos de trabajo y proyectos.",
"work_management_description": "Gestiona tu trabajo y proyectos con facilidad.",
"documentation": "Documentación",
"message_support": "Mensaje al soporte",
@@ -506,7 +504,6 @@
"new_password_must_be_different_from_old_password": "La nueva contraseña debe ser diferente a la contraseña anterior",
"edited": "Modificado",
"bot": "Bot",
"project_view": {
"sort_by": {
"created_at": "Creado el",
@@ -514,12 +511,10 @@
"name": "Nombre"
}
},
"toast": {
"success": "¡Éxito!",
"error": "¡Error!"
},
"links": {
"toasts": {
"created": {
@@ -548,7 +543,6 @@
}
}
},
"home": {
"empty": {
"quickstart_guide": "Guía de inicio rápido",
@@ -616,7 +610,6 @@
"title": "Inicio",
"star_us_on_github": "Danos una estrella en GitHub"
},
"link": {
"modal": {
"url": {
@@ -630,7 +623,6 @@
}
}
},
"common": {
"all": "Todo",
"states": "Estados",
@@ -872,22 +864,22 @@
"deleting": "Eliminando",
"pending": "Pendiente",
"invite": "Invitar",
"view": "Ver"
"view": "Ver",
"deactivated_user": "Usuario desactivado",
"apply": "Aplicar",
"applying": "Aplicando"
},
"chart": {
"x_axis": "Eje X",
"y_axis": "Eje Y",
"metric": "Métrica"
},
"form": {
"title": {
"required": "El título es obligatorio",
"max_length": "El título debe tener menos de {length} caracteres"
}
},
"entity": {
"grouping_title": "Agrupación de {entity}",
"priority": "Prioridad de {entity}",
@@ -911,7 +903,6 @@
"failed": "Error al agregar {entity}"
}
},
"epic": {
"all": "Todos los Epics",
"label": "{count, plural, one {Epic} other {Epics}}",
@@ -929,7 +920,6 @@
"required": "El título del epic es obligatorio."
}
},
"issue": {
"label": "{count, plural, one {Elemento de trabajo} other {Elementos de trabajo}}",
"all": "Todos los elementos de trabajo",
@@ -1096,7 +1086,6 @@
},
"open_in_full_screen": "Abrir elemento de trabajo en pantalla completa"
},
"attachment": {
"error": "No se pudo adjuntar el archivo. Intenta subirlo de nuevo.",
"only_one_file_allowed": "Solo se puede subir un archivo a la vez.",
@@ -1104,7 +1093,6 @@
"drag_and_drop": "Arrastra y suelta en cualquier lugar para subir",
"delete": "Eliminar archivo adjunto"
},
"label": {
"select": "Seleccionar etiqueta",
"create": {
@@ -1114,7 +1102,6 @@
"type": "Escribe para agregar una nueva etiqueta"
}
},
"sub_work_item": {
"update": {
"success": "Sub-elemento actualizado correctamente",
@@ -1125,7 +1112,6 @@
"error": "Error al eliminar el sub-elemento"
}
},
"view": {
"label": "{count, plural, one {Vista} other {Vistas}}",
"create": {
@@ -1135,7 +1121,6 @@
"label": "Actualizar vista"
}
},
"inbox_issue": {
"status": {
"pending": {
@@ -1221,7 +1206,6 @@
}
}
},
"workspace_creation": {
"heading": "Crea tu espacio de trabajo",
"subheading": "Para comenzar a usar Plane, necesitas crear o unirte a un espacio de trabajo.",
@@ -1273,7 +1257,6 @@
}
}
},
"workspace_dashboard": {
"empty_state": {
"general": {
@@ -1289,7 +1272,6 @@
}
}
},
"workspace_analytics": {
"label": "Análisis",
"page_label": "{workspace} - Análisis",
@@ -1334,7 +1316,6 @@
}
}
},
"workspace_projects": {
"label": "{count, plural, one {Proyecto} other {Proyectos}}",
"create": {
@@ -1408,7 +1389,6 @@
}
}
},
"workspace_views": {
"add_view": "Agregar vista",
"empty_state": {
@@ -1443,7 +1423,6 @@
}
}
},
"workspace_settings": {
"label": "Configuración del espacio de trabajo",
"page_label": "{workspace} - Configuración general",
@@ -1625,7 +1604,6 @@
}
}
},
"profile": {
"label": "Perfil",
"page_label": "Tu trabajo",
@@ -1688,7 +1666,6 @@
}
}
},
"project_settings": {
"general": {
"enter_project_id": "Ingresa el ID del proyecto",
@@ -1742,12 +1719,15 @@
"title": "Activar estimaciones para mi proyecto",
"description": "Te ayudan a comunicar la complejidad y la carga de trabajo del equipo.",
"no_estimate": "Sin estimación",
"new": "Nuevo sistema de estimación",
"create": {
"custom": "Personalizado",
"start_from_scratch": "Comenzar desde cero",
"choose_template": "Elegir una plantilla",
"choose_estimate_system": "Elegir un sistema de estimación",
"enter_estimate_point": "Ingresar estimación"
"enter_estimate_point": "Ingresar estimación",
"step": "Paso {step} de {total}",
"label": "Crear estimación"
},
"toasts": {
"created": {
@@ -1796,6 +1776,25 @@
"already_exists": "El valor de la estimación ya existe.",
"unsaved_changes": "Tienes cambios sin guardar. Por favor guárdalos antes de hacer clic en Hecho",
"remove_empty": "La estimación no puede estar vacía. Ingresa un valor en cada campo o elimina aquellos para los que no tienes valores."
},
"systems": {
"points": {
"label": "Puntos",
"fibonacci": "Fibonacci",
"linear": "Lineal",
"squares": "Cuadrados",
"custom": "Personalizado"
},
"categories": {
"label": "Categorías",
"t_shirt_sizes": "Tallas de camiseta",
"easy_to_hard": "Fácil a difícil",
"custom": "Personalizado"
},
"time": {
"label": "Tiempo",
"hours": "Horas"
}
}
},
"automations": {
@@ -1812,7 +1811,6 @@
"auto_close_status": "Estado de cierre automático"
}
},
"empty_state": {
"labels": {
"title": "Aún no hay etiquetas",
@@ -1825,7 +1823,6 @@
}
}
},
"project_cycles": {
"add_cycle": "Agregar ciclo",
"more_details": "Más detalles",
@@ -1951,7 +1948,6 @@
}
}
},
"project_issues": {
"empty_state": {
"no_issues": {
@@ -1980,7 +1976,6 @@
}
}
},
"project_module": {
"add_module": "Agregar Módulo",
"update_module": "Actualizar Módulo",
@@ -2034,7 +2029,6 @@
}
}
},
"project_views": {
"empty_state": {
"general": {
@@ -2054,7 +2048,6 @@
}
}
},
"project_page": {
"empty_state": {
"general": {
@@ -2084,7 +2077,6 @@
}
}
},
"command_k": {
"empty_state": {
"search": {
@@ -2092,7 +2084,6 @@
}
}
},
"issue_relation": {
"empty_state": {
"search": {
@@ -2103,7 +2094,6 @@
}
}
},
"issue_comment": {
"empty_state": {
"general": {
@@ -2112,7 +2102,6 @@
}
}
},
"notification": {
"label": "Bandeja de entrada",
"page_label": "{workspace} - Bandeja de entrada",
@@ -2169,7 +2158,6 @@
"custom": "Personalizado"
}
},
"active_cycle": {
"empty_state": {
"progress": {
@@ -2189,7 +2177,6 @@
}
}
},
"disabled_project": {
"empty_state": {
"inbox": {
@@ -2252,7 +2239,6 @@
}
}
},
"stickies": {
"title": "Tus notas adhesivas",
"placeholder": "haz clic para escribir aquí",
@@ -2310,7 +2296,6 @@
}
}
},
"role_details": {
"guest": {
"title": "Invitado",
@@ -2325,7 +2310,6 @@
"description": "Todos los permisos establecidos como verdaderos dentro del espacio de trabajo."
}
},
"user_roles": {
"product_or_project_manager": "Gerente de Producto / Proyecto",
"development_or_engineering": "Desarrollo / Ingeniería",
@@ -2338,7 +2322,6 @@
"human_resources": "Recursos Humanos",
"other": "Otro"
},
"importer": {
"github": {
"title": "GitHub",
@@ -2349,7 +2332,6 @@
"description": "Importa elementos de trabajo y epics desde proyectos y epics de Jira."
}
},
"exporter": {
"csv": {
"title": "CSV",
@@ -2378,7 +2360,6 @@
"created": "Creados",
"subscribed": "Suscritos"
},
"themes": {
"theme_options": {
"system_preference": {
@@ -2424,20 +2405,17 @@
"manual": "Manual"
}
},
"cycle": {
"label": "{count, plural, one {Ciclo} other {Ciclos}}",
"no_cycle": "Sin ciclo"
},
"module": {
"label": "{count, plural, one {Módulo} other {Módulos}}",
"no_module": "Sin módulo"
},
"description_versions": {
"last_edited_by": "Última edición por",
"previously_edited_by": "Editado anteriormente por",
"edited_by": "Editado por"
}
}
}
+35 -57
View File
@@ -18,7 +18,6 @@
"pro": "Pro",
"upgrade": "Mettre à niveau"
},
"auth": {
"common": {
"email": {
@@ -168,7 +167,6 @@
}
}
},
"submit": "Soumettre",
"cancel": "Annuler",
"loading": "Chargement",
@@ -350,7 +348,7 @@
"couldnt_remove_the_project_from_favorites": "Impossible de supprimer le projet des favoris. Veuillez réessayer.",
"add_to_favorites": "Ajouter aux favoris",
"remove_from_favorites": "Supprimer des favoris",
"publish_settings": "Paramètres de publication",
"publish_project": "Publier le projet",
"publish": "Publier",
"copy_link": "Copier le lien",
"leave_project": "Quitter le projet",
@@ -367,12 +365,12 @@
"work_management": "Gestion du travail",
"projects_and_issues": "Projets et éléments de travail",
"projects_and_issues_description": "Activez ou désactivez ces éléments pour ce projet.",
"cycles_description": "Planifiez le travail comme vous le souhaitez par projet et changez la fréquence d'une période à l'autre.",
"modules_description": "Regroupez le travail en configurations de type sous-projet avec leurs propres responsables et assignés.",
"views_description": "Enregistrez les tris, filtres et options d'affichage pour plus tard ou partagez-les.",
"pages_description": "Écrivez n'importe quoi comme vous écrivez n'importe quoi.",
"intake_description": "Restez informé des éléments de travail auxquels vous êtes abonné. Activez ceci pour être notifié.",
"time_tracking_description": "Suivez le temps passé sur les éléments de travail et les projets.",
"cycles_description": "Planifiez le travail par projet dans un cadre temporel et ajustez la période au besoin. Un cycle peut durer 2 semaines, le suivant 1 semaine.",
"modules_description": "Organisez le travail en sous-projets avec des responsables et des personnes assignées dédiés.",
"views_description": "Enregistrez des tris, filtres et options d'affichage personnalisés ou partagez-les avec votre équipe.",
"pages_description": "Créez et modifiez du contenu libre : notes, documents, tout ce que vous voulez.",
"intake_description": "Permettez aux non-membres de partager des bugs, des retours et des suggestions, sans perturber votre flux de travail.",
"time_tracking_description": "Enregistrez le temps passé sur les éléments de travail et les projets.",
"work_management_description": "Gérez votre travail et vos projets facilement.",
"documentation": "Documentation",
"message_support": "Contacter le support",
@@ -504,7 +502,6 @@
"new_password_must_be_different_from_old_password": "Le nouveau mot de passe doit être différent du mot de passe précédent",
"edited": "Modifié",
"bot": "Bot",
"project_view": {
"sort_by": {
"created_at": "Créé le",
@@ -512,12 +509,10 @@
"name": "Nom"
}
},
"toast": {
"success": "Succès !",
"error": "Erreur !"
},
"links": {
"toasts": {
"created": {
@@ -546,7 +541,6 @@
}
}
},
"home": {
"empty": {
"quickstart_guide": "Guide de démarrage rapide",
@@ -614,7 +608,6 @@
"title": "Accueil",
"star_us_on_github": "Donnez-nous une étoile sur GitHub"
},
"link": {
"modal": {
"url": {
@@ -628,7 +621,6 @@
}
}
},
"common": {
"all": "Tout",
"states": "États",
@@ -870,22 +862,22 @@
"deleting": "Suppression",
"pending": "En attente",
"invite": "Inviter",
"view": "Afficher"
"view": "Afficher",
"deactivated_user": "Utilisateur désactivé",
"apply": "Appliquer",
"applying": "Application"
},
"chart": {
"x_axis": "Axe X",
"y_axis": "Axe Y",
"metric": "Métrique"
},
"form": {
"title": {
"required": "Le titre est requis",
"max_length": "Le titre doit contenir moins de {length} caractères"
}
},
"entity": {
"grouping_title": "Regroupement {entity}",
"priority": "Priorité {entity}",
@@ -909,7 +901,6 @@
"failed": "Erreur lors de l'ajout de {entity}"
}
},
"epic": {
"all": "Tous les Epics",
"label": "{count, plural, one {Epic} other {Epics}}",
@@ -927,7 +918,6 @@
"required": "Le titre de l'Epic est requis."
}
},
"issue": {
"label": "{count, plural, one {Élément de travail} other {Éléments de travail}}",
"all": "Tous les éléments de travail",
@@ -1094,7 +1084,6 @@
},
"open_in_full_screen": "Ouvrir l'élément de travail en plein écran"
},
"attachment": {
"error": "Le fichier n'a pas pu être joint. Essayez de le télécharger à nouveau.",
"only_one_file_allowed": "Un seul fichier peut être téléchargé à la fois.",
@@ -1102,7 +1091,6 @@
"drag_and_drop": "Glissez-déposez n'importe où pour télécharger",
"delete": "Supprimer la pièce jointe"
},
"label": {
"select": "Sélectionner une étiquette",
"create": {
@@ -1112,7 +1100,6 @@
"type": "Tapez pour ajouter une nouvelle étiquette"
}
},
"sub_work_item": {
"update": {
"success": "Sous-élément de travail mis à jour avec succès",
@@ -1123,7 +1110,6 @@
"error": "Erreur lors de la suppression du sous-élément de travail"
}
},
"view": {
"label": "{count, plural, one {Vue} other {Vues}}",
"create": {
@@ -1133,7 +1119,6 @@
"label": "Mettre à jour la vue"
}
},
"inbox_issue": {
"status": {
"pending": {
@@ -1219,7 +1204,6 @@
}
}
},
"workspace_creation": {
"heading": "Créez votre espace de travail",
"subheading": "Pour commencer à utiliser Plane, vous devez créer ou rejoindre un espace de travail.",
@@ -1271,7 +1255,6 @@
}
}
},
"workspace_dashboard": {
"empty_state": {
"general": {
@@ -1287,7 +1270,6 @@
}
}
},
"workspace_analytics": {
"label": "Analytique",
"page_label": "{workspace} - Analytique",
@@ -1332,7 +1314,6 @@
}
}
},
"workspace_projects": {
"label": "{count, plural, one {Projet} other {Projets}}",
"create": {
@@ -1406,7 +1387,6 @@
}
}
},
"workspace_views": {
"add_view": "Ajouter une vue",
"empty_state": {
@@ -1441,7 +1421,6 @@
}
}
},
"workspace_settings": {
"label": "Paramètres de l'espace de travail",
"page_label": "{workspace} - Paramètres généraux",
@@ -1623,7 +1602,6 @@
}
}
},
"profile": {
"label": "Profil",
"page_label": "Votre travail",
@@ -1686,7 +1664,6 @@
}
}
},
"project_settings": {
"general": {
"enter_project_id": "Saisissez l'ID du projet",
@@ -1740,12 +1717,15 @@
"title": "Activer les estimations pour mon projet",
"description": "Elles vous aident à communiquer la complexité et la charge de travail de l'équipe.",
"no_estimate": "Sans estimation",
"new": "Nouveau système d'estimation",
"create": {
"custom": "Personnalisé",
"start_from_scratch": "Commencer depuis zéro",
"choose_template": "Choisir un modèle",
"choose_estimate_system": "Choisir un système d'estimation",
"enter_estimate_point": "Saisir une estimation"
"enter_estimate_point": "Saisir une estimation",
"step": "Étape {step} de {total}",
"label": "Créer une estimation"
},
"toasts": {
"created": {
@@ -1794,6 +1774,25 @@
"already_exists": "La valeur de l'estimation existe déjà.",
"unsaved_changes": "Vous avez des modifications non enregistrées. Veuillez les enregistrer avant de cliquer sur Terminé",
"remove_empty": "L'estimation ne peut pas être vide. Saisissez une valeur dans chaque champ ou supprimez ceux pour lesquels vous n'avez pas de valeurs."
},
"systems": {
"points": {
"label": "Points",
"fibonacci": "Fibonacci",
"linear": "Linéaire",
"squares": "Carrés",
"custom": "Personnalisé"
},
"categories": {
"label": "Catégories",
"t_shirt_sizes": "Tailles de T-Shirt",
"easy_to_hard": "Facile à difficile",
"custom": "Personnalisé"
},
"time": {
"label": "Temps",
"hours": "Heures"
}
}
},
"automations": {
@@ -1810,7 +1809,6 @@
"auto_close_status": "Statut de fermeture automatique"
}
},
"empty_state": {
"labels": {
"title": "Pas encore d'étiquettes",
@@ -1823,7 +1821,6 @@
}
}
},
"project_cycles": {
"add_cycle": "Ajouter un cycle",
"more_details": "Plus de détails",
@@ -1949,7 +1946,6 @@
}
}
},
"project_issues": {
"empty_state": {
"no_issues": {
@@ -1978,7 +1974,6 @@
}
}
},
"project_module": {
"add_module": "Ajouter un module",
"update_module": "Mettre à jour le module",
@@ -2032,7 +2027,6 @@
}
}
},
"project_views": {
"empty_state": {
"general": {
@@ -2052,7 +2046,6 @@
}
}
},
"project_page": {
"empty_state": {
"general": {
@@ -2082,7 +2075,6 @@
}
}
},
"command_k": {
"empty_state": {
"search": {
@@ -2090,7 +2082,6 @@
}
}
},
"issue_relation": {
"empty_state": {
"search": {
@@ -2101,7 +2092,6 @@
}
}
},
"issue_comment": {
"empty_state": {
"general": {
@@ -2110,7 +2100,6 @@
}
}
},
"notification": {
"label": "Boîte de réception",
"page_label": "{workspace} - Boîte de réception",
@@ -2167,7 +2156,6 @@
"custom": "Personnalisé"
}
},
"active_cycle": {
"empty_state": {
"progress": {
@@ -2187,7 +2175,6 @@
}
}
},
"disabled_project": {
"empty_state": {
"inbox": {
@@ -2250,7 +2237,6 @@
}
}
},
"stickies": {
"title": "Vos notes adhésives",
"placeholder": "cliquez pour écrire ici",
@@ -2308,7 +2294,6 @@
}
}
},
"role_details": {
"guest": {
"title": "Invité",
@@ -2323,7 +2308,6 @@
"description": "Toutes les permissions sont activées dans l'espace de travail."
}
},
"user_roles": {
"product_or_project_manager": "Chef de produit / Chef de projet",
"development_or_engineering": "Développement / Ingénierie",
@@ -2336,7 +2320,6 @@
"human_resources": "Ressources Humaines",
"other": "Autre"
},
"importer": {
"github": {
"title": "GitHub",
@@ -2347,7 +2330,6 @@
"description": "Importez des éléments de travail et des epics depuis les projets et epics Jira."
}
},
"exporter": {
"csv": {
"title": "CSV",
@@ -2376,7 +2358,6 @@
"created": "Créés",
"subscribed": "Suivis"
},
"themes": {
"theme_options": {
"system_preference": {
@@ -2422,20 +2403,17 @@
"manual": "Manuel"
}
},
"cycle": {
"label": "{count, plural, one {Cycle} other {Cycles}}",
"no_cycle": "Pas de cycle"
},
"module": {
"label": "{count, plural, one {Module} other {Modules}}",
"no_module": "Pas de module"
},
"description_versions": {
"last_edited_by": "Dernière modification par",
"previously_edited_by": "Précédemment modifié par",
"edited_by": "Modifié par"
}
}
}
+35 -57
View File
@@ -18,7 +18,6 @@
"pro": "Pro",
"upgrade": "Upgrade"
},
"auth": {
"common": {
"email": {
@@ -168,7 +167,6 @@
}
}
},
"submit": "Kirim",
"cancel": "Batal",
"loading": "Memuat",
@@ -350,7 +348,7 @@
"couldnt_remove_the_project_from_favorites": "Tidak dapat menghapus proyek dari favorit. Silakan coba lagi.",
"add_to_favorites": "Tambah ke favorit",
"remove_from_favorites": "Hapus dari favorit",
"publish_settings": "Pengaturan publikasi",
"publish_project": "Publikasikan proyek",
"publish": "Publikasikan",
"copy_link": "Salin tautan",
"leave_project": "Tinggalkan proyek",
@@ -367,12 +365,12 @@
"work_management": "Manajemen kerja",
"projects_and_issues": "Proyek dan item kerja",
"projects_and_issues_description": "Aktifkan atau nonaktifkan ini untuk proyek ini.",
"cycles_description": "Batasi waktu kerja sesuai keinginan Anda per proyek dan ubah frekuensi dari satu periode ke periode berikutnya.",
"modules_description": "Kerja kelompok menjadi pengaturan sub-proyek dengan pemimpin dan penugasnya sendiri.",
"views_description": "Simpan jenis, filter, dan opsi tampilan untuk nanti atau bagikan.",
"pages_description": "Tulis apapun seperti yang Anda tulis.",
"intake_description": "Tetap terhubung dengan item kerja yang Anda ikuti. Aktifkan ini untuk mendapatkan pemberitahuan.",
"time_tracking_description": "Lacak waktu yang dihabiskan untuk item kerja dan proyek.",
"cycles_description": "Tetapkan batas waktu kerja per proyek dan sesuaikan periode waktunya sesuai kebutuhan. Satu siklus bisa 2 minggu, berikutnya 1 minggu.",
"modules_description": "Atur pekerjaan ke dalam sub-proyek dengan pemimpin dan penanggung jawab khusus.",
"views_description": "Simpan pengurutan, filter, dan opsi tampilan khusus atau bagikan dengan tim Anda.",
"pages_description": "Buat dan edit konten bebas bentuk: catatan, dokumen, apa saja.",
"intake_description": "Izinkan non-anggota membagikan bug, masukan, dan saran tanpa mengganggu alur kerja Anda.",
"time_tracking_description": "Catat waktu yang dihabiskan untuk item kerja dan proyek.",
"work_management_description": "Kelola pekerjaan dan proyek Anda dengan mudah.",
"documentation": "Dokumentasi",
"message_support": "Pesan dukungan",
@@ -502,7 +500,6 @@
"export": "Ekspor",
"member": "{count, plural, one{# anggota} other{# anggota}}",
"new_password_must_be_different_from_old_password": "Kata sandi baru harus berbeda dari kata sandi lama",
"project_view": {
"sort_by": {
"created_at": "Dibuat pada",
@@ -510,12 +507,10 @@
"name": "Nama"
}
},
"toast": {
"success": "Sukses!",
"error": "Kesalahan!"
},
"links": {
"toasts": {
"created": {
@@ -544,7 +539,6 @@
}
}
},
"home": {
"empty": {
"quickstart_guide": "Panduan pemula Anda",
@@ -612,7 +606,6 @@
"title": "Beranda",
"star_us_on_github": "Bintang kami di GitHub"
},
"link": {
"modal": {
"url": {
@@ -626,7 +619,6 @@
}
}
},
"common": {
"all": "Semua",
"states": "Negara-negara",
@@ -869,22 +861,22 @@
"deleting": "Menghapus",
"pending": "Tertunda",
"invite": "Undang",
"view": "Lihat"
"view": "Lihat",
"deactivated_user": "Pengguna dinonaktifkan",
"apply": "Terapkan",
"applying": "Terapkan"
},
"chart": {
"x_axis": "Sumbu-X",
"y_axis": "Sumbu-Y",
"metric": "Metrik"
},
"form": {
"title": {
"required": "Judul wajib diisi",
"max_length": "Judul harus kurang dari {length} karakter"
}
},
"entity": {
"grouping_title": "Pengelompokan {entity}",
"priority": "Prioritas {entity}",
@@ -908,7 +900,6 @@
"failed": "Terjadi kesalahan saat menambahkan {entity}"
}
},
"epic": {
"all": "Semua Epik",
"label": "{count, plural, one {Epik} other {Epik}}",
@@ -926,7 +917,6 @@
"required": "Judul epik wajib diisi."
}
},
"issue": {
"label": "{count, plural, one {Item Kerja} other {Item Kerja}}",
"all": "Semua Item Kerja",
@@ -1093,7 +1083,6 @@
},
"open_in_full_screen": "Buka item kerja dalam layar penuh"
},
"attachment": {
"error": "File tidak dapat dilampirkan. Coba unggah lagi.",
"only_one_file_allowed": "Hanya satu file yang dapat diunggah pada satu waktu.",
@@ -1101,7 +1090,6 @@
"drag_and_drop": "Seret dan jatuhkan di mana saja untuk mengunggah",
"delete": "Hapus lampiran"
},
"label": {
"select": "Pilih label",
"create": {
@@ -1111,7 +1099,6 @@
"type": "Ketik untuk menambah label baru"
}
},
"sub_work_item": {
"update": {
"success": "Sub-item kerja berhasil diperbarui",
@@ -1122,7 +1109,6 @@
"error": "Kesalahan saat menghapus sub-item kerja"
}
},
"view": {
"label": "{count, plural, one {Tampilan} other {Tampilan}}",
"create": {
@@ -1132,7 +1118,6 @@
"label": "Perbarui Tampilan"
}
},
"inbox_issue": {
"status": {
"pending": {
@@ -1218,7 +1203,6 @@
}
}
},
"workspace_creation": {
"heading": "Buat ruang kerja Anda",
"subheading": "Untuk mulai menggunakan Plane, Anda perlu membuat atau bergabung dengan ruang kerja.",
@@ -1270,7 +1254,6 @@
}
}
},
"workspace_dashboard": {
"empty_state": {
"general": {
@@ -1286,7 +1269,6 @@
}
}
},
"workspace_analytics": {
"label": "Analitik",
"page_label": "{workspace} - Analitik",
@@ -1331,7 +1313,6 @@
}
}
},
"workspace_projects": {
"label": "{count, plural, one {Proyek} other {Proyek}}",
"create": {
@@ -1406,7 +1387,6 @@
}
}
},
"workspace_views": {
"add_view": "Tambah tampilan",
"empty_state": {
@@ -1441,7 +1421,6 @@
}
}
},
"workspace_settings": {
"label": "Pengaturan ruang kerja",
"page_label": "{workspace} - Pengaturan Umum",
@@ -1623,7 +1602,6 @@
}
}
},
"profile": {
"label": "Profil",
"page_label": "Pekerjaan Anda",
@@ -1686,7 +1664,6 @@
}
}
},
"project_settings": {
"general": {
"enter_project_id": "Masukkan ID proyek",
@@ -1740,12 +1717,15 @@
"title": "Aktifkan perkiraan untuk proyek saya",
"description": "Ini membantu Anda dalam mengkomunikasikan kompleksitas dan beban kerja tim.",
"no_estimate": "Tidak ada perkiraan",
"new": "Sistem perkiraan baru",
"create": {
"custom": "Kustom",
"start_from_scratch": "Mulai dari awal",
"choose_template": "Pilih template",
"choose_estimate_system": "Pilih sistem perkiraan",
"enter_estimate_point": "Masukkan perkiraan"
"enter_estimate_point": "Masukkan perkiraan",
"step": "Langkah {step} dari {total}",
"label": "Buat perkiraan"
},
"toasts": {
"created": {
@@ -1794,6 +1774,25 @@
"already_exists": "Nilai perkiraan sudah ada.",
"unsaved_changes": "Anda memiliki beberapa perubahan yang belum disimpan, Harap simpan sebelum mengklik selesai",
"remove_empty": "Perkiraan tidak boleh kosong. Masukkan nilai di setiap bidang atau hapus yang tidak memiliki nilai."
},
"systems": {
"points": {
"label": "Poin",
"fibonacci": "Fibonacci",
"linear": "Linear",
"squares": "Kuadrat",
"custom": "Kustom"
},
"categories": {
"label": "Kategori",
"t_shirt_sizes": "Ukuran Baju",
"easy_to_hard": "Mudah ke sulit",
"custom": "Kustom"
},
"time": {
"label": "Waktu",
"hours": "Jam"
}
}
},
"automations": {
@@ -1810,7 +1809,6 @@
"auto_close_status": "Status penutupan otomatis"
}
},
"empty_state": {
"labels": {
"title": "Belum ada label",
@@ -1823,7 +1821,6 @@
}
}
},
"project_cycles": {
"add_cycle": "Tambah siklus",
"more_details": "Detail lebih lanjut",
@@ -1943,7 +1940,6 @@
}
}
},
"project_issues": {
"empty_state": {
"no_issues": {
@@ -1972,7 +1968,6 @@
}
}
},
"project_module": {
"add_module": "Tambah Modul",
"update_module": "Perbarui Modul",
@@ -2026,7 +2021,6 @@
}
}
},
"project_views": {
"empty_state": {
"general": {
@@ -2046,7 +2040,6 @@
}
}
},
"project_page": {
"empty_state": {
"general": {
@@ -2076,7 +2069,6 @@
}
}
},
"command_k": {
"empty_state": {
"search": {
@@ -2084,7 +2076,6 @@
}
}
},
"issue_relation": {
"empty_state": {
"search": {
@@ -2095,7 +2086,6 @@
}
}
},
"issue_comment": {
"empty_state": {
"general": {
@@ -2104,7 +2094,6 @@
}
}
},
"notification": {
"label": "Kotak Masuk",
"page_label": "{workspace} - Kotak Masuk",
@@ -2161,7 +2150,6 @@
"custom": "Kustom"
}
},
"active_cycle": {
"empty_state": {
"progress": {
@@ -2181,7 +2169,6 @@
}
}
},
"disabled_project": {
"empty_state": {
"inbox": {
@@ -2244,7 +2231,6 @@
}
}
},
"stickies": {
"title": "Catatan tempel Anda",
"placeholder": "klik untuk mengetik di sini",
@@ -2302,7 +2288,6 @@
}
}
},
"role_details": {
"guest": {
"title": "Tamu",
@@ -2317,7 +2302,6 @@
"description": "Semua izin diatur ke true dalam ruang kerja."
}
},
"user_roles": {
"product_or_project_manager": "Manajer Produk / Proyek",
"development_or_engineering": "Pengembangan / Rekayasa",
@@ -2330,7 +2314,6 @@
"human_resources": "Sumber Daya Manusia",
"other": "Lainnya"
},
"importer": {
"github": {
"title": "Github",
@@ -2341,7 +2324,6 @@
"description": "Impor item kerja dan epik dari proyek dan epik Jira."
}
},
"exporter": {
"csv": {
"title": "CSV",
@@ -2370,7 +2352,6 @@
"created": "Dibuat",
"subscribed": "Disubscribe"
},
"themes": {
"theme_options": {
"system_preference": {
@@ -2416,20 +2397,17 @@
"manual": "Manual"
}
},
"cycle": {
"label": "{count, plural, one {Siklus} other {Siklus}}",
"no_cycle": "Tidak ada siklus"
},
"module": {
"label": "{count, plural, one {Modul} other {Modul}}",
"no_module": "Tidak ada modul"
},
"description_versions": {
"last_edited_by": "Terakhir disunting oleh",
"previously_edited_by": "Sebelumnya disunting oleh",
"edited_by": "Disunting oleh"
}
}
}
+35 -57
View File
@@ -18,7 +18,6 @@
"pro": "Pro",
"upgrade": "Aggiorna"
},
"auth": {
"common": {
"email": {
@@ -349,7 +348,7 @@
"couldnt_remove_the_project_from_favorites": "Impossibile rimuovere il progetto dai preferiti. Per favore, riprova.",
"add_to_favorites": "Aggiungi ai preferiti",
"remove_from_favorites": "Rimuovi dai preferiti",
"publish_settings": "Impostazioni di pubblicazione",
"publish_project": "Pubblica progetto",
"publish": "Pubblica",
"copy_link": "Copia link",
"leave_project": "Lascia progetto",
@@ -366,12 +365,12 @@
"work_management": "Gestione del lavoro",
"projects_and_issues": "Progetti ed elementi di lavoro",
"projects_and_issues_description": "Attiva o disattiva queste opzioni per questo progetto.",
"cycles_description": "Definisci i cicli di lavoro per progetto e modifica la frequenza da un periodo all'altro.",
"modules_description": "Raggruppa il lavoro in configurazioni simili a sotto-progetti con i propri responsabili e assegnatari.",
"views_description": "Salva ordinamenti, filtri e opzioni di visualizzazione per dopo o condividili.",
"pages_description": "Scrivi qualsiasi cosa, come faresti normalmente.",
"intake_description": "Rimani aggiornato sugli elementi di lavoro a cui sei iscritto. Abilita questa opzione per ricevere notifiche.",
"time_tracking_description": "Traccia il tempo speso sugli elementi di lavoro e sui progetti.",
"cycles_description": "Definisci il tempo di lavoro per progetto e adatta il periodo secondo necessità. Un ciclo può durare 2 settimane, il successivo 1 settimana.",
"modules_description": "Organizza il lavoro in sotto-progetti con responsabili e assegnatari dedicati.",
"views_description": "Salva ordinamenti, filtri e opzioni di visualizzazione personalizzati o condividili con il tuo team.",
"pages_description": "Crea e modifica contenuti liberi: appunti, documenti, qualsiasi cosa.",
"intake_description": "Consenti ai non membri di segnalare bug, feedback e suggerimenti senza interrompere il tuo flusso di lavoro.",
"time_tracking_description": "Registra il tempo trascorso su elementi di lavoro e progetti.",
"work_management_description": "Gestisci il tuo lavoro e i tuoi progetti con facilità.",
"documentation": "Documentazione",
"message_support": "Contatta il supporto",
@@ -501,10 +500,8 @@
"export": "Esporta",
"member": "{count, plural, one {# membro} other {# membri}}",
"new_password_must_be_different_from_old_password": "La nuova password deve essere diversa dalla password precedente",
"edited": "Modificato",
"bot": "Bot",
"project_view": {
"sort_by": {
"created_at": "Creato il",
@@ -512,12 +509,10 @@
"name": "Nome"
}
},
"toast": {
"success": "Successo!",
"error": "Errore!"
},
"links": {
"toasts": {
"created": {
@@ -546,7 +541,6 @@
}
}
},
"home": {
"empty": {
"quickstart_guide": "La tua guida rapida",
@@ -614,7 +608,6 @@
"title": "Home",
"star_us_on_github": "Metti una stella su GitHub"
},
"link": {
"modal": {
"url": {
@@ -628,7 +621,6 @@
}
}
},
"common": {
"all": "Tutti",
"states": "Stati",
@@ -868,22 +860,22 @@
"deleting": "Eliminazione in corso",
"pending": "In sospeso",
"invite": "Invita",
"view": "Visualizza"
"view": "Visualizza",
"deactivated_user": "Utente disattivato",
"apply": "Applica",
"applying": "Applicazione"
},
"chart": {
"x_axis": "Asse X",
"y_axis": "Asse Y",
"metric": "Metrica"
},
"form": {
"title": {
"required": "Il titolo è obbligatorio",
"max_length": "Il titolo deve contenere meno di {length} caratteri"
}
},
"entity": {
"grouping_title": "Raggruppamento di {entity}",
"priority": "Priorità di {entity}",
@@ -907,7 +899,6 @@
"failed": "Errore nell'aggiunta di {entity}"
}
},
"epic": {
"all": "Tutti gli Epic",
"label": "{count, plural, one {Epic} other {Epic}}",
@@ -925,7 +916,6 @@
"required": "Il titolo dell'Epic è obbligatorio."
}
},
"issue": {
"label": "{count, plural, one {Elemento di lavoro} other {Elementi di lavoro}}",
"all": "Tutti gli elementi di lavoro",
@@ -1092,7 +1082,6 @@
},
"open_in_full_screen": "Apri l'elemento di lavoro a schermo intero"
},
"attachment": {
"error": "Impossibile allegare il file. Riprova a caricarlo.",
"only_one_file_allowed": "È possibile caricare un solo file alla volta.",
@@ -1100,7 +1089,6 @@
"drag_and_drop": "Trascina e rilascia ovunque per caricare",
"delete": "Elimina allegato"
},
"label": {
"select": "Seleziona etichetta",
"create": {
@@ -1110,7 +1098,6 @@
"type": "Digita per aggiungere una nuova etichetta"
}
},
"sub_work_item": {
"update": {
"success": "Sotto-elemento di lavoro aggiornato con successo",
@@ -1121,7 +1108,6 @@
"error": "Errore nella rimozione del sotto-elemento di lavoro"
}
},
"view": {
"label": "{count, plural, one {Visualizzazione} other {Visualizzazioni}}",
"create": {
@@ -1131,7 +1117,6 @@
"label": "Aggiorna visualizzazione"
}
},
"inbox_issue": {
"status": {
"pending": {
@@ -1217,7 +1202,6 @@
}
}
},
"workspace_creation": {
"heading": "Crea il tuo spazio di lavoro",
"subheading": "Per iniziare a usare Plane, devi creare o unirti a uno spazio di lavoro.",
@@ -1269,7 +1253,6 @@
}
}
},
"workspace_dashboard": {
"empty_state": {
"general": {
@@ -1285,7 +1268,6 @@
}
}
},
"workspace_analytics": {
"label": "Analisi",
"page_label": "{workspace} - Analisi",
@@ -1330,7 +1312,6 @@
}
}
},
"workspace_projects": {
"label": "{count, plural, one {Progetto} other {Progetti}}",
"create": {
@@ -1405,7 +1386,6 @@
}
}
},
"workspace_views": {
"add_view": "Aggiungi visualizzazione",
"empty_state": {
@@ -1440,7 +1420,6 @@
}
}
},
"workspace_settings": {
"label": "Impostazioni dello spazio di lavoro",
"page_label": "{workspace} - Impostazioni generali",
@@ -1622,7 +1601,6 @@
}
}
},
"profile": {
"label": "Profilo",
"page_label": "Il tuo lavoro",
@@ -1685,7 +1663,6 @@
}
}
},
"project_settings": {
"general": {
"enter_project_id": "Inserisci l'ID del progetto",
@@ -1739,12 +1716,15 @@
"title": "Abilita le stime per il mio progetto",
"description": "Ti aiutano a comunicare la complessità e il carico di lavoro del team.",
"no_estimate": "Nessuna stima",
"new": "Nuovo sistema di stima",
"create": {
"custom": "Personalizzato",
"start_from_scratch": "Inizia da zero",
"choose_template": "Scegli un modello",
"choose_estimate_system": "Scegli un sistema di stima",
"enter_estimate_point": "Inserisci stima"
"enter_estimate_point": "Inserisci stima",
"step": "Passo {step} di {total}",
"label": "Crea stima"
},
"toasts": {
"created": {
@@ -1793,6 +1773,25 @@
"already_exists": "Il valore della stima esiste già.",
"unsaved_changes": "Hai delle modifiche non salvate. Salva prima di cliccare su Fatto",
"remove_empty": "La stima non può essere vuota. Inserisci un valore in ogni campo o rimuovi quelli per cui non hai valori."
},
"systems": {
"points": {
"label": "Punti",
"fibonacci": "Fibonacci",
"linear": "Lineare",
"squares": "Quadrati",
"custom": "Personalizzato"
},
"categories": {
"label": "Categorie",
"t_shirt_sizes": "Taglie T-Shirt",
"easy_to_hard": "Da facile a difficile",
"custom": "Personalizzato"
},
"time": {
"label": "Tempo",
"hours": "Ore"
}
}
},
"automations": {
@@ -1809,7 +1808,6 @@
"auto_close_status": "Stato di chiusura automatica"
}
},
"empty_state": {
"labels": {
"title": "Nessuna etichetta ancora",
@@ -1822,7 +1820,6 @@
}
}
},
"project_cycles": {
"add_cycle": "Aggiungi ciclo",
"more_details": "Altri dettagli",
@@ -1948,7 +1945,6 @@
}
}
},
"project_issues": {
"empty_state": {
"no_issues": {
@@ -1977,7 +1973,6 @@
}
}
},
"project_module": {
"add_module": "Aggiungi Modulo",
"update_module": "Aggiorna Modulo",
@@ -2031,7 +2026,6 @@
}
}
},
"project_views": {
"empty_state": {
"general": {
@@ -2051,7 +2045,6 @@
}
}
},
"project_page": {
"empty_state": {
"general": {
@@ -2081,7 +2074,6 @@
}
}
},
"command_k": {
"empty_state": {
"search": {
@@ -2089,7 +2081,6 @@
}
}
},
"issue_relation": {
"empty_state": {
"search": {
@@ -2100,7 +2091,6 @@
}
}
},
"issue_comment": {
"empty_state": {
"general": {
@@ -2109,7 +2099,6 @@
}
}
},
"notification": {
"label": "Notifiche",
"page_label": "{workspace} - Notifiche",
@@ -2166,7 +2155,6 @@
"custom": "Personalizzato"
}
},
"active_cycle": {
"empty_state": {
"progress": {
@@ -2186,7 +2174,6 @@
}
}
},
"disabled_project": {
"empty_state": {
"inbox": {
@@ -2249,7 +2236,6 @@
}
}
},
"stickies": {
"title": "I tuoi stickies",
"placeholder": "clicca per scrivere qui",
@@ -2307,7 +2293,6 @@
}
}
},
"role_details": {
"guest": {
"title": "Ospite",
@@ -2322,7 +2307,6 @@
"description": "Tutti i permessi impostati su true all'interno dello spazio di lavoro."
}
},
"user_roles": {
"product_or_project_manager": "Product / Project Manager",
"development_or_engineering": "Sviluppo / Ingegneria",
@@ -2335,7 +2319,6 @@
"human_resources": "Risorse umane",
"other": "Altro"
},
"importer": {
"github": {
"title": "Github",
@@ -2346,7 +2329,6 @@
"description": "Importa elementi di lavoro ed epic dai progetti e dagli epic di Jira."
}
},
"exporter": {
"csv": {
"title": "CSV",
@@ -2375,7 +2357,6 @@
"created": "Creati",
"subscribed": "Iscritti"
},
"themes": {
"theme_options": {
"system_preference": {
@@ -2421,20 +2402,17 @@
"manual": "Manuale"
}
},
"cycle": {
"label": "{count, plural, one {Ciclo} other {Cicli}}",
"no_cycle": "Nessun ciclo"
},
"module": {
"label": "{count, plural, one {Modulo} other {Moduli}}",
"no_module": "Nessun modulo"
},
"description_versions": {
"last_edited_by": "Ultima modifica di",
"previously_edited_by": "Precedentemente modificato da",
"edited_by": "Modificato da"
}
}
}
+35 -57
View File
@@ -18,7 +18,6 @@
"pro": "プロ",
"upgrade": "アップグレード"
},
"auth": {
"common": {
"email": {
@@ -168,7 +167,6 @@
}
}
},
"submit": "送信",
"cancel": "キャンセル",
"loading": "読み込み中",
@@ -350,7 +348,7 @@
"couldnt_remove_the_project_from_favorites": "プロジェクトをお気に入りから削除できませんでした。もう一度お試しください。",
"add_to_favorites": "お気に入りに追加",
"remove_from_favorites": "お気に入りから削除",
"publish_settings": "公開設定",
"publish_project": "プロジェクトを公開",
"publish": "公開",
"copy_link": "リンクをコピー",
"leave_project": "プロジェクトを退出",
@@ -367,12 +365,12 @@
"work_management": "作業管理",
"projects_and_issues": "プロジェクトと作業項目",
"projects_and_issues_description": "このプロジェクトでオン/オフを切り替えます。",
"cycles_description": "プロジェクトごとに作業時間枠で区切り、期間を次の期間に変更します。",
"modules_description": "サブプロジェクトのような設定で、独自のリーダーと担当者を持つグループ作業を行います。",
"views_description": "並び替え、フィルター、表示オプションを後で使用するために保存するか、共有します。",
"pages_description": "何でも書けるように何でも書きます。",
"intake_description": "購読している作業項目の最新情報を受け取ります。通知を受け取るには有効にしてください。",
"time_tracking_description": "作業項目プロジェクトの作業時間を追跡します。",
"cycles_description": "プロジェクトごとに作業時間枠を設定し、必要に応じて期間を調整します。1サイクルは2週間、次は1週間でもかまいません。",
"modules_description": "専任のリーダーと担当者を持つサブプロジェクトに作業を整理します。",
"views_description": "カスタムの並び替え、フィルター、表示オプションを保存するか、チームと共有します。",
"pages_description": "自由形式のコンテンツを作成・編集できます。メモ、ドキュメント、何でもOKです。",
"intake_description": "非メンバーがバグ、フィードバック、提案を共有できるようにし、ワークフローを妨げないようにします。",
"time_tracking_description": "作業項目プロジェクトに費やした時間を記録します。",
"work_management_description": "作業とプロジェクトを簡単に管理します。",
"documentation": "ドキュメント",
"message_support": "サポートにメッセージ",
@@ -504,7 +502,6 @@
"new_password_must_be_different_from_old_password": "新しいパスワードは古いパスワードと異なる必要があります",
"edited": "編集済み",
"bot": "ボット",
"project_view": {
"sort_by": {
"created_at": "作成日時",
@@ -512,12 +509,10 @@
"name": "名前"
}
},
"toast": {
"success": "成功!",
"error": "エラー!"
},
"links": {
"toasts": {
"created": {
@@ -546,7 +541,6 @@
}
}
},
"home": {
"empty": {
"quickstart_guide": "クイックスタートガイド",
@@ -614,7 +608,6 @@
"title": "ホーム",
"star_us_on_github": "GitHubでスターをつける"
},
"link": {
"modal": {
"url": {
@@ -628,7 +621,6 @@
}
}
},
"common": {
"all": "すべて",
"states": "ステータス",
@@ -870,22 +862,22 @@
"deleting": "デリーティング",
"pending": "保留中",
"invite": "招待",
"view": "ビュー"
"view": "ビュー",
"deactivated_user": "無効化されたユーザー",
"apply": "適用",
"applying": "適用中"
},
"chart": {
"x_axis": "エックス アクシス",
"y_axis": "ワイ アクシス",
"metric": "メトリック"
},
"form": {
"title": {
"required": "タイトルは必須です",
"max_length": "タイトルは{length}文字未満である必要があります"
}
},
"entity": {
"grouping_title": "{entity}のグループ化",
"priority": "{entity}の優先度",
@@ -909,7 +901,6 @@
"failed": "{entity}の追加中にエラーが発生しました"
}
},
"epic": {
"all": "すべてのエピック",
"label": "{count, plural, one {エピック} other {エピック}}",
@@ -927,7 +918,6 @@
"required": "エピックのタイトルは必須です。"
}
},
"issue": {
"label": "{count, plural, one {作業項目} other {作業項目}}",
"all": "すべての作業項目",
@@ -1094,7 +1084,6 @@
},
"open_in_full_screen": "作業項目をフルスクリーンで開く"
},
"attachment": {
"error": "ファイルを添付できませんでした。もう一度アップロードしてください。",
"only_one_file_allowed": "一度にアップロードできるファイルは1つだけです。",
@@ -1102,7 +1091,6 @@
"drag_and_drop": "どこにでもドラッグ&ドロップでアップロード",
"delete": "添付ファイルを削除"
},
"label": {
"select": "ラベルを選択",
"create": {
@@ -1112,7 +1100,6 @@
"type": "新しいラベルを追加するには入力してください"
}
},
"sub_work_item": {
"update": {
"success": "サブ作業項目を更新しました",
@@ -1123,7 +1110,6 @@
"error": "サブ作業項目の削除中にエラーが発生しました"
}
},
"view": {
"label": "{count, plural, one {ビュー} other {ビュー}}",
"create": {
@@ -1133,7 +1119,6 @@
"label": "ビューを更新"
}
},
"inbox_issue": {
"status": {
"pending": {
@@ -1219,7 +1204,6 @@
}
}
},
"workspace_creation": {
"heading": "ワークスペースを作成",
"subheading": "Planeを使用するには、ワークスペースを作成するか参加する必要があります。",
@@ -1271,7 +1255,6 @@
}
}
},
"workspace_dashboard": {
"empty_state": {
"general": {
@@ -1287,7 +1270,6 @@
}
}
},
"workspace_analytics": {
"label": "アナリティクス",
"page_label": "{workspace} - アナリティクス",
@@ -1332,7 +1314,6 @@
}
}
},
"workspace_projects": {
"label": "{count, plural, one {プロジェクト} other {プロジェクト}}",
"create": {
@@ -1406,7 +1387,6 @@
}
}
},
"workspace_views": {
"add_view": "ビューを追加",
"empty_state": {
@@ -1441,7 +1421,6 @@
}
}
},
"workspace_settings": {
"label": "ワークスペース設定",
"page_label": "{workspace} - 一般設定",
@@ -1623,7 +1602,6 @@
}
}
},
"profile": {
"label": "プロフィール",
"page_label": "あなたの作業",
@@ -1686,7 +1664,6 @@
}
}
},
"project_settings": {
"general": {
"enter_project_id": "プロジェクトIDを入力",
@@ -1740,12 +1717,15 @@
"title": "プロジェクトの見積もりを有効にする",
"description": "チームの複雑さと作業負荷を伝えるのに役立ちます。",
"no_estimate": "見積もりなし",
"new": "新しい見積もりシステム",
"create": {
"custom": "カスタム",
"start_from_scratch": "最初から開始",
"choose_template": "テンプレートを選択",
"choose_estimate_system": "見積もりシステムを選択",
"enter_estimate_point": "見積もりを入力"
"enter_estimate_point": "見積もりを入力",
"step": "ステップ {step} の {total}",
"label": "見積もりを作成"
},
"toasts": {
"created": {
@@ -1794,6 +1774,25 @@
"already_exists": "見積もり値は既に存在します。",
"unsaved_changes": "未保存の変更があります。完了をクリックする前に保存してください",
"remove_empty": "見積もりは空にできません。各フィールドに値を入力するか、値がないフィールドを削除してください。"
},
"systems": {
"points": {
"label": "ポイント",
"fibonacci": "フィボナッチ",
"linear": "リニア",
"squares": "二乗",
"custom": "カスタム"
},
"categories": {
"label": "カテゴリー",
"t_shirt_sizes": "Tシャツサイズ",
"easy_to_hard": "簡単から難しい",
"custom": "カスタム"
},
"time": {
"label": "時間",
"hours": "時間"
}
}
},
"automations": {
@@ -1810,7 +1809,6 @@
"auto_close_status": "自動クローズステータス"
}
},
"empty_state": {
"labels": {
"title": "ラベルがまだありません",
@@ -1823,7 +1821,6 @@
}
}
},
"project_cycles": {
"add_cycle": "サイクルを追加",
"more_details": "詳細情報",
@@ -1949,7 +1946,6 @@
}
}
},
"project_issues": {
"empty_state": {
"no_issues": {
@@ -1978,7 +1974,6 @@
}
}
},
"project_module": {
"add_module": "モジュールを追加",
"update_module": "モジュールを更新",
@@ -2032,7 +2027,6 @@
}
}
},
"project_views": {
"empty_state": {
"general": {
@@ -2052,7 +2046,6 @@
}
}
},
"project_page": {
"empty_state": {
"general": {
@@ -2082,7 +2075,6 @@
}
}
},
"command_k": {
"empty_state": {
"search": {
@@ -2090,7 +2082,6 @@
}
}
},
"issue_relation": {
"empty_state": {
"search": {
@@ -2101,7 +2092,6 @@
}
}
},
"issue_comment": {
"empty_state": {
"general": {
@@ -2110,7 +2100,6 @@
}
}
},
"notification": {
"label": "受信トレイ",
"page_label": "{workspace} - 受信トレイ",
@@ -2167,7 +2156,6 @@
"custom": "カスタム"
}
},
"active_cycle": {
"empty_state": {
"progress": {
@@ -2187,7 +2175,6 @@
}
}
},
"disabled_project": {
"empty_state": {
"inbox": {
@@ -2250,7 +2237,6 @@
}
}
},
"stickies": {
"title": "あなたの付箋",
"placeholder": "ここをクリックして入力",
@@ -2308,7 +2294,6 @@
}
}
},
"role_details": {
"guest": {
"title": "ゲスト",
@@ -2323,7 +2308,6 @@
"description": "ワークスペース内のすべての権限が有効。"
}
},
"user_roles": {
"product_or_project_manager": "プロダクト/プロジェクトマネージャー",
"development_or_engineering": "開発/エンジニアリング",
@@ -2336,7 +2320,6 @@
"human_resources": "人事",
"other": "その他"
},
"importer": {
"github": {
"title": "GitHub",
@@ -2347,7 +2330,6 @@
"description": "Jiraプロジェクトとエピックから作業項目とエピックをインポートします。"
}
},
"exporter": {
"csv": {
"title": "CSV",
@@ -2376,7 +2358,6 @@
"created": "作成済み",
"subscribed": "購読中"
},
"themes": {
"theme_options": {
"system_preference": {
@@ -2422,20 +2403,17 @@
"manual": "手動"
}
},
"cycle": {
"label": "{count, plural, one {サイクル} other {サイクル}}",
"no_cycle": "サイクルなし"
},
"module": {
"label": "{count, plural, one {モジュール} other {モジュール}}",
"no_module": "モジュールなし"
},
"description_versions": {
"last_edited_by": "最終編集者",
"previously_edited_by": "以前の編集者",
"edited_by": "編集者"
}
}
}
+35 -57
View File
@@ -18,7 +18,6 @@
"pro": "프로",
"upgrade": "업그레이드"
},
"auth": {
"common": {
"email": {
@@ -168,7 +167,6 @@
}
}
},
"submit": "제출",
"cancel": "취소",
"loading": "로딩 중",
@@ -350,7 +348,7 @@
"couldnt_remove_the_project_from_favorites": "프로젝트를 즐겨찾기에서 제거하지 못했습니다. 다시 시도해주세요.",
"add_to_favorites": "즐겨찾기에 추가",
"remove_from_favorites": "즐겨찾기에서 제거",
"publish_settings": "설정 게시",
"publish_project": "프로젝트 게시",
"publish": "게시",
"copy_link": "링크 복사",
"leave_project": "프로젝트 떠나기",
@@ -367,12 +365,12 @@
"work_management": "작업 관리",
"projects_and_issues": "프로젝트 및 작업 항목",
"projects_and_issues_description": "이 프로젝트에서 이들을 켜거나 끕니다.",
"cycles_description": "프로젝트별로 작업 시간 상자로 나누고, 한 기간에서 다음 기간으로 빈도를 변경합니다.",
"modules_description": "작업을 하위 프로젝트와 같은 설정으로 그룹화하고, 각 설정에 리드와 담당자를 지정합니다.",
"views_description": "정렬, 필터 및 표시 옵션을 나중에 저장하거나 공유합니다.",
"pages_description": "무엇이든 작성하세요.",
"intake_description": "구독한 작업 항목에 대한 최신 정보를 유지하세요. 알림을 받으려면 이 기능을 활성화하세요.",
"time_tracking_description": "작업 항목 및 프로젝트에 소요된 시간을 추적합니다.",
"cycles_description": "프로젝트별로 작업 시간을 설정하고 필요에 따라 기간을 조정하세요. 한 주기는 2주일일 수 있고, 다음은 1주일일 수 있습니다.",
"modules_description": "작업을 전담 리더와 담당자가 있는 하위 프로젝트로 구성하세요.",
"views_description": "사용자 정의 정렬, 필터 및 표시 옵션을 저장하거나 팀과 공유하세요.",
"pages_description": "자유 형식의 콘텐츠를 작성하고 편집하세요. 메모, 문서, 무엇이든 가능합니다.",
"intake_description": "비회원이 버그, 피드백, 제안을 공유할 수 있도록 하되, 워크플로우를 방해하지 않도록 합니다.",
"time_tracking_description": "작업 항목 및 프로젝트에 소요된 시간을 기록하세요.",
"work_management_description": "작업 및 프로젝트를 쉽게 관리합니다.",
"documentation": "문서",
"message_support": "지원 메시지",
@@ -504,7 +502,6 @@
"new_password_must_be_different_from_old_password": "새 비밀번호는 이전 비밀번호와 다르게 설정해야 합니다",
"edited": "수정됨",
"bot": "봇",
"project_view": {
"sort_by": {
"created_at": "생성일",
@@ -512,12 +509,10 @@
"name": "이름"
}
},
"toast": {
"success": "성공!",
"error": "오류!"
},
"links": {
"toasts": {
"created": {
@@ -546,7 +541,6 @@
}
}
},
"home": {
"empty": {
"quickstart_guide": "빠른 시작 가이드",
@@ -614,7 +608,6 @@
"title": "홈",
"star_us_on_github": "GitHub에서 별표"
},
"link": {
"modal": {
"url": {
@@ -628,7 +621,6 @@
}
}
},
"common": {
"all": "모두",
"states": "상태",
@@ -871,22 +863,22 @@
"deleting": "삭제 중",
"pending": "보류 중",
"invite": "초대",
"view": "보기"
"view": "보기",
"deactivated_user": "비활성화된 사용자",
"apply": "적용",
"applying": "적용 중"
},
"chart": {
"x_axis": "X축",
"y_axis": "Y축",
"metric": "메트릭"
},
"form": {
"title": {
"required": "제목이 필요합니다",
"max_length": "제목은 {length}자 미만이어야 합니다"
}
},
"entity": {
"grouping_title": "{entity} 그룹화",
"priority": "{entity} 우선순위",
@@ -910,7 +902,6 @@
"failed": "{entity} 추가 중 오류 발생"
}
},
"epic": {
"all": "모든 에픽",
"label": "{count, plural, one {에픽} other {에픽}}",
@@ -928,7 +919,6 @@
"required": "에픽 제목이 필요합니다."
}
},
"issue": {
"label": "{count, plural, one {작업 항목} other {작업 항목}}",
"all": "모든 작업 항목",
@@ -1095,7 +1085,6 @@
},
"open_in_full_screen": "작업 항목을 전체 화면으로 열기"
},
"attachment": {
"error": "파일을 첨부할 수 없습니다. 다시 업로드하세요.",
"only_one_file_allowed": "한 번에 하나의 파일만 업로드할 수 있습니다.",
@@ -1103,7 +1092,6 @@
"drag_and_drop": "업로드하려면 아무 곳에나 드래그 앤 드롭하세요",
"delete": "첨부 파일 삭제"
},
"label": {
"select": "레이블 선택",
"create": {
@@ -1113,7 +1101,6 @@
"type": "새 레이블을 추가하려면 입력하세요"
}
},
"sub_work_item": {
"update": {
"success": "하위 작업 항목이 성공적으로 업데이트되었습니다",
@@ -1124,7 +1111,6 @@
"error": "하위 작업 항목 제거 중 오류 발생"
}
},
"view": {
"label": "{count, plural, one {뷰} other {뷰}}",
"create": {
@@ -1134,7 +1120,6 @@
"label": "뷰 업데이트"
}
},
"inbox_issue": {
"status": {
"pending": {
@@ -1220,7 +1205,6 @@
}
}
},
"workspace_creation": {
"heading": "작업 공간 생성",
"subheading": "Plane을 사용하려면 작업 공간을 생성하거나 참여해야 합니다.",
@@ -1272,7 +1256,6 @@
}
}
},
"workspace_dashboard": {
"empty_state": {
"general": {
@@ -1288,7 +1271,6 @@
}
}
},
"workspace_analytics": {
"label": "분석",
"page_label": "{workspace} - 분석",
@@ -1333,7 +1315,6 @@
}
}
},
"workspace_projects": {
"label": "{count, plural, one {프로젝트} other {프로젝트}}",
"create": {
@@ -1408,7 +1389,6 @@
}
}
},
"workspace_views": {
"add_view": "뷰 추가",
"empty_state": {
@@ -1443,7 +1423,6 @@
}
}
},
"workspace_settings": {
"label": "작업 공간 설정",
"page_label": "{workspace} - 일반 설정",
@@ -1625,7 +1604,6 @@
}
}
},
"profile": {
"label": "프로필",
"page_label": "나의 작업",
@@ -1688,7 +1666,6 @@
}
}
},
"project_settings": {
"general": {
"enter_project_id": "프로젝트 ID 입력",
@@ -1742,12 +1719,15 @@
"title": "프로젝트 추정 활성화",
"description": "팀의 복잡성과 작업량을 전달하는 데 도움이 됩니다.",
"no_estimate": "추정 없음",
"new": "새 추정 시스템",
"create": {
"custom": "사용자 지정",
"start_from_scratch": "처음부터 시작",
"choose_template": "템플릿 선택",
"choose_estimate_system": "추정 시스템 선택",
"enter_estimate_point": "추정 입력"
"enter_estimate_point": "추정 입력",
"step": "단계 {step}/{total}",
"label": "추정 생성"
},
"toasts": {
"created": {
@@ -1796,6 +1776,25 @@
"already_exists": "추정 값이 이미 존재합니다.",
"unsaved_changes": "저장되지 않은 변경 사항이 있습니다. 완료를 클릭하기 전에 저장하세요",
"remove_empty": "추정은 비어있을 수 없습니다. 각 필드에 값을 입력하거나 값이 없는 필드를 제거하세요."
},
"systems": {
"points": {
"label": "포인트",
"fibonacci": "피보나치",
"linear": "선형",
"squares": "제곱",
"custom": "사용자 정의"
},
"categories": {
"label": "카테고리",
"t_shirt_sizes": "티셔츠 사이즈",
"easy_to_hard": "쉬움에서 어려움",
"custom": "사용자 정의"
},
"time": {
"label": "시간",
"hours": "시간"
}
}
},
"automations": {
@@ -1812,7 +1811,6 @@
"auto_close_status": "자동 닫기 상태"
}
},
"empty_state": {
"labels": {
"title": "레이블 없음",
@@ -1825,7 +1823,6 @@
}
}
},
"project_cycles": {
"add_cycle": "주기 추가",
"more_details": "자세히 보기",
@@ -1951,7 +1948,6 @@
}
}
},
"project_issues": {
"empty_state": {
"no_issues": {
@@ -1980,7 +1976,6 @@
}
}
},
"project_module": {
"add_module": "모듈 추가",
"update_module": "모듈 업데이트",
@@ -2034,7 +2029,6 @@
}
}
},
"project_views": {
"empty_state": {
"general": {
@@ -2054,7 +2048,6 @@
}
}
},
"project_page": {
"empty_state": {
"general": {
@@ -2084,7 +2077,6 @@
}
}
},
"command_k": {
"empty_state": {
"search": {
@@ -2092,7 +2084,6 @@
}
}
},
"issue_relation": {
"empty_state": {
"search": {
@@ -2103,7 +2094,6 @@
}
}
},
"issue_comment": {
"empty_state": {
"general": {
@@ -2112,7 +2102,6 @@
}
}
},
"notification": {
"label": "받은 편지함",
"page_label": "{workspace} - 받은 편지함",
@@ -2169,7 +2158,6 @@
"custom": "사용자 정의"
}
},
"active_cycle": {
"empty_state": {
"progress": {
@@ -2189,7 +2177,6 @@
}
}
},
"disabled_project": {
"empty_state": {
"inbox": {
@@ -2252,7 +2239,6 @@
}
}
},
"stickies": {
"title": "나의 스티키",
"placeholder": "여기에 입력하려면 클릭하세요",
@@ -2310,7 +2296,6 @@
}
}
},
"role_details": {
"guest": {
"title": "게스트",
@@ -2325,7 +2310,6 @@
"description": "작업 공간 내에서 모든 권한이 true로 설정됨."
}
},
"user_roles": {
"product_or_project_manager": "제품 / 프로젝트 관리자",
"development_or_engineering": "개발 / 엔지니어링",
@@ -2338,7 +2322,6 @@
"human_resources": "인사 / 자원",
"other": "기타"
},
"importer": {
"github": {
"title": "Github",
@@ -2349,7 +2332,6 @@
"description": "Jira 프로젝트 및 에픽에서 작업 항목과 에픽을 가져옵니다."
}
},
"exporter": {
"csv": {
"title": "CSV",
@@ -2378,7 +2360,6 @@
"created": "생성됨",
"subscribed": "구독됨"
},
"themes": {
"theme_options": {
"system_preference": {
@@ -2424,20 +2405,17 @@
"manual": "수동"
}
},
"cycle": {
"label": "{count, plural, one {주기} other {주기}}",
"no_cycle": "주기 없음"
},
"module": {
"label": "{count, plural, one {모듈} other {모듈}}",
"no_module": "모듈 없음"
},
"description_versions": {
"last_edited_by": "마지막 편집자",
"previously_edited_by": "이전 편집자",
"edited_by": "편집자"
}
}
}
+35 -15
View File
@@ -348,7 +348,7 @@
"couldnt_remove_the_project_from_favorites": "Nie udało się usunąć projektu z ulubionych. Spróbuj ponownie.",
"add_to_favorites": "Dodaj do ulubionych",
"remove_from_favorites": "Usuń z ulubionych",
"publish_settings": "Ustawienia publikowania",
"publish_project": "Opublikuj projekt",
"publish": "Opublikuj",
"copy_link": "Kopiuj link",
"leave_project": "Opuść projekt",
@@ -365,12 +365,12 @@
"work_management": "Zarządzanie pracą",
"projects_and_issues": "Projekty i elementy pracy",
"projects_and_issues_description": "Włączaj lub wyłączaj te funkcje w projekcie.",
"cycles_description": "Ograniczaj pracę w czasie, zmieniaj częstotliwość pomiędzy okresami.",
"modules_description": "Grupuj pracę w podobne podprojekty z własnymi leaderami i przypisanymi osobami.",
"views_description": "Zapisuj sortowanie, filtry i opcje wyświetlania do późniejszego użytku lub udostępniania.",
"pages_description": "Pisz dowolne treści w standardowej formie.",
"intake_description": "Śledź zgłoszenia, aby mieć wgląd w nowe zadania i prośby.",
"time_tracking_description": "Rejestruj czas spędzony na poszczególnych elementach pracy i projektach.",
"cycles_description": "Określ ramy czasowe pracy dla każdego projektu i dostosuj okres w razie potrzeby. Jeden cykl może trwać 2 tygodnie, a następny 1 tydzień.",
"modules_description": "Organizuj pracę w podprojekty z dedykowanymi liderami i przypisanymi osobami.",
"views_description": "Zapisz niestandardowe sortowania, filtry i opcje wyświetlania lub udostępnij je zespołowi.",
"pages_description": "Twórz i edytuj treści o swobodnej formie notatki, dokumenty, cokolwiek.",
"intake_description": "Pozwól osobom spoza zespołu zgłaszać błędy, opinie i sugestie bez zakłócania przepływu pracy.",
"time_tracking_description": "Rejestruj czas spędzony na elementach pracy i projektach.",
"work_management_description": "Łatwo zarządzaj swoją pracą i projektami.",
"documentation": "Dokumentacja",
"message_support": "Skontaktuj się z pomocą",
@@ -500,10 +500,8 @@
"export": "Eksportuj",
"member": "{count, plural, one{# członek} few{# członkowie} other{# członków}}",
"new_password_must_be_different_from_old_password": "Nowe hasło musi być innym niż stare hasło",
"edited": "Edytowano",
"bot": "Bot",
"project_view": {
"sort_by": {
"created_at": "Utworzono dnia",
@@ -865,7 +863,10 @@
"deleting": "Usuwanie",
"pending": "Oczekujące",
"invite": "Zaproś",
"view": "Widok"
"view": "Widok",
"deactivated_user": "Dezaktywowany użytkownik",
"apply": "Zastosuj",
"applying": "Zastosowanie"
},
"chart": {
"x_axis": "Oś X",
@@ -1717,12 +1718,15 @@
"title": "Włącz szacunki dla mojego projektu",
"description": "Pomagają w komunikacji o złożoności i obciążeniu zespołu.",
"no_estimate": "Bez szacunku",
"new": "Nowy system szacowania",
"create": {
"custom": "Niestandardowy",
"start_from_scratch": "Zacznij od zera",
"choose_template": "Wybierz szablon",
"choose_estimate_system": "Wybierz system szacowania",
"enter_estimate_point": "Wprowadź punkt szacunkowy"
"enter_estimate_point": "Wprowadź punkt szacunkowy",
"step": "Krok {step} z {total}",
"label": "Utwórz szacunek"
},
"toasts": {
"created": {
@@ -1771,6 +1775,25 @@
"already_exists": "Wartość szacunku już istnieje.",
"unsaved_changes": "Masz niezapisane zmiany. Zapisz je przed kliknięciem 'gotowe'",
"remove_empty": "Szacunek nie może być pusty. Wprowadź wartość w każde pole lub usuń te, dla których nie masz wartości."
},
"systems": {
"points": {
"label": "Punkty",
"fibonacci": "Fibonacci",
"linear": "Liniowy",
"squares": "Kwadraty",
"custom": "Własny"
},
"categories": {
"label": "Kategorie",
"t_shirt_sizes": "Rozmiary koszulek",
"easy_to_hard": "Od łatwego do trudnego",
"custom": "Własne"
},
"time": {
"label": "Czas",
"hours": "Godziny"
}
}
},
"automations": {
@@ -2381,20 +2404,17 @@
"manual": "Ręcznie"
}
},
"cycle": {
"label": "{count, plural, one {Cykl} few {Cykle} other {Cyklów}}",
"no_cycle": "Brak cyklu"
},
"module": {
"label": "{count, plural, one {Moduł} few {Moduły} other {Modułów}}",
"no_module": "Brak modułu"
},
"description_versions": {
"last_edited_by": "Ostatnio edytowane przez",
"previously_edited_by": "Wcześniej edytowane przez",
"edited_by": "Edytowane przez"
}
}
}
@@ -18,7 +18,6 @@
"pro": "Pro",
"upgrade": "Upgrade"
},
"auth": {
"common": {
"email": {
@@ -168,7 +167,6 @@
}
}
},
"submit": "Enviar",
"cancel": "Cancelar",
"loading": "Carregando",
@@ -350,7 +348,7 @@
"couldnt_remove_the_project_from_favorites": "Não foi possível remover o projeto dos favoritos. Por favor, tente novamente.",
"add_to_favorites": "Adicionar aos favoritos",
"remove_from_favorites": "Remover dos favoritos",
"publish_settings": "Configurações de publicação",
"publish_project": "Publicar projeto",
"publish": "Publicar",
"copy_link": "Copiar link",
"leave_project": "Sair do projeto",
@@ -367,12 +365,12 @@
"work_management": "Gerenciamento de trabalho",
"projects_and_issues": "Projetos e itens de trabalho",
"projects_and_issues_description": "Ative ou desative estes neste projeto.",
"cycles_description": "Defina o tempo de trabalho como achar melhor por projeto e altere a frequência de um período para o próximo.",
"modules_description": "Agrupe o trabalho em configurações semelhantes a subprojetos com seus próprios líderes e responsáveis.",
"views_description": "Salve classificações, filtros e opções de exibição para mais tarde ou compartilhe-os.",
"pages_description": "Escreva qualquer coisa como você escreveria normalmente.",
"intake_description": "Mantenha-se informado sobre os itens de trabalho aos quais você está inscrito. Ative isso para ser notificado.",
"time_tracking_description": "Rastreie o tempo gasto em itens de trabalho e projetos.",
"cycles_description": "Defina o tempo de trabalho por projeto e ajuste o período conforme necessário. Um ciclo pode durar 2 semanas, o próximo 1 semana.",
"modules_description": "Organize o trabalho em subprojetos com líderes e responsáveis dedicados.",
"views_description": "Salve classificações, filtros e opções de exibição personalizadas ou compartilhe com sua equipe.",
"pages_description": "Crie e edite conteúdo livre anotações, documentos, qualquer coisa.",
"intake_description": "Permita que não membros compartilhem bugs, feedbacks e sugestões sem interromper seu fluxo de trabalho.",
"time_tracking_description": "Registre o tempo gasto em itens de trabalho e projetos.",
"work_management_description": "Gerencie seu trabalho e projetos com facilidade.",
"documentation": "Documentação",
"message_support": "Suporte por mensagem",
@@ -504,7 +502,6 @@
"new_password_must_be_different_from_old_password": "Nova senha deve ser diferente da senha antiga",
"edited": "editado",
"bot": "robô",
"project_view": {
"sort_by": {
"created_at": "Criado em",
@@ -512,12 +509,10 @@
"name": "Nome"
}
},
"toast": {
"success": "Sucesso!",
"error": "Erro!"
},
"links": {
"toasts": {
"created": {
@@ -546,7 +541,6 @@
}
}
},
"home": {
"empty": {
"quickstart_guide": "Seu guia de início rápido",
@@ -614,7 +608,6 @@
"title": "Página inicial",
"star_us_on_github": "Nos dê uma estrela no GitHub"
},
"link": {
"modal": {
"url": {
@@ -628,7 +621,6 @@
}
}
},
"common": {
"all": "Todos",
"states": "Estados",
@@ -871,22 +863,22 @@
"deleting": "Excluindo",
"pending": "Pendente",
"invite": "Convidar",
"view": "Visualizar"
"view": "Visualizar",
"deactivated_user": "Usuário desativado",
"apply": "Aplicar",
"applying": "Aplicando"
},
"chart": {
"x_axis": "Eixo X",
"y_axis": "Eixo Y",
"metric": "Métrica"
},
"form": {
"title": {
"required": "Título é obrigatório",
"max_length": "O título deve ter menos de {length} caracteres"
}
},
"entity": {
"grouping_title": "Agrupamento de {entity}",
"priority": "Prioridade de {entity}",
@@ -910,7 +902,6 @@
"failed": "Erro ao adicionar {entity}"
}
},
"epic": {
"all": "Todos os Épicos",
"label": "{count, plural, one {Épico} other {Épicos}}",
@@ -928,7 +919,6 @@
"required": "O título do épico é obrigatório."
}
},
"issue": {
"label": "{count, plural, one {Item de trabalho} other {Itens de trabalho}}",
"all": "Todos os Itens de trabalho",
@@ -1095,7 +1085,6 @@
},
"open_in_full_screen": "Abrir item de trabalho em tela cheia"
},
"attachment": {
"error": "Não foi possível anexar o arquivo. Tente enviar novamente.",
"only_one_file_allowed": "Apenas um arquivo pode ser enviado por vez.",
@@ -1103,7 +1092,6 @@
"drag_and_drop": "Arraste e solte em qualquer lugar para enviar",
"delete": "Excluir anexo"
},
"label": {
"select": "Selecionar etiqueta",
"create": {
@@ -1113,7 +1101,6 @@
"type": "Digite para adicionar uma nova etiqueta"
}
},
"sub_work_item": {
"update": {
"success": "Sub-item de trabalho atualizado com sucesso",
@@ -1124,7 +1111,6 @@
"error": "Erro ao remover sub-item de trabalho"
}
},
"view": {
"label": "{count, plural, one {Visualização} other {Visualizações}}",
"create": {
@@ -1134,7 +1120,6 @@
"label": "Atualizar Visualização"
}
},
"inbox_issue": {
"status": {
"pending": {
@@ -1220,7 +1205,6 @@
}
}
},
"workspace_creation": {
"heading": "Crie seu espaço de trabalho",
"subheading": "Para começar a usar o Plane, você precisa criar ou entrar em um espaço de trabalho.",
@@ -1272,7 +1256,6 @@
}
}
},
"workspace_dashboard": {
"empty_state": {
"general": {
@@ -1288,7 +1271,6 @@
}
}
},
"workspace_analytics": {
"label": "Análises",
"page_label": "{workspace} - Análises",
@@ -1333,7 +1315,6 @@
}
}
},
"workspace_projects": {
"label": "{count, plural, one {Projeto} other {Projetos}}",
"create": {
@@ -1408,7 +1389,6 @@
}
}
},
"workspace_views": {
"add_view": "Adicionar visualização",
"empty_state": {
@@ -1443,7 +1423,6 @@
}
}
},
"workspace_settings": {
"label": "Configurações do espaço de trabalho",
"page_label": "{workspace} - Configurações gerais",
@@ -1625,7 +1604,6 @@
}
}
},
"profile": {
"label": "Perfil",
"page_label": "Seu trabalho",
@@ -1688,7 +1666,6 @@
}
}
},
"project_settings": {
"general": {
"enter_project_id": "Inserir ID do projeto",
@@ -1742,12 +1719,15 @@
"title": "Habilitar estimativas para meu projeto",
"description": "Elas ajudam você a comunicar a complexidade e a carga de trabalho da equipe.",
"no_estimate": "Sem estimativa",
"new": "Novo sistema de estimativa",
"create": {
"custom": "Personalizado",
"start_from_scratch": "Começar do zero",
"choose_template": "Escolher um modelo",
"choose_estimate_system": "Escolher um sistema de estimativa",
"enter_estimate_point": "Inserir estimativa"
"enter_estimate_point": "Inserir estimativa",
"step": "Passo {step} de {total}",
"label": "Criar estimativa"
},
"toasts": {
"created": {
@@ -1796,6 +1776,25 @@
"already_exists": "O valor da estimativa já existe.",
"unsaved_changes": "Você tem algumas alterações não salvas. Por favor, salve-as antes de clicar em concluir",
"remove_empty": "A estimativa não pode estar vazia. Insira um valor em cada campo ou remova aqueles para os quais você não tem valores."
},
"systems": {
"points": {
"label": "Pontos",
"fibonacci": "Fibonacci",
"linear": "Linear",
"squares": "Quadrados",
"custom": "Personalizado"
},
"categories": {
"label": "Categorias",
"t_shirt_sizes": "Tamanhos de Camiseta",
"easy_to_hard": "Fácil a difícil",
"custom": "Personalizado"
},
"time": {
"label": "Tempo",
"hours": "Horas"
}
}
},
"automations": {
@@ -1812,7 +1811,6 @@
"auto_close_status": "Status de fechamento automático"
}
},
"empty_state": {
"labels": {
"title": "Nenhuma etiqueta ainda",
@@ -1825,7 +1823,6 @@
}
}
},
"project_cycles": {
"add_cycle": "Adicionar ciclo",
"more_details": "Mais detalhes",
@@ -1945,7 +1942,6 @@
}
}
},
"project_issues": {
"empty_state": {
"no_issues": {
@@ -1974,7 +1970,6 @@
}
}
},
"project_module": {
"add_module": "Adicionar Módulo",
"update_module": "Atualizar Módulo",
@@ -2028,7 +2023,6 @@
}
}
},
"project_views": {
"empty_state": {
"general": {
@@ -2048,7 +2042,6 @@
}
}
},
"project_page": {
"empty_state": {
"general": {
@@ -2078,7 +2071,6 @@
}
}
},
"command_k": {
"empty_state": {
"search": {
@@ -2086,7 +2078,6 @@
}
}
},
"issue_relation": {
"empty_state": {
"search": {
@@ -2097,7 +2088,6 @@
}
}
},
"issue_comment": {
"empty_state": {
"general": {
@@ -2106,7 +2096,6 @@
}
}
},
"notification": {
"label": "Caixa de entrada",
"page_label": "{workspace} - Caixa de entrada",
@@ -2163,7 +2152,6 @@
"custom": "Personalizado"
}
},
"active_cycle": {
"empty_state": {
"progress": {
@@ -2245,7 +2233,6 @@
}
}
},
"stickies": {
"title": "Suas anotações",
"placeholder": "clique para digitar aqui",
@@ -2303,7 +2290,6 @@
}
}
},
"role_details": {
"guest": {
"title": "Convidado",
@@ -2318,7 +2304,6 @@
"description": "Todas as permissões definidas como verdadeiras dentro do espaço de trabalho."
}
},
"user_roles": {
"product_or_project_manager": "Gerente de Produto / Projeto",
"development_or_engineering": "Desenvolvimento / Engenharia",
@@ -2331,7 +2316,6 @@
"human_resources": "Recursos Humanos",
"other": "Outro"
},
"importer": {
"github": {
"title": "Github",
@@ -2342,7 +2326,6 @@
"description": "Importe itens de trabalho e épicos de projetos e épicos do Jira."
}
},
"exporter": {
"csv": {
"title": "CSV",
@@ -2371,7 +2354,6 @@
"created": "Criado",
"subscribed": "Inscrito"
},
"themes": {
"theme_options": {
"system_preference": {
@@ -2417,20 +2399,17 @@
"manual": "Manual"
}
},
"cycle": {
"label": "{count, plural, one {Ciclo} other {Ciclos}}",
"no_cycle": "Nenhum ciclo"
},
"module": {
"label": "{count, plural, one {Módulo} other {Módulos}}",
"no_module": "Nenhum módulo"
},
"description_versions": {
"last_edited_by": "Última edição por",
"previously_edited_by": "Anteriormente editado por",
"edited_by": "Editado por"
}
}
}

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