Compare commits
79
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d15ab1be1 | ||
|
|
6d84a1b5bc | ||
|
|
58ac1dcba1 | ||
|
|
7430ccf9a8 | ||
|
|
85ee7f9af0 | ||
|
|
06ecbe884c | ||
|
|
8a6a5a8ca7 | ||
|
|
87ea13c32e | ||
|
|
e3ceb4825a | ||
|
|
de009d6d10 | ||
|
|
7b3f206f57 | ||
|
|
2018114543 | ||
|
|
ff8c5ee910 | ||
|
|
0e8ff37ccc | ||
|
|
2ddd7096e4 | ||
|
|
44c241d94b | ||
|
|
add35b5ea6 | ||
|
|
448a34aa5f | ||
|
|
8c57543f72 | ||
|
|
9062a7b67d | ||
|
|
89603bb2d6 | ||
|
|
3ab959c682 | ||
|
|
d96ab2e7af | ||
|
|
5d8f66ae22 | ||
|
|
ce4a375b62 | ||
|
|
92645c1cee | ||
|
|
ac14d57f8b | ||
|
|
71ebe5ca36 | ||
|
|
bb71e60fcb | ||
|
|
3691cef351 | ||
|
|
c2d8703acf | ||
|
|
507946886f | ||
|
|
24944a03c6 | ||
|
|
cb045abfe1 | ||
|
|
24cc69fd7b | ||
|
|
88c26b334d | ||
|
|
200be0ac7f | ||
|
|
ae657af958 | ||
|
|
f4c4848a0d | ||
|
|
6914dc9f42 | ||
|
|
742559bc63 | ||
|
|
70dacc5e6a | ||
|
|
5b96c970cd | ||
|
|
906d095a1e | ||
|
|
0af9b09844 | ||
|
|
edb68a1bc6 | ||
|
|
790ecee629 | ||
|
|
0cabde03f0 | ||
|
|
d3d3bf79f9 | ||
|
|
6321977f1f | ||
|
|
208df80c86 | ||
|
|
bc27bc9dd2 | ||
|
+2 |
1acaae9d5e | ||
|
|
fbbca0c519 | ||
|
|
a6216beb7e | ||
|
|
732963b591 | ||
|
|
625cbf872b | ||
|
|
d26fb8ce02 | ||
|
|
e4f9d027fe | ||
|
|
c1407daa47 | ||
|
|
2622dd691c | ||
|
|
870ca17e13 | ||
|
|
4652ad0fc3 | ||
|
|
be9dbc1b18 | ||
|
|
3fd2550d69 | ||
|
|
d6bcd8dd15 | ||
|
|
873e4330bc | ||
|
|
ade0aa1643 | ||
|
|
6a13a64996 | ||
|
|
5e6c02358d | ||
|
|
61d6d928ba | ||
|
|
a555df27e7 | ||
|
|
df671d13c7 | ||
|
|
c7ebaf6ba0 | ||
|
|
119d46dc2b | ||
|
|
25f7d813b9 | ||
|
|
ec2af13258 | ||
|
|
8833e4e23b | ||
|
|
752a27a175 |
@@ -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 }}
|
||||
@@ -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,
|
||||
|
||||
@@ -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 | [](https://developers.plane.so/self-hosting/methods/docker-compose) |
|
||||
| Kubernetes | [](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
|
||||
|
||||
That’s it! You’re all set to begin coding. Remember to refresh your browser if changes don’t auto-reload. Happy contributing! 🎉
|
||||
|
||||
## Built with
|
||||
[](https://nextjs.org/)<br/>
|
||||
[](https://www.djangoproject.com/)<br/>
|
||||
## ⚙️ Built with
|
||||
[](https://nextjs.org/)
|
||||
[](https://www.djangoproject.com/)
|
||||
[](https://nodejs.org/en)
|
||||
|
||||
## 📸 Screenshots
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
FROM node:18-alpine
|
||||
FROM node:20-alpine
|
||||
RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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}}
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
]
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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(
|
||||
{
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')},
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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",)
|
||||
|
||||
@@ -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>")
|
||||
|
||||
@@ -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>")
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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,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
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
Vendored
+5
@@ -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
@@ -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(() => {
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export const SIDEBAR_CLICKED = "Sidenav clicked";
|
||||
@@ -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";
|
||||
|
||||
@@ -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,5 +1,5 @@
|
||||
export const ORGANIZATION_SIZE = [
|
||||
"Just myself",
|
||||
"Just myself", // TODO: translate
|
||||
"2-10",
|
||||
"11-50",
|
||||
"51-200",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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,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";
|
||||
|
||||
@@ -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,2 +1,3 @@
|
||||
export * from "./use-local-storage";
|
||||
export * from "./use-outside-click-detector";
|
||||
export * from "./use-platform-os";
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
build/*
|
||||
dist/*
|
||||
out/*
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
.turbo
|
||||
out/
|
||||
dist/
|
||||
build/
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
},
|
||||
];
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
@@ -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": "受信箱"
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -9,6 +9,6 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@plane/constants": "*",
|
||||
"axios": "^1.4.0"
|
||||
"axios": "^1.7.9"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user