Compare commits

...
Author SHA1 Message Date
pablohashescobar 5d15ab1be1 chore: update preferences 2025-01-10 17:22:09 +05:30
pablohashescobar 6d84a1b5bc Merge branch 'preview' of github.com:makeplane/plane into chore-preference-migrations 2025-01-10 17:10:25 +05:30
Prateek ShouryaandGitHub 58ac1dcba1 [WEB-2924] improvement: enhance spreadsheet modularity for improved global compatibility (#6374) 2025-01-10 14:30:24 +05:30
Anmol Singh BhatiaandGitHub 7430ccf9a8 [WEB-3089 | WEB-3098] chore: project store and empty state updated (#6373)
* chore: empty state config updated

* chore: project store updated
2025-01-10 14:18:24 +05:30
Bavisetti NarayanandGitHub 85ee7f9af0 chore: removed extra permission in stickies (#6372) 2025-01-10 14:17:41 +05:30
SangeethaandGitHub 06ecbe884c [WEB-3101] fix: quick links error in home (#6371) 2025-01-10 13:02:14 +05:30
Prateek ShouryaandGitHub 8a6a5a8ca7 improvement: optimize Treemap chart for large datasets (#6369) 2025-01-10 12:54:29 +05:30
Vamsi KrishnaandGitHub 87ea13c32e fix: prevented display filter mismatch in global view creation (#6341) 2025-01-10 10:21:52 +05:30
Anmol Singh BhatiaandGitHub e3ceb4825a chore: app theme store updated (#6356) 2025-01-10 10:21:22 +05:30
SangeethaandGitHub de009d6d10 [WEB 3053]feat: support LibreOffice file attachments in issues (#6343)
* feat: API endpoints for stickies

* Support libre office in issue attachments

* Remove flash
2025-01-10 10:20:38 +05:30
Vamsi KrishnaandGitHub 7b3f206f57 fix: prevented multiple api call while updating comment (#6352) 2025-01-10 10:20:13 +05:30
Vamsi KrishnaandGitHub 2018114543 fix: added missing translation (#6367) 2025-01-10 10:19:51 +05:30
Aaryan KhandelwalandGitHub ff8c5ee910 fix: floating toolbar ui (#6368) 2025-01-10 10:18:45 +05:30
pablohashescobar 0e8ff37ccc chore: add migration for workspace user preference 2025-01-09 20:16:36 +05:30
guru_sainathandGitHub 2ddd7096e4 [WEB-3087] fix: handle cycle start and end timezone conversion in list, create, and update (#6366)
* chore: handled cycle start and start timezone conversion in list, create and update

* chore: yarn lock
2025-01-09 18:20:18 +05:30
pablohashescobar 44c241d94b fix: home preferences 2025-01-09 17:59:31 +05:30
add35b5ea6 [WEB-3092] fix: fixed recents + removed home route (#6365)
* fix: fixed recents + removed home route

* Return current users recents

---------

Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
2025-01-09 17:25:49 +05:30
guru_sainathandGitHub 448a34aa5f [WEB-3087] fix: project_id handling in cycle create write serializer (#6358)
* chore: handled cycle create write serailizer project_id current instance

* chore: updated instance validation in cycle write serializer
2025-01-09 16:23:53 +05:30
Aaryan KhandelwalandGitHub 8c57543f72 chore: update tiptap-task-item version and required css (#6364) 2025-01-09 16:21:35 +05:30
Vamsi KrishnaandGitHub 9062a7b67d fix: copy module link title (#6362) 2025-01-09 16:15:29 +05:30
sriram veeraghanta 89603bb2d6 fix: node version update from 18 to 20 2025-01-09 16:05:39 +05:30
Aaryan KhandelwalandGitHub 3ab959c682 [PE-228] fix: update tiptap package versions (#6361)
* fix: tiptap package versions

* chore: update tiptap/core package version
2025-01-09 14:52:41 +05:30
d96ab2e7af [WEB-3088] fix: home edits (#6357)
* fix: added delete sticky confirmation modal

* fix: prevented quick links reordering

* fix: quick links css

* fix: minor css

* fix: empty states

* Filter quick_tutorial and new_at_plane

* fix: stickies search backend change

* fix: stickies editor enhanced

* fix: sticky delete function

---------

Co-authored-by: gakshita <akshitagoyal1516@gmail.com>
2025-01-09 14:51:04 +05:30
sriram veeraghanta 5d8f66ae22 fix: upgrade live server to node 20 2025-01-08 20:12:49 +05:30
Aaryan KhandelwalandGitHub ce4a375b62 fix: remove invalid export (#6355) 2025-01-08 16:01:31 +05:30
Aaryan KhandelwalandGitHub 92645c1cee chore: add page flag hook (#6354) 2025-01-08 15:42:14 +05:30
Aaryan KhandelwalandGitHub ac14d57f8b fix: update fallback logic for newly created pages (#6345) 2025-01-08 13:30:06 +05:30
Prateek ShouryaandGitHub 71ebe5ca36 chore: bypass interceptors for unauthorized errors for few auth related endpoints (#6351) 2025-01-08 13:21:24 +05:30
Vamsi KrishnaandGitHub bb71e60fcb [WEB-2783]fix: dropdown visibility in onboarding (#6329)
* fix: dropdown visibility on scroll to bottom

* chore: removed unused variables
2025-01-08 13:10:09 +05:30
Anmol Singh BhatiaandGitHub 3691cef351 [WEB-2943] chore: issue reaction and code refactor (#6350)
* chore: issue reaction component updated

* chore: project store updated
2025-01-08 13:03:58 +05:30
Aaryan KhandelwalandGitHub c2d8703acf chore: add missing tiptap/html package to the editor (#6346) 2025-01-08 12:55:09 +05:30
sriram veeraghanta 507946886f fix: yarn lock update 2025-01-08 00:19:55 +05:30
SangeethaandGitHub 24944a03c6 Add default page size in the list endpoint (#6344) 2025-01-07 20:31:38 +05:30
cb045abfe1 [WEB-3048] feat: added-stickies (#6339)
* feat: added-stickies

* fix: recents empty state fixed

* fix: added border

* Change sort_order field

* fix: remvoved btn

* fix: sticky toolbar

* fix: build

* fix: sticky search

* fix: minor css fix

* fix: issue identifier css handled

* fix: issue type default icon

* fix: added tooltip for color palette and delete

---------

Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
2025-01-07 20:30:42 +05:30
Aaryan KhandelwalandGitHub 24cc69fd7b fix: page link construction (#6312) 2025-01-07 20:27:48 +05:30
Aaryan KhandelwalandGitHub 88c26b334d [PE-219] chore: new live server endpoint to convert description_html to all other formats (#6310)
* chore: new convert doucment endpoint created

* chore: update types
2025-01-07 19:15:37 +05:30
Prateek ShouryaandGitHub 200be0ac7f [WEB-3065] refactor: replace admin services with service packages (#6342)
* [WEB-3065] refactor: replace admin services with service packages

* chore: minor updates

* chore: error handling
2025-01-07 19:07:47 +05:30
Aaryan KhandelwalandGitHub ae657af958 fix: reset text and background color (#6336) 2025-01-07 18:04:26 +05:30
Vamsi KrishnaandGitHub f4c4848a0d chore: update issue-states settings ui (#6338) 2025-01-07 17:16:42 +05:30
guru_sainathandGitHub 6914dc9f42 fix: start date and end date comparassion fix in cycle create and update (#6333) 2025-01-07 15:20:35 +05:30
SangeethaandGitHub 742559bc63 feat: API endpoints for stickies (#6335) 2025-01-07 15:18:01 +05:30
Bavisetti NarayanandGitHub 70dacc5e6a fix: removed the unused triage state (#6337) 2025-01-07 15:17:29 +05:30
Prateek ShouryaandGitHub 5b96c970cd chore: minor improvements related to members dropdown and project member list (#6340) 2025-01-07 15:04:10 +05:30
Vamsi KrishnaandGitHub 906d095a1e fix: added tooltips for truncated favorites (#6330) 2025-01-07 13:16:26 +05:30
0af9b09844 fix: manage widgets integrations (#6331)
* wip

* chore: wip

* fix: preserved old component

* fix

* fix: seperate route added

* fix

* Only return user ID of project members

* Return issue ID

* fix: recents api integrations

* fix: types

* fix: types

* fix: added tooltips

* chore: added apis

* fix: widgets fix

* fix: lint

* fix: integrated dashboard apis

* fix: added ee dummy component for header

* fix: removed logs

---------

Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
2025-01-07 12:57:35 +05:30
sriram veeraghanta edb68a1bc6 fix: remove sentry from web app and build fixes 2025-01-07 00:37:40 +05:30
790ecee629 [WEB-2846] feat: home integrations (#6321)
* wip

* chore: wip

* fix: preserved old component

* fix

* fix: seperate route added

* fix

* Only return user ID of project members

* Return issue ID

* fix: recents api integrations

* fix: types

* fix: types

* fix: added tooltips

* chore: added apis

---------

Co-authored-by: sangeethailango <sangeethailango21@gmail.com>
Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
2025-01-06 20:36:13 +05:30
guru_sainathandGitHub 0cabde03f0 [WEB-3044] fix: Correct handling of equal start and end dates in cycle create and update (#6328)
* chore: handled start and end date issue when user send the start and end date equal

* chore: updated variable name and comment

* chore: typo
2025-01-06 20:31:31 +05:30
Aaryan KhandelwalandGitHub d3d3bf79f9 refactor: page actions (#6327) 2025-01-06 20:31:07 +05:30
rahulrameshaandGitHub 6321977f1f use getBlockById from the store for modules (#6325) 2025-01-06 20:28:32 +05:30
Vamsi KrishnaandGitHub 208df80c86 chore: added issue activity filters to local storage (#6324)
chore: added sort order to local storage
2025-01-06 20:27:55 +05:30
Anmol Singh BhatiaandGitHub bc27bc9dd2 [WEB-3040] chore: project breadcrumb improvement (#6322)
* chore: project breadcrumb link component added to CE and EE

* chore: project breadcrumb redirection improvement
2025-01-06 19:00:05 +05:30
+2 1acaae9d5e [WEB-3038]feat: home preferences (#6308)
* WIP

* WIP

* WIP

* WIP

* Create home preference if not exist

* chore: handled the unique state name validation (#6299)

* fix: changed the response structure (#6301)

* [WEB-1964]chore: cycles actions restructuring (#6298)

* chore: cycles quick actions restructuring

* chore: added additional actions to cycle list actions

* chore: cycle quick action structure

* chore: added additional actions to cycle list actions

* chore: added end cycle hook

* fix: updated end cycle export

---------

Co-authored-by: gurusinath <gurusainath007@gmail.com>

* fix: active cycle graph tooltip and endpoint validation (#6306)

* [WEB-2870]feat: language support (#6215)

* fix: adding language support package

* fix: language support implementation using mobx

* fix: adding more languages for support

* fix: profile settings translations

* feat: added language support for sidebar and user settings

* feat: added language support for deactivation modal

* fix: added project sync after transfer issues (#6200)

* code refactor and improvement (#6203)

* chore: package code refactoring

* chore: component restructuring and refactor

* chore: comment create improvement

* refactor: enhance workspace and project wrapper modularity (#6207)

* [WEB-2678]feat: added functionality to add labels directly from dropdown (#6211)

* enhancement:added functionality to add features directly from dropdown

* fix: fixed import order

* fix: fixed lint errors

* chore: added common component for project activity (#6212)

* chore: added common component for project activity

* fix: added enum

* fix: added enum for initiatives

* - Do not clear temp files that are locked. (#6214)

- Handle edge cases in sync workspace

* fix: labels empty state for drop down (#6216)

* refactor: remove cn helper function from the editor package (#6217)

* * feat: added language support to issue create modal in sidebar
* fix: project activity type

* * fix: added missing translations
* fix: modified translation for plurals

* fix: fixed spanish translation

* dev: language type error in space user profile types

* fix: type fixes

* chore: added alpha tag

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: Akshita Goyal <36129505+gakshita@users.noreply.github.com>
Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Co-authored-by: gurusinath <gurusainath007@gmail.com>

* feat: introduced stacked bar chart and tree map chart. (#6305)

* feat: add issue attachment external endpoint (#6307)

* [PE-97] chore: re-order pages options (#6303)

* chore: re-order pages dropdown options

* chore: re-order pages dropdown options

* fix: remove localdb tracing

* [WEB-2937] feat: home recent activies list endpoint (#6295)

* Crud for wuick links

* Validate quick link existence

* Add custom method for destroy and retrieve

* Add List method

* Remove print statements

* List all the workspace quick links

* feat: endpoint to get recently active items

* Resolve conflicts

* Resolve conflicts

* Add filter to only list required entities

* Return required fields

* Add filter

* Add filter

* fix: remove emoji edit for uneditable pages (#6304)

* Removed duplicate imports

* feat: patch api

* Enable sort order to be updatable

* Return key name
only insert missing keys
use serializer to return data

* Remove random generation of sort_order

* Remove name field
Remove random generation of sort_order

---------

Co-authored-by: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com>
Co-authored-by: Vamsi Krishna <46787868+mathalav55@users.noreply.github.com>
Co-authored-by: gurusinath <gurusainath007@gmail.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: Akshita Goyal <36129505+gakshita@users.noreply.github.com>
Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Co-authored-by: Nikhil <118773738+pablohashescobar@users.noreply.github.com>
2025-01-06 17:36:10 +05:30
Aaryan KhandelwalandGitHub fbbca0c519 fix: user config dependencies (#6326) 2025-01-06 16:31:12 +05:30
a6216beb7e chore: language support phase 2 (#6323)
* fix: adding langauge support for sidebar items

* fix: worksapce sidebar item refactor

* chore: code cleanup

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
2025-01-06 15:32:11 +05:30
DanciaandGitHub 732963b591 minor fixes (#6314) 2025-01-06 14:57:25 +05:30
625cbf872b [WEB-2711]fix: guest mentions and assignees (#6315)
* chore: issue search filter

* * fix: restricting guest user from assignees and mentions

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2025-01-06 13:06:16 +05:30
Vamsi KrishnaandGitHub d26fb8ce02 [WEB-2926]fix: added tooltips for favorites (#6320)
* * fix: added tooltips for favorites

* chore: code formatting
2025-01-06 13:05:46 +05:30
e4f9d027fe feat: workspace home preference model (#6300)
* feat: workspace home preference model

* chore: changed page title to textfield

* chore: add sort order

* chore: added null value in sticky title

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2025-01-03 20:24:35 +05:30
Prateek ShouryaandGitHub c1407daa47 [WEB-3035] chore: add labels to treemap chart (#6313) 2025-01-03 20:03:11 +05:30
Aaryan KhandelwalandGitHub 2622dd691c fix: remove emoji edit for uneditable pages (#6304) 2025-01-03 15:54:41 +05:30
SangeethaandGitHub 870ca17e13 [WEB-2937] feat: home recent activies list endpoint (#6295)
* Crud for wuick links

* Validate quick link existence

* Add custom method for destroy and retrieve

* Add List method

* Remove print statements

* List all the workspace quick links

* feat: endpoint to get recently active items

* Resolve conflicts

* Resolve conflicts

* Add filter to only list required entities

* Return required fields

* Add filter

* Add filter
2025-01-03 15:09:01 +05:30
sriram veeraghanta 4652ad0fc3 fix: remove localdb tracing 2025-01-03 15:01:57 +05:30
Aaryan KhandelwalandGitHub be9dbc1b18 [PE-97] chore: re-order pages options (#6303)
* chore: re-order pages dropdown options

* chore: re-order pages dropdown options
2025-01-03 14:58:43 +05:30
NikhilandGitHub 3fd2550d69 feat: add issue attachment external endpoint (#6307) 2025-01-03 14:29:40 +05:30
Prateek ShouryaandGitHub d6bcd8dd15 feat: introduced stacked bar chart and tree map chart. (#6305) 2025-01-03 14:24:14 +05:30
873e4330bc [WEB-2870]feat: language support (#6215)
* fix: adding language support package

* fix: language support implementation using mobx

* fix: adding more languages for support

* fix: profile settings translations

* feat: added language support for sidebar and user settings

* feat: added language support for deactivation modal

* fix: added project sync after transfer issues (#6200)

* code refactor and improvement (#6203)

* chore: package code refactoring

* chore: component restructuring and refactor

* chore: comment create improvement

* refactor: enhance workspace and project wrapper modularity (#6207)

* [WEB-2678]feat: added functionality to add labels directly from dropdown (#6211)

* enhancement:added functionality to add features directly from dropdown

* fix: fixed import order

* fix: fixed lint errors

* chore: added common component for project activity (#6212)

* chore: added common component for project activity

* fix: added enum

* fix: added enum for initiatives

* - Do not clear temp files that are locked. (#6214)

- Handle edge cases in sync workspace

* fix: labels empty state for drop down (#6216)

* refactor: remove cn helper function from the editor package (#6217)

* * feat: added language support to issue create modal in sidebar
* fix: project activity type

* * fix: added missing translations
* fix: modified translation for plurals

* fix: fixed spanish translation

* dev: language type error in space user profile types

* fix: type fixes

* chore: added alpha tag

---------

Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: Anmol Singh Bhatia <121005188+anmolsinghbhatia@users.noreply.github.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: Akshita Goyal <36129505+gakshita@users.noreply.github.com>
Co-authored-by: Satish Gandham <satish.iitg@gmail.com>
Co-authored-by: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com>
Co-authored-by: gurusinath <gurusainath007@gmail.com>
2025-01-03 14:16:26 +05:30
Anmol Singh BhatiaandGitHub ade0aa1643 fix: active cycle graph tooltip and endpoint validation (#6306) 2025-01-03 14:08:02 +05:30
6a13a64996 [WEB-1964]chore: cycles actions restructuring (#6298)
* chore: cycles quick actions restructuring

* chore: added additional actions to cycle list actions

* chore: cycle quick action structure

* chore: added additional actions to cycle list actions

* chore: added end cycle hook

* fix: updated end cycle export

---------

Co-authored-by: gurusinath <gurusainath007@gmail.com>
2025-01-02 18:27:34 +05:30
Bavisetti NarayanandGitHub 5e6c02358d fix: changed the response structure (#6301) 2025-01-02 15:51:38 +05:30
Bavisetti NarayanandGitHub 61d6d928ba chore: handled the unique state name validation (#6299) 2025-01-02 14:43:43 +05:30
Vamsi KrishnaandGitHub a555df27e7 [WEB-2941]chore: added transfer issues button to cycles (#6296)
* chore: removed refacotr changes:w

* chore: removed requirements modifications
2025-01-02 12:33:45 +05:30
Manish GuptaandGitHub df671d13c7 improvement: build action improvement (#6248)
* updated action to use makeplane/actions repo

* updated action
2025-01-02 00:00:04 +05:30
Akshita GoyalandGitHub c7ebaf6ba0 chore: added react masonry package (#6293) 2024-12-31 16:15:40 +05:30
Bavisetti NarayanandGitHub 119d46dc2b chore: search issues endpoint (#6291) 2024-12-31 15:29:38 +05:30
SangeethaandGitHub 25f7d813b9 [WEB-2928] feat: Home Quick links CRUD (#6290)
* Crud for wuick links

* Validate quick link existence

* Add custom method for destroy and retrieve

* Add List method

* Remove print statements

* List all the workspace quick links

* Filter by user
2024-12-31 15:06:24 +05:30
Aaryan KhandelwalandGitHub ec2af13258 chore: merge two separate info popovers (#6289) 2024-12-31 14:01:27 +05:30
8833e4e23b Integrates LiteLLM for Unified Access to Multiple LLM Models (#5925)
* adds litellm gateway

* Fixes repeating code

* Fixes error exposing

* Fixes error for None text

* handles logging exception

* Adds multiple providers support

* handling edge cases

* adds new envs to instance store

* strategy pattern for llm config

---------

Co-authored-by: akash5100 <akashzsh08@gmail.com>
2024-12-31 13:57:26 +05:30
752a27a175 [PE-97] refactor: pages actions (#6234)
* dev: support for edition specific options in pages

* refactor: page quick actions

* chore: add customizable page actions

* fix: type errors

* dev: hook to get page operations

* refactor: remove unnecessary props

* chore: add permisssions to duplicate page endpoint

* chore: memoize arranged options

* chore: use enum for page access

* chore: add type assertion

* fix: auth for access change and delete

* fix: removing readonly editor

* chore: add sync for page access cahnge

* fix: sync state

* fix: indexeddb sync loader added

* fix: remove node error fixed

* style: page title and checkbox

* chore: removing the syncing logic

* revert: is editable check removed in display message

* fix: editable field optional

* fix: editable removed as optional prop

* fix: extra options import fix

* fix: remove readonly stuff

* fix: added toggle access

* chore: add access change sync

* fix: full width toggle

* refactor: types and enums added

* refactore: update store action

* chore: changed the duplicate viewset

* fix: remove the page binary

* fix: duplicate page action

* fix: merge conflicts

---------

Co-authored-by: Palanikannan M <akashmalinimurugu@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
2024-12-31 12:54:09 +05:30
391 changed files with 11740 additions and 4176 deletions
-126
View File
@@ -1,126 +0,0 @@
name: "Build and Push Docker Image"
description: "Reusable action for building and pushing Docker images"
inputs:
docker-username:
description: "The Dockerhub username"
required: true
docker-token:
description: "The Dockerhub Token"
required: true
# Docker Image Options
docker-image-owner:
description: "The owner of the Docker image"
required: true
docker-image-name:
description: "The name of the Docker image"
required: true
build-context:
description: "The build context"
required: true
default: "."
dockerfile-path:
description: "The path to the Dockerfile"
required: true
build-args:
description: "The build arguments"
required: false
default: ""
# Buildx Options
buildx-driver:
description: "Buildx driver"
required: true
default: "docker-container"
buildx-version:
description: "Buildx version"
required: true
default: "latest"
buildx-platforms:
description: "Buildx platforms"
required: true
default: "linux/amd64"
buildx-endpoint:
description: "Buildx endpoint"
required: true
default: "default"
# Release Build Options
build-release:
description: "Flag to publish release"
required: false
default: "false"
build-prerelease:
description: "Flag to publish prerelease"
required: false
default: "false"
release-version:
description: "The release version"
required: false
default: "latest"
runs:
using: "composite"
steps:
- name: Set Docker Tag
shell: bash
env:
IMG_OWNER: ${{ inputs.docker-image-owner }}
IMG_NAME: ${{ inputs.docker-image-name }}
BUILD_RELEASE: ${{ inputs.build-release }}
IS_PRERELEASE: ${{ inputs.build-prerelease }}
REL_VERSION: ${{ inputs.release-version }}
run: |
FLAT_BRANCH_VERSION=$(echo "${{ github.ref_name }}" | sed 's/[^a-zA-Z0-9.-]//g')
if [ "${{ env.BUILD_RELEASE }}" == "true" ]; then
semver_regex="^v([0-9]+)\.([0-9]+)\.([0-9]+)(-[a-zA-Z0-9]+(-[a-zA-Z0-9]+)*)?$"
if [[ ! ${{ env.REL_VERSION }} =~ $semver_regex ]]; then
echo "Invalid Release Version Format : ${{ env.REL_VERSION }}"
echo "Please provide a valid SemVer version"
echo "e.g. v1.2.3 or v1.2.3-alpha-1"
echo "Exiting the build process"
exit 1 # Exit with status 1 to fail the step
fi
TAG=${{ env.IMG_OWNER }}/${{ env.IMG_NAME }}:${{ env.REL_VERSION }}
if [ "${{ env.IS_PRERELEASE }}" != "true" ]; then
TAG=${TAG},${{ env.IMG_OWNER }}/${{ env.IMG_NAME }}:stable
fi
elif [ "${{ env.TARGET_BRANCH }}" == "master" ]; then
TAG=${{ env.IMG_OWNER }}/${{ env.IMG_NAME }}:latest
else
TAG=${{ env.IMG_OWNER }}/${{ env.IMG_NAME }}:${FLAT_BRANCH_VERSION}
fi
echo "DOCKER_TAGS=${TAG}" >> $GITHUB_ENV
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ inputs.docker-username }}
password: ${{ inputs.docker-token}}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
with:
driver: ${{ inputs.buildx-driver }}
version: ${{ inputs.buildx-version }}
endpoint: ${{ inputs.buildx-endpoint }}
- name: Check out the repo
uses: actions/checkout@v4
- name: Build and Push Docker Image
uses: docker/build-push-action@v5.1.0
with:
context: ${{ inputs.build-context }}
file: ${{ inputs.dockerfile-path }}
platforms: ${{ inputs.buildx-platforms }}
tags: ${{ env.DOCKER_TAGS }}
push: true
build-args: ${{ inputs.build-args }}
env:
DOCKER_BUILDKIT: 1
DOCKER_USERNAME: ${{ inputs.docker-username }}
DOCKER_PASSWORD: ${{ inputs.docker-token }}
+27 -45
View File
@@ -36,7 +36,7 @@ env:
jobs:
branch_build_setup:
name: Build Setup
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
outputs:
gh_branch_name: ${{ steps.set_env_variables.outputs.TARGET_BRANCH }}
gh_buildx_driver: ${{ steps.set_env_variables.outputs.BUILDX_DRIVER }}
@@ -160,20 +160,17 @@ jobs:
branch_build_push_admin:
if: ${{ needs.branch_build_setup.outputs.build_admin == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Admin Docker Image
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: [branch_build_setup]
steps:
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v4
- name: Admin Build and Push
uses: ./.github/actions/build-push-ce
uses: makeplane/actions/build-push@v1.0.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_admin }}
build-context: .
@@ -186,20 +183,17 @@ jobs:
branch_build_push_web:
if: ${{ needs.branch_build_setup.outputs.build_web == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Web Docker Image
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: [branch_build_setup]
steps:
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v4
- name: Web Build and Push
uses: ./.github/actions/build-push-ce
uses: makeplane/actions/build-push@v1.0.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_web }}
build-context: .
@@ -212,20 +206,17 @@ jobs:
branch_build_push_space:
if: ${{ needs.branch_build_setup.outputs.build_space == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Space Docker Image
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: [branch_build_setup]
steps:
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v4
- name: Space Build and Push
uses: ./.github/actions/build-push-ce
uses: makeplane/actions/build-push@v1.0.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_space }}
build-context: .
@@ -238,20 +229,17 @@ jobs:
branch_build_push_live:
if: ${{ needs.branch_build_setup.outputs.build_live == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Live Collaboration Docker Image
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: [branch_build_setup]
steps:
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v4
- name: Live Build and Push
uses: ./.github/actions/build-push-ce
uses: makeplane/actions/build-push@v1.0.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_live }}
build-context: .
@@ -264,20 +252,17 @@ jobs:
branch_build_push_apiserver:
if: ${{ needs.branch_build_setup.outputs.build_apiserver == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push API Server Docker Image
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: [branch_build_setup]
steps:
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v4
- name: Backend Build and Push
uses: ./.github/actions/build-push-ce
uses: makeplane/actions/build-push@v1.0.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_backend }}
build-context: ./apiserver
@@ -290,20 +275,17 @@ jobs:
branch_build_push_proxy:
if: ${{ needs.branch_build_setup.outputs.build_proxy == 'true' || github.event_name == 'workflow_dispatch' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
name: Build-Push Proxy Docker Image
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: [branch_build_setup]
steps:
- id: checkout_files
name: Checkout Files
uses: actions/checkout@v4
- name: Proxy Build and Push
uses: ./.github/actions/build-push-ce
uses: makeplane/actions/build-push@v1.0.0
with:
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
release-version: ${{ needs.branch_build_setup.outputs.release_version }}
docker-username: ${{ secrets.DOCKERHUB_USERNAME }}
docker-token: ${{ secrets.DOCKERHUB_TOKEN }}
dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }}
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
docker-image-owner: makeplane
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_proxy }}
build-context: ./nginx
@@ -316,7 +298,7 @@ jobs:
attach_assets_to_build:
if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
name: Attach Assets to Release
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs: [branch_build_setup]
steps:
- name: Checkout
@@ -341,7 +323,7 @@ jobs:
publish_release:
if: ${{ needs.branch_build_setup.outputs.build_type == 'Release' }}
name: Build Release
runs-on: ubuntu-20.04
runs-on: ubuntu-22.04
needs:
[
branch_build_setup,
+5 -4
View File
@@ -6,6 +6,7 @@
</a>
</p>
<h1 align="center"><b>Plane</b></h1>
<p align="center"><b>Open-source project management that unlocks customer value</b></p>
<p align="center">
<a href="https://discord.com/invite/A92xrEGCge">
@@ -57,7 +58,7 @@ Prefer full control over your data and infrastructure? Install and run Plane on
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://developers.plane.so/self-hosting/methods/docker-compose) |
| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://developers.plane.so/self-hosting/methods/kubernetes) |
`Instance admins` can manage and customize settings using [God mode](https://developers.plane.so/self-hosting/govern/instance-admin).
`Instance admins` can configure instance settings with [God mode](https://developers.plane.so/self-hosting/govern/instance-admin).
## 🌟 Features
@@ -117,9 +118,9 @@ Setting up your local environment is simple and straightforward. Follow these st
Thats it! Youre all set to begin coding. Remember to refresh your browser if changes dont auto-reload. Happy contributing! 🎉
## Built with
[![Next JS](https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white)](https://nextjs.org/)<br/>
[![Django](https://img.shields.io/badge/Django-092E20?style=for-the-badge&logo=django&logoColor=green)](https://www.djangoproject.com/)<br/>
## ⚙️ Built with
[![Next JS](https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white)](https://nextjs.org/)
[![Django](https://img.shields.io/badge/Django-092E20?style=for-the-badge&logo=django&logoColor=green)](https://www.djangoproject.com/)
[![Node JS](https://img.shields.io/badge/node.js-339933?style=for-the-badge&logo=Node.js&logoColor=white)](https://nodejs.org/en)
## 📸 Screenshots
+5 -3
View File
@@ -1,7 +1,9 @@
FROM node:20-alpine as base
# *****************************************************************************
# STAGE 1: Build the project
# *****************************************************************************
FROM node:18-alpine AS builder
FROM base AS builder
RUN apk add --no-cache libc6-compat
WORKDIR /app
@@ -13,7 +15,7 @@ RUN turbo prune --scope=admin --docker
# *****************************************************************************
# STAGE 2: Install dependencies & build the project
# *****************************************************************************
FROM node:18-alpine AS installer
FROM base AS installer
RUN apk add --no-cache libc6-compat
WORKDIR /app
@@ -52,7 +54,7 @@ RUN yarn turbo run build --filter=admin
# *****************************************************************************
# STAGE 3: Copy the project and start it
# *****************************************************************************
FROM node:18-alpine AS runner
FROM base AS runner
WORKDIR /app
COPY --from=installer /app/admin/next.config.js .
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:18-alpine
FROM node:20-alpine
RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
+2 -2
View File
@@ -1,9 +1,9 @@
import React, { FC, useEffect, useState } from "react";
import { Dialog, Transition } from "@headlessui/react";
// plane imports
import { InstanceService } from "@plane/services";
// ui
import { Button, Input } from "@plane/ui";
// services
import { InstanceService } from "@/services/instance.service";
type Props = {
isOpen: boolean;
+5 -7
View File
@@ -2,18 +2,16 @@ import { useState, useEffect } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { Controller, useForm } from "react-hook-form";
// constants
// plane imports
import { WEB_BASE_URL, ORGANIZATION_SIZE, RESTRICTED_URLS } from "@plane/constants";
// types
import { InstanceWorkspaceService } from "@plane/services";
import { IWorkspace } from "@plane/types";
// components
import { Button, CustomSelect, getButtonStyling, Input, setToast, TOAST_TYPE } from "@plane/ui";
// hooks
import { useWorkspace } from "@/hooks/store";
// services
import { WorkspaceService } from "@/services/workspace.service";
const workspaceService = new WorkspaceService();
const instanceWorkspaceService = new InstanceWorkspaceService();
export const WorkspaceCreateForm = () => {
// router
@@ -40,8 +38,8 @@ export const WorkspaceCreateForm = () => {
const workspaceBaseURL = encodeURI(WEB_BASE_URL || window.location.origin + "/");
const handleCreateWorkspace = async (formData: IWorkspace) => {
await workspaceService
.workspaceSlugCheck(formData.slug)
await instanceWorkspaceService
.slugCheck(formData.slug)
.then(async (res) => {
if (res.status === true && !RESTRICTED_URLS.includes(formData.slug)) {
setSlugError(false);
@@ -7,12 +7,11 @@ import { LogOut, UserCog2, Palette } from "lucide-react";
import { Menu, Transition } from "@headlessui/react";
// plane internal packages
import { API_BASE_URL } from "@plane/constants";
import {AuthService } from "@plane/services";
import { Avatar } from "@plane/ui";
import { getFileURL, cn } from "@plane/utils";
// hooks
import { useTheme, useUser } from "@/hooks/store";
// services
import { AuthService } from "@/services/auth.service";
// service initialization
const authService = new AuthService();
@@ -6,12 +6,11 @@ import { useSearchParams } from "next/navigation";
import { Eye, EyeOff } from "lucide-react";
// plane internal packages
import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants";
import { AuthService } from "@plane/services";
import { Button, Checkbox, Input, Spinner } from "@plane/ui";
import { getPasswordStrength } from "@plane/utils";
// components
import { Banner, PasswordStrengthMeter } from "@/components/common";
// services
import { AuthService } from "@/services/auth.service";
// service initialization
const authService = new AuthService();
+1 -2
View File
@@ -5,13 +5,12 @@ import { useSearchParams } from "next/navigation";
import { Eye, EyeOff } from "lucide-react";
// plane internal packages
import { API_BASE_URL, EAdminAuthErrorCodes, TAuthErrorInfo } from "@plane/constants";
import { AuthService } from "@plane/services";
import { Button, Input, Spinner } from "@plane/ui";
// components
import { Banner } from "@/components/common";
// helpers
import { authErrorHandler } from "@/lib/auth-helpers";
// services
import { AuthService } from "@/services/auth.service";
// local components
import { AuthBanner } from "../authentication";
-53
View File
@@ -1,53 +0,0 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
// store
// import { rootStore } from "@/lib/store-context";
export abstract class APIService {
protected baseURL: string;
private axiosInstance: AxiosInstance;
constructor(baseURL: string) {
this.baseURL = baseURL;
this.axiosInstance = axios.create({
baseURL,
withCredentials: true,
});
this.setupInterceptors();
}
private setupInterceptors() {
// this.axiosInstance.interceptors.response.use(
// (response) => response,
// (error) => {
// const store = rootStore;
// if (error.response && error.response.status === 401 && store.user.currentUser) store.user.reset();
// return Promise.reject(error);
// }
// );
}
get<ResponseType>(url: string, params = {}): Promise<AxiosResponse<ResponseType>> {
return this.axiosInstance.get(url, { params });
}
post<RequestType, ResponseType>(url: string, data: RequestType, config = {}): Promise<AxiosResponse<ResponseType>> {
return this.axiosInstance.post(url, data, config);
}
put<RequestType, ResponseType>(url: string, data: RequestType, config = {}): Promise<AxiosResponse<ResponseType>> {
return this.axiosInstance.put(url, data, config);
}
patch<RequestType, ResponseType>(url: string, data: RequestType, config = {}): Promise<AxiosResponse<ResponseType>> {
return this.axiosInstance.patch(url, data, config);
}
delete<RequestType>(url: string, data?: RequestType, config = {}) {
return this.axiosInstance.delete(url, { data, ...config });
}
request<T>(config: AxiosRequestConfig = {}): Promise<AxiosResponse<T>> {
return this.axiosInstance(config);
}
}
-21
View File
@@ -1,21 +0,0 @@
import { API_BASE_URL } from "@plane/constants";
// services
import { APIService } from "@/services/api.service";
type TCsrfTokenResponse = {
csrf_token: string;
};
export class AuthService extends APIService {
constructor() {
super(API_BASE_URL);
}
async requestCSRFToken(): Promise<TCsrfTokenResponse> {
return this.get<TCsrfTokenResponse>("/auth/get-csrf-token/")
.then((response) => response.data)
.catch((error) => {
throw error;
});
}
}
-72
View File
@@ -1,72 +0,0 @@
// plane internal packages
import { API_BASE_URL } from "@plane/constants";
import type {
IFormattedInstanceConfiguration,
IInstance,
IInstanceAdmin,
IInstanceConfiguration,
IInstanceInfo,
} from "@plane/types";
// helpers
import { APIService } from "@/services/api.service";
export class InstanceService extends APIService {
constructor() {
super(API_BASE_URL);
}
async getInstanceInfo(): Promise<IInstanceInfo> {
return this.get<IInstanceInfo>("/api/instances/")
.then((response) => response.data)
.catch((error) => {
throw error?.response?.data;
});
}
async getInstanceAdmins(): Promise<IInstanceAdmin[]> {
return this.get<IInstanceAdmin[]>("/api/instances/admins/")
.then((response) => response.data)
.catch((error) => {
throw error;
});
}
async updateInstanceInfo(data: Partial<IInstance>): Promise<IInstance> {
return this.patch<Partial<IInstance>, IInstance>("/api/instances/", data)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async getInstanceConfigurations() {
return this.get<IInstanceConfiguration[]>("/api/instances/configurations/")
.then((response) => response.data)
.catch((error) => {
throw error;
});
}
async updateInstanceConfigurations(
data: Partial<IFormattedInstanceConfiguration>
): Promise<IInstanceConfiguration[]> {
return this.patch<Partial<IFormattedInstanceConfiguration>, IInstanceConfiguration[]>(
"/api/instances/configurations/",
data
)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
async sendTestEmail(receiverEmail: string): Promise<undefined> {
return this.post<{ receiver_email: string }, undefined>("/api/instances/email-credentials-check/", {
receiver_email: receiverEmail,
})
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
}
-29
View File
@@ -1,29 +0,0 @@
// plane internal packages
import { API_BASE_URL } from "@plane/constants";
import type { IUser } from "@plane/types";
// services
import { APIService } from "@/services/api.service";
interface IUserSession extends IUser {
isAuthenticated: boolean;
}
export class UserService extends APIService {
constructor() {
super(API_BASE_URL);
}
async authCheck(): Promise<IUserSession> {
return this.get<any>("/api/instances/admins/me/")
.then((response) => ({ ...response?.data, isAuthenticated: true }))
.catch(() => ({ isAuthenticated: false }));
}
async currentUser(): Promise<IUser> {
return this.get<IUser>("/api/instances/admins/me/")
.then((response) => response?.data)
.catch((error) => {
throw error?.response;
});
}
}
-52
View File
@@ -1,52 +0,0 @@
// plane internal packages
import { API_BASE_URL } from "@plane/constants";
import type { IWorkspace, TWorkspacePaginationInfo } from "@plane/types";
// services
import { APIService } from "@/services/api.service";
export class WorkspaceService extends APIService {
constructor() {
super(API_BASE_URL);
}
/**
* @description Fetches all workspaces
* @returns Promise<TWorkspacePaginationInfo>
*/
async getWorkspaces(nextPageCursor?: string): Promise<TWorkspacePaginationInfo> {
return this.get<TWorkspacePaginationInfo>("/api/instances/workspaces/", {
cursor: nextPageCursor,
})
.then((response) => response.data)
.catch((error) => {
throw error?.response?.data;
});
}
/**
* @description Checks if a slug is available
* @param slug - string
* @returns Promise<any>
*/
async workspaceSlugCheck(slug: string): Promise<any> {
const params = new URLSearchParams({ slug });
return this.get(`/api/instances/workspace-slug-check/?${params.toString()}`)
.then((response) => response?.data)
.catch((error) => {
throw error?.response?.data;
});
}
/**
* @description Creates a new workspace
* @param data - IWorkspace
* @returns Promise<IWorkspace>
*/
async createWorkspace(data: IWorkspace): Promise<IWorkspace> {
return this.post<IWorkspace, IWorkspace>("/api/instances/workspaces/", data)
.then((response) => response.data)
.catch((error) => {
throw error?.response?.data;
});
}
}
+6 -7
View File
@@ -2,6 +2,7 @@ import set from "lodash/set";
import { observable, action, computed, makeObservable, runInAction } from "mobx";
// plane internal packages
import { EInstanceStatus, TInstanceStatus } from "@plane/constants";
import {InstanceService} from "@plane/services";
import {
IInstance,
IInstanceAdmin,
@@ -10,8 +11,6 @@ import {
IInstanceInfo,
IInstanceConfig,
} from "@plane/types";
// services
import { InstanceService } from "@/services/instance.service";
// root store
import { CoreRootStore } from "@/store/root.store";
@@ -96,7 +95,7 @@ export class InstanceStore implements IInstanceStore {
try {
if (this.instance === undefined) this.isLoading = true;
this.error = undefined;
const instanceInfo = await this.instanceService.getInstanceInfo();
const instanceInfo = await this.instanceService.info();
// handling the new user popup toggle
if (this.instance === undefined && !instanceInfo?.instance?.workspaces_exist)
this.store.theme.toggleNewUserPopup();
@@ -125,7 +124,7 @@ export class InstanceStore implements IInstanceStore {
*/
updateInstanceInfo = async (data: Partial<IInstance>) => {
try {
const instanceResponse = await this.instanceService.updateInstanceInfo(data);
const instanceResponse = await this.instanceService.update(data);
if (instanceResponse) {
runInAction(() => {
if (this.instance) set(this.instance, "instance", instanceResponse);
@@ -144,7 +143,7 @@ export class InstanceStore implements IInstanceStore {
*/
fetchInstanceAdmins = async () => {
try {
const instanceAdmins = await this.instanceService.getInstanceAdmins();
const instanceAdmins = await this.instanceService.admins();
if (instanceAdmins) runInAction(() => (this.instanceAdmins = instanceAdmins));
return instanceAdmins;
} catch (error) {
@@ -159,7 +158,7 @@ export class InstanceStore implements IInstanceStore {
*/
fetchInstanceConfigurations = async () => {
try {
const instanceConfigurations = await this.instanceService.getInstanceConfigurations();
const instanceConfigurations = await this.instanceService.configurations();
if (instanceConfigurations) runInAction(() => (this.instanceConfigurations = instanceConfigurations));
return instanceConfigurations;
} catch (error) {
@@ -174,7 +173,7 @@ export class InstanceStore implements IInstanceStore {
*/
updateInstanceConfigurations = async (data: Partial<IFormattedInstanceConfiguration>) => {
try {
const response = await this.instanceService.updateInstanceConfigurations(data);
const response = await this.instanceService.updateConfigurations(data);
runInAction(() => {
this.instanceConfigurations = this.instanceConfigurations?.map((config) => {
const item = response.find((item) => item.key === config.key);
+2 -4
View File
@@ -1,10 +1,8 @@
import { action, observable, runInAction, makeObservable } from "mobx";
// plane internal packages
import { EUserStatus, TUserStatus } from "@plane/constants";
import { AuthService, UserService } from "@plane/services";
import { IUser } from "@plane/types";
// services
import { AuthService } from "@/services/auth.service";
import { UserService } from "@/services/user.service";
// root store
import { CoreRootStore } from "@/store/root.store";
@@ -58,7 +56,7 @@ export class UserStore implements IUserStore {
fetchCurrentUser = async () => {
try {
if (this.currentUser === undefined) this.isLoading = true;
const currentUser = await this.userService.currentUser();
const currentUser = await this.userService.adminDetails();
if (currentUser) {
await this.store.instance.fetchInstanceAdmins();
runInAction(() => {
+7 -7
View File
@@ -1,8 +1,8 @@
import set from "lodash/set";
import { action, observable, runInAction, makeObservable, computed } from "mobx";
// plane imports
import { InstanceWorkspaceService } from "@plane/services";
import { IWorkspace, TLoader, TPaginationInfo } from "@plane/types";
// services
import { WorkspaceService } from "@/services/workspace.service";
// root store
import { CoreRootStore } from "@/store/root.store";
@@ -29,7 +29,7 @@ export class WorkspaceStore implements IWorkspaceStore {
workspaces: Record<string, IWorkspace> = {};
paginationInfo: TPaginationInfo | undefined = undefined;
// services
workspaceService;
instanceWorkspaceService;
constructor(private store: CoreRootStore) {
makeObservable(this, {
@@ -48,7 +48,7 @@ export class WorkspaceStore implements IWorkspaceStore {
// curd actions
createWorkspace: action,
});
this.workspaceService = new WorkspaceService();
this.instanceWorkspaceService = new InstanceWorkspaceService();
}
// computed
@@ -84,7 +84,7 @@ export class WorkspaceStore implements IWorkspaceStore {
} else {
this.loader = "init-loader";
}
const paginatedWorkspaceData = await this.workspaceService.getWorkspaces();
const paginatedWorkspaceData = await this.instanceWorkspaceService.list();
runInAction(() => {
const { results, ...paginationInfo } = paginatedWorkspaceData;
results.forEach((workspace: IWorkspace) => {
@@ -109,7 +109,7 @@ export class WorkspaceStore implements IWorkspaceStore {
if (!this.paginationInfo || this.paginationInfo.next_page_results === false) return [];
try {
this.loader = "pagination";
const paginatedWorkspaceData = await this.workspaceService.getWorkspaces(this.paginationInfo.next_cursor);
const paginatedWorkspaceData = await this.instanceWorkspaceService.list(this.paginationInfo.next_cursor);
runInAction(() => {
const { results, ...paginationInfo } = paginatedWorkspaceData;
results.forEach((workspace: IWorkspace) => {
@@ -135,7 +135,7 @@ export class WorkspaceStore implements IWorkspaceStore {
createWorkspace = async (data: IWorkspace): Promise<IWorkspace> => {
try {
this.loader = "mutation";
const workspace = await this.workspaceService.createWorkspace(data);
const workspace = await this.instanceWorkspaceService.create(data);
runInAction(() => {
set(this.workspaces, [workspace.id], workspace);
});
+2 -1
View File
@@ -18,11 +18,12 @@
"@plane/types": "*",
"@plane/ui": "*",
"@plane/utils": "*",
"@plane/services": "*",
"@sentry/nextjs": "^8.32.0",
"@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.17.0",
"autoprefixer": "10.4.14",
"axios": "^1.7.4",
"axios": "^1.7.9",
"lodash": "^4.17.21",
"lucide-react": "^0.356.0",
"mobx": "^6.12.0",
+12 -2
View File
@@ -6,6 +6,7 @@ from .base import BaseSerializer
from plane.db.models import Cycle, CycleIssue
from plane.utils.timezone_converter import convert_to_utc
class CycleSerializer(BaseSerializer):
total_issues = serializers.IntegerField(read_only=True)
cancelled_issues = serializers.IntegerField(read_only=True)
@@ -30,11 +31,20 @@ class CycleSerializer(BaseSerializer):
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
)
data["start_date"] = convert_to_utc(
str(data.get("start_date").date()), project_id, is_start_date=True
date=str(data.get("start_date").date()),
project_id=project_id,
is_start_date=True,
)
data["end_date"] = convert_to_utc(
str(data.get("end_date", None).date()), project_id
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
+149 -38
View File
@@ -1,9 +1,10 @@
# Python imports
import json
from django.core.serializers.json import DjangoJSONEncoder
import uuid
# Django imports
from django.core.serializers.json import DjangoJSONEncoder
from django.http import HttpResponseRedirect
from django.db import IntegrityError
from django.db.models import (
Case,
@@ -19,11 +20,11 @@ from django.db.models import (
Subquery,
)
from django.utils import timezone
from django.conf import settings
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from rest_framework.parsers import MultiPartParser, FormParser
# Module imports
from plane.api.serializers import (
@@ -50,8 +51,10 @@ from plane.db.models import (
Project,
ProjectMember,
CycleIssue,
Workspace,
)
from plane.settings.storage import S3Storage
from plane.bgtasks.storage_metadata_task import get_asset_object_metadata
from .base import BaseAPIView
@@ -940,37 +943,162 @@ class IssueAttachmentEndpoint(BaseAPIView):
serializer_class = IssueAttachmentSerializer
permission_classes = [ProjectEntityPermission]
model = FileAsset
parser_classes = (MultiPartParser, FormParser)
def post(self, request, slug, project_id, issue_id):
serializer = IssueAttachmentSerializer(data=request.data)
name = request.data.get("name")
type = request.data.get("type", False)
size = request.data.get("size")
external_id = request.data.get("external_id")
external_source = request.data.get("external_source")
# Check if the request is valid
if not name or not size:
return Response(
{"error": "Invalid request.", "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
size_limit = min(size, settings.FILE_SIZE_LIMIT)
if not type or type not in settings.ATTACHMENT_MIME_TYPES:
return Response(
{"error": "Invalid file type.", "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
# Get the workspace
workspace = Workspace.objects.get(slug=slug)
# asset key
asset_key = f"{workspace.id}/{uuid.uuid4().hex}-{name}"
if (
request.data.get("external_id")
and request.data.get("external_source")
and FileAsset.objects.filter(
project_id=project_id,
workspace__slug=slug,
issue_id=issue_id,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
issue_id=issue_id,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
).exists()
):
issue_attachment = FileAsset.objects.filter(
workspace__slug=slug,
asset = FileAsset.objects.filter(
project_id=project_id,
external_id=request.data.get("external_id"),
workspace__slug=slug,
external_source=request.data.get("external_source"),
external_id=request.data.get("external_id"),
issue_id=issue_id,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
).first()
return Response(
{
"error": "Issue attachment with the same external id and external source already exists",
"id": str(issue_attachment.id),
"error": "Issue with the same external id and external source already exists",
"id": str(asset.id),
},
status=status.HTTP_409_CONFLICT,
)
if serializer.is_valid():
serializer.save(project_id=project_id, issue_id=issue_id)
# Create a File Asset
asset = FileAsset.objects.create(
attributes={"name": name, "type": type, "size": size_limit},
asset=asset_key,
size=size_limit,
workspace_id=workspace.id,
created_by=request.user,
issue_id=issue_id,
project_id=project_id,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
external_id=external_id,
external_source=external_source,
)
# Get the presigned URL
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post(
object_name=asset_key, file_type=type, file_size=size_limit
)
# Return the presigned URL
return Response(
{
"upload_data": presigned_url,
"asset_id": str(asset.id),
"attachment": IssueAttachmentSerializer(asset).data,
"asset_url": asset.asset_url,
},
status=status.HTTP_200_OK,
)
def delete(self, request, slug, project_id, issue_id, pk):
issue_attachment = FileAsset.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id
)
issue_attachment.is_deleted = True
issue_attachment.deleted_at = timezone.now()
issue_attachment.save()
issue_activity.delay(
type="attachment.activity.deleted",
requested_data=None,
actor_id=str(self.request.user.id),
issue_id=str(issue_id),
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
# Get the storage metadata
if not issue_attachment.storage_metadata:
get_asset_object_metadata.delay(str(issue_attachment.id))
issue_attachment.save()
return Response(status=status.HTTP_204_NO_CONTENT)
def get(self, request, slug, project_id, issue_id, pk=None):
if pk:
# Get the asset
asset = FileAsset.objects.get(
id=pk, workspace__slug=slug, project_id=project_id
)
# Check if the asset is uploaded
if not asset.is_uploaded:
return Response(
{"error": "The asset is not uploaded.", "status": False},
status=status.HTTP_400_BAD_REQUEST,
)
storage = S3Storage(request=request)
presigned_url = storage.generate_presigned_url(
object_name=asset.asset.name,
disposition="attachment",
filename=asset.attributes.get("name"),
)
return HttpResponseRedirect(presigned_url)
# Get all the attachments
issue_attachments = FileAsset.objects.filter(
issue_id=issue_id,
entity_type=FileAsset.EntityTypeContext.ISSUE_ATTACHMENT,
workspace__slug=slug,
project_id=project_id,
is_uploaded=True,
)
# Serialize the attachments
serializer = IssueAttachmentSerializer(issue_attachments, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def patch(self, request, slug, project_id, issue_id, pk):
issue_attachment = FileAsset.objects.get(
pk=pk, workspace__slug=slug, project_id=project_id
)
serializer = IssueAttachmentSerializer(issue_attachment)
# Send this activity only if the attachment is not uploaded before
if not issue_attachment.is_uploaded:
issue_activity.delay(
type="attachment.activity.created",
requested_data=None,
@@ -982,30 +1110,13 @@ class IssueAttachmentEndpoint(BaseAPIView):
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
def delete(self, request, slug, project_id, issue_id, pk):
issue_attachment = FileAsset.objects.get(pk=pk)
issue_attachment.asset.delete(save=False)
issue_attachment.delete()
issue_activity.delay(
type="attachment.activity.deleted",
requested_data=None,
actor_id=str(self.request.user.id),
issue_id=str(self.kwargs.get("issue_id", None)),
project_id=str(self.kwargs.get("project_id", None)),
current_instance=None,
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
)
# Update the attachment
issue_attachment.is_uploaded = True
issue_attachment.created_by = request.user
# Get the storage metadata
if not issue_attachment.storage_metadata:
get_asset_object_metadata.delay(str(issue_attachment.id))
issue_attachment.save()
return Response(status=status.HTTP_204_NO_CONTENT)
def get(self, request, slug, project_id, issue_id):
issue_attachments = FileAsset.objects.filter(
issue_id=issue_id, workspace__slug=slug, project_id=project_id
)
serializer = IssueAttachmentSerializer(issue_attachments, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
-10
View File
@@ -288,16 +288,6 @@ class ProjectAPIEndpoint(BaseAPIView):
is_default=True,
)
# Create the triage state in Backlog group
State.objects.get_or_create(
name="Triage",
group="triage",
description="Default state for managing all Intake Issues",
project_id=pk,
color="#ff7700",
is_triage=True,
)
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
model_activity.delay(
@@ -19,6 +19,10 @@ from .workspace import (
WorkspaceMemberAdminSerializer,
WorkspaceMemberMeSerializer,
WorkspaceUserPropertiesSerializer,
WorkspaceUserLinkSerializer,
WorkspaceRecentVisitSerializer,
WorkspaceHomePreferenceSerializer,
StickySerializer,
)
from .project import (
ProjectSerializer,
+16 -3
View File
@@ -20,12 +20,25 @@ class CycleWriteSerializer(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
project_id = (
self.initial_data.get("project_id", None)
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(
str(data.get("start_date").date()), project_id, is_start_date=True
date=str(data.get("start_date").date()),
project_id=project_id,
is_start_date=True,
)
data["end_date"] = convert_to_utc(
str(data.get("end_date", None).date()), project_id
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
+3 -3
View File
@@ -1,6 +1,6 @@
# Module imports
from .base import BaseSerializer
from plane.db.models import Dashboard, Widget
from plane.db.models import DeprecatedDashboard, DeprecatedWidget
# Third party frameworks
from rest_framework import serializers
@@ -8,7 +8,7 @@ from rest_framework import serializers
class DashboardSerializer(BaseSerializer):
class Meta:
model = Dashboard
model = DeprecatedDashboard
fields = "__all__"
@@ -17,5 +17,5 @@ class WidgetSerializer(BaseSerializer):
widget_filters = serializers.JSONField(read_only=True)
class Meta:
model = Widget
model = DeprecatedWidget
fields = ["id", "key", "is_visible", "widget_filters"]
@@ -53,7 +53,6 @@ def get_entity_model_and_serializer(entity_type):
}
return entity_map.get(entity_type, (None, None))
class UserFavoriteSerializer(serializers.ModelSerializer):
entity_data = serializers.SerializerMethodField()
+4
View File
@@ -54,6 +54,8 @@ class PageSerializer(BaseSerializer):
labels = validated_data.pop("labels", None)
project_id = self.context["project_id"]
owned_by_id = self.context["owned_by_id"]
description = self.context["description"]
description_binary = self.context["description_binary"]
description_html = self.context["description_html"]
# Get the workspace id from the project
@@ -62,6 +64,8 @@ class PageSerializer(BaseSerializer):
# Create the page
page = Page.objects.create(
**validated_data,
description=description,
description_binary=description_binary,
description_html=description_html,
owned_by_id=owned_by_id,
workspace_id=project.workspace_id,
@@ -1,19 +1,34 @@
# Third party imports
from rest_framework import serializers
from rest_framework import status
from rest_framework.response import Response
# Module imports
from .base import BaseSerializer, DynamicBaseSerializer
from .user import UserLiteSerializer, UserAdminLiteSerializer
from plane.db.models import (
Workspace,
WorkspaceMember,
WorkspaceMemberInvite,
WorkspaceTheme,
WorkspaceUserProperties,
WorkspaceUserLink,
UserRecentVisit,
Issue,
Page,
Project,
ProjectMember,
WorkspaceHomePreference,
Sticky,
)
from plane.utils.constants import RESTRICTED_WORKSPACE_SLUGS
# Django imports
from django.core.validators import URLValidator
from django.core.exceptions import ValidationError
class WorkSpaceSerializer(DynamicBaseSerializer):
owner = UserLiteSerializer(read_only=True)
@@ -106,3 +121,139 @@ class WorkspaceUserPropertiesSerializer(BaseSerializer):
model = WorkspaceUserProperties
fields = "__all__"
read_only_fields = ["workspace", "user"]
class WorkspaceUserLinkSerializer(BaseSerializer):
class Meta:
model = WorkspaceUserLink
fields = "__all__"
read_only_fields = ["workspace", "owner"]
def to_internal_value(self, data):
url = data.get("url", "")
if url and not url.startswith(("http://", "https://")):
data["url"] = "http://" + url
return super().to_internal_value(data)
def validate_url(self, value):
url_validator = URLValidator()
try:
url_validator(value)
except ValidationError:
raise serializers.ValidationError({"error": "Invalid URL format."})
return value
class IssueRecentVisitSerializer(serializers.ModelSerializer):
project_identifier = serializers.SerializerMethodField()
class Meta:
model = Issue
fields = [
"id",
"name",
"state",
"priority",
"assignees",
"type",
"sequence_id",
"project_id",
"project_identifier",
]
def get_project_identifier(self, obj):
project = obj.project
return project.identifier if project else None
class ProjectRecentVisitSerializer(serializers.ModelSerializer):
project_members = serializers.SerializerMethodField()
class Meta:
model = Project
fields = ["id", "name", "logo_props", "project_members", "identifier"]
def get_project_members(self, obj):
members = ProjectMember.objects.filter(project_id=obj.id).values_list(
"member", flat=True
)
return members
class PageRecentVisitSerializer(serializers.ModelSerializer):
project_id = serializers.SerializerMethodField()
project_identifier = serializers.SerializerMethodField()
class Meta:
model = Page
fields = [
"id",
"name",
"logo_props",
"project_id",
"owned_by",
"project_identifier",
]
def get_project_id(self, obj):
return (
obj.project_id
if hasattr(obj, "project_id")
else obj.projects.values_list("id", flat=True).first()
)
def get_project_identifier(self, obj):
project = obj.projects.first()
return project.identifier if project else None
def get_entity_model_and_serializer(entity_type):
entity_map = {
"issue": (Issue, IssueRecentVisitSerializer),
"page": (Page, PageRecentVisitSerializer),
"project": (Project, ProjectRecentVisitSerializer),
}
return entity_map.get(entity_type, (None, None))
class WorkspaceRecentVisitSerializer(BaseSerializer):
entity_data = serializers.SerializerMethodField()
class Meta:
model = UserRecentVisit
fields = ["id", "entity_name", "entity_identifier", "entity_data", "visited_at"]
read_only_fields = ["workspace", "owner", "created_by", "updated_by"]
def get_entity_data(self, obj):
entity_name = obj.entity_name
entity_identifier = obj.entity_identifier
entity_model, entity_serializer = get_entity_model_and_serializer(entity_name)
if entity_model and entity_serializer:
try:
entity = entity_model.objects.get(pk=entity_identifier)
return entity_serializer(entity).data
except entity_model.DoesNotExist:
return None
return None
class WorkspaceHomePreferenceSerializer(BaseSerializer):
class Meta:
model = WorkspaceHomePreference
fields = ["key", "is_enabled", "sort_order"]
read_only_fields = ["worspace", "created_by", "update_by"]
class StickySerializer(BaseSerializer):
class Meta:
model = Sticky
fields = "__all__"
read_only_fields = ["workspace", "owner"]
extra_kwargs = {"name": {"required": False}}
+6
View File
@@ -8,6 +8,7 @@ from plane.app.views import (
SubPagesEndpoint,
PagesDescriptionViewSet,
PageVersionEndpoint,
PageDuplicateEndpoint,
)
@@ -78,4 +79,9 @@ urlpatterns = [
PageVersionEndpoint.as_view(),
name="page-versions",
),
path(
"workspaces/<str:slug>/projects/<uuid:project_id>/pages/<uuid:page_id>/duplicate/",
PageDuplicateEndpoint.as_view(),
name="page-duplicate",
),
]
+45
View File
@@ -27,6 +27,10 @@ from plane.app.views import (
WorkspaceFavoriteEndpoint,
WorkspaceFavoriteGroupEndpoint,
WorkspaceDraftIssueViewSet,
QuickLinkViewSet,
UserRecentVisitViewSet,
WorkspacePreferenceViewSet,
WorkspaceStickyViewSet,
)
@@ -213,4 +217,45 @@ urlpatterns = [
WorkspaceDraftIssueViewSet.as_view({"post": "create_draft_to_issue"}),
name="workspace-drafts-issues",
),
# quick link
path(
"workspaces/<str:slug>/quick-links/",
QuickLinkViewSet.as_view({"get": "list", "post": "create"}),
name="workspace-quick-links",
),
path(
"workspaces/<str:slug>/quick-links/<uuid:pk>/",
QuickLinkViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="workspace-quick-links",
),
# Widgets
path(
"workspaces/<str:slug>/home-preferences/",
WorkspacePreferenceViewSet.as_view(),
name="workspace-home-preference",
),
path(
"workspaces/<str:slug>/home-preferences/<str:key>/",
WorkspacePreferenceViewSet.as_view(),
name="workspace-home-preference",
),
path(
"workspaces/<str:slug>/recent-visits/",
UserRecentVisitViewSet.as_view({"get": "list"}),
name="workspace-recent-visits",
),
path(
"workspaces/<str:slug>/stickies/",
WorkspaceStickyViewSet.as_view({"get": "list", "post": "create"}),
name="workspace-sticky",
),
path(
"workspaces/<str:slug>/stickies/<uuid:pk>/",
WorkspaceStickyViewSet.as_view(
{"get": "retrieve", "patch": "partial_update", "delete": "destroy"}
),
name="workspace-sticky",
),
]
+5
View File
@@ -41,10 +41,12 @@ from .workspace.base import (
from .workspace.draft import WorkspaceDraftIssueViewSet
from .workspace.preference import WorkspacePreferenceViewSet
from .workspace.favorite import (
WorkspaceFavoriteEndpoint,
WorkspaceFavoriteGroupEndpoint,
)
from .workspace.recent_visit import UserRecentVisitViewSet
from .workspace.member import (
WorkSpaceMemberViewSet,
@@ -72,6 +74,8 @@ from .workspace.user import (
from .workspace.estimate import WorkspaceEstimatesEndpoint
from .workspace.module import WorkspaceModulesEndpoint
from .workspace.cycle import WorkspaceCyclesEndpoint
from .workspace.quick_link import QuickLinkViewSet
from .workspace.sticky import WorkspaceStickyViewSet
from .state.base import StateViewSet
from .view.base import (
@@ -155,6 +159,7 @@ from .page.base import (
PageLogEndpoint,
SubPagesEndpoint,
PagesDescriptionViewSet,
PageDuplicateEndpoint,
)
from .page.version import PageVersionEndpoint
+35 -15
View File
@@ -54,11 +54,7 @@ from plane.bgtasks.recent_visited_task import recent_visited_task
# Module imports
from .. import BaseAPIView, BaseViewSet
from plane.bgtasks.webhook_task import model_activity
from plane.utils.timezone_converter import (
convert_utc_to_project_timezone,
convert_to_utc,
user_timezone_converter,
)
from plane.utils.timezone_converter import convert_to_utc, user_timezone_converter
class CycleViewSet(BaseViewSet):
@@ -143,10 +139,7 @@ class CycleViewSet(BaseViewSet):
& Q(end_date__gte=current_time_in_utc),
then=Value("CURRENT"),
),
When(
start_date__gt=current_time_in_utc,
then=Value("UPCOMING"),
),
When(start_date__gt=current_time_in_utc, then=Value("UPCOMING")),
When(end_date__lt=current_time_in_utc, then=Value("COMPLETED")),
When(
Q(start_date__isnull=True) & Q(end_date__isnull=True),
@@ -259,7 +252,9 @@ class CycleViewSet(BaseViewSet):
"created_by",
)
datetime_fields = ["start_date", "end_date"]
data = user_timezone_converter(data, datetime_fields, request.user.user_timezone)
data = user_timezone_converter(
data, datetime_fields, request.user.user_timezone
)
return Response(data, status=status.HTTP_200_OK)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
@@ -271,7 +266,9 @@ class CycleViewSet(BaseViewSet):
request.data.get("start_date", None) is not None
and request.data.get("end_date", None) is not None
):
serializer = CycleWriteSerializer(data=request.data)
serializer = CycleWriteSerializer(
data=request.data, context={"project_id": project_id}
)
if serializer.is_valid():
serializer.save(project_id=project_id, owned_by=request.user)
cycle = (
@@ -306,6 +303,11 @@ class CycleViewSet(BaseViewSet):
.first()
)
datetime_fields = ["start_date", "end_date"]
cycle = user_timezone_converter(
cycle, datetime_fields, request.user.user_timezone
)
# Send the model activity
model_activity.delay(
model_name="cycle",
@@ -358,7 +360,9 @@ class CycleViewSet(BaseViewSet):
status=status.HTTP_400_BAD_REQUEST,
)
serializer = CycleWriteSerializer(cycle, data=request.data, partial=True)
serializer = CycleWriteSerializer(
cycle, data=request.data, partial=True, context={"project_id": project_id}
)
if serializer.is_valid():
serializer.save()
cycle = queryset.values(
@@ -388,6 +392,11 @@ class CycleViewSet(BaseViewSet):
"created_by",
).first()
datetime_fields = ["start_date", "end_date"]
cycle = user_timezone_converter(
cycle, datetime_fields, request.user.user_timezone
)
# Send the model activity
model_activity.delay(
model_name="cycle",
@@ -457,7 +466,9 @@ class CycleViewSet(BaseViewSet):
queryset = queryset.first()
datetime_fields = ["start_date", "end_date"]
data = user_timezone_converter(data, datetime_fields, request.user.user_timezone)
data = user_timezone_converter(
data, datetime_fields, request.user.user_timezone
)
recent_visited_task.delay(
slug=slug,
@@ -533,8 +544,17 @@ class CycleDateCheckEndpoint(BaseAPIView):
status=status.HTTP_400_BAD_REQUEST,
)
start_date = convert_to_utc(str(start_date), project_id, is_start_date=True)
end_date = convert_to_utc(str(end_date), project_id)
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
cycles = Cycle.objects.filter(
+7 -7
View File
@@ -32,15 +32,15 @@ from plane.app.serializers import (
WidgetSerializer,
)
from plane.db.models import (
Dashboard,
DashboardWidget,
DeprecatedDashboard,
DeprecatedDashboardWidget,
Issue,
IssueActivity,
FileAsset,
IssueLink,
IssueRelation,
Project,
Widget,
DeprecatedWidget,
WorkspaceMember,
CycleIssue,
)
@@ -687,7 +687,7 @@ class DashboardEndpoint(BaseAPIView):
if not dashboard_id:
dashboard_type = request.GET.get("dashboard_type", None)
if dashboard_type == "home":
dashboard, created = Dashboard.objects.get_or_create(
dashboard, created = DeprecatedDashboard.objects.get_or_create(
type_identifier=dashboard_type,
owned_by=request.user,
is_default=True,
@@ -707,7 +707,7 @@ class DashboardEndpoint(BaseAPIView):
updated_dashboard_widgets = []
for widget_key in widgets_to_fetch:
widget = Widget.objects.filter(key=widget_key).values_list(
widget = DeprecatedWidget.objects.filter(key=widget_key).values_list(
"id", flat=True
)
if widget:
@@ -722,9 +722,9 @@ class DashboardEndpoint(BaseAPIView):
)
widgets = (
Widget.objects.annotate(
DeprecatedWidget.objects.annotate(
is_visible=Exists(
DashboardWidget.objects.filter(
DeprecatedDashboardWidget.objects.filter(
widget_id=OuterRef("pk"),
dashboard_id=dashboard.id,
is_visible=True,
+153 -69
View File
@@ -1,71 +1,169 @@
# Python imports
import requests
# Python import
import os
from typing import List, Dict, Tuple
# Third party import
import litellm
import requests
# Third party imports
from openai import OpenAI
from rest_framework.response import Response
from rest_framework import status
from rest_framework.response import Response
# Django imports
# Module imports
from ..base import BaseAPIView
from plane.app.permissions import allow_permission, ROLE
from plane.db.models import Workspace, Project
from plane.app.serializers import ProjectLiteSerializer, WorkspaceLiteSerializer
# Module import
from plane.app.permissions import ROLE, allow_permission
from plane.app.serializers import (ProjectLiteSerializer,
WorkspaceLiteSerializer)
from plane.db.models import Project, Workspace
from plane.license.utils.instance_value import get_configuration_value
from plane.utils.exception_logger import log_exception
from ..base import BaseAPIView
class LLMProvider:
"""Base class for LLM provider configurations"""
name: str = ""
models: List[str] = []
default_model: str = ""
@classmethod
def get_config(cls) -> Dict[str, str | List[str]]:
return {
"name": cls.name,
"models": cls.models,
"default_model": cls.default_model,
}
class OpenAIProvider(LLMProvider):
name = "OpenAI"
models = ["gpt-3.5-turbo", "gpt-4o-mini", "gpt-4o", "o1-mini", "o1-preview"]
default_model = "gpt-4o-mini"
class AnthropicProvider(LLMProvider):
name = "Anthropic"
models = [
"claude-3-5-sonnet-20240620",
"claude-3-haiku-20240307",
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-2.1",
"claude-2",
"claude-instant-1.2",
"claude-instant-1"
]
default_model = "claude-3-sonnet-20240229"
class GeminiProvider(LLMProvider):
name = "Gemini"
models = ["gemini-pro", "gemini-1.5-pro-latest", "gemini-pro-vision"]
default_model = "gemini-pro"
SUPPORTED_PROVIDERS = {
"openai": OpenAIProvider,
"anthropic": AnthropicProvider,
"gemini": GeminiProvider,
}
def get_llm_config() -> Tuple[str | None, str | None, str | None]:
"""
Helper to get LLM configuration values, returns:
- api_key, model, provider
"""
api_key, provider_key, model = get_configuration_value([
{
"key": "LLM_API_KEY",
"default": os.environ.get("LLM_API_KEY", None),
},
{
"key": "LLM_PROVIDER",
"default": os.environ.get("LLM_PROVIDER", "openai"),
},
{
"key": "LLM_MODEL",
"default": os.environ.get("LLM_MODEL", None),
},
])
provider = SUPPORTED_PROVIDERS.get(provider_key.lower())
if not provider:
log_exception(ValueError(f"Unsupported provider: {provider_key}"))
return None, None, None
if not api_key:
log_exception(ValueError(f"Missing API key for provider: {provider.name}"))
return None, None, None
# If no model specified, use provider's default
if not model:
model = provider.default_model
# Validate model is supported by provider
if model not in provider.models:
log_exception(ValueError(
f"Model {model} not supported by {provider.name}. "
f"Supported models: {', '.join(provider.models)}"
))
return None, None, None
return api_key, model, provider_key
def get_llm_response(task, prompt, api_key: str, model: str, provider: str) -> Tuple[str | None, str | None]:
"""Helper to get LLM completion response"""
final_text = task + "\n" + prompt
try:
# For Gemini, prepend provider name to model
if provider.lower() == "gemini":
model = f"gemini/{model}"
response = litellm.completion(
model=model,
messages=[{"role": "user", "content": final_text}],
api_key=api_key,
)
text = response.choices[0].message.content.strip()
return text, None
except Exception as e:
log_exception(e)
error_type = e.__class__.__name__
if error_type == "AuthenticationError":
return None, f"Invalid API key for {provider}"
elif error_type == "RateLimitError":
return None, f"Rate limit exceeded for {provider}"
else:
return None, f"Error occurred while generating response from {provider}"
class GPTIntegrationEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def post(self, request, slug, project_id):
OPENAI_API_KEY, GPT_ENGINE = get_configuration_value(
[
{
"key": "OPENAI_API_KEY",
"default": os.environ.get("OPENAI_API_KEY", None),
},
{
"key": "GPT_ENGINE",
"default": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"),
},
]
)
api_key, model, provider = get_llm_config()
# Get the configuration value
# Check the keys
if not OPENAI_API_KEY or not GPT_ENGINE:
if not api_key or not model or not provider:
return Response(
{"error": "OpenAI API key and engine is required"},
{"error": "LLM provider API key and model are required"},
status=status.HTTP_400_BAD_REQUEST,
)
prompt = request.data.get("prompt", False)
task = request.data.get("task", False)
if not task:
return Response(
{"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
)
final_text = task + "\n" + prompt
client = OpenAI(api_key=OPENAI_API_KEY)
response = client.chat.completions.create(
model=GPT_ENGINE, messages=[{"role": "user", "content": final_text}]
)
text, error = get_llm_response(task, request.data.get("prompt", False), api_key, model, provider)
if not text and error:
return Response(
{"error": "An internal error has occurred."},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
workspace = Workspace.objects.get(slug=slug)
project = Project.objects.get(pk=project_id)
text = response.choices[0].message.content.strip()
text_html = text.replace("\n", "<br/>")
return Response(
{
"response": text,
"response_html": text_html,
"response_html": text.replace("\n", "<br/>"),
"project_detail": ProjectLiteSerializer(project).data,
"workspace_detail": WorkspaceLiteSerializer(workspace).data,
},
@@ -76,47 +174,33 @@ class GPTIntegrationEndpoint(BaseAPIView):
class WorkspaceGPTIntegrationEndpoint(BaseAPIView):
@allow_permission(allowed_roles=[ROLE.ADMIN, ROLE.MEMBER], level="WORKSPACE")
def post(self, request, slug):
OPENAI_API_KEY, GPT_ENGINE = get_configuration_value(
[
{
"key": "OPENAI_API_KEY",
"default": os.environ.get("OPENAI_API_KEY", None),
},
{
"key": "GPT_ENGINE",
"default": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"),
},
]
)
# Get the configuration value
# Check the keys
if not OPENAI_API_KEY or not GPT_ENGINE:
api_key, model, provider = get_llm_config()
if not api_key or not model or not provider:
return Response(
{"error": "OpenAI API key and engine is required"},
{"error": "LLM provider API key and model are required"},
status=status.HTTP_400_BAD_REQUEST,
)
prompt = request.data.get("prompt", False)
task = request.data.get("task", False)
if not task:
return Response(
{"error": "Task is required"}, status=status.HTTP_400_BAD_REQUEST
)
final_text = task + "\n" + prompt
text, error = get_llm_response(task, request.data.get("prompt", False), api_key, model, provider)
if not text and error:
return Response(
{"error": "An internal error has occurred."},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
client = OpenAI(api_key=OPENAI_API_KEY)
response = client.chat.completions.create(
model=GPT_ENGINE, messages=[{"role": "user", "content": final_text}]
)
text = response.choices[0].message.content.strip()
text_html = text.replace("\n", "<br/>")
return Response(
{"response": text, "response_html": text_html}, status=status.HTTP_200_OK
{
"response": text,
"response_html": text.replace("\n", "<br/>"),
},
status=status.HTTP_200_OK,
)
@@ -120,10 +120,12 @@ class IssueAttachmentV2Endpoint(BaseAPIView):
# Get the presigned URL
storage = S3Storage(request=request)
# Generate a presigned URL to share an S3 object
presigned_url = storage.generate_presigned_post(
object_name=asset_key, file_type=type, file_size=size_limit
)
# Return the presigned URL
return Response(
{
+11 -18
View File
@@ -268,27 +268,20 @@ class IssueRelationViewSet(BaseViewSet):
)
def remove_relation(self, request, slug, project_id, issue_id):
relation_type = request.data.get("relation_type", None)
related_issue = request.data.get("related_issue", None)
if relation_type in ["blocking", "start_after", "finish_after"]:
issue_relation = IssueRelation.objects.get(
workspace__slug=slug,
project_id=project_id,
issue_id=related_issue,
related_issue_id=issue_id,
)
else:
issue_relation = IssueRelation.objects.get(
workspace__slug=slug,
project_id=project_id,
issue_id=issue_id,
related_issue_id=related_issue,
)
current_instance = json.dumps(
IssueRelationSerializer(issue_relation).data, cls=DjangoJSONEncoder
issue_relations = IssueRelation.objects.filter(
workspace__slug=slug,
project_id=project_id,
).filter(
Q(issue_id=related_issue, related_issue_id=issue_id) |
Q(issue_id=issue_id, related_issue_id=related_issue)
)
issue_relation.delete()
issue_relations = issue_relations.first()
current_instance = json.dumps(
IssueRelationSerializer(issue_relations).data, cls=DjangoJSONEncoder
)
issue_relations.delete()
issue_activity.delay(
type="issue_relation.activity.deleted",
requested_data=json.dumps(request.data, cls=DjangoJSONEncoder),
+48
View File
@@ -121,6 +121,8 @@ class PageViewSet(BaseViewSet):
context={
"project_id": project_id,
"owned_by_id": request.user.id,
"description": request.data.get("description", {}),
"description_binary": request.data.get("description_binary", None),
"description_html": request.data.get("description_html", "<p></p>"),
},
)
@@ -553,3 +555,49 @@ class PagesDescriptionViewSet(BaseViewSet):
return Response({"message": "Updated successfully"})
else:
return Response({"error": "No binary data provided"})
class PageDuplicateEndpoint(BaseAPIView):
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def post(self, request, slug, project_id, page_id):
page = Page.objects.filter(
pk=page_id, workspace__slug=slug, projects__id=project_id
).first()
# get all the project ids where page is present
project_ids = ProjectPage.objects.filter(page_id=page_id).values_list(
"project_id", flat=True
)
page.pk = None
page.name = f"{page.name} (Copy)"
page.description_binary = None
page.owned_by = request.user
page.save()
for project_id in project_ids:
ProjectPage.objects.create(
workspace_id=page.workspace_id,
project_id=project_id,
page_id=page.id,
created_by_id=page.created_by_id,
updated_by_id=page.updated_by_id,
)
page_transaction.delay(
{"description_html": page.description_html}, None, page.id
)
page = (
Page.objects.filter(pk=page.id)
.annotate(
project_ids=Coalesce(
ArrayAgg(
"projects__id", distinct=True, filter=~Q(projects__id=True)
),
Value([], output_field=ArrayField(UUIDField())),
)
)
.first()
)
serializer = PageDetailSerializer(page)
return Response(serializer.data, status=status.HTTP_201_CREATED)
-10
View File
@@ -416,16 +416,6 @@ class ProjectViewSet(BaseViewSet):
is_default=True,
)
# Create the triage state in Backlog group
State.objects.get_or_create(
name="Triage",
group="triage",
description="Default state for managing all Intake Issues",
project_id=pk,
color="#ff7700",
is_triage=True,
)
project = self.get_queryset().filter(pk=serializer.data["id"]).first()
model_activity.delay(
+34 -27
View File
@@ -277,28 +277,14 @@ class SearchEndpoint(BaseAPIView):
for field in fields:
q |= Q(**{f"{field}__icontains": query})
base_filters = Q(
q,
is_active=True,
workspace__slug=slug,
member__is_bot=False,
project_id=project_id,
role__gt=10,
)
if issue_id:
issue_created_by = (
Issue.objects.filter(id=issue_id)
.values_list("created_by_id", flat=True)
.first()
)
# Add condition to include `issue_created_by` in the query
filters = Q(member_id=issue_created_by) | base_filters
else:
filters = base_filters
# Query to fetch users
users = (
ProjectMember.objects.filter(filters)
ProjectMember.objects.filter(
q,
is_active=True,
workspace__slug=slug,
member__is_bot=False,
project_id=project_id,
)
.annotate(
member__avatar_url=Case(
When(
@@ -318,14 +304,35 @@ class SearchEndpoint(BaseAPIView):
)
)
.order_by("-created_at")
.values(
"member__avatar_url",
"member__display_name",
"member__id",
)[:count]
)
response_data["user_mention"] = list(users)
if issue_id:
issue_created_by = (
Issue.objects.filter(id=issue_id)
.values_list("created_by_id", flat=True)
.first()
)
users = (
users.filter(Q(role__gt=10) | Q(member_id=issue_created_by))
.distinct()
.values(
"member__avatar_url",
"member__display_name",
"member__id",
)
)
else:
users = (
users.filter(Q(role__gt=10))
.distinct()
.values(
"member__avatar_url",
"member__display_name",
"member__id",
)
)
response_data["user_mention"] = list(users[:count])
elif query_type == "project":
fields = ["name", "identifier"]
+10 -11
View File
@@ -1,5 +1,3 @@
# Python imports
# Django imports
from django.db.models import Q
@@ -9,7 +7,7 @@ from rest_framework.response import Response
# Module imports
from .base import BaseAPIView
from plane.db.models import Issue, ProjectMember
from plane.db.models import Issue, ProjectMember, IssueRelation
from plane.utils.issue_search import search_issues
@@ -47,17 +45,18 @@ class IssueSearchEndpoint(BaseAPIView):
)
if issue_relation == "true" and issue_id:
issue = Issue.issue_objects.filter(pk=issue_id).first()
related_issue_ids = IssueRelation.objects.filter(
Q(related_issue=issue) | Q(issue=issue)
).values_list(
"issue_id", "related_issue_id"
).distinct()
related_issue_ids = [item for sublist in related_issue_ids for item in sublist]
if issue:
issues = issues.filter(
~Q(pk=issue_id),
~(
Q(issue_related__issue=issue)
& Q(issue_related__deleted_at__isnull=True)
),
~(
Q(issue_relation__related_issue=issue)
& Q(issue_relation__deleted_at__isnull=True)
),
~Q(pk__in=related_issue_ids),
)
if sub_issue == "true" and issue_id:
issue = Issue.issue_objects.filter(pk=issue_id).first()
+16 -5
View File
@@ -1,6 +1,9 @@
# Python imports
from itertools import groupby
# Django imports
from django.db.utils import IntegrityError
# Third party imports
from rest_framework.response import Response
from rest_framework import status
@@ -37,11 +40,19 @@ class StateViewSet(BaseViewSet):
@invalidate_cache(path="workspaces/:slug/states/", url_params=True, user=False)
@allow_permission([ROLE.ADMIN])
def create(self, request, slug, project_id):
serializer = StateSerializer(data=request.data)
if serializer.is_valid():
serializer.save(project_id=project_id)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
try:
serializer = StateSerializer(data=request.data)
if serializer.is_valid():
serializer.save(project_id=project_id)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
except IntegrityError as e:
if "already exists" in str(e):
return Response(
{"name": "The state name is already taken"},
status=status.HTTP_400_BAD_REQUEST,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST])
def list(self, request, slug, project_id):
@@ -0,0 +1,90 @@
# Module imports
from ..base import BaseAPIView
from plane.db.models.workspace import WorkspaceHomePreference
from plane.app.permissions import allow_permission, ROLE
from plane.db.models import Workspace
from plane.app.serializers.workspace import WorkspaceHomePreferenceSerializer
# Django imports
from django.db.models import Count
# Third party imports
from rest_framework.response import Response
from rest_framework import status
class WorkspacePreferenceViewSet(BaseAPIView):
model = WorkspaceHomePreference
def get_serializer_class(self):
return WorkspaceHomePreferenceSerializer
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def get(self, request, slug):
workspace = Workspace.objects.get(slug=slug)
get_preference = WorkspaceHomePreference.objects.filter(
user=request.user, workspace_id=workspace.id
)
create_preference_keys = []
keys = [
key
for key, _ in WorkspaceHomePreference.HomeWidgetKeys.choices
if key not in ["quick_tutorial", "new_at_plane"]
]
sort_order_counter = 1
for preference in keys:
if preference not in get_preference.values_list("key", flat=True):
create_preference_keys.append(preference)
sort_order = 1000 - sort_order_counter
preference = WorkspaceHomePreference.objects.bulk_create(
[
WorkspaceHomePreference(
key=key,
user=request.user,
workspace=workspace,
sort_order=sort_order,
)
for key in create_preference_keys
],
batch_size=10,
ignore_conflicts=True,
)
sort_order_counter += 1
preference = WorkspaceHomePreference.objects.filter(
user=request.user, workspace_id=workspace.id
)
return Response(
preference.values("key", "is_enabled", "config", "sort_order"),
status=status.HTTP_200_OK,
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def patch(self, request, slug, key):
preference = WorkspaceHomePreference.objects.filter(
key=key, workspace__slug=slug
).first()
if preference:
serializer = WorkspaceHomePreferenceSerializer(
preference, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
{"detail": "Preference not found"}, status=status.HTTP_400_BAD_REQUEST
)
@@ -0,0 +1,74 @@
# Third party imports
from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.db.models import WorkspaceUserLink, Workspace
from plane.app.serializers import WorkspaceUserLinkSerializer
from ..base import BaseViewSet
from plane.app.permissions import allow_permission, ROLE
class QuickLinkViewSet(BaseViewSet):
model = WorkspaceUserLink
def get_serializer_class(self):
return WorkspaceUserLinkSerializer
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def create(self, request, slug):
workspace = Workspace.objects.get(slug=slug)
serializer = WorkspaceUserLinkSerializer(data=request.data)
if serializer.is_valid():
serializer.save(workspace_id=workspace.id, owner=request.user)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def partial_update(self, request, slug, pk):
quick_link = WorkspaceUserLink.objects.filter(
pk=pk, workspace__slug=slug, owner=request.user
).first()
if quick_link:
serializer = WorkspaceUserLinkSerializer(
quick_link, data=request.data, partial=True
)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
return Response(
{"detail": "Quick link not found."}, status=status.HTTP_404_NOT_FOUND
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def retrieve(self, request, slug, pk):
try:
quick_link = WorkspaceUserLink.objects.get(
pk=pk, workspace__slug=slug, owner=request.user
)
serializer = WorkspaceUserLinkSerializer(quick_link)
return Response(serializer.data, status=status.HTTP_200_OK)
except WorkspaceUserLink.DoesNotExist:
return Response(
{"error": "Quick link not found."}, status=status.HTTP_404_NOT_FOUND
)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def destroy(self, request, slug, pk):
quick_link = WorkspaceUserLink.objects.get(
pk=pk, workspace__slug=slug, owner=request.user
)
quick_link.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def list(self, request, slug):
quick_links = WorkspaceUserLink.objects.filter(
workspace__slug=slug, owner=request.user
)
serializer = WorkspaceUserLinkSerializer(quick_links, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -0,0 +1,35 @@
# Third party imports
from rest_framework import status
from rest_framework.response import Response
from plane.db.models import UserRecentVisit
from plane.app.serializers import WorkspaceRecentVisitSerializer
# Modules imports
from ..base import BaseViewSet
from plane.app.permissions import allow_permission, ROLE
class UserRecentVisitViewSet(BaseViewSet):
model = UserRecentVisit
def get_serializer_class(self):
return WorkspaceRecentVisitSerializer
@allow_permission([ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE")
def list(self, request, slug):
user_recent_visits = UserRecentVisit.objects.filter(
workspace__slug=slug, user=request.user
)
entity_name = request.query_params.get("entity_name")
if entity_name:
user_recent_visits = user_recent_visits.filter(entity_name=entity_name)
user_recent_visits = user_recent_visits.filter(
entity_name__in=["issue", "page", "project"]
)
serializer = WorkspaceRecentVisitSerializer(user_recent_visits[:20], many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
@@ -0,0 +1,59 @@
# Third party imports
from rest_framework.response import Response
from rest_framework import status
# Module imports
from plane.app.views.base import BaseViewSet
from plane.app.permissions import ROLE, allow_permission
from plane.db.models import Sticky, Workspace
from plane.app.serializers import StickySerializer
class WorkspaceStickyViewSet(BaseViewSet):
serializer_class = StickySerializer
model = Sticky
def get_queryset(self):
return self.filter_queryset(
super()
.get_queryset()
.filter(workspace__slug=self.kwargs.get("slug"))
.filter(owner_id=self.request.user.id)
.select_related("workspace", "owner")
.distinct()
)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def create(self, request, slug):
workspace = Workspace.objects.get(slug=slug)
serializer = StickySerializer(data=request.data)
if serializer.is_valid():
serializer.save(workspace_id=workspace.id, owner_id=request.user.id)
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@allow_permission(
allowed_roles=[ROLE.ADMIN, ROLE.MEMBER, ROLE.GUEST], level="WORKSPACE"
)
def list(self, request, slug):
query = request.query_params.get("query", False)
stickies = self.get_queryset()
if query:
stickies = stickies.filter(name__icontains=query)
return self.paginate(
request=request,
queryset=(stickies),
on_results=lambda stickies: StickySerializer(stickies, many=True).data,
default_per_page=20,
)
@allow_permission(allowed_roles=[], creator=True, model=Sticky, level="WORKSPACE")
def partial_update(self, request, *args, **kwargs):
return super().partial_update(request, *args, **kwargs)
@allow_permission(allowed_roles=[], creator=True, model=Sticky, level="WORKSPACE")
def destroy(self, request, *args, **kwargs):
return super().destroy(request, *args, **kwargs)
@@ -0,0 +1,120 @@
# Generated by Django 4.2.17 on 2025-01-02 07:47
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
("db", "0088_sticky_sort_order_workspaceuserlink"),
]
operations = [
migrations.CreateModel(
name="WorkspaceHomePreference",
fields=[
(
"created_at",
models.DateTimeField(auto_now_add=True, verbose_name="Created At"),
),
(
"updated_at",
models.DateTimeField(
auto_now=True, verbose_name="Last Modified At"
),
),
(
"deleted_at",
models.DateTimeField(
blank=True, null=True, verbose_name="Deleted At"
),
),
(
"id",
models.UUIDField(
db_index=True,
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
unique=True,
),
),
("key", models.CharField(max_length=255)),
("is_enabled", models.BooleanField(default=True)),
("config", models.JSONField(default=dict)),
(
"created_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_created_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Created By",
),
),
(
"updated_by",
models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="%(class)s_updated_by",
to=settings.AUTH_USER_MODEL,
verbose_name="Last Modified By",
),
),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="workspace_user_home_preferences",
to=settings.AUTH_USER_MODEL,
),
),
(
"workspace",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="workspace_user_home_preferences",
to="db.workspace",
),
),
],
options={
"verbose_name": "Workspace Home Preference",
"verbose_name_plural": "Workspace Home Preferences",
"db_table": "workspace_home_preferences",
"ordering": ("-created_at",),
},
),
migrations.AddConstraint(
model_name="workspacehomepreference",
constraint=models.UniqueConstraint(
condition=models.Q(("deleted_at__isnull", True)),
fields=("workspace", "user", "key"),
name="workspace_user_home_preferences_unique_workspace_user_key_when_deleted_at_null",
),
),
migrations.AlterUniqueTogether(
name="workspacehomepreference",
unique_together={("workspace", "user", "key", "deleted_at")},
),
migrations.AlterField(
model_name="page",
name="name",
field=models.TextField(blank=True),
),
migrations.AlterField(
model_name="sticky",
name="name",
field=models.TextField(blank=True, null=True),
),
migrations.AddField(
model_name='workspacehomepreference',
name='sort_order',
field=models.PositiveIntegerField(default=65535),
),
]
@@ -0,0 +1,87 @@
# Generated by Django 4.2.17 on 2025-01-09 14:43
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import uuid
class Migration(migrations.Migration):
dependencies = [
('db', '0089_workspacehomepreference_and_more'),
]
operations = [
migrations.RenameModel(
old_name='Dashboard',
new_name='DeprecatedDashboard',
),
migrations.RenameModel(
old_name='DashboardWidget',
new_name='DeprecatedDashboardWidget',
),
migrations.RenameModel(
old_name='Widget',
new_name='DeprecatedWidget',
),
migrations.AlterModelOptions(
name='deprecateddashboard',
options={'ordering': ('-created_at',), 'verbose_name': 'DeprecatedDashboard', 'verbose_name_plural': 'DeprecatedDashboards'},
),
migrations.AlterModelOptions(
name='deprecateddashboardwidget',
options={'ordering': ('-created_at',), 'verbose_name': 'Deprecated Dashboard Widget', 'verbose_name_plural': 'Deprecated Dashboard Widgets'},
),
migrations.AlterModelOptions(
name='deprecatedwidget',
options={'ordering': ('-created_at',), 'verbose_name': 'DeprecatedWidget', 'verbose_name_plural': 'DeprecatedWidgets'},
),
migrations.AlterField(
model_name='workspacehomepreference',
name='sort_order',
field=models.FloatField(default=65535),
),
migrations.AlterModelTable(
name='deprecateddashboard',
table='deprecated_dashboards',
),
migrations.AlterModelTable(
name='deprecateddashboardwidget',
table='deprecated_dashboard_widgets',
),
migrations.AlterModelTable(
name='deprecatedwidget',
table='deprecated_widgets',
),
migrations.CreateModel(
name='WorkspaceUserPreference',
fields=[
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Last Modified At')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='Deleted At')),
('id', models.UUIDField(db_index=True, default=uuid.uuid4, editable=False, primary_key=True, serialize=False, unique=True)),
('key', models.CharField(max_length=255)),
('is_pinned', models.BooleanField(default=False)),
('sort_order', models.FloatField(default=65535)),
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_created_by', to=settings.AUTH_USER_MODEL, verbose_name='Created By')),
('updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='%(class)s_updated_by', to=settings.AUTH_USER_MODEL, verbose_name='Last Modified By')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_user_preferences', to=settings.AUTH_USER_MODEL)),
('workspace', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workspace_user_preferences', to='db.workspace')),
],
options={
'verbose_name': 'Workspace User Preference',
'verbose_name_plural': 'Workspace User Preferences',
'db_table': 'workspace_user_preferences',
'ordering': ('-created_at',),
},
),
migrations.AddConstraint(
model_name='workspaceuserpreference',
constraint=models.UniqueConstraint(condition=models.Q(('deleted_at__isnull', True)), fields=('workspace', 'user', 'key'), name='workspace_user_preferences_unique_workspace_user_key_when_deleted_at_null'),
),
migrations.AlterUniqueTogether(
name='workspaceuserpreference',
unique_together={('workspace', 'user', 'key', 'deleted_at')},
),
]
+2 -1
View File
@@ -3,7 +3,7 @@ from .api import APIActivityLog, APIToken
from .asset import FileAsset
from .base import BaseModel
from .cycle import Cycle, CycleIssue, CycleUserProperties
from .dashboard import Dashboard, DashboardWidget, Widget
from .dashboard import DeprecatedDashboard, DeprecatedDashboardWidget, DeprecatedWidget
from .deploy_board import DeployBoard
from .draft import (
DraftIssue,
@@ -69,6 +69,7 @@ from .workspace import (
WorkspaceTheme,
WorkspaceUserProperties,
WorkspaceUserLink,
WorkspaceHomePreference
)
from .favorite import UserFavorite
+14 -14
View File
@@ -8,7 +8,7 @@ from ..mixins import TimeAuditModel
from .base import BaseModel
class Dashboard(BaseModel):
class DeprecatedDashboard(BaseModel):
DASHBOARD_CHOICES = (
("workspace", "Workspace"),
("project", "Project"),
@@ -36,13 +36,13 @@ class Dashboard(BaseModel):
return f"{self.name}"
class Meta:
verbose_name = "Dashboard"
verbose_name_plural = "Dashboards"
db_table = "dashboards"
verbose_name = "DeprecatedDashboard"
verbose_name_plural = "DeprecatedDashboards"
db_table = "deprecated_dashboards"
ordering = ("-created_at",)
class Widget(TimeAuditModel):
class DeprecatedWidget(TimeAuditModel):
id = models.UUIDField(
default=uuid.uuid4, unique=True, editable=False, db_index=True, primary_key=True
)
@@ -55,18 +55,18 @@ class Widget(TimeAuditModel):
return f"{self.key}"
class Meta:
verbose_name = "Widget"
verbose_name_plural = "Widgets"
db_table = "widgets"
verbose_name = "DeprecatedWidget"
verbose_name_plural = "DeprecatedWidgets"
db_table = "deprecated_widgets"
ordering = ("-created_at",)
class DashboardWidget(BaseModel):
class DeprecatedDashboardWidget(BaseModel):
widget = models.ForeignKey(
Widget, on_delete=models.CASCADE, related_name="dashboard_widgets"
DeprecatedWidget, on_delete=models.CASCADE, related_name="dashboard_widgets"
)
dashboard = models.ForeignKey(
Dashboard, on_delete=models.CASCADE, related_name="dashboard_widgets"
DeprecatedDashboard, on_delete=models.CASCADE, related_name="dashboard_widgets"
)
is_visible = models.BooleanField(default=True)
sort_order = models.FloatField(default=65535)
@@ -86,7 +86,7 @@ class DashboardWidget(BaseModel):
name="dashboard_widget_unique_widget_dashboard_when_deleted_at_null",
)
]
verbose_name = "Dashboard Widget"
verbose_name_plural = "Dashboard Widgets"
db_table = "dashboard_widgets"
verbose_name = "Deprecated Dashboard Widget"
verbose_name_plural = "Deprecated Dashboard Widgets"
db_table = "deprecated_dashboard_widgets"
ordering = ("-created_at",)
+1 -1
View File
@@ -20,7 +20,7 @@ class Page(BaseModel):
workspace = models.ForeignKey(
"db.Workspace", on_delete=models.CASCADE, related_name="pages"
)
name = models.CharField(max_length=255, blank=True)
name = models.TextField(blank=True)
description = models.JSONField(default=dict, blank=True)
description_binary = models.BinaryField(null=True)
description_html = models.TextField(blank=True, default="<p></p>")
+1 -1
View File
@@ -7,7 +7,7 @@ from .base import BaseModel
class Sticky(BaseModel):
name = models.TextField()
name = models.TextField(null=True, blank=True)
description = models.JSONField(blank=True, default=dict)
description_html = models.TextField(blank=True, default="<p></p>")
+84 -1
View File
@@ -1,4 +1,5 @@
# Python imports
from django.db.models.functions import Ln
import pytz
# Django imports
@@ -341,4 +342,86 @@ class WorkspaceUserLink(WorkspaceBaseModel):
ordering = ("-created_at",)
def __str__(self):
return f"{self.workspace.id} {self.url}"
return f"{self.workspace.id} {self.url}"
class WorkspaceHomePreference(BaseModel):
"""Preference for the home page of a workspace for a user"""
class HomeWidgetKeys(models.TextChoices):
QUICK_LINKS = "quick_links", "Quick Links"
RECENTS = "recents", "Recents"
MY_STICKIES = "my_stickies", "My Stickies"
NEW_AT_PLANE = "new_at_plane", "New at Plane"
QUICK_TUTORIAL = "quick_tutorial", "Quick Tutorial"
workspace = models.ForeignKey(
"db.Workspace",
on_delete=models.CASCADE,
related_name="workspace_user_home_preferences",
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="workspace_user_home_preferences",
)
key = models.CharField(max_length=255)
is_enabled = models.BooleanField(default=True)
config = models.JSONField(default=dict)
sort_order = models.FloatField(default=65535)
class Meta:
unique_together = ["workspace", "user", "key", "deleted_at"]
constraints = [
models.UniqueConstraint(
fields=["workspace", "user", "key"],
condition=models.Q(deleted_at__isnull=True),
name="workspace_user_home_preferences_unique_workspace_user_key_when_deleted_at_null",
)
]
verbose_name = "Workspace Home Preference"
verbose_name_plural = "Workspace Home Preferences"
db_table = "workspace_home_preferences"
ordering = ("-created_at",)
def __str__(self):
return f"{self.workspace.name} {self.user.email} {self.key}"
class WorkspaceUserPreference(BaseModel):
"""Preference for the workspace for a user"""
class UserPreferenceKeys(models.TextChoices):
CYCLES = "cycles", "Cycles"
VIEWS = "views", "Views"
ANALYTICS = "analytics", "Analytics"
PROJECTS = "projects", "Projects"
workspace = models.ForeignKey(
"db.Workspace",
on_delete=models.CASCADE,
related_name="workspace_user_preferences",
)
user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.CASCADE,
related_name="workspace_user_preferences",
)
key = models.CharField(max_length=255)
is_pinned = models.BooleanField(default=False)
sort_order = models.FloatField(default=65535)
class Meta:
unique_together = ["workspace", "user", "key", "deleted_at"]
constraints = [
models.UniqueConstraint(
fields=["workspace", "user", "key"],
condition=models.Q(deleted_at__isnull=True),
name="workspace_user_preferences_unique_workspace_user_key_when_deleted_at_null",
)
]
verbose_name = "Workspace User Preference"
verbose_name_plural = "Workspace User Preferences"
db_table = "workspace_user_preferences"
ordering = ("-created_at",)
@@ -132,20 +132,33 @@ class Command(BaseCommand):
"is_encrypted": False,
},
{
"key": "OPENAI_API_KEY",
"value": os.environ.get("OPENAI_API_KEY"),
"category": "OPENAI",
"key": "LLM_API_KEY",
"value": os.environ.get("LLM_API_KEY"),
"category": "AI",
"is_encrypted": True,
},
{
"key": "GPT_ENGINE",
"key": "LLM_PROVIDER",
"value": os.environ.get("LLM_PROVIDER", "openai"),
"category": "AI",
"is_encrypted": False,
},
{
"key": "LLM_MODEL",
"value": os.environ.get("LLM_MODEL", "gpt-4o-mini"),
"category": "AI",
"is_encrypted": False,
},
# Deprecated, use LLM_MODEL
{
"key": "GPT_ENGINE",
"value": os.environ.get("GPT_ENGINE", "gpt-3.5-turbo"),
"category": "SMTP",
"is_encrypted": False,
},
{
"key": "UNSPLASH_ACCESS_KEY",
"value": os.environ.get("UNSPLASH_ACESS_KEY", ""),
"value": os.environ.get("UNSPLASH_ACCESS_KEY", ""),
"category": "UNSPLASH",
"is_encrypted": True,
},
+12
View File
@@ -361,6 +361,18 @@ ATTACHMENT_MIME_TYPES = [
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
"text/plain",
"application/rtf",
"application/vnd.oasis.opendocument.spreadsheet",
"application/vnd.oasis.opendocument.text",
"application/vnd.oasis.opendocument.presentation",
"application/vnd.oasis.opendocument.graphics",
# Microsoft Visio
"application/vnd.visio",
# Netpbm format
"image/x-portable-graymap",
"image/x-portable-bitmap",
"image/x-portable-pixmap",
# Open Office Bae
"application/vnd.oasis.opendocument.database",
# Audio
"audio/mpeg",
"audio/wav",
-10
View File
@@ -130,15 +130,6 @@ class IntakeIssuePublicViewSet(BaseViewSet):
{"error": "Invalid priority"}, status=status.HTTP_400_BAD_REQUEST
)
# Create or get state
state, _ = State.objects.get_or_create(
name="Triage",
group="backlog",
description="Default state for managing all Intake Issues",
project_id=project_deploy_board.project_id,
color="#ff7700",
)
# create an issue
issue = Issue.objects.create(
name=request.data.get("issue", {}).get("name"),
@@ -148,7 +139,6 @@ class IntakeIssuePublicViewSet(BaseViewSet):
),
priority=request.data.get("issue", {}).get("priority", "low"),
project_id=project_deploy_board.project_id,
state=state,
)
# Create an Issue Activity
+10 -2
View File
@@ -3,6 +3,7 @@ from plane.db.models import Project
from datetime import datetime, time
from datetime import timedelta
def user_timezone_converter(queryset, datetime_fields, user_timezone):
# Create a timezone object for the user's timezone
user_tz = pytz.timezone(user_timezone)
@@ -28,7 +29,9 @@ def user_timezone_converter(queryset, datetime_fields, user_timezone):
return queryset_values
def convert_to_utc(date, project_id, is_start_date=False):
def convert_to_utc(
date, project_id, is_start_date=False, is_start_date_end_date_equal=False
):
"""
Converts a start date string to the project's local timezone at 12:00 AM
and then converts it to UTC for storage.
@@ -60,7 +63,12 @@ def convert_to_utc(date, project_id, is_start_date=False):
# If it's an start date, add one minute
if is_start_date:
localized_datetime += timedelta(minutes=1)
localized_datetime += timedelta(minutes=0, seconds=1)
# 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)
# Convert the localized datetime to UTC
utc_datetime = localized_datetime.astimezone(pytz.utc)
+1 -1
View File
@@ -37,7 +37,7 @@ uvicorn==0.29.0
# sockets
channels==4.1.0
# ai
openai==1.25.0
litellm==1.51.0
# slack
slack-sdk==3.27.1
# apm
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:18-alpine
FROM node:20-alpine
RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:18-alpine AS base
FROM node:20-alpine AS base
# The web Dockerfile is copy-pasted into our main docs at /docs/handbook/deploying-with-docker.
# Make sure you update this Dockerfile, the Dockerfile in the web workspace and copy that over to Dockerfile in the docs.
+3 -3
View File
@@ -25,9 +25,9 @@
"@plane/types": "*",
"@sentry/node": "^8.28.0",
"@sentry/profiling-node": "^8.28.0",
"@tiptap/core": "^2.4.0",
"@tiptap/html": "^2.3.0",
"axios": "^1.7.2",
"@tiptap/core": "2.10.4",
"@tiptap/html": "2.11.0",
"axios": "^1.7.9",
"compression": "^1.7.4",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
+44
View File
@@ -0,0 +1,44 @@
// plane editor
import {
getAllDocumentFormatsFromDocumentEditorBinaryData,
getAllDocumentFormatsFromRichTextEditorBinaryData,
getBinaryDataFromDocumentEditorHTMLString,
getBinaryDataFromRichTextEditorHTMLString,
} from "@plane/editor";
// plane types
import { TDocumentPayload } from "@plane/types";
type TArgs = {
document_html: string;
variant: "rich" | "document";
};
export const convertHTMLDocumentToAllFormats = (args: TArgs): TDocumentPayload => {
const { document_html, variant } = args;
let allFormats: TDocumentPayload;
if (variant === "rich") {
const contentBinary = getBinaryDataFromRichTextEditorHTMLString(document_html);
const { contentBinaryEncoded, contentHTML, contentJSON } =
getAllDocumentFormatsFromRichTextEditorBinaryData(contentBinary);
allFormats = {
description: contentJSON,
description_html: contentHTML,
description_binary: contentBinaryEncoded,
};
} else if (variant === "document") {
const contentBinary = getBinaryDataFromDocumentEditorHTMLString(document_html);
const { contentBinaryEncoded, contentHTML, contentJSON } =
getAllDocumentFormatsFromDocumentEditorBinaryData(contentBinary);
allFormats = {
description: contentJSON,
description_html: contentHTML,
description_binary: contentBinaryEncoded,
};
} else {
throw new Error(`Invalid variant provided: ${variant}`);
}
return allFormats;
};
+5
View File
@@ -6,3 +6,8 @@ export type TDocumentTypes = "project_page" | TAdditionalDocumentTypes;
export type HocusPocusServerContext = {
cookie: string;
};
export type TConvertDocumentRequestBody = {
description_html: string;
variant: "rich" | "document";
};
+36 -14
View File
@@ -1,20 +1,19 @@
import "@/core/config/sentry-config.js";
import express from "express";
import expressWs from "express-ws";
import * as Sentry from "@sentry/node";
import compression from "compression";
import helmet from "helmet";
// cors
import cors from "cors";
// core hocuspocus server
import expressWs from "express-ws";
import express from "express";
import helmet from "helmet";
// config
import "@/core/config/sentry-config.js";
// hocuspocus server
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
// helpers
import { convertHTMLDocumentToAllFormats } from "@/core/helpers/convert-document.js";
import { logger, manualLogger } from "@/core/helpers/logger.js";
import { errorHandler } from "@/core/helpers/error-handler.js";
// types
import { TConvertDocumentRequestBody } from "@/core/types/common.js";
const app = express();
expressWs(app);
@@ -29,7 +28,7 @@ app.use(
compression({
level: 6,
threshold: 5 * 1000,
}),
})
);
// Logging middleware
@@ -62,6 +61,31 @@ router.ws("/collaboration", (ws, req) => {
}
});
router.post("/convert-document", (req, res) => {
const { description_html, variant } = req.body as TConvertDocumentRequestBody;
try {
if (description_html === undefined || variant === undefined) {
res.status(400).send({
message: "Missing required fields",
});
return;
}
const { description, description_binary } = convertHTMLDocumentToAllFormats({
document_html: description_html,
variant,
});
res.status(200).json({
description,
description_binary,
});
} catch (error) {
manualLogger.error("Error in /convert-document endpoint:", error);
res.status(500).send({
message: `Internal server error. ${error}`,
});
}
});
app.use(process.env.LIVE_BASE_PATH || "/live", router);
app.use((_req, res) => {
@@ -82,9 +106,7 @@ const gracefulShutdown = async () => {
try {
// Close the HocusPocus server WebSocket connections
await HocusPocusServer.destroy();
manualLogger.info(
"HocusPocus server WebSocket connections closed gracefully.",
);
manualLogger.info("HocusPocus server WebSocket connections closed gracefully.");
// Close the Express server
liveServer.close(() => {
+1
View File
@@ -0,0 +1 @@
export const SIDEBAR_CLICKED = "Sidenav clicked";
+1
View File
@@ -1,6 +1,7 @@
export * from "./ai";
export * from "./auth";
export * from "./endpoints";
export * from "./event";
export * from "./file";
export * from "./instance";
export * from "./issue";
+17
View File
@@ -19,3 +19,20 @@ export type TUserStatus = {
status: EUserStatus | undefined;
message?: string;
};
export enum EUserPermissionsLevel {
WORKSPACE = "WORKSPACE",
PROJECT = "PROJECT",
}
export enum EUserWorkspaceRoles {
ADMIN = 20,
MEMBER = 15,
GUEST = 5,
}
export enum EUserProjectRoles {
ADMIN = 20,
MEMBER = 15,
GUEST = 5,
}
+1 -1
View File
@@ -1,5 +1,5 @@
export const ORGANIZATION_SIZE = [
"Just myself",
"Just myself", // TODO: translate
"2-10",
"11-50",
"51-200",
+18 -18
View File
@@ -40,23 +40,24 @@
"@plane/types": "*",
"@plane/ui": "*",
"@plane/utils": "*",
"@tiptap/core": "^2.1.13",
"@tiptap/extension-blockquote": "^2.1.13",
"@tiptap/extension-character-count": "^2.6.5",
"@tiptap/extension-collaboration": "^2.3.2",
"@tiptap/extension-image": "^2.1.13",
"@tiptap/extension-list-item": "^2.1.13",
"@tiptap/extension-mention": "^2.1.13",
"@tiptap/extension-placeholder": "^2.3.0",
"@tiptap/extension-task-item": "^2.1.13",
"@tiptap/extension-task-list": "^2.1.13",
"@tiptap/extension-text-align": "^2.8.0",
"@tiptap/extension-text-style": "^2.7.1",
"@tiptap/extension-underline": "^2.1.13",
"@tiptap/pm": "^2.1.13",
"@tiptap/react": "^2.1.13",
"@tiptap/starter-kit": "^2.1.13",
"@tiptap/suggestion": "^2.0.13",
"@tiptap/core": "2.10.4",
"@tiptap/extension-blockquote": "2.10.4",
"@tiptap/extension-character-count": "2.11.0",
"@tiptap/extension-collaboration": "2.11.0",
"@tiptap/extension-image": "2.11.0",
"@tiptap/extension-list-item": "2.11.0",
"@tiptap/extension-mention": "2.11.0",
"@tiptap/extension-placeholder": "2.11.0",
"@tiptap/extension-task-item": "2.11.0",
"@tiptap/extension-task-list": "2.11.0",
"@tiptap/extension-text-align": "2.11.0",
"@tiptap/extension-text-style": "2.11.0",
"@tiptap/extension-underline": "2.11.0",
"@tiptap/html": "2.11.0",
"@tiptap/pm": "2.11.0",
"@tiptap/react": "2.11.0",
"@tiptap/starter-kit": "2.11.0",
"@tiptap/suggestion": "2.11.0",
"class-variance-authority": "^0.7.0",
"highlight.js": "^11.8.0",
"jsx-dom-cjs": "^8.0.3",
@@ -65,7 +66,6 @@
"lucide-react": "^0.378.0",
"prosemirror-codemark": "^0.4.2",
"prosemirror-utils": "^1.2.2",
"react-moveable": "^0.54.2",
"tippy.js": "^6.3.7",
"tiptap-markdown": "^0.8.9",
"uuid": "^10.0.0",
@@ -71,7 +71,7 @@ export const BubbleMenuColorSelector: FC<Props> = (props) => {
<button
type="button"
className="flex-shrink-0 size-6 grid place-items-center rounded text-custom-text-300 border-[0.5px] border-custom-border-400 hover:bg-custom-background-80 transition-colors"
onClick={() => TextColorItem(editor).command(undefined)}
onClick={() => TextColorItem(editor).command({ color: undefined })}
>
<Ban className="size-4" />
</button>
@@ -94,7 +94,7 @@ export const BubbleMenuColorSelector: FC<Props> = (props) => {
<button
type="button"
className="flex-shrink-0 size-6 grid place-items-center rounded text-custom-text-300 border-[0.5px] border-custom-border-400 hover:bg-custom-background-80 transition-colors"
onClick={() => BackgroundColorItem(editor).command(undefined)}
onClick={() => BackgroundColorItem(editor).command({ color: undefined })}
>
<Ban className="size-4" />
</button>
@@ -87,12 +87,9 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
}, []);
return (
<BubbleMenu
{...bubbleMenuProps}
className="flex py-2 divide-x divide-custom-border-200 rounded-lg border border-custom-border-200 bg-custom-background-100 shadow-custom-shadow-rg"
>
<BubbleMenu {...bubbleMenuProps}>
{!isSelecting && (
<>
<div className="flex py-2 divide-x divide-custom-border-200 rounded-lg border border-custom-border-200 bg-custom-background-100 shadow-custom-shadow-rg">
<div className="px-2">
{!props.editor.isActive("table") && (
<BubbleMenuNodeSelector
@@ -161,7 +158,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
editor.commands.setTextSelection(pos ?? 0);
}}
/>
</>
</div>
)}
</BubbleMenu>
);
@@ -3,4 +3,6 @@ export const DocumentCollaborativeEvents = {
unlock: { client: "unlocked", server: "unlock" },
archive: { client: "archived", server: "archive" },
unarchive: { client: "unarchived", server: "unarchive" },
"make-public": { client: "made-public", server: "make-public" },
"make-private": { client: "made-private", server: "make-private" },
} as const;
@@ -51,6 +51,7 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
const { disabledExtensions, enableHistory, fileHandler, mentionHandler, placeholder, tabIndex } = args;
return [
// @ts-expect-error tiptap types are incorrect
StarterKit.configure({
bulletList: {
HTMLAttributes: {
@@ -42,6 +42,7 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
const { disabledExtensions, fileHandler, mentionHandler } = props;
return [
// @ts-expect-error tiptap types are incorrect
StarterKit.configure({
bulletList: {
HTMLAttributes: {
@@ -13,41 +13,51 @@ export const setText = (editor: Editor, range?: Range) => {
export const toggleHeadingOne = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
// @ts-expect-error tiptap types are incorrect
else editor.chain().focus().toggleHeading({ level: 1 }).run();
};
export const toggleHeadingTwo = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
// @ts-expect-error tiptap types are incorrect
else editor.chain().focus().toggleHeading({ level: 2 }).run();
};
export const toggleHeadingThree = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
// @ts-expect-error tiptap types are incorrect
else editor.chain().focus().toggleHeading({ level: 3 }).run();
};
export const toggleHeadingFour = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 4 }).run();
// @ts-expect-error tiptap types are incorrect
else editor.chain().focus().toggleHeading({ level: 4 }).run();
};
export const toggleHeadingFive = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 5 }).run();
// @ts-expect-error tiptap types are incorrect
else editor.chain().focus().toggleHeading({ level: 5 }).run();
};
export const toggleHeadingSix = (editor: Editor, range?: Range) => {
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 6 }).run();
// @ts-expect-error tiptap types are incorrect
else editor.chain().focus().toggleHeading({ level: 6 }).run();
};
export const toggleBold = (editor: Editor, range?: Range) => {
// @ts-expect-error tiptap types are incorrect
if (range) editor.chain().focus().deleteRange(range).toggleBold().run();
// @ts-expect-error tiptap types are incorrect
else editor.chain().focus().toggleBold().run();
};
export const toggleItalic = (editor: Editor, range?: Range) => {
// @ts-expect-error tiptap types are incorrect
if (range) editor.chain().focus().deleteRange(range).toggleItalic().run();
// @ts-expect-error tiptap types are incorrect
else editor.chain().focus().toggleItalic().run();
};
@@ -86,12 +96,16 @@ export const toggleCodeBlock = (editor: Editor, range?: Range) => {
};
export const toggleOrderedList = (editor: Editor, range?: Range) => {
// @ts-expect-error tiptap types are incorrect
if (range) editor.chain().focus().deleteRange(range).toggleOrderedList().run();
// @ts-expect-error tiptap types are incorrect
else editor.chain().focus().toggleOrderedList().run();
};
export const toggleBulletList = (editor: Editor, range?: Range) => {
// @ts-expect-error tiptap types are incorrect
if (range) editor.chain().focus().deleteRange(range).toggleBulletList().run();
// @ts-expect-error tiptap types are incorrect
else editor.chain().focus().toggleBulletList().run();
};
@@ -101,7 +115,9 @@ export const toggleTaskList = (editor: Editor, range?: Range) => {
};
export const toggleStrike = (editor: Editor, range?: Range) => {
// @ts-expect-error tiptap types are incorrect
if (range) editor.chain().focus().deleteRange(range).toggleStrike().run();
// @ts-expect-error tiptap types are incorrect
else editor.chain().focus().toggleStrike().run();
};
@@ -0,0 +1,142 @@
import { getSchema } from "@tiptap/core";
import { generateHTML, generateJSON } from "@tiptap/html";
import { prosemirrorJSONToYDoc, yXmlFragmentToProseMirrorRootNode } from "y-prosemirror";
import * as Y from "yjs";
// extensions
import {
CoreEditorExtensionsWithoutProps,
DocumentEditorExtensionsWithoutProps,
} from "@/extensions/core-without-props";
// editor extension configs
const RICH_TEXT_EDITOR_EXTENSIONS = CoreEditorExtensionsWithoutProps;
const DOCUMENT_EDITOR_EXTENSIONS = [...CoreEditorExtensionsWithoutProps, ...DocumentEditorExtensionsWithoutProps];
// editor schemas
// @ts-expect-error tiptap types are incorrect
const richTextEditorSchema = getSchema(RICH_TEXT_EDITOR_EXTENSIONS);
// @ts-expect-error tiptap types are incorrect
const documentEditorSchema = getSchema(DOCUMENT_EDITOR_EXTENSIONS);
/**
* @description apply updates to a doc and return the updated doc in binary format
* @param {Uint8Array} document
* @param {Uint8Array} updates
* @returns {Uint8Array}
*/
export const applyUpdates = (document: Uint8Array, updates?: Uint8Array): Uint8Array => {
const yDoc = new Y.Doc();
Y.applyUpdate(yDoc, document);
if (updates) {
Y.applyUpdate(yDoc, updates);
}
const encodedDoc = Y.encodeStateAsUpdate(yDoc);
return encodedDoc;
};
/**
* @description this function encodes binary data to base64 string
* @param {Uint8Array} document
* @returns {string}
*/
export const convertBinaryDataToBase64String = (document: Uint8Array): string =>
Buffer.from(document).toString("base64");
/**
* @description this function decodes base64 string to binary data
* @param {string} document
* @returns {ArrayBuffer}
*/
export const convertBase64StringToBinaryData = (document: string): ArrayBuffer => Buffer.from(document, "base64");
/**
* @description this function generates the binary equivalent of html content for the rich text editor
* @param {string} descriptionHTML
* @returns {Uint8Array}
*/
export const getBinaryDataFromRichTextEditorHTMLString = (descriptionHTML: string): Uint8Array => {
// convert HTML to JSON
// @ts-expect-error tiptap types are incorrect
const contentJSON = generateJSON(descriptionHTML ?? "<p></p>", RICH_TEXT_EDITOR_EXTENSIONS);
// convert JSON to Y.Doc format
const transformedData = prosemirrorJSONToYDoc(richTextEditorSchema, contentJSON, "default");
// convert Y.Doc to Uint8Array format
const encodedData = Y.encodeStateAsUpdate(transformedData);
return encodedData;
};
/**
* @description this function generates the binary equivalent of html content for the document editor
* @param {string} descriptionHTML
* @returns {Uint8Array}
*/
export const getBinaryDataFromDocumentEditorHTMLString = (descriptionHTML: string): Uint8Array => {
// convert HTML to JSON
// @ts-expect-error tiptap types are incorrect
const contentJSON = generateJSON(descriptionHTML ?? "<p></p>", DOCUMENT_EDITOR_EXTENSIONS);
// convert JSON to Y.Doc format
const transformedData = prosemirrorJSONToYDoc(documentEditorSchema, contentJSON, "default");
// convert Y.Doc to Uint8Array format
const encodedData = Y.encodeStateAsUpdate(transformedData);
return encodedData;
};
/**
* @description this function generates all document formats for the provided binary data for the rich text editor
* @param {Uint8Array} description
* @returns
*/
export const getAllDocumentFormatsFromRichTextEditorBinaryData = (
description: Uint8Array
): {
contentBinaryEncoded: string;
contentJSON: object;
contentHTML: string;
} => {
// encode binary description data
const base64Data = convertBinaryDataToBase64String(description);
const yDoc = new Y.Doc();
Y.applyUpdate(yDoc, description);
// convert to JSON
const type = yDoc.getXmlFragment("default");
const contentJSON = yXmlFragmentToProseMirrorRootNode(type, richTextEditorSchema).toJSON();
// convert to HTML
// @ts-expect-error tiptap types are incorrect
const contentHTML = generateHTML(contentJSON, RICH_TEXT_EDITOR_EXTENSIONS);
return {
contentBinaryEncoded: base64Data,
contentJSON,
contentHTML,
};
};
/**
* @description this function generates all document formats for the provided binary data for the document editor
* @param {Uint8Array} description
* @returns
*/
export const getAllDocumentFormatsFromDocumentEditorBinaryData = (
description: Uint8Array
): {
contentBinaryEncoded: string;
contentJSON: object;
contentHTML: string;
} => {
// encode binary description data
const base64Data = convertBinaryDataToBase64String(description);
const yDoc = new Y.Doc();
Y.applyUpdate(yDoc, description);
// convert to JSON
const type = yDoc.getXmlFragment("default");
const contentJSON = yXmlFragmentToProseMirrorRootNode(type, documentEditorSchema).toJSON();
// convert to HTML
// @ts-expect-error tiptap types are incorrect
const contentHTML = generateHTML(contentJSON, DOCUMENT_EDITOR_EXTENSIONS);
return {
contentBinaryEncoded: base64Data,
contentJSON,
contentHTML,
};
};
-16
View File
@@ -1,16 +0,0 @@
import * as Y from "yjs";
/**
* @description apply updates to a doc and return the updated doc in base64(binary) format
* @param {Uint8Array} document
* @param {Uint8Array} updates
* @returns {string} base64(binary) form of the updated doc
*/
export const applyUpdates = (document: Uint8Array, updates: Uint8Array): Uint8Array => {
const yDoc = new Y.Doc();
Y.applyUpdate(yDoc, document);
Y.applyUpdate(yDoc, updates);
const encodedDoc = Y.encodeStateAsUpdate(yDoc);
return encodedDoc;
};
+1 -1
View File
@@ -24,7 +24,7 @@ export * from "@/constants/common";
// helpers
export * from "@/helpers/common";
export * from "@/helpers/editor-commands";
export * from "@/helpers/yjs";
export * from "@/helpers/yjs-utils";
export * from "@/extensions/table/table";
// components
+1
View File
@@ -1,4 +1,5 @@
export * from "@/extensions/core-without-props";
export * from "@/constants/document-collaborative-events";
export * from "@/helpers/get-document-server-event";
export * from "@/helpers/yjs-utils";
export * from "@/types/document-collaborative-events";
+3 -3
View File
@@ -119,13 +119,13 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
pointer-events: none;
}
ul[data-type="taskList"] li > label input[type="checkbox"][checked] {
ul[data-type="taskList"] li > label input[type="checkbox"]:checked {
background-color: rgba(var(--color-primary-100)) !important;
border-color: rgba(var(--color-primary-100)) !important;
color: white !important;
}
ul[data-type="taskList"] li > label input[type="checkbox"][checked]:hover {
ul[data-type="taskList"] li > label input[type="checkbox"]:checked:hover {
background-color: rgba(var(--color-primary-300)) !important;
border-color: rgba(var(--color-primary-300)) !important;
}
@@ -174,7 +174,7 @@ ul[data-type="taskList"] li > label input[type="checkbox"] {
clip-path: polygon(14% 44%, 0 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}
&[checked]::before {
&:checked::before {
transform: scale(1) translate(-50%, -50%);
}
}
+1
View File
@@ -1,2 +1,3 @@
export * from "./use-local-storage";
export * from "./use-outside-click-detector";
export * from "./use-platform-os";
+34
View File
@@ -0,0 +1,34 @@
import { useState, useEffect } from "react";
export const usePlatformOS = () => {
const [platformData, setPlatformData] = useState({
isMobile: false,
platform: "",
});
useEffect(() => {
const detectPlatform = () => {
const userAgent = window.navigator.userAgent;
const isMobile = /iPhone|iPad|iPod|Android/i.test(userAgent);
let platform = "";
if (!isMobile) {
if (userAgent.indexOf("Win") !== -1) {
platform = "Windows";
} else if (userAgent.indexOf("Mac") !== -1) {
platform = "MacOS";
} else if (userAgent.indexOf("Linux") !== -1) {
platform = "Linux";
} else {
platform = "Unknown";
}
}
setPlatformData({ isMobile, platform });
};
detectPlatform();
}, []);
return platformData;
};
+3
View File
@@ -0,0 +1,3 @@
build/*
dist/*
out/*
+9
View File
@@ -0,0 +1,9 @@
/** @type {import("eslint").Linter.Config} */
module.exports = {
root: true,
extends: ["@plane/eslint-config/library.js"],
parser: "@typescript-eslint/parser",
parserOptions: {
project: true,
},
};
+4
View File
@@ -0,0 +1,4 @@
.turbo
out/
dist/
build/
+5
View File
@@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}
+20
View File
@@ -0,0 +1,20 @@
{
"name": "@plane/i18n",
"version": "0.24.1",
"description": "I18n shared across multiple apps internally",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"lint": "eslint src --ext .ts,.tsx",
"lint:errors": "eslint src --ext .ts,.tsx --quiet"
},
"dependencies": {
"@plane/utils": "*"
},
"devDependencies": {
"@plane/eslint-config": "*",
"@types/node": "^22.5.4",
"typescript": "^5.3.3"
}
}
+29
View File
@@ -0,0 +1,29 @@
import { observer } from "mobx-react";
import React, { createContext, useEffect } from "react";
import { Language, languages } from "../config";
import { TranslationStore } from "./store";
// Create the store instance
const translationStore = new TranslationStore();
// Create Context
export const TranslationContext = createContext<TranslationStore>(translationStore);
export const TranslationProvider = observer(({ children }: { children: React.ReactNode }) => {
// Handle storage events for cross-tab synchronization
useEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.key === "userLanguage" && event.newValue) {
const newLang = event.newValue as Language;
if (languages.includes(newLang)) {
translationStore.setLanguage(newLang);
}
}
};
window.addEventListener("storage", handleStorageChange);
return () => window.removeEventListener("storage", handleStorageChange);
}, []);
return <TranslationContext.Provider value={translationStore}>{children}</TranslationContext.Provider>;
});
+42
View File
@@ -0,0 +1,42 @@
import { makeObservable, observable } from "mobx";
import { Language, fallbackLng, languages, translations } from "../config";
export class TranslationStore {
currentLocale: Language = fallbackLng;
constructor() {
makeObservable(this, {
currentLocale: observable.ref,
});
this.initializeLanguage();
}
get availableLanguages() {
return languages;
}
t(key: string) {
return translations[this.currentLocale]?.[key] || translations[fallbackLng][key] || key;
}
setLanguage(lng: Language) {
try {
localStorage.setItem("userLanguage", lng);
this.currentLocale = lng;
} catch (error) {
console.error(error);
}
}
initializeLanguage() {
if (typeof window === "undefined") return;
const savedLocale = localStorage.getItem("userLanguage") as Language;
if (savedLocale && languages.includes(savedLocale)) {
this.setLanguage(savedLocale);
} else {
const browserLang = navigator.language.split("-")[0] as Language;
const newLocale = languages.includes(browserLang as Language) ? (browserLang as Language) : fallbackLng;
this.setLanguage(newLocale);
}
}
}
+39
View File
@@ -0,0 +1,39 @@
import en from "../locales/en/translations.json";
import fr from "../locales/fr/translations.json";
import es from "../locales/es/translations.json";
import ja from "../locales/ja/translations.json";
export type Language = (typeof languages)[number];
export type Translations = {
[key: string]: {
[key: string]: string;
};
};
export const fallbackLng = "en";
export const languages = ["en", "fr", "es", "ja"] as const;
export const translations: Translations = {
en,
fr,
es,
ja,
};
export const SUPPORTED_LANGUAGES = [
{
label: "English",
value: "en",
},
{
label: "French",
value: "fr",
},
{
label: "Spanish",
value: "es",
},
{
label: "Japanese",
value: "ja",
},
];
+1
View File
@@ -0,0 +1 @@
export * from "./use-translation";
@@ -0,0 +1,17 @@
import { useContext } from "react";
import { TranslationContext } from "../components";
import { Language } from "../config";
export function useTranslation() {
const store = useContext(TranslationContext);
if (!store) {
throw new Error("useTranslation must be used within a TranslationProvider");
}
return {
t: (key: string) => store.t(key),
currentLocale: store.currentLocale,
changeLanguage: (lng: Language) => store.setLanguage(lng),
languages: store.availableLanguages,
};
}
+3
View File
@@ -0,0 +1,3 @@
export * from "./config";
export * from "./components";
export * from "./hooks";
@@ -0,0 +1,320 @@
{
"submit": "Submit",
"cancel": "Cancel",
"loading": "Loading",
"error": "Error",
"success": "Success",
"warning": "Warning",
"info": "Info",
"close": "Close",
"yes": "Yes",
"no": "No",
"ok": "OK",
"name": "Name",
"description": "Description",
"search": "Search",
"add_member": "Add member",
"remove_member": "Remove member",
"add_members": "Add members",
"remove_members": "Remove members",
"add": "Add",
"remove": "Remove",
"add_new": "Add new",
"remove_selected": "Remove selected",
"first_name": "First name",
"last_name": "Last name",
"email": "Email",
"display_name": "Display name",
"role": "Role",
"timezone": "Timezone",
"avatar": "Avatar",
"cover_image": "Cover image",
"password": "Password",
"change_cover": "Change cover",
"language": "Language",
"saving": "Saving...",
"save_changes": "Save changes",
"deactivate_account": "Deactivate account",
"deactivate_account_description": "When deactivating an account, all of the data and resources within that account will be permanently removed and cannot be recovered.",
"profile_settings": "Profile settings",
"your_account": "Your account",
"profile": "Profile",
"security": "Security",
"activity": "Activity",
"appearance": "Appearance",
"notifications": "Notifications",
"inbox": "Inbox",
"workspaces": "Workspaces",
"create_workspace": "Create workspace",
"invitations": "Invitations",
"summary": "Summary",
"assigned": "Assigned",
"created": "Created",
"subscribed": "Subscribed",
"you_do_not_have_the_permission_to_access_this_page": "You do not have the permission to access this page.",
"failed_to_sign_out_please_try_again": "Failed to sign out. Please try again.",
"password_changed_successfully": "Password changed successfully.",
"something_went_wrong_please_try_again": "Something went wrong. Please try again.",
"change_password": "Change password",
"passwords_dont_match": "Passwords don't match",
"current_password": "Current password",
"new_password": "New password",
"confirm_password": "Confirm password",
"this_field_is_required": "This field is required",
"changing_password": "Changing password",
"please_enter_your_password": "Please enter your password.",
"password_length_should_me_more_than_8_characters": "Password length should me more than 8 characters.",
"password_is_weak": "Password is weak.",
"password_is_strong": "Password is strong.",
"load_more": "Load more",
"select_or_customize_your_interface_color_scheme": "Select or customize your interface color scheme.",
"theme": "Theme",
"system_preference": "System preference",
"light": "Light",
"dark": "Dark",
"light_contrast": "Light high contrast",
"dark_contrast": "Dark high contrast",
"custom": "Custom theme",
"select_your_theme": "Select your theme",
"customize_your_theme": "Customize your theme",
"background_color": "Background color",
"text_color": "Text color",
"primary_color": "Primary(Theme) color",
"sidebar_background_color": "Sidebar background color",
"sidebar_text_color": "Sidebar text color",
"set_theme": "Set theme",
"enter_a_valid_hex_code_of_6_characters": "Enter a valid hex code of 6 characters",
"background_color_is_required": "Background color is required",
"text_color_is_required": "Text color is required",
"primary_color_is_required": "Primary color is required",
"sidebar_background_color_is_required": "Sidebar background color is required",
"sidebar_text_color_is_required": "Sidebar text color is required",
"updating_theme": "Updating theme",
"theme_updated_successfully": "Theme updated successfully",
"failed_to_update_the_theme": "Failed to update the theme",
"email_notifications": "Email notifications",
"stay_in_the_loop_on_issues_you_are_subscribed_to_enable_this_to_get_notified": "Stay in the loop on Issues you are subscribed to. Enable this to get notified.",
"email_notification_setting_updated_successfully": "Email notification setting updated successfully",
"failed_to_update_email_notification_setting": "Failed to update email notification setting",
"notify_me_when": "Notify me when",
"property_changes": "Property changes",
"property_changes_description": "Notify me when issue's properties like assignees, priority, estimates or anything else changes.",
"state_change": "State change",
"state_change_description": "Notify me when the issues moves to a different state",
"issue_completed": "Issue completed",
"issue_completed_description": "Notify me only when an issue is completed",
"comments": "Comments",
"comments_description": "Notify me when someone leaves a comment on the issue",
"mentions": "Mentions",
"mentions_description": "Notify me only when someone mentions me in the comments or description",
"create_your_workspace": "Create your workspace",
"only_your_instance_admin_can_create_workspaces": "Only your instance admin can create workspaces",
"only_your_instance_admin_can_create_workspaces_description": "If you know your instance admin's email address, click the button below to get in touch with them.",
"go_back": "Go back",
"request_instance_admin": "Request instance admin",
"plane_logo": "Plane logo",
"workspace_creation_disabled": "Workspace creation disabled",
"workspace_request_subject": "Requesting a new workspace",
"workspace_request_body": "Hi instance admin(s),\n\nPlease create a new workspace with the URL [/workspace-name] for [purpose of creating the workspace].\n\nThanks,\n{{firstName}} {{lastName}}\n{{email}}",
"creating_workspace": "Creating workspace",
"workspace_created_successfully": "Workspace created successfully",
"create_workspace_page": "Create workspace page",
"workspace_could_not_be_created_please_try_again": "Workspace could not be created. Please try again.",
"workspace_could_not_be_created_please_try_again_description": "Some error occurred while creating workspace. Please try again.",
"this_is_a_required_field": "This is a required field.",
"name_your_workspace": "Name your workspace",
"workspaces_names_can_contain_only_space_dash_and_alphanumeric_characters": "Workspaces names can contain only (' '), ('-'), ('_') and alphanumeric characters.",
"limit_your_name_to_80_characters": "Limit your name to 80 characters.",
"set_your_workspace_url": "Set your workspace's URL",
"limit_your_url_to_48_characters": "Limit your URL to 48 characters.",
"how_many_people_will_use_this_workspace": "How many people will use this workspace?",
"how_many_people_will_use_this_workspace_description": "This will help us to determine the number of seats you need to purchase.",
"select_a_range": "Select a range",
"urls_can_contain_only_dash_and_alphanumeric_characters": "URLs can contain only ('-') and alphanumeric characters.",
"something_familiar_and_recognizable_is_always_best": "Something familiar and recognizable is always best.",
"workspace_url_is_already_taken": "Workspace URL is already taken!",
"old_password": "Old password",
"general_settings": "General settings",
"sign_out": "Sign out",
"signing_out": "Signing out",
"active_cycles": "Active cycles",
"active_cycles_description": "Monitor cycles across projects, track high-priority issues, and zoom in cycles that need attention.",
"on_demand_snapshots_of_all_your_cycles": "On-demand snapshots of all your cycles",
"upgrade": "Upgrade",
"10000_feet_view": "10,000-feet view of all active cycles.",
"10000_feet_view_description": "Zoom out to see running cycles across all your projects at once instead of going from Cycle to Cycle in each project.",
"get_snapshot_of_each_active_cycle": "Get a snapshot of each active cycle.",
"get_snapshot_of_each_active_cycle_description": "Track high-level metrics for all active cycles, see their state of progress, and get a sense of scope against deadlines.",
"compare_burndowns": "Compare burndowns.",
"compare_burndowns_description": "Monitor how each of your teams are performing with a peek into each cycle's burndown report.",
"quickly_see_make_or_break_issues": "Quickly see make-or-break issues.",
"quickly_see_make_or_break_issues_description": "Preview high-priority issues for each cycle against due dates. See all of them per cycle in one click.",
"zoom_into_cycles_that_need_attention": "Zoom into cycles that need attention.",
"zoom_into_cycles_that_need_attention_description": "Investigate the state of any cycle that doesn't conform to expectations in one click.",
"stay_ahead_of_blockers": "Stay ahead of blockers.",
"stay_ahead_of_blockers_description": "Spot challenges from one project to another and see inter-cycle dependencies that aren't obvious from any other view.",
"analytics": "Analytics",
"workspace_invites": "Workspace invites",
"workspace_settings": "Workspace settings",
"enter_god_mode": "Enter god mode",
"workspace_logo": "Workspace logo",
"new_issue": "New issue",
"home": "Home",
"your_work": "Your work",
"drafts": "Drafts",
"projects": "Projects",
"views": "Views",
"workspace": "Workspace",
"archives": "Archives",
"settings": "Settings",
"failed_to_move_favorite": "Failed to move favorite",
"your_favorites": "Your favorites",
"no_favorites_yet": "No favorites yet",
"create_folder": "Create folder",
"new_folder": "New folder",
"favorite_updated_successfully": "Favorite updated successfully",
"favorite_created_successfully": "Favorite created successfully",
"folder_already_exists": "Folder already exists",
"folder_name_cannot_be_empty": "Folder name cannot be empty",
"something_went_wrong": "Something went wrong",
"failed_to_reorder_favorite": "Failed to reorder favorite",
"favorite_removed_successfully": "Favorite removed successfully",
"failed_to_create_favorite": "Failed to create favorite",
"failed_to_rename_favorite": "Failed to rename favorite",
"project_link_copied_to_clipboard": "Project link copied to clipboard",
"link_copied": "Link copied",
"your_projects": "Your projects",
"add_project": "Add project",
"create_project": "Create project",
"failed_to_remove_project_from_favorites": "Couldn't remove the project from favorites. Please try again.",
"project_created_successfully": "Project created successfully",
"project_created_successfully_description": "Project created successfully. You can now start adding issues to it.",
"project_cover_image_alt": "Project cover image",
"name_is_required": "Name is required",
"title_should_be_less_than_255_characters": "Title should be less than 255 characters",
"project_name": "Project name",
"project_id_must_be_at_least_1_character": "Project ID must at least be of 1 character",
"project_id_must_be_at_most_5_characters": "Project ID must at most be of 5 characters",
"project_id": "Project ID",
"project_id_tooltip_content": "Helps you identify issues in the project uniquely. Max 5 characters.",
"description_placeholder": "Description...",
"only_alphanumeric_non_latin_characters_allowed": "Only Alphanumeric & Non-latin characters are allowed.",
"project_id_is_required": "Project ID is required",
"select_network": "Select network",
"lead": "Lead",
"private": "Private",
"public": "Public",
"accessible_only_by_invite": "Accessible only by invite",
"anyone_in_the_workspace_except_guests_can_join": "Anyone in the workspace except Guests can join",
"creating": "Creating",
"creating_project": "Creating project",
"adding_project_to_favorites": "Adding project to favorites",
"project_added_to_favorites": "Project added to favorites",
"couldnt_add_the_project_to_favorites": "Couldn't add the project to favorites. Please try again.",
"removing_project_from_favorites": "Removing project from favorites",
"project_removed_from_favorites": "Project removed from favorites",
"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": "Publish",
"copy_link": "Copy link",
"leave_project": "Leave project",
"join_the_project_to_rearrange": "Join the project to rearrange",
"drag_to_rearrange": "Drag to rearrange",
"congrats": "Congrats!",
"project": "Project",
"open_project": "Open project",
"issues": "Issues",
"cycles": "Cycles",
"modules": "Modules",
"pages": "Pages",
"intake": "Intake",
"time_tracking": "Time Tracking",
"work_management": "Work management",
"projects_and_issues": "Projects and issues",
"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 Issues you are subscribed to. Enable this to get notified.",
"time_tracking_description": "Track time spent on issues and projects.",
"work_management_description": "Manage your work and projects with ease.",
"documentation": "Documentation",
"message_support": "Message support",
"contact_sales": "Contact sales",
"hyper_mode": "Hyper Mode",
"keyboard_shortcuts": "Keyboard shortcuts",
"whats_new": "What's new?",
"version": "Version",
"we_are_having_trouble_fetching_the_updates": "We are having trouble fetching the updates.",
"our_changelogs": "our changelogs",
"for_the_latest_updates": "for the latest updates.",
"please_visit": "Please visit",
"docs": "Docs",
"full_changelog": "Full changelog",
"support": "Support",
"discord": "Discord",
"powered_by_plane_pages": "Powered by Plane Pages",
"please_select_at_least_one_invitation": "Please select at least one invitation.",
"please_select_at_least_one_invitation_description": "Please select at least one invitation to join the workspace.",
"we_see_that_someone_has_invited_you_to_join_a_workspace": "We see that someone has invited you to join a workspace",
"join_a_workspace": "Join a workspace",
"we_see_that_someone_has_invited_you_to_join_a_workspace_description": "We see that someone has invited you to join a workspace",
"join_a_workspace_description": "Join a workspace",
"accept_and_join": "Accept & Join",
"go_home": "Go Home",
"no_pending_invites": "No pending invites",
"you_can_see_here_if_someone_invites_you_to_a_workspace": "You can see here if someone invites you to a workspace",
"back_to_home": "Back to home",
"workspace_name": "workspace-name",
"deactivate_your_account": "Deactivate your account",
"deactivate_your_account_description": "Once deactivated, you can't be assigned issues and be billed for your workspace. To reactivate your account, you will need an invite to a workspace at this email address.",
"deactivating": "Deactivating",
"confirm": "Confirm",
"draft_created": "Draft created",
"issue_created_successfully": "Issue created successfully",
"draft_creation_failed": "Draft creation failed",
"issue_creation_failed": "Issue creation failed",
"draft_issue": "Draft issue",
"issue_updated_successfully": "Issue updated successfully",
"issue_could_not_be_updated": "Issue could not be updated",
"create_a_draft": "Create a draft",
"save_to_drafts": "Save to Drafts",
"save": "Save",
"update": "Update",
"updating": "Updating",
"create_new_issue": "Create new issue",
"editor_is_not_ready_to_discard_changes": "Editor is not ready to discard changes",
"failed_to_move_issue_to_project": "Failed to move issue to project",
"create_more": "Create more",
"add_to_project": "Add to project",
"discard": "Discard",
"duplicate_issue_found": "Duplicate issue found",
"duplicate_issues_found": "Duplicate issues found",
"no_matching_results": "No matching results",
"title_is_required": "Title is required",
"title": "Title",
"state": "State",
"priority": "Priority",
"none": "None",
"urgent": "Urgent",
"high": "High",
"medium": "Medium",
"low": "Low",
"members": "Members",
"assignee": "Assignee",
"assignees": "Assignees",
"you": "You",
"labels": "Labels",
"create_new_label": "Create new label",
"start_date": "Start date",
"due_date": "Due date",
"cycle": "Cycle",
"estimate": "Estimate",
"change_parent_issue": "Change parent issue",
"remove_parent_issue": "Remove parent issue",
"add_parent": "Add parent",
"loading_members": "Loading members..."
}
@@ -0,0 +1,320 @@
{
"submit": "Enviar",
"cancel": "Cancelar",
"loading": "Cargando",
"error": "Error",
"success": "Éxito",
"warning": "Advertencia",
"info": "Información",
"close": "Cerrar",
"yes": "Sí",
"no": "No",
"ok": "OK",
"name": "Nombre",
"description": "Descripción",
"search": "Buscar",
"add_member": "Agregar miembro",
"remove_member": "Eliminar miembro",
"add_members": "Agregar miembros",
"remove_members": "Eliminar miembros",
"add": "Agregar",
"remove": "Eliminar",
"add_new": "Agregar nuevo",
"remove_selected": "Eliminar seleccionados",
"first_name": "Nombre",
"last_name": "Apellido",
"email": "Correo electrónico",
"display_name": "Nombre para mostrar",
"role": "Rol",
"timezone": "Zona horaria",
"avatar": "Avatar",
"cover_image": "Imagen de portada",
"password": "Contraseña",
"change_cover": "Cambiar portada",
"language": "Idioma",
"saving": "Guardando...",
"save_changes": "Guardar cambios",
"deactivate_account": "Desactivar cuenta",
"deactivate_account_description": "Al desactivar una cuenta, todos los datos y recursos dentro de esa cuenta se eliminarán permanentemente y no se podrán recuperar.",
"profile_settings": "Configuración de perfil",
"your_account": "Tu cuenta",
"profile": "Perfil",
"security": "Seguridad",
"activity": "Actividad",
"appearance": "Apariencia",
"notifications": "Notificaciones",
"workspaces": "Espacios de trabajo",
"create_workspace": "Crear espacio de trabajo",
"invitations": "Invitaciones",
"summary": "Resumen",
"assigned": "Asignado",
"created": "Creado",
"subscribed": "Suscrito",
"you_do_not_have_the_permission_to_access_this_page": "No tienes permiso para acceder a esta página.",
"failed_to_sign_out_please_try_again": "Error al cerrar sesión. Por favor, inténtalo de nuevo.",
"password_changed_successfully": "Contraseña cambiada con éxito.",
"something_went_wrong_please_try_again": "Algo salió mal. Por favor, inténtalo de nuevo.",
"change_password": "Cambiar contraseña",
"passwords_dont_match": "Las contraseñas no coinciden",
"current_password": "Contraseña actual",
"new_password": "Nueva contraseña",
"confirm_password": "Confirmar contraseña",
"this_field_is_required": "Este campo es obligatorio",
"changing_password": "Cambiando contraseña",
"please_enter_your_password": "Por favor, introduce tu contraseña.",
"password_length_should_me_more_than_8_characters": "La longitud de la contraseña debe ser más de 8 caracteres.",
"password_is_weak": "La contraseña es débil.",
"password_is_strong": "La contraseña es fuerte.",
"load_more": "Cargar más",
"select_or_customize_your_interface_color_scheme": "Selecciona o personaliza el esquema de color de tu interfaz.",
"theme": "Tema",
"system_preference": "Preferencia del sistema",
"light": "Claro",
"dark": "Oscuro",
"light_contrast": "Alto contraste claro",
"dark_contrast": "Alto contraste oscuro",
"custom": "Tema personalizado",
"select_your_theme": "Selecciona tu tema",
"customize_your_theme": "Personaliza tu tema",
"background_color": "Color de fondo",
"text_color": "Color del texto",
"primary_color": "Color primario (Tema)",
"sidebar_background_color": "Color de fondo de la barra lateral",
"sidebar_text_color": "Color del texto de la barra lateral",
"set_theme": "Establecer tema",
"enter_a_valid_hex_code_of_6_characters": "Introduce un código hexadecimal válido de 6 caracteres",
"background_color_is_required": "El color de fondo es obligatorio",
"text_color_is_required": "El color del texto es obligatorio",
"primary_color_is_required": "El color primario es obligatorio",
"sidebar_background_color_is_required": "El color de fondo de la barra lateral es obligatorio",
"sidebar_text_color_is_required": "El color del texto de la barra lateral es obligatorio",
"updating_theme": "Actualizando tema",
"theme_updated_successfully": "Tema actualizado con éxito",
"failed_to_update_the_theme": "Error al actualizar el tema",
"email_notifications": "Notificaciones por correo electrónico",
"stay_in_the_loop_on_issues_you_are_subscribed_to_enable_this_to_get_notified": "Mantente al tanto de los problemas a los que estás suscrito. Activa esto para recibir notificaciones.",
"email_notification_setting_updated_successfully": "Configuración de notificaciones por correo electrónico actualizada con éxito",
"failed_to_update_email_notification_setting": "Error al actualizar la configuración de notificaciones por correo electrónico",
"notify_me_when": "Notifícame cuando",
"property_changes": "Cambios de propiedad",
"property_changes_description": "Notifícame cuando cambien las propiedades del problema como asignados, prioridad, estimaciones o cualquier otra cosa.",
"state_change": "Cambio de estado",
"state_change_description": "Notifícame cuando el problema se mueva a un estado diferente",
"issue_completed": "Problema completado",
"issue_completed_description": "Notifícame solo cuando un problema esté completado",
"comments": "Comentarios",
"comments_description": "Notifícame cuando alguien deje un comentario en el problema",
"mentions": "Menciones",
"mentions_description": "Notifícame solo cuando alguien me mencione en los comentarios o en la descripción",
"create_your_workspace": "Crea tu espacio de trabajo",
"only_your_instance_admin_can_create_workspaces": "Solo tu administrador de instancia puede crear espacios de trabajo",
"only_your_instance_admin_can_create_workspaces_description": "Si conoces el correo electrónico de tu administrador de instancia, haz clic en el botón de abajo para ponerte en contacto con él.",
"go_back": "Regresar",
"request_instance_admin": "Solicitar administrador de instancia",
"plane_logo": "Logo de Plane",
"workspace_creation_disabled": "Creación de espacio de trabajo deshabilitada",
"workspace_request_subject": "Solicitando un nuevo espacio de trabajo",
"workspace_request_body": "Hola administrador(es) de instancia,\n\nPor favor, crea un nuevo espacio de trabajo con la URL [/nombre-del-espacio-de-trabajo] para [propósito de crear el espacio de trabajo].\n\nGracias,\n{{firstName}} {{lastName}}\n{{email}}",
"creating_workspace": "Creando espacio de trabajo",
"workspace_created_successfully": "Espacio de trabajo creado con éxito",
"create_workspace_page": "Página de creación de espacio de trabajo",
"workspace_could_not_be_created_please_try_again": "No se pudo crear el espacio de trabajo. Por favor, inténtalo de nuevo.",
"workspace_could_not_be_created_please_try_again_description": "Ocurrió un error al crear el espacio de trabajo. Por favor, inténtalo de nuevo.",
"this_is_a_required_field": "Este es un campo obligatorio.",
"name_your_workspace": "Nombra tu espacio de trabajo",
"workspaces_names_can_contain_only_space_dash_and_alphanumeric_characters": "Los nombres de los espacios de trabajo solo pueden contener (' '), ('-'), ('_') y caracteres alfanuméricos.",
"limit_your_name_to_80_characters": "Limita tu nombre a 80 caracteres.",
"set_your_workspace_url": "Establece la URL de tu espacio de trabajo",
"limit_your_url_to_48_characters": "Limita tu URL a 48 caracteres.",
"how_many_people_will_use_this_workspace": "¿Cuántas personas usarán este espacio de trabajo?",
"how_many_people_will_use_this_workspace_description": "Esto nos ayudará a determinar el número de asientos que necesitas comprar.",
"select_a_range": "Selecciona un rango",
"urls_can_contain_only_dash_and_alphanumeric_characters": "Las URLs solo pueden contener ('-') y caracteres alfanuméricos.",
"something_familiar_and_recognizable_is_always_best": "Algo familiar y reconocible siempre es mejor.",
"workspace_url_is_already_taken": "¡La URL del espacio de trabajo ya está tomada!",
"old_password": "Contraseña antigua",
"general_settings": "Configuración general",
"sign_out": "Cerrar sesión",
"signing_out": "Cerrando sesión",
"active_cycles": "Ciclos activos",
"active_cycles_description": "Monitorea ciclos a través de proyectos, rastrea problemas de alta prioridad y enfócate en ciclos que necesitan atención.",
"on_demand_snapshots_of_all_your_cycles": "Instantáneas bajo demanda de todos tus ciclos",
"upgrade": "Actualizar",
"10000_feet_view": "Vista de 10,000 pies de todos los ciclos activos.",
"10000_feet_view_description": "Amplía para ver ciclos en ejecución en todos tus proyectos a la vez en lugar de ir de ciclo en ciclo en cada proyecto.",
"get_snapshot_of_each_active_cycle": "Obtén una instantánea de cada ciclo activo.",
"get_snapshot_of_each_active_cycle_description": "Rastrea métricas de alto nivel para todos los ciclos activos, ve su estado de progreso y obtén una idea del alcance frente a los plazos.",
"compare_burndowns": "Compara burndowns.",
"compare_burndowns_description": "Monitorea cómo se están desempeñando cada uno de tus equipos con un vistazo al informe de burndown de cada ciclo.",
"quickly_see_make_or_break_issues": "Ve rápidamente problemas críticos.",
"quickly_see_make_or_break_issues_description": "Previsualiza problemas de alta prioridad para cada ciclo contra fechas de vencimiento. Vélos todos por ciclo en un clic.",
"zoom_into_cycles_that_need_attention": "Enfócate en ciclos que necesitan atención.",
"zoom_into_cycles_that_need_attention_description": "Investiga el estado de cualquier ciclo que no cumpla con las expectativas en un clic.",
"stay_ahead_of_blockers": "Anticípate a los bloqueadores.",
"stay_ahead_of_blockers_description": "Detecta desafíos de un proyecto a otro y ve dependencias entre ciclos que no son obvias desde ninguna otra vista.",
"analytics": "Analítica",
"workspace_invites": "Invitaciones al espacio de trabajo",
"workspace_settings": "Configuración del espacio de trabajo",
"enter_god_mode": "Entrar en modo dios",
"workspace_logo": "Logo del espacio de trabajo",
"new_issue": "Nuevo problema",
"home": "Inicio",
"your_work": "Tu trabajo",
"drafts": "Borradores",
"projects": "Proyectos",
"views": "Vistas",
"workspace": "Espacio de trabajo",
"archives": "Archivos",
"settings": "Configuración",
"failed_to_move_favorite": "Error al mover favorito",
"your_favorites": "Tus favoritos",
"no_favorites_yet": "Aún no hay favoritos",
"create_folder": "Crear carpeta",
"new_folder": "Nueva carpeta",
"favorite_updated_successfully": "Favorito actualizado con éxito",
"favorite_created_successfully": "Favorito creado con éxito",
"folder_already_exists": "La carpeta ya existe",
"folder_name_cannot_be_empty": "El nombre de la carpeta no puede estar vacío",
"something_went_wrong": "Algo salió mal",
"failed_to_reorder_favorite": "Error al reordenar favorito",
"favorite_removed_successfully": "Favorito eliminado con éxito",
"failed_to_create_favorite": "Error al crear favorito",
"failed_to_rename_favorite": "Error al renombrar favorito",
"project_link_copied_to_clipboard": "Enlace del proyecto copiado al portapapeles",
"link_copied": "Enlace copiado",
"your_projects": "Tus proyectos",
"add_project": "Agregar proyecto",
"create_project": "Crear proyecto",
"failed_to_remove_project_from_favorites": "No se pudo eliminar el proyecto de favoritos. Por favor, inténtalo de nuevo.",
"project_created_successfully": "Proyecto creado con éxito",
"project_created_successfully_description": "Proyecto creado con éxito. Ahora puedes comenzar a agregar problemas a él.",
"project_cover_image_alt": "Imagen de portada del proyecto",
"name_is_required": "El nombre es obligatorio",
"title_should_be_less_than_255_characters": "El título debe tener menos de 255 caracteres",
"project_name": "Nombre del proyecto",
"project_id_must_be_at_least_1_character": "El ID del proyecto debe tener al menos 1 carácter",
"project_id_must_be_at_most_5_characters": "El ID del proyecto debe tener como máximo 5 caracteres",
"project_id": "ID del proyecto",
"project_id_tooltip_content": "Te ayuda a identificar problemas en el proyecto de manera única. Máximo 5 caracteres.",
"description_placeholder": "Descripción...",
"only_alphanumeric_non_latin_characters_allowed": "Solo se permiten caracteres alfanuméricos y no latinos.",
"project_id_is_required": "El ID del proyecto es obligatorio",
"select_network": "Seleccionar red",
"lead": "Líder",
"private": "Privado",
"public": "Público",
"accessible_only_by_invite": "Accesible solo por invitación",
"anyone_in_the_workspace_except_guests_can_join": "Cualquiera en el espacio de trabajo excepto invitados puede unirse",
"creating": "Creando",
"creating_project": "Creando proyecto",
"adding_project_to_favorites": "Agregando proyecto a favoritos",
"project_added_to_favorites": "Proyecto agregado a favoritos",
"couldnt_add_the_project_to_favorites": "No se pudo agregar el proyecto a favoritos. Por favor, inténtalo de nuevo.",
"removing_project_from_favorites": "Eliminando proyecto de favoritos",
"project_removed_from_favorites": "Proyecto eliminado de favoritos",
"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": "Publicar",
"copy_link": "Copiar enlace",
"leave_project": "Abandonar proyecto",
"join_the_project_to_rearrange": "Únete al proyecto para reordenar",
"drag_to_rearrange": "Arrastra para reordenar",
"congrats": "¡Felicitaciones!",
"project": "Proyecto",
"open_project": "Abrir proyecto",
"issues": "Problemas",
"cycles": "Ciclos",
"modules": "Módulos",
"pages": "Páginas",
"intake": "Entrada",
"time_tracking": "Seguimiento de tiempo",
"work_management": "Gestión del trabajo",
"projects_and_issues": "Proyectos y problemas",
"projects_and_issues_description": "Activa o desactiva estos en este proyecto.",
"cycles_description": "Organiza el trabajo como mejor te parezca 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 escribes cualquier cosa.",
"intake_description": "Mantente al tanto de los problemas a los que estás suscrito. Activa esto para recibir notificaciones.",
"time_tracking_description": "Rastrea el tiempo dedicado a problemas y proyectos.",
"work_management_description": "Gestiona tu trabajo y proyectos con facilidad.",
"documentation": "Documentación",
"message_support": "Mensaje al soporte",
"contact_sales": "Contactar ventas",
"hyper_mode": "Modo Hyper",
"keyboard_shortcuts": "Atajos de teclado",
"whats_new": "¿Qué hay de nuevo?",
"version": "Versión",
"we_are_having_trouble_fetching_the_updates": "Estamos teniendo problemas para obtener las actualizaciones.",
"our_changelogs": "nuestros registros de cambios",
"for_the_latest_updates": "para las últimas actualizaciones.",
"please_visit": "Por favor, visita",
"docs": "Documentos",
"full_changelog": "Registro de cambios completo",
"support": "Soporte",
"discord": "Discord",
"powered_by_plane_pages": "Desarrollado por Plane Pages",
"please_select_at_least_one_invitation": "Por favor, selecciona al menos una invitación.",
"please_select_at_least_one_invitation_description": "Por favor, selecciona al menos una invitación para unirte al espacio de trabajo.",
"we_see_that_someone_has_invited_you_to_join_a_workspace": "Vemos que alguien te ha invitado a unirte a un espacio de trabajo",
"join_a_workspace": "Unirse a un espacio de trabajo",
"we_see_that_someone_has_invited_you_to_join_a_workspace_description": "Vemos que alguien te ha invitado a unirte a un espacio de trabajo",
"join_a_workspace_description": "Unirse a un espacio de trabajo",
"accept_and_join": "Aceptar y unirse",
"go_home": "Ir a inicio",
"no_pending_invites": "No hay invitaciones pendientes",
"you_can_see_here_if_someone_invites_you_to_a_workspace": "Puedes ver aquí si alguien te invita a un espacio de trabajo",
"back_to_home": "Volver al inicio",
"workspace_name": "nombre-del-espacio-de-trabajo",
"deactivate_your_account": "Desactivar tu cuenta",
"deactivate_your_account_description": "Una vez desactivada, no podrás ser asignado a problemas ni se te facturará por tu espacio de trabajo. Para reactivar tu cuenta, necesitarás una invitación a un espacio de trabajo con esta dirección de correo electrónico.",
"deactivating": "Desactivando",
"confirm": "Confirmar",
"draft_created": "Borrador creado",
"issue_created_successfully": "Problema creado con éxito",
"draft_creation_failed": "Creación del borrador fallida",
"issue_creation_failed": "Creación del problema fallida",
"draft_issue": "Borrador de problema",
"issue_updated_successfully": "Problema actualizado con éxito",
"issue_could_not_be_updated": "No se pudo actualizar el problema",
"create_a_draft": "Crear un borrador",
"save_to_drafts": "Guardar en borradores",
"save": "Guardar",
"update": "Actualizar",
"updating": "Actualizando",
"create_new_issue": "Crear nuevo problema",
"editor_is_not_ready_to_discard_changes": "El editor no está listo para descartar los cambios",
"failed_to_move_issue_to_project": "Error al mover el problema al proyecto",
"create_more": "Crear más",
"add_to_project": "Agregar al proyecto",
"discard": "Descartar",
"duplicate_issue_found": "Problema duplicado encontrado",
"duplicate_issues_found": "Problemas duplicados encontrados",
"no_matching_results": "No hay resultados coincidentes",
"title_is_required": "El título es obligatorio",
"title": "Título",
"state": "Estado",
"priority": "Prioridad",
"none": "Ninguno",
"urgent": "Urgente",
"high": "Alta",
"medium": "Media",
"low": "Baja",
"members": "Miembros",
"assignee": "Asignado",
"assignees": "Asignados",
"you": "Tú",
"labels": "Etiquetas",
"create_new_label": "Crear nueva etiqueta",
"start_date": "Fecha de inicio",
"due_date": "Fecha de vencimiento",
"cycle": "Ciclo",
"estimate": "Estimación",
"change_parent_issue": "Cambiar problema padre",
"remove_parent_issue": "Eliminar problema padre",
"add_parent": "Agregar padre",
"loading_members": "Cargando miembros...",
"inbox": "bandeja de entrada"
}
@@ -0,0 +1,320 @@
{
"submit": "Soumettre",
"cancel": "Annuler",
"loading": "Chargement",
"error": "Erreur",
"success": "Succès",
"warning": "Avertissement",
"info": "Info",
"close": "Fermer",
"yes": "Oui",
"no": "Non",
"ok": "OK",
"name": "Nom",
"description": "Description",
"search": "Rechercher",
"add_member": "Ajouter un membre",
"remove_member": "Supprimer un membre",
"add_members": "Ajouter des membres",
"remove_members": "Supprimer des membres",
"add": "Ajouter",
"remove": "Supprimer",
"add_new": "Ajouter nouveau",
"remove_selected": "Supprimer la sélection",
"first_name": "Prénom",
"last_name": "Nom de famille",
"email": "Email",
"display_name": "Nom d'affichage",
"role": "Rôle",
"timezone": "Fuseau horaire",
"avatar": "Avatar",
"cover_image": "Image de couverture",
"password": "Mot de passe",
"change_cover": "Modifier la couverture",
"language": "Langue",
"saving": "Enregistrement...",
"save_changes": "Enregistrer les modifications",
"deactivate_account": "Désactiver le compte",
"deactivate_account_description": "Lors de la désactivation d'un compte, toutes les données et ressources de ce compte seront définitivement supprimées et ne pourront pas être récupérées.",
"profile_settings": "Paramètres du profil",
"your_account": "Votre compte",
"profile": "Profil",
"security": " Sécurité",
"activity": "Activité",
"appearance": "Apparence",
"notifications": "Notifications",
"workspaces": "Workspaces",
"create_workspace": "Créer un workspace",
"invitations": "Invitations",
"summary": "Résumé",
"assigned": "Assigné",
"created": "Créé",
"subscribed": "Souscrit",
"you_do_not_have_the_permission_to_access_this_page": "Vous n'avez pas les permissions pour accéder à cette page.",
"failed_to_sign_out_please_try_again": "Impossible de se déconnecter. Veuillez réessayer.",
"password_changed_successfully": "Mot de passe changé avec succès.",
"something_went_wrong_please_try_again": "Quelque chose s'est mal passé. Veuillez réessayer.",
"change_password": "Changer le mot de passe",
"changing_password": "Changement de mot de passe",
"current_password": "Mot de passe actuel",
"new_password": "Nouveau mot de passe",
"confirm_password": "Confirmer le mot de passe",
"this_field_is_required": "Ce champ est requis",
"passwords_dont_match": "Les mots de passe ne correspondent pas",
"please_enter_your_password": "Veuillez entrer votre mot de passe.",
"password_length_should_me_more_than_8_characters": "La longueur du mot de passe doit être supérieure à 8 caractères.",
"password_is_weak": "Le mot de passe est faible.",
"password_is_strong": "Le mot de passe est fort.",
"load_more": "Charger plus",
"select_or_customize_your_interface_color_scheme": "Sélectionnez ou personnalisez votre schéma de couleurs de l'interface.",
"theme": "Thème",
"system_preference": "Préférence du système",
"light": "Clair",
"dark": "Foncé",
"light_contrast": "Clair de haut contraste",
"dark_contrast": "Foncé de haut contraste",
"custom": "Thème personnalisé",
"select_your_theme": "Sélectionnez votre thème",
"customize_your_theme": "Personnalisez votre thème",
"background_color": "Couleur de fond",
"text_color": "Couleur de texte",
"primary_color": "Couleur primaire (thème)",
"sidebar_background_color": "Couleur de fond du sidebar",
"sidebar_text_color": "Couleur de texte du sidebar",
"set_theme": "Définir le thème",
"enter_a_valid_hex_code_of_6_characters": "Entrez un code hexadécimal valide de 6 caractères",
"background_color_is_required": "La couleur de fond est requise",
"text_color_is_required": "La couleur de texte est requise",
"primary_color_is_required": "La couleur primaire est requise",
"sidebar_background_color_is_required": "La couleur de fond du sidebar est requise",
"sidebar_text_color_is_required": "La couleur de texte du sidebar est requise",
"updating_theme": "Mise à jour du thème",
"theme_updated_successfully": "Thème mis à jour avec succès",
"failed_to_update_the_theme": "Impossible de mettre à jour le thème",
"email_notifications": "Notifications par email",
"stay_in_the_loop_on_issues_you_are_subscribed_to_enable_this_to_get_notified": "Restez dans la boucle sur les problèmes auxquels vous êtes abonné. Activez cela pour être notifié.",
"email_notification_setting_updated_successfully": "Paramètres de notification par email mis à jour avec succès",
"failed_to_update_email_notification_setting": "Impossible de mettre à jour les paramètres de notification par email",
"notify_me_when": "Me notifier lorsque",
"property_changes": "Changements de propriété",
"property_changes_description": "Me notifier lorsque les propriétés du problème comme les assignés, la priorité, les estimations ou tout autre chose changent.",
"state_change": "Changement d'état",
"state_change_description": "Me notifier lorsque le problème passe à un autre état",
"issue_completed": "Problème terminé",
"issue_completed_description": "Me notifier uniquement lorsqu'un problème est terminé",
"comments": "Commentaires",
"comments_description": "Me notifier lorsqu'un utilisateur commente un problème",
"mentions": "Mention",
"mentions_description": "Me notifier uniquement lorsqu'un utilisateur mentionne un problème",
"create_your_workspace": "Créer votre workspace",
"only_your_instance_admin_can_create_workspaces": "Seuls les administrateurs de votre instance peuvent créer des workspaces",
"only_your_instance_admin_can_create_workspaces_description": "Si vous connaissez l'adresse email de votre administrateur d'instance, cliquez sur le bouton ci-dessous pour les contacter.",
"go_back": "Retour",
"request_instance_admin": "Demander à l'administrateur de l'instance",
"plane_logo": "Logo de Plane",
"workspace_creation_disabled": "Création d'espace de travail désactivée",
"workspace_request_subject": "Demande de création d'un espace de travail",
"workspace_request_body": "Bonjour administrateur(s) de l'instance,\n\nVeuillez créer un nouveau espace de travail avec l'URL [/workspace-name] pour [raison de la création de l'espace de travail].\n\nMerci,\n{{firstName}} {{lastName}}\n{{email}}",
"creating_workspace": "Création de l'espace de travail",
"workspace_created_successfully": "Espace de travail créé avec succès",
"create_workspace_page": "Page de création d'espace de travail",
"workspace_could_not_be_created_please_try_again": "L'espace de travail ne peut pas être créé. Veuillez réessayer.",
"workspace_could_not_be_created_please_try_again_description": "Une erreur est survenue lors de la création de l'espace de travail. Veuillez réessayer.",
"this_is_a_required_field": "Ce champ est requis.",
"name_your_workspace": "Nommez votre espace de travail",
"workspaces_names_can_contain_only_space_dash_and_alphanumeric_characters": "Les noms des espaces de travail peuvent contenir uniquement des espaces, des tirets et des caractères alphanumériques.",
"limit_your_name_to_80_characters": "Limitez votre nom à 80 caractères.",
"set_your_workspace_url": "Définir l'URL de votre espace de travail",
"limit_your_url_to_48_characters": "Limitez votre URL à 48 caractères.",
"how_many_people_will_use_this_workspace": "Combien de personnes utiliseront cet espace de travail ?",
"how_many_people_will_use_this_workspace_description": "Cela nous aidera à déterminer le nombre de sièges que vous devez acheter.",
"select_a_range": "Sélectionner une plage",
"urls_can_contain_only_dash_and_alphanumeric_characters": "Les URLs peuvent contenir uniquement des tirets et des caractères alphanumériques.",
"something_familiar_and_recognizable_is_always_best": "Ce qui est familier et reconnaissable est toujours le meilleur.",
"workspace_url_is_already_taken": "L'URL de l'espace de travail est déjà utilisée !",
"old_password": "Mot de passe actuel",
"general_settings": "Paramètres généraux",
"sign_out": "Déconnexion",
"signing_out": "Déconnexion",
"active_cycles": "Cycles actifs",
"active_cycles_description": "Surveillez les cycles dans les projets, suivez les issues de haute priorité et zoomez sur les cycles qui nécessitent attention.",
"on_demand_snapshots_of_all_your_cycles": "Captures instantanées sur demande de tous vos cycles",
"upgrade": "Mettre à niveau",
"10000_feet_view": "Vue d'ensemble de tous les cycles actifs",
"10000_feet_view_description": "Prenez du recul pour voir les cycles en cours dans tous vos projets en même temps au lieu de passer d'un cycle à l'autre dans chaque projet.",
"get_snapshot_of_each_active_cycle": "Obtenez un aperçu de chaque cycle actif",
"get_snapshot_of_each_active_cycle_description": "Suivez les métriques de haut niveau pour tous les cycles actifs, observez leur état d'avancement et évaluez leur portée par rapport aux échéances.",
"compare_burndowns": "Comparez les graphiques d'avancement",
"compare_burndowns_description": "Surveillez les performances de chacune de vos équipes en consultant le rapport d'avancement de chaque cycle.",
"quickly_see_make_or_break_issues": "Identifiez rapidement les problèmes critiques",
"quickly_see_make_or_break_issues_description": "Visualisez les problèmes hautement prioritaires de chaque cycle par rapport aux dates d'échéance. Consultez-les tous par cycle en un seul clic.",
"zoom_into_cycles_that_need_attention": "Concentrez-vous sur les cycles nécessitant attention",
"zoom_into_cycles_that_need_attention_description": "Examinez en un clic l'état de tout cycle qui ne répond pas aux attentes.",
"stay_ahead_of_blockers": "Anticipez les blocages",
"stay_ahead_of_blockers_description": "Repérez les défis d'un projet à l'autre et identifiez les dépendances entre cycles qui ne sont pas évidentes depuis d'autres vues.",
"analytics": "Analyse",
"workspace_invites": "Invitations de l'espace de travail",
"workspace_settings": "Paramètres de l'espace de travail",
"enter_god_mode": "Entrer en mode dieu",
"workspace_logo": "Logo de l'espace de travail",
"new_issue": "Nouveau problème",
"home": "Accueil",
"your_work": "Votre travail",
"drafts": "Brouillons",
"projects": "Projets",
"views": "Vues",
"workspace": "Espace de travail",
"archives": "Archives",
"settings": "Paramètres",
"failed_to_move_favorite": "Impossible de déplacer le favori",
"your_favorites": "Vos favoris",
"no_favorites_yet": "Aucun favori pour le moment",
"create_folder": "Créer un dossier",
"new_folder": "Nouveau dossier",
"favorite_updated_successfully": "Favori mis à jour avec succès",
"favorite_created_successfully": "Favori créé avec succès",
"folder_already_exists": "Le dossier existe déjà",
"folder_name_cannot_be_empty": "Le nom du dossier ne peut pas être vide",
"something_went_wrong": "Quelque chose s'est mal passé",
"failed_to_reorder_favorite": "Impossible de réordonner le favori",
"favorite_removed_successfully": "Favori supprimé avec succès",
"failed_to_create_favorite": "Impossible de créer le favori",
"failed_to_rename_favorite": "Impossible de renommer le favori",
"project_link_copied_to_clipboard": "Lien du projet copié dans le presse-papiers",
"link_copied": "Lien copié",
"your_projects": "Vos projets",
"add_project": "Ajouter un projet",
"create_project": "Créer un projet",
"failed_to_remove_project_from_favorites": "Impossible de supprimer le projet des favoris. Veuillez réessayer.",
"project_created_successfully": "Projet créé avec succès",
"project_created_successfully_description": "Projet créé avec succès. Vous pouvez maintenant ajouter des issues à ce projet.",
"project_cover_image_alt": "Image de couverture du projet",
"name_is_required": "Le nom est requis",
"title_should_be_less_than_255_characters": "Le titre doit être inférieur à 255 caractères",
"project_name": "Nom du projet",
"project_id_must_be_at_least_1_character": "Le projet ID doit être au moins de 1 caractère",
"project_id_must_be_at_most_5_characters": "Le projet ID doit être au plus de 5 caractères",
"project_id": "ID du projet",
"project_id_tooltip_content": "Aide à identifier les issues du projet de manière unique. Max 5 caractères.",
"description_placeholder": "Description...",
"only_alphanumeric_non_latin_characters_allowed": "Seuls les caractères alphanumériques et non latins sont autorisés.",
"project_id_is_required": "Le projet ID est requis",
"select_network": "Sélectionner le réseau",
"lead": "Lead",
"private": "Privé",
"public": "Public",
"accessible_only_by_invite": "Accessible uniquement par invitation",
"anyone_in_the_workspace_except_guests_can_join": "Tout le monde dans l'espace de travail, sauf les invités, peut rejoindre",
"creating": "Création",
"creating_project": "Création du projet",
"adding_project_to_favorites": "Ajout du projet aux favoris",
"project_added_to_favorites": "Projet ajouté aux favoris",
"couldnt_add_the_project_to_favorites": "Impossible d'ajouter le projet aux favoris. Veuillez réessayer.",
"removing_project_from_favorites": "Suppression du projet des favoris",
"project_removed_from_favorites": "Projet supprimé des favoris",
"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": "Publier",
"copy_link": "Copier le lien",
"leave_project": "Quitter le projet",
"join_the_project_to_rearrange": "Rejoindre le projet pour réorganiser",
"drag_to_rearrange": "Glisser pour réorganiser",
"congrats": "Félicitations !",
"project": "Projet",
"open_project": "Ouvrir le projet",
"issues": "Problèmes",
"cycles": "Cycles",
"modules": "Modules",
"pages": "Pages",
"intake": "Intake",
"time_tracking": "Suivi du temps",
"work_management": "Gestion du travail",
"projects_and_issues": "Projets et problèmes",
"projects_and_issues_description": "Activer ou désactiver ces fonctionnalités pour ce projet.",
"cycles_description": "Organisez votre travail en périodes définies selon vos besoins par projet et modifiez la fréquence d'une période à l'autre.",
"modules_description": "Regroupez le travail en sous-projets avec leurs propres responsables et assignés.",
"views_description": "Enregistrez vos tris, filtres et options d'affichage pour plus tard ou partagez-les.",
"pages_description": "Rédigez tout type de contenu librement.",
"intake_description": "Restez informé des tickets auxquels vous êtes abonné. Activez cette option pour recevoir des notifications.",
"time_tracking_description": "Suivez le temps passé sur les tickets et les projets.",
"work_management_description": "Gérez votre travail et vos projets en toute simplicité.",
"documentation": "Documentation",
"message_support": "Contacter le support",
"contact_sales": "Contacter les ventes",
"hyper_mode": "Mode hyper",
"keyboard_shortcuts": "Raccourcis clavier",
"whats_new": "Nouveautés?",
"version": "Version",
"we_are_having_trouble_fetching_the_updates": "Nous avons des difficultés à récupérer les mises à jour.",
"our_changelogs": "nos changelogs",
"for_the_latest_updates": "pour les dernières mises à jour.",
"please_visit": "Veuillez visiter",
"docs": "Documentation",
"full_changelog": "Journal complet",
"support": "Support",
"discord": "Discord",
"powered_by_plane_pages": "Propulsé par Plane Pages",
"please_select_at_least_one_invitation": "Veuillez sélectionner au moins une invitation.",
"please_select_at_least_one_invitation_description": "Veuillez sélectionner au moins une invitation pour rejoindre l'espace de travail.",
"we_see_that_someone_has_invited_you_to_join_a_workspace": "Nous voyons que quelqu'un vous a invité à rejoindre un espace de travail",
"join_a_workspace": "Rejoindre un espace de travail",
"we_see_that_someone_has_invited_you_to_join_a_workspace_description": "Nous voyons que quelqu'un vous a invité à rejoindre un espace de travail",
"join_a_workspace_description": "Rejoindre un espace de travail",
"accept_and_join": "Accepter et rejoindre",
"go_home": "Retour à l'accueil",
"no_pending_invites": "Aucune invitation en attente",
"you_can_see_here_if_someone_invites_you_to_a_workspace": "Vous pouvez voir ici si quelqu'un vous invite à rejoindre un espace de travail",
"back_to_home": "Retour à l'accueil",
"workspace_name": "espace-de-travail",
"deactivate_your_account": "Désactiver votre compte",
"deactivate_your_account_description": "Une fois désactivé, vous ne pourrez pas être assigné à des problèmes et être facturé pour votre espace de travail. Pour réactiver votre compte, vous aurez besoin d'une invitation à un espace de travail à cette adresse email.",
"deactivating": "Désactivation",
"confirm": "Confirmer",
"draft_created": "Brouillon créé",
"issue_created_successfully": "Problème créé avec succès",
"draft_creation_failed": "Création du brouillon échouée",
"issue_creation_failed": "Création du problème échouée",
"draft_issue": "Problème en brouillon",
"issue_updated_successfully": "Problème mis à jour avec succès",
"issue_could_not_be_updated": "Le problème n'a pas pu être mis à jour",
"create_a_draft": "Créer un brouillon",
"save_to_drafts": "Enregistrer dans les brouillons",
"save": "Enregistrer",
"update": "Mettre à jour",
"updating": "Mise à jour",
"create_new_issue": "Créer un nouveau problème",
"editor_is_not_ready_to_discard_changes": "L'éditeur n'est pas prêt à annuler les modifications",
"failed_to_move_issue_to_project": "Impossible de déplacer le problème vers le projet",
"create_more": "Créer plus",
"add_to_project": "Ajouter au projet",
"discard": "Annuler",
"duplicate_issue_found": "Problème en double trouvé",
"duplicate_issues_found": "Problèmes en double trouvés",
"no_matching_results": "Aucun résultat correspondant",
"title_is_required": "Le titre est requis",
"title": "Titre",
"state": "État",
"priority": "Priorité",
"none": "Aucune",
"urgent": "Urgent",
"high": "Haute",
"medium": "Moyenne",
"low": "Basse",
"members": "Membres",
"assignee": "Assigné",
"assignees": "Assignés",
"you": "Vous",
"labels": "Étiquettes",
"create_new_label": "Créer une nouvelle étiquette",
"start_date": "Date de début",
"due_date": "Date d'échéance",
"cycle": "Cycle",
"estimate": "Estimation",
"change_parent_issue": "Modifier le problème parent",
"remove_parent_issue": "Supprimer le problème parent",
"add_parent": "Ajouter un parent",
"loading_members": "Chargement des membres...",
"inbox": "boîte de réception"
}
@@ -0,0 +1,320 @@
{
"submit": "送信",
"cancel": "キャンセル",
"loading": "読み込み中",
"error": "エラー",
"success": "成功",
"warning": "警告",
"info": "情報",
"close": "閉じる",
"yes": "はい",
"no": "いいえ",
"ok": "OK",
"name": "名前",
"description": "説明",
"search": "検索",
"add_member": "メンバーを追加",
"remove_member": "メンバーを削除",
"add_members": "メンバーを追加",
"remove_members": "メンバーを削除",
"add": "追加",
"remove": "削除",
"add_new": "新規追加",
"remove_selected": "選択項目を削除",
"first_name": "名",
"last_name": "姓",
"email": "メールアドレス",
"display_name": "表示名",
"role": "役割",
"timezone": "タイムゾーン",
"avatar": "アバター",
"cover_image": "カバー画像",
"password": "パスワード",
"change_cover": "カバーを変更",
"language": "言語",
"saving": "保存中...",
"save_changes": "変更を保存",
"deactivate_account": "アカウントを無効化",
"deactivate_account_description": "アカウントを無効化すると、そのアカウント内のすべてのデータとリソースが完全に削除され、復元することはできません。",
"profile_settings": "プロフィール設定",
"your_account": "アカウント",
"profile": "プロフィール",
"security": "セキュリティ",
"activity": "アクティビティ",
"appearance": "アピアンス",
"notifications": "通知",
"workspaces": "ワークスペース",
"create_workspace": "ワークスペースを作成",
"invitations": "招待",
"summary": "概要",
"assigned": "割り当て済み",
"created": "作成済み",
"subscribed": "購読済み",
"you_do_not_have_the_permission_to_access_this_page": "このページにアクセスする権限がありません。",
"failed_to_sign_out_please_try_again": "サインアウトに失敗しました。もう一度お試しください。",
"password_changed_successfully": "パスワードが正常に変更されました。",
"something_went_wrong_please_try_again": "何かがうまくいきませんでした。もう一度お試しください。",
"change_password": "パスワードを変更",
"passwords_dont_match": "パスワードが一致しません",
"current_password": "現在のパスワード",
"new_password": "新しいパスワード",
"confirm_password": "パスワードを確認",
"this_field_is_required": "このフィールドは必須です",
"changing_password": "パスワードを変更中",
"please_enter_your_password": "パスワードを入力してください。",
"password_length_should_me_more_than_8_characters": "パスワードの長さは8文字以上である必要があります。",
"password_is_weak": "パスワードが弱いです。",
"password_is_strong": "パスワードが強いです。",
"load_more": "もっと読み込む",
"select_or_customize_your_interface_color_scheme": "インターフェースのカラースキームを選択またはカスタマイズしてください。",
"theme": "テーマ",
"system_preference": "システム設定",
"light": "ライト",
"dark": "ダーク",
"light_contrast": "ライト高コントラスト",
"dark_contrast": "ダーク高コントラスト",
"custom": "カスタムテーマ",
"select_your_theme": "テーマを選択",
"customize_your_theme": "テーマをカスタマイズ",
"background_color": "背景色",
"text_color": "テキスト色",
"primary_color": "プライマリ(テーマ)色",
"sidebar_background_color": "サイドバー背景色",
"sidebar_text_color": "サイドバーテキスト色",
"set_theme": "テーマを設定",
"enter_a_valid_hex_code_of_6_characters": "6文字の有効な16進コードを入力してください",
"background_color_is_required": "背景色は必須です",
"text_color_is_required": "テキスト色は必須です",
"primary_color_is_required": "プライマリ色は必須です",
"sidebar_background_color_is_required": "サイドバー背景色は必須です",
"sidebar_text_color_is_required": "サイドバーテキスト色は必須です",
"updating_theme": "テーマを更新中",
"theme_updated_successfully": "テーマが正常に更新されました",
"failed_to_update_the_theme": "テーマの更新に失敗しました",
"email_notifications": "メール通知",
"stay_in_the_loop_on_issues_you_are_subscribed_to_enable_this_to_get_notified": "購読している問題についての通知を受け取るには、これを有効にしてください。",
"email_notification_setting_updated_successfully": "メール通知設定が正常に更新されました",
"failed_to_update_email_notification_setting": "メール通知設定の更新に失敗しました",
"notify_me_when": "通知する条件",
"property_changes": "プロパティの変更",
"property_changes_description": "担当者、優先度、見積もりなどのプロパティが変更されたときに通知します。",
"state_change": "状態の変更",
"state_change_description": "問題が別の状態に移動したときに通知します",
"issue_completed": "問題が完了",
"issue_completed_description": "問題が完了したときのみ通知します",
"comments": "コメント",
"comments_description": "誰かが問題にコメントを残したときに通知します",
"mentions": "メンション",
"mentions_description": "コメントや説明で誰かが自分をメンションしたときのみ通知します",
"create_your_workspace": "ワークスペースを作成",
"only_your_instance_admin_can_create_workspaces": "ワークスペースを作成できるのはインスタンス管理者のみです",
"only_your_instance_admin_can_create_workspaces_description": "インスタンス管理者のメールアドレスを知っている場合は、以下のボタンをクリックして連絡を取ってください。",
"go_back": "戻る",
"request_instance_admin": "インスタンス管理者にリクエスト",
"plane_logo": "プレーンロゴ",
"workspace_creation_disabled": "ワークスペースの作成が無効化されています",
"workspace_request_subject": "新しいワークスペースのリクエスト",
"workspace_request_body": "インスタンス管理者様\n\nURL [/workspace-name] で新しいワークスペースを作成してください。[ワークスペース作成の目的]\n\nありがとうございます。\n{{firstName}} {{lastName}}\n{{email}}",
"creating_workspace": "ワークスペースを作成中",
"workspace_created_successfully": "ワークスペースが正常に作成されました",
"create_workspace_page": "ワークスペース作成ページ",
"workspace_could_not_be_created_please_try_again": "ワークスペースを作成できませんでした。もう一度お試しください。",
"workspace_could_not_be_created_please_try_again_description": "ワークスペースの作成中にエラーが発生しました。もう一度お試しください。",
"this_is_a_required_field": "これは必須フィールドです。",
"name_your_workspace": "ワークスペースに名前を付ける",
"workspaces_names_can_contain_only_space_dash_and_alphanumeric_characters": "ワークスペース名にはスペース、ダッシュ、アンダースコア、英数字のみを含めることができます。",
"limit_your_name_to_80_characters": "名前は80文字以内にしてください。",
"set_your_workspace_url": "ワークスペースのURLを設定",
"limit_your_url_to_48_characters": "URLは48文字以内にしてください。",
"how_many_people_will_use_this_workspace": "このワークスペースを使用する人数は?",
"how_many_people_will_use_this_workspace_description": "購入するシート数を決定するのに役立ちます。",
"select_a_range": "範囲を選択",
"urls_can_contain_only_dash_and_alphanumeric_characters": "URLにはダッシュと英数字のみを含めることができます。",
"something_familiar_and_recognizable_is_always_best": "親しみやすく認識しやすいものが常に最適です。",
"workspace_url_is_already_taken": "ワークスペースのURLは既に使用されています!",
"old_password": "古いパスワード",
"general_settings": "一般設定",
"sign_out": "サインアウト",
"signing_out": "サインアウト中",
"active_cycles": "アクティブサイクル",
"active_cycles_description": "プロジェクト全体のサイクルを監視し、高優先度の問題を追跡し、注意が必要なサイクルにズームインします。",
"on_demand_snapshots_of_all_your_cycles": "すべてのサイクルのオンデマンドスナップショット",
"upgrade": "アップグレード",
"10000_feet_view": "すべてのアクティブサイクルの10,000フィートビュー。",
"10000_feet_view_description": "各プロジェクトのサイクルを個別に見るのではなく、すべてのプロジェクトのサイクルを一度に見るためにズームアウトします。",
"get_snapshot_of_each_active_cycle": "各アクティブサイクルのスナップショットを取得します。",
"get_snapshot_of_each_active_cycle_description": "すべてのアクティブサイクルの高レベルのメトリクスを追跡し、進捗状況を確認し、期限に対するスコープの感覚を得ます。",
"compare_burndowns": "バーンダウンを比較します。",
"compare_burndowns_description": "各チームのパフォーマンスを監視し、各サイクルのバーンダウンレポートを覗き見します。",
"quickly_see_make_or_break_issues": "重要な問題をすばやく確認します。",
"quickly_see_make_or_break_issues_description": "各サイクルの期限に対する高優先度の問題をプレビューします。1クリックでサイクルごとにすべてを確認できます。",
"zoom_into_cycles_that_need_attention": "注意が必要なサイクルにズームインします。",
"zoom_into_cycles_that_need_attention_description": "期待に沿わないサイクルの状態を1クリックで調査します。",
"stay_ahead_of_blockers": "ブロッカーを先取りします。",
"stay_ahead_of_blockers_description": "プロジェクト間の課題を見つけ、他のビューからは明らかでないサイクル間の依存関係を確認します。",
"analytics": "分析",
"workspace_invites": "ワークスペースの招待",
"workspace_settings": "ワークスペース設定",
"enter_god_mode": "ゴッドモードに入る",
"workspace_logo": "ワークスペースロゴ",
"new_issue": "新しい問題",
"home": "ホーム",
"your_work": "あなたの作業",
"drafts": "下書き",
"projects": "プロジェクト",
"views": "ビュー",
"workspace": "ワークスペース",
"archives": "アーカイブ",
"settings": "設定",
"failed_to_move_favorite": "お気に入りの移動に失敗しました",
"your_favorites": "あなたのお気に入り",
"no_favorites_yet": "まだお気に入りはありません",
"create_folder": "フォルダーを作成",
"new_folder": "新しいフォルダー",
"favorite_updated_successfully": "お気に入りが正常に更新されました",
"favorite_created_successfully": "お気に入りが正常に作成されました",
"folder_already_exists": "フォルダーは既に存在します",
"folder_name_cannot_be_empty": "フォルダー名を空にすることはできません",
"something_went_wrong": "何かがうまくいきませんでした",
"failed_to_reorder_favorite": "お気に入りの並べ替えに失敗しました",
"favorite_removed_successfully": "お気に入りが正常に削除されました",
"failed_to_create_favorite": "お気に入りの作成に失敗しました",
"failed_to_rename_favorite": "お気に入りの名前変更に失敗しました",
"project_link_copied_to_clipboard": "プロジェクトリンクがクリップボードにコピーされました",
"link_copied": "リンクがコピーされました",
"your_projects": "あなたのプロジェクト",
"add_project": "プロジェクトを追加",
"create_project": "プロジェクトを作成",
"failed_to_remove_project_from_favorites": "お気に入りからプロジェクトを削除できませんでした。もう一度お試しください。",
"project_created_successfully": "プロジェクトが正常に作成されました",
"project_created_successfully_description": "プロジェクトが正常に作成されました。今すぐ問題を追加し始めることができます。",
"project_cover_image_alt": "プロジェクトカバー画像",
"name_is_required": "名前は必須です",
"title_should_be_less_than_255_characters": "タイトルは255文字未満である必要があります",
"project_name": "プロジェクト名",
"project_id_must_be_at_least_1_character": "プロジェクトIDは少なくとも1文字である必要があります",
"project_id_must_be_at_most_5_characters": "プロジェクトIDは最大5文字である必要があります",
"project_id": "プロジェクトID",
"project_id_tooltip_content": "プロジェクト内の問題を一意に識別するのに役立ちます。最大5文字。",
"description_placeholder": "説明...",
"only_alphanumeric_non_latin_characters_allowed": "英数字と非ラテン文字のみが許可されます。",
"project_id_is_required": "プロジェクトIDは必須です",
"select_network": "ネットワークを選択",
"lead": "リード",
"private": "プライベート",
"public": "パブリック",
"accessible_only_by_invite": "招待によってのみアクセス可能",
"anyone_in_the_workspace_except_guests_can_join": "ゲストを除くワークスペース内の誰でも参加できます",
"creating": "作成中",
"creating_project": "プロジェクトを作成中",
"adding_project_to_favorites": "プロジェクトをお気に入りに追加中",
"project_added_to_favorites": "プロジェクトがお気に入りに追加されました",
"couldnt_add_the_project_to_favorites": "プロジェクトをお気に入りに追加できませんでした。もう一度お試しください。",
"removing_project_from_favorites": "お気に入りからプロジェクトを削除中",
"project_removed_from_favorites": "プロジェクトがお気に入りから削除されました",
"couldnt_remove_the_project_from_favorites": "お気に入りからプロジェクトを削除できませんでした。もう一度お試しください。",
"add_to_favorites": "お気に入りに追加",
"remove_from_favorites": "お気に入りから削除",
"publish_settings": "公開設定",
"publish": "公開",
"copy_link": "リンクをコピー",
"leave_project": "プロジェクトを離れる",
"join_the_project_to_rearrange": "プロジェクトに参加して並べ替え",
"drag_to_rearrange": "ドラッグして並べ替え",
"congrats": "おめでとうございます!",
"project": "プロジェクト",
"open_project": "プロジェクトを開く",
"issues": "問題",
"cycles": "サイクル",
"modules": "モジュール",
"pages": "ページ",
"intake": "インテーク",
"time_tracking": "時間追跡",
"work_management": "作業管理",
"projects_and_issues": "プロジェクトと問題",
"projects_and_issues_description": "このプロジェクトでオンまたはオフに切り替えます。",
"cycles_description": "プロジェクトごとに作業をタイムボックス化し、期間ごとに頻度を変更します。",
"modules_description": "独自のリードと担当者を持つサブプロジェクトのようなセットアップに作業をグループ化します。",
"views_description": "後で使用するために、または共有するためにソート、フィルター、表示オプションを保存します。",
"pages_description": "何かを書くように何かを書く。",
"intake_description": "購読している問題についての通知を受け取るには、これを有効にしてください。",
"time_tracking_description": "問題とプロジェクトに費やした時間を追跡します。",
"work_management_description": "作業とプロジェクトを簡単に管理します。",
"documentation": "ドキュメント",
"message_support": "サポートにメッセージを送る",
"contact_sales": "営業に連絡",
"hyper_mode": "ハイパーモード",
"keyboard_shortcuts": "キーボードショートカット",
"whats_new": "新着情報",
"version": "バージョン",
"we_are_having_trouble_fetching_the_updates": "更新の取得に問題が発生しています。",
"our_changelogs": "私たちの変更履歴",
"for_the_latest_updates": "最新の更新情報については",
"please_visit": "訪問してください",
"docs": "ドキュメント",
"full_changelog": "完全な変更履歴",
"support": "サポート",
"discord": "ディスコード",
"powered_by_plane_pages": "Plane Pagesによって提供されています",
"please_select_at_least_one_invitation": "少なくとも1つの招待を選択してください。",
"please_select_at_least_one_invitation_description": "ワークスペースに参加するために少なくとも1つの招待を選択してください。",
"we_see_that_someone_has_invited_you_to_join_a_workspace": "誰かがワークスペースに参加するようにあなたを招待したことがわかります",
"join_a_workspace": "ワークスペースに参加",
"we_see_that_someone_has_invited_you_to_join_a_workspace_description": "誰かがワークスペースに参加するようにあなたを招待したことがわかります",
"join_a_workspace_description": "ワークスペースに参加",
"accept_and_join": "受け入れて参加",
"go_home": "ホームに戻る",
"no_pending_invites": "保留中の招待はありません",
"you_can_see_here_if_someone_invites_you_to_a_workspace": "誰かがワークスペースに招待した場合、ここで確認できます",
"back_to_home": "ホームに戻る",
"workspace_name": "ワークスペース名",
"deactivate_your_account": "アカウントを無効化",
"deactivate_your_account_description": "無効化すると、問題を割り当てられなくなり、ワークスペースの請求もされなくなります。アカウントを再有効化するには、このメールアドレスに招待されたワークスペースが必要です。",
"deactivating": "無効化中",
"confirm": "確認",
"draft_created": "下書きが作成されました",
"issue_created_successfully": "問題が正常に作成されました",
"draft_creation_failed": "下書き作成に失敗しました",
"issue_creation_failed": "問題作成に失敗しました",
"draft_issue": "下書き問題",
"issue_updated_successfully": "問題が正常に更新されました",
"issue_could_not_be_updated": "問題を更新できませんでした",
"create_a_draft": "下書きを作成",
"save_to_drafts": "下書きに保存",
"save": "保存",
"update": "更新",
"updating": "更新中",
"create_new_issue": "新しい問題を作成",
"editor_is_not_ready_to_discard_changes": "エディターは変更を破棄する準備ができていません",
"failed_to_move_issue_to_project": "問題をプロジェクトに移動できませんでした",
"create_more": "作成する",
"add_to_project": "プロジェクトに追加",
"discard": "破棄",
"duplicate_issue_found": "重複問題が見つかりました",
"duplicate_issues_found": "重複問題が見つかりました",
"no_matching_results": "一致する結果はありません",
"title_is_required": "タイトルは必須です",
"title": "タイトル",
"state": "ステータス",
"priority": "優先度",
"none": "なし",
"urgent": "緊急",
"high": "高",
"medium": "中",
"low": "低",
"members": "メンバー",
"assignee": "アサイン者",
"assignees": "アサイン者",
"you": "あなた",
"labels": "ラベル",
"create_new_label": "新しいラベルを作成",
"start_date": "開始日",
"due_date": "期限日",
"cycle": "サイクル",
"estimate": "見積もり",
"change_parent_issue": "親問題を変更",
"remove_parent_issue": "親問題を削除",
"add_parent": "親問題を追加",
"loading_members": "メンバーを読み込んでいます...",
"inbox": "受信箱"
}
+10
View File
@@ -0,0 +1,10 @@
{
"extends": "@plane/typescript-config/react-library.json",
"compilerOptions": {
"jsx": "react",
"lib": ["esnext", "dom"],
"resolveJsonModule": true
},
"include": ["./src"],
"exclude": ["dist", "build", "node_modules"]
}
+1 -1
View File
@@ -9,6 +9,6 @@
},
"dependencies": {
"@plane/constants": "*",
"axios": "^1.4.0"
"axios": "^1.7.9"
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
// plane web constants
import { AI_EDITOR_TASKS, API_BASE_URL } from "@plane/constants";
// services
import { APIService } from "@/api.service";
import { APIService } from "../api.service";
/**
* Payload type for AI editor tasks
+3 -2
View File
@@ -9,7 +9,7 @@ import { APIService } from "../api.service";
* Provides methods for user authentication, password management, and session handling
* @extends {APIService}
*/
export default class AuthService extends APIService {
export class AuthService extends APIService {
/**
* Creates an instance of AuthService
* Initializes with the base API URL
@@ -22,9 +22,10 @@ export default class AuthService extends APIService {
* Requests a CSRF token for form submission security
* @returns {Promise<ICsrfTokenData>} Object containing the CSRF token
* @throws {Error} Throws the complete error object if the request fails
* @remarks This method uses the validateStatus: null option to bypass interceptors for unauthorized errors.
*/
async requestCSRFToken(): Promise<ICsrfTokenData> {
return this.get("/auth/get-csrf-token/")
return this.get("/auth/get-csrf-token/", { validateStatus: null })
.then((response) => response.data)
.catch((error) => {
throw error;

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