Compare commits

..

8 Commits

Author SHA1 Message Date
NarayanBavisetti 5436b489ab chore: total issue count 2024-05-01 12:52:46 +05:30
Daniel Alba 12b4f74e1d feat(dashboard): improve label readability (#4321)
change none label for all time in dashbard filters
2024-05-01 00:08:18 +05:30
sriram veeraghanta 16da29b3ff fix: merge conflicts resolved 2024-04-30 20:26:12 +05:30
sriram veeraghanta c389c6b440 fix: merge conflicts resolved 2024-04-29 20:02:53 +05:30
gurusainath 56e99937bb chore: updated admin-sidebar 2024-04-29 15:36:58 +05:30
sriram veeraghanta 7061ef4631 fix: merge conflicts resolved 2024-04-29 13:52:07 +05:30
gurusainath bd75bf7bb6 chore: updated file structure for admin 2024-04-29 13:43:59 +05:30
sriram veeraghanta e178bba9c0 feat: session authentication and god-mode implementation (#4302)
* dev: move authentication to base class for credentials

* chore: new account creation

* dev: return error as query parameter

* dev: accounts and profile endpoints for user

* fix: user store updates

* fix: store fixes

* fix: type fixes

* dev: set is_password_autoset and is_email_verifier for auth providers

* dev: move all auth configuration to different apps

* dev: fix circular imports

* dev: remove unused imports

* dev: fix imports for authentication

* dev: update endpoints to use rest framework api viewa

* fix: onboarding fixes

* dev: session model changes

* fix: session model and add check for last name first name and avatar

* dev: fix referer redirect

* dev: remove auth imports

* dev: fix imports

* dev: update migrations

* fix: instance admin login

* comflict: conflicts resolved

* dev: fix import errors and email check endpoint

* fix: error messages and redirects after login

* dev: configs api

* fix: is github enabled boolean

* dev: merge config and instance api

* conflict: merge conflict resolved

* dev: instance admin sign up endpoint

* dev: enable magic link login

* dev: configure instance variables for github and google enabled

* chore: typo fixes

* fix: god mode docker file changes

* build-error: resolved build errors

* fix: docker compose changes

* dev: add email credential check endpoint

* fix: minor package changes

* fix: docker related changes

* dev: add nginx rules in the nginx template

* dev: refactor the url patterns

* fix: docker changes

* fix: docker files for god-mode

* fix: static export

* fix: nginx conf

* dev: smtp sender refused exception

* fix: godmode fixes

* chore: god mode revamp.

* dev: add csrf secured flag

* fix: oauth redirect uri and session settings

* chore: god mode app changes.  (#3982)

* chore: send test email functionality.

* style: authentication methods page UI revamp.

* chore: create workspace popup.

* fix: user me endpoint

* dev: fix redirection after authentication

* dev: handle god mode redirection

* fix: redirections

* fix: auth related hooks

* fix: store related fixes

* dev: fix session authentication for rest apis

* fix: linting errors

* fix: removing references of useStore=

* dev: fix redirection and password validation

* dev: add useUser hook

* fix: build fixes and lint issues

* fix: removing useApplication hook

* fix: build errors

* fix: delete unused files

* fix: auth build fixes

* fix: bugfixes

* dev: alter avatar to support more than 255 chars

* dev: fix profile endpoint and increase session expiry time and update session on every request

* chore: resolved the migration

* chore: resolved merge conflicts

* dev: error codes and error messages for the auth flow

* dev: instance admin sign up and sign in endpoint

* dev: use zxcvbn to validate password strength

* dev: add extra parameters when error handling on instance god mode

* chore: auth init

* chore: signin/ signup form ui updates and password strength meter.

* chore: update password fields.

* chore: validations and error handling.

* chore: updated sign-up form

* chore: updated workflow and updated the code structure

* chore: instance empty state for god-mode.

* chore: instance and auth wrappers update

* fix: renaming godmode

* fix: docker changes

* chore: updated authentication wrappers

* chore: updated the authentication workflow and rendered all pages

* fix: build errors

* fix: docker related fixes

* fix: tailing slash added to space and admin for valid nginx locations

* chore: seperate pages for signup and login

* git-action modified for admin file changes

* feature build action updated for admin app

* self host modified

* chore: resolved build errors and handled signin and signup in a seperate route

* chore: sign-in and sign-up revamp.

* fix: migration conflicts

* dev: migrations

* chore: handled redirection

* dev: admin url

* dev: create seperate endpoint for instance admin me

* dev: instance admin endpoint

* git action fixed

* chore: handled auth wrappers

* dev: add serializer and remove print logs

* fix: build errors

* dev: fix migrations

* dev: instance folder structuring

* fix: linting errors

* chore: resolved build errors

* chore: updated store and auth workflow and updates api service types

* chore: Replaced Next Link with Anchoer tag for god-mode redirection

* add 3333 port to allowed origins

* make password login working again

* dev: fix redirection, add admin signout endpoint and fix email credential check endpoint

* fix unique code sign in

* fix small build error

* enable sign out

* dev: add google client secret variable to configure instance

* dev: add referer for redirection

* fix origin urls for oauths

* admin setup and login separation

* dev: fix user redirection and tour completed endpoint

* fix build errors

* dev: add set password endpoint

* dev: remove user creation logic for redirection

* fix unique code page

* fix forgot password

* chore: onboarding revamp.

* dev: fix workspace slug redirection in login

* chore: invited user onboarding flow update.

* chore: fix switch or delete account modal.

* fix members exception

* refactor auth flows and add invitations to auth flow

* fix sig in sign up url

* fix action url

* fix build errors

* dev: fix user set password when logging in

* dev: reset password endpoint

* chore: confirm password validation for signup and onboarding.

* enable reset password

* fix build error

* chore: minor UI updates.

* chore: forgot and reset password UI revamp.

* fix authentication re directions

* dev: auth redirections

* change url paths for signup and signin

* dev: make the user logged in when changing passwords

* dev: next path redirection for web and space app

* dev: next path for magic sign in endpoint

* dev: github space endpoint

* chore: minor ui updates and fixes in web app.

* set password screen

* fix multiple unique code generation

* dev: next path base redirection

* dev: remove print logs

* dev: auth space endpoints

* fix build errors

* dev: invalidate cache on configuration update, god mode exception errors and authentication failed code

* dev: fix space endpoints and add extra endpoints

* chore: space auth revamp.

* dev: add sign up for space app

* fix: build errors.

* fix: auth redirection logic.

* chore: space app onboarding revamp.

---------

Co-authored-by: pablohashescobar <nikhilschacko@gmail.com>
Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: Prateek Shourya <prateekshourya29@gmail.com>
Co-authored-by: Manish Gupta <59428681+mguptahub@users.noreply.github.com>
Co-authored-by: Manish Gupta <manish@mgupta.me>
Co-authored-by: = <=>
Co-authored-by: rahulramesha <rahulramesham@gmail.com>
2024-04-29 12:12:33 +05:30
654 changed files with 20435 additions and 17110 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ module.exports = {
extends: ["custom"],
settings: {
next: {
rootDir: ["web/", "space/", "admin/"],
rootDir: ["web/", "space/"],
},
},
};
+15 -15
View File
@@ -22,11 +22,11 @@ jobs:
gh_buildx_version: ${{ steps.set_env_variables.outputs.BUILDX_VERSION }}
gh_buildx_platforms: ${{ steps.set_env_variables.outputs.BUILDX_PLATFORMS }}
gh_buildx_endpoint: ${{ steps.set_env_variables.outputs.BUILDX_ENDPOINT }}
build_proxy: ${{ steps.changed_files.outputs.proxy_any_changed }}
build_apiserver: ${{ steps.changed_files.outputs.apiserver_any_changed }}
build_frontend: ${{ steps.changed_files.outputs.frontend_any_changed }}
build_admin: ${{ steps.changed_files.outputs.admin_any_changed }}
build_space: ${{ steps.changed_files.outputs.space_any_changed }}
build_web: ${{ steps.changed_files.outputs.web_any_changed }}
build_backend: ${{ steps.changed_files.outputs.backend_any_changed }}
build_proxy: ${{ steps.changed_files.outputs.proxy_any_changed }}
steps:
- id: set_env_variables
@@ -54,12 +54,8 @@ jobs:
uses: tj-actions/changed-files@v42
with:
files_yaml: |
apiserver:
- apiserver/**
proxy:
- nginx/**
admin:
- admin/**
frontend:
- web/**
- packages/**
- 'package.json'
- 'yarn.lock'
@@ -72,16 +68,20 @@ jobs:
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
web:
- web/**
admin:
- admin/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
backend:
- apiserver/**
proxy:
- nginx/**
branch_build_push_web:
if: ${{ needs.branch_build_setup.outputs.build_web == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
branch_build_push_frontend:
if: ${{ needs.branch_build_setup.outputs.build_frontend == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
@@ -236,8 +236,8 @@ jobs:
DOCKER_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKERHUB_TOKEN }}
branch_build_push_apiserver:
if: ${{ needs.branch_build_setup.outputs.build_apiserver == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
branch_build_push_backend:
if: ${{ needs.branch_build_setup.outputs.build_backend == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' || needs.branch_build_setup.outputs.gh_branch_name == 'master' }}
runs-on: ubuntu-20.04
needs: [branch_build_setup]
env:
+25 -58
View File
@@ -10,9 +10,8 @@ jobs:
runs-on: ubuntu-latest
outputs:
apiserver_changed: ${{ steps.changed-files.outputs.apiserver_any_changed }}
admin_changed: ${{ steps.changed-files.outputs.admin_any_changed }}
space_changed: ${{ steps.changed-files.outputs.space_any_changed }}
web_changed: ${{ steps.changed-files.outputs.web_any_changed }}
space_changed: ${{ steps.changed-files.outputs.deploy_any_changed }}
steps:
- uses: actions/checkout@v3
- name: Get changed files
@@ -22,20 +21,6 @@ jobs:
files_yaml: |
apiserver:
- apiserver/**
admin:
- admin/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
space:
- space/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
web:
- web/**
- packages/**
@@ -43,6 +28,13 @@ jobs:
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
deploy:
- space/**
- packages/**
- 'package.json'
- 'yarn.lock'
- 'tsconfig.json'
- 'turbo.json'
lint-apiserver:
needs: get-changed-files
@@ -53,7 +45,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.x" # Specify the Python version you need
python-version: '3.x' # Specify the Python version you need
- name: Install Pylint
run: python -m pip install ruff
- name: Install Apiserver Dependencies
@@ -61,32 +53,6 @@ jobs:
- name: Lint apiserver
run: ruff check --fix apiserver
lint-admin:
needs: get-changed-files
if: needs.get-changed-files.outputs.admin_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.x
- run: yarn install
- run: yarn lint --filter=admin
lint-space:
needs: get-changed-files
if: needs.get-changed-files.outputs.space_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.x
- run: yarn install
- run: yarn lint --filter=space
lint-web:
needs: get-changed-files
if: needs.get-changed-files.outputs.web_changed == 'true'
@@ -100,8 +66,9 @@ jobs:
- run: yarn install
- run: yarn lint --filter=web
build-admin:
needs: lint-admin
lint-space:
needs: get-changed-files
if: needs.get-changed-files.outputs.space_changed == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
@@ -110,19 +77,7 @@ jobs:
with:
node-version: 18.x
- run: yarn install
- run: yarn build --filter=admin
build-space:
needs: lint-space
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.x
- run: yarn install
- run: yarn build --filter=space
- run: yarn lint --filter=space
build-web:
needs: lint-web
@@ -135,3 +90,15 @@ jobs:
node-version: 18.x
- run: yarn install
- run: yarn build --filter=web
build-space:
needs: lint-space
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 18.x
- run: yarn install
- run: yarn build --filter=space
+11 -17
View File
@@ -5,17 +5,17 @@ on:
inputs:
web-build:
required: false
description: "Build Web"
description: 'Build Web'
type: boolean
default: true
space-build:
required: false
description: "Build Space"
description: 'Build Space'
type: boolean
default: false
admin-build:
required: false
description: "Build Admin"
description: 'Build Admin'
type: boolean
default: false
@@ -35,7 +35,7 @@ jobs:
echo "BUILD_SPACE=$BUILD_SPACE"
echo "BUILD_ADMIN=$BUILD_ADMIN"
outputs:
web-build: ${{ env.BUILD_WEB}}
web-build: ${{ env.BUILD_WEB}}
space-build: ${{env.BUILD_SPACE}}
admin-build: ${{env.BUILD_ADMIN}}
@@ -53,7 +53,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "18"
node-version: '18'
- name: Install AWS cli
run: |
sudo apt-get update
@@ -79,7 +79,7 @@ jobs:
FILE_EXPIRY=$(date -u -d "+2 days" +"%Y-%m-%dT%H:%M:%SZ")
aws s3 cp $TAR_NAME s3://${{ env.AWS_BUCKET }}/${{github.sha}}/$TAR_NAME --expires $FILE_EXPIRY
feature-build-space:
if: ${{ needs.setup-feature-build.outputs.space-build == 'true' }}
needs: setup-feature-build
@@ -89,7 +89,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
NEXT_PUBLIC_SPACE_BASE_PATH: "/spaces"
NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1
NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }}
outputs:
do-build: ${{ needs.setup-feature-build.outputs.space-build }}
@@ -98,7 +98,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "18"
node-version: '18'
- name: Install AWS cli
run: |
sudo apt-get update
@@ -134,7 +134,7 @@ jobs:
AWS_ACCESS_KEY_ID: ${{ vars.FEATURE_PREVIEW_AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.FEATURE_PREVIEW_AWS_SECRET_ACCESS_KEY }}
AWS_BUCKET: ${{ vars.FEATURE_PREVIEW_AWS_BUCKET }}
NEXT_PUBLIC_ADMIN_BASE_PATH: "/god-mode"
NEXT_PUBLIC_DEPLOY_WITH_NGINX: 1
NEXT_PUBLIC_API_BASE_URL: ${{ vars.FEATURE_PREVIEW_NEXT_PUBLIC_API_BASE_URL }}
outputs:
do-build: ${{ needs.setup-feature-build.outputs.admin-build }}
@@ -143,7 +143,7 @@ jobs:
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "18"
node-version: '18'
- name: Install AWS cli
run: |
sudo apt-get update
@@ -172,13 +172,7 @@ jobs:
feature-deploy:
if: ${{ always() && (needs.setup-feature-build.outputs.web-build == 'true' || needs.setup-feature-build.outputs.space-build == 'true' || needs.setup-feature-build.outputs.admin-build == 'true') }}
needs:
[
setup-feature-build,
feature-build-web,
feature-build-space,
feature-build-admin,
]
needs: [setup-feature-build, feature-build-web, feature-build-space, feature-build-admin]
name: Feature Deploy
runs-on: ubuntu-latest
env:
-1
View File
@@ -81,4 +81,3 @@ tmp/
## packages
dist
.temp/
deploy/selfhost/plane-app/
+7 -7
View File
@@ -7,7 +7,7 @@
</p>
<h3 align="center"><b>Plane</b></h3>
<p align="center"><b>Open-source project management that unlocks customer value</b></p>
<p align="center"><b>Open-source project management that unlocks customer value.</b></p>
<p align="center">
<a href="https://discord.com/invite/A92xrEGCge">
@@ -40,22 +40,22 @@
</a>
</p>
Meet [Plane](https://dub.sh/plane-website-readme), an open-source project management tool to track issues, run ~sprints~ cycles, and manage product roadmaps without the chaos of managing the tool itself. 🧘‍♀️
Meet [Plane](https://dub.sh/plane-website-readme). An open-source software development tool to manage issues, sprints, and product roadmaps with peace of mind. 🧘‍♀️
> Plane is evolving every day. Your suggestions, ideas, and reported bugs help us immensely. Do not hesitate to join in the conversation on [Discord](https://discord.com/invite/A92xrEGCge) or raise a GitHub issue. We read everything and respond to most.
> Plane is still in its early days, not everything will be perfect yet, and hiccups may happen. Please let us know of any suggestions, ideas, or bugs that you encounter on our [Discord](https://discord.com/invite/A92xrEGCge) or GitHub issues, and we will use your feedback to improve in our upcoming releases.
## ⚡ Installation
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account.
The easiest way to get started with Plane is by creating a [Plane Cloud](https://app.plane.so) account where we offer a hosted solution for users.
If you would like to self-host Plane, please see our [deployment guide](https://docs.plane.so/docker-compose).
If you want more control over your data, prefer to self-host Plane, please refer to our [deployment documentation](https://docs.plane.so/docker-compose).
| Installation methods | Docs link |
| Installation Methods | Documentation Link |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Docker | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=for-the-badge&logo=docker&logoColor=white)](https://docs.plane.so/self-hosting/methods/docker-compose) |
| Kubernetes | [![Kubernetes](https://img.shields.io/badge/kubernetes-%23326ce5.svg?style=for-the-badge&logo=kubernetes&logoColor=white)](https://docs.plane.so/kubernetes) |
`Instance admins` can configure instance settings with [God-mode](https://docs.plane.so/instance-admin).
`Instance admin` can configure instance settings using our [God-mode](https://docs.plane.so/instance-admin) feature.
## 🚀 Features
-3
View File
@@ -1,3 +0,0 @@
NEXT_PUBLIC_API_BASE_URL=""
NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
NEXT_PUBLIC_WEB_BASE_URL=""
+7 -30
View File
@@ -1,6 +1,3 @@
# *****************************************************************************
# STAGE 1: Build the project
# *****************************************************************************
FROM node:18-alpine AS builder
RUN apk add --no-cache libc6-compat
WORKDIR /app
@@ -10,9 +7,6 @@ COPY . .
RUN turbo prune --scope=admin --docker
# *****************************************************************************
# STAGE 2: Install dependencies & build the project
# *****************************************************************************
FROM node:18-alpine AS installer
RUN apk add --no-cache libc6-compat
@@ -27,25 +21,13 @@ COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
ARG NEXT_PUBLIC_API_BASE_URL=""
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_URL="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
RUN yarn turbo run build --filter=admin
# *****************************************************************************
# STAGE 3: Copy the project and start it
# *****************************************************************************
FROM node:18-alpine AS runner
WORKDIR /app
@@ -56,17 +38,12 @@ COPY --from=installer /app/admin/.next/standalone ./
COPY --from=installer /app/admin/.next/static ./admin/.next/static
COPY --from=installer /app/admin/public ./admin/public
ARG NEXT_PUBLIC_API_BASE_URL=""
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ARG NEXT_PUBLIC_DEPLOY_WITH_NGINX=1
ARG NEXT_PUBLIC_WEB_BASE_URL=""
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
ARG NEXT_PUBLIC_SPACE_BASE_URL="/spaces"
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
ENV NEXT_PUBLIC_DEPLOY_WITH_NGINX=$NEXT_PUBLIC_DEPLOY_WITH_NGINX
ENV NEXT_TELEMETRY_DISABLED 1
ENV TURBO_TELEMETRY_DISABLED 1
-17
View File
@@ -1,17 +0,0 @@
FROM node:18-alpine
RUN apk add --no-cache libc6-compat
# Set working directory
WORKDIR /app
COPY . .
RUN yarn global add turbo
RUN yarn install
ENV NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
EXPOSE 3000
VOLUME [ "/app/node_modules", "/app/admin/node_modules" ]
CMD ["yarn", "dev", "--filter=admin"]
+2 -2
View File
@@ -6,7 +6,7 @@ import { IFormattedInstanceConfiguration, TInstanceAIConfigurationKeys } from "@
// components
import { ControllerInput, TControllerInputFormField } from "components/common";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks";
type IInstanceAIForm = {
config: IFormattedInstanceConfiguration;
@@ -96,7 +96,7 @@ export const InstanceAIForm: FC<IInstanceAIForm> = (props) => {
<div className="pb-1 text-xl font-medium text-custom-text-100">OpenAI</div>
<div className="text-sm font-normal text-custom-text-300">If you use ChatGPT, this is for you.</div>
</div>
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-x-12 gap-y-8 lg:grid-cols-3">
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-x-16 gap-y-8 lg:grid-cols-3">
{aiFormFields.map((field) => (
<ControllerInput
key={field.key}
+1 -1
View File
@@ -7,7 +7,7 @@ import { Loader } from "@plane/ui";
import { PageHeader } from "@/components/core";
import { InstanceAIForm } from "./components";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks";
const InstanceAIPage = observer(() => {
// store
@@ -3,7 +3,7 @@
import React from "react";
import { observer } from "mobx-react-lite";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks";
// ui
import { ToggleSwitch } from "@plane/ui";
// types
@@ -3,7 +3,7 @@
import React from "react";
import { observer } from "mobx-react-lite";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks";
// ui
import { ToggleSwitch } from "@plane/ui";
// types
@@ -2,7 +2,7 @@ import { FC, useState } from "react";
import { useForm } from "react-hook-form";
import Link from "next/link";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks";
// ui
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
// components
@@ -4,7 +4,7 @@ import React from "react";
import Link from "next/link";
import { observer } from "mobx-react-lite";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks";
// ui
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
// icons
+1 -1
View File
@@ -11,7 +11,7 @@ import { PageHeader } from "@/components/core";
import { AuthenticationMethodCard } from "../components";
import { InstanceGithubConfigForm } from "./components";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks";
// helpers
import { resolveGeneralTheme } from "@/helpers/common.helper";
// icons
@@ -2,7 +2,7 @@ import { FC, useState } from "react";
import { useForm } from "react-hook-form";
import Link from "next/link";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks";
// ui
import { Button, TOAST_TYPE, getButtonStyling, setToast } from "@plane/ui";
// components
@@ -4,7 +4,7 @@ import React from "react";
import Link from "next/link";
import { observer } from "mobx-react-lite";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks";
// ui
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
// icons
+1 -1
View File
@@ -10,7 +10,7 @@ import { PageHeader } from "@/components/core";
import { AuthenticationMethodCard } from "../components";
import { InstanceGoogleConfigForm } from "./components";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks";
// icons
import GoogleLogo from "@/public/logos/google-logo.svg";
+1 -1
View File
@@ -14,7 +14,7 @@ import { GoogleConfiguration } from "./google/components";
import { GithubConfiguration } from "./github/components";
import { PageHeader } from "@/components/core";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks";
// helpers
import { resolveGeneralTheme } from "@/helpers/common.helper";
// images
+43 -104
View File
@@ -1,9 +1,9 @@
import React, { FC, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { FC, useState } from "react";
import { Controller, useForm } from "react-hook-form";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks";
// ui
import { Button, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui";
import { Button, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
// components
import { ControllerInput, TControllerInputFormField } from "components/common";
import { SendTestEmailModal } from "./test-email-modal";
@@ -16,14 +16,6 @@ type IInstanceEmailForm = {
type EmailFormValues = Record<TInstanceEmailConfigurationKeys, string>;
type TEmailSecurityKeys = "EMAIL_USE_TLS" | "EMAIL_USE_SSL" | "NONE";
const EMAIL_SECURITY_OPTIONS: { [key in TEmailSecurityKeys]: string } = {
EMAIL_USE_TLS: "TLS",
EMAIL_USE_SSL: "SSL",
NONE: "No email security",
};
export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
const { config } = props;
// states
@@ -34,9 +26,8 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
const {
handleSubmit,
watch,
setValue,
control,
formState: { errors, isValid, isDirty, isSubmitting },
formState: { errors, isSubmitting },
} = useForm<EmailFormValues>({
defaultValues: {
EMAIL_HOST: config["EMAIL_HOST"],
@@ -44,7 +35,7 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
EMAIL_HOST_USER: config["EMAIL_HOST_USER"],
EMAIL_HOST_PASSWORD: config["EMAIL_HOST_PASSWORD"],
EMAIL_USE_TLS: config["EMAIL_USE_TLS"],
EMAIL_USE_SSL: config["EMAIL_USE_SSL"],
// EMAIL_USE_SSL: config["EMAIL_USE_SSL"],
EMAIL_FROM: config["EMAIL_FROM"],
},
});
@@ -66,26 +57,13 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
error: Boolean(errors.EMAIL_PORT),
required: true,
},
{
key: "EMAIL_FROM",
type: "text",
label: "Sender email address",
description:
"This is the email address your users will see when getting emails from this instance. You will need to verify this address.",
placeholder: "no-reply@projectplane.so",
error: Boolean(errors.EMAIL_FROM),
required: true,
},
];
const OptionalEmailFormFields: TControllerInputFormField[] = [
{
key: "EMAIL_HOST_USER",
type: "text",
label: "Username",
placeholder: "getitdone@projectplane.so",
error: Boolean(errors.EMAIL_HOST_USER),
required: false,
required: true,
},
{
key: "EMAIL_HOST_PASSWORD",
@@ -93,7 +71,17 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
label: "Password",
placeholder: "Password",
error: Boolean(errors.EMAIL_HOST_PASSWORD),
required: false,
required: true,
},
{
key: "EMAIL_FROM",
type: "text",
label: "From address",
description:
"This is the email address your users will see when getting emails from this instance. You will need to verify this address.",
placeholder: "no-reply@projectplane.so",
error: Boolean(errors.EMAIL_FROM),
required: true,
},
];
@@ -111,34 +99,11 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
.catch((err) => console.error(err));
};
const useTLSValue = watch("EMAIL_USE_TLS");
const useSSLValue = watch("EMAIL_USE_SSL");
const emailSecurityKey: TEmailSecurityKeys = useMemo(() => {
if (useTLSValue === "1") return "EMAIL_USE_TLS";
if (useSSLValue === "1") return "EMAIL_USE_SSL";
return "NONE";
}, [useTLSValue, useSSLValue]);
const handleEmailSecurityChange = (key: TEmailSecurityKeys) => {
if (key === "EMAIL_USE_SSL") {
setValue("EMAIL_USE_TLS", "0");
setValue("EMAIL_USE_SSL", "1");
}
if (key === "EMAIL_USE_TLS") {
setValue("EMAIL_USE_TLS", "1");
setValue("EMAIL_USE_SSL", "0");
}
if (key === "NONE") {
setValue("EMAIL_USE_TLS", "0");
setValue("EMAIL_USE_SSL", "0");
}
};
return (
<div className="space-y-8">
<div>
<SendTestEmailModal isOpen={isSendTestEmailModalOpen} handleClose={() => setIsSendTestEmailModalOpen(false)} />
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-start justify-between gap-10 lg:grid-cols-2">
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-center justify-between gap-x-20 gap-y-10 lg:grid-cols-2">
{emailFormFields.map((field) => (
<ControllerInput
key={field.key}
@@ -152,67 +117,41 @@ export const InstanceEmailForm: FC<IInstanceEmailForm> = (props) => {
required={field.required}
/>
))}
<div className="flex flex-col gap-1">
<h4 className="text-sm text-custom-text-300">Email security</h4>
<CustomSelect
value={emailSecurityKey}
label={EMAIL_SECURITY_OPTIONS[emailSecurityKey]}
onChange={handleEmailSecurityChange}
buttonClassName="rounded-md border-custom-border-200"
optionsClassName="w-full"
input
>
{Object.entries(EMAIL_SECURITY_OPTIONS).map(([key, value]) => (
<CustomSelect.Option key={key} value={key} className="w-full">
{value}
</CustomSelect.Option>
))}
</CustomSelect>
</div>
</div>
<div className="flex flex-col gap-6 my-6 pt-4 border-t border-custom-border-100">
<div className="flex w-full max-w-md flex-col gap-y-10 px-1">
<div className="mr-8 flex items-center gap-10 pt-4">
<div className="grow">
<div className="text-sm font-medium text-custom-text-100">Authentication (optional)</div>
<div className="text-xs font-normal text-custom-text-300">
We recommend setting up a username password for your SMTP server
</div>
<div className="flex w-full max-w-md flex-col gap-y-10 px-1">
<div className="mr-8 flex items-center gap-10 pt-4">
<div className="grow">
<div className="text-sm font-medium text-custom-text-100">
Turn TLS {Boolean(parseInt(watch("EMAIL_USE_TLS"))) ? "off" : "on"}
</div>
<div className="text-xs font-normal text-custom-text-300">
Use this if your email domain supports TLS.
</div>
</div>
</div>
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-center justify-between gap-10 lg:grid-cols-2">
{OptionalEmailFormFields.map((field) => (
<ControllerInput
key={field.key}
<div className="shrink-0">
<Controller
control={control}
type={field.type}
name={field.key}
label={field.label}
description={field.description}
placeholder={field.placeholder}
error={field.error}
required={field.required}
name="EMAIL_USE_TLS"
render={({ field: { value, onChange } }) => (
<ToggleSwitch
value={Boolean(parseInt(value))}
onChange={() => {
Boolean(parseInt(value)) === true ? onChange("0") : onChange("1");
}}
size="sm"
/>
)}
/>
))}
</div>
</div>
</div>
</div>
<div className="flex max-w-4xl items-center py-1 gap-4">
<Button
variant="primary"
onClick={handleSubmit(onSubmit)}
loading={isSubmitting}
disabled={!isValid || !isDirty}
>
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
{isSubmitting ? "Saving..." : "Save changes"}
</Button>
<Button
variant="outline-primary"
onClick={() => setIsSendTestEmailModalOpen(true)}
loading={isSubmitting}
disabled={!isValid}
>
<Button variant="outline-primary" onClick={() => setIsSendTestEmailModalOpen(true)} loading={isSubmitting}>
Send test email
</Button>
</div>
+1 -1
View File
@@ -7,7 +7,7 @@ import { Loader } from "@plane/ui";
import { PageHeader } from "@/components/core";
import { InstanceEmailForm } from "./components";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks";
const InstanceEmailPage = observer(() => {
// store
@@ -6,7 +6,7 @@ import { Button, Input, TOAST_TYPE, ToggleSwitch, setToast } from "@plane/ui";
// components
import { ControllerInput } from "components/common";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks";
export interface IGeneralConfigurationForm {
instance: IInstance["instance"];
+1 -1
View File
@@ -5,7 +5,7 @@ import { observer } from "mobx-react-lite";
import { PageHeader } from "@/components/core";
import { GeneralConfigurationForm } from "./components";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks";
const GeneralPage = observer(() => {
const { instance, instanceAdmins } = useInstance();
@@ -5,7 +5,7 @@ import { IFormattedInstanceConfiguration, TInstanceImageConfigurationKeys } from
// components
import { ControllerInput } from "components/common";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks";
type IInstanceImageConfigForm = {
config: IFormattedInstanceConfiguration;
+1 -1
View File
@@ -7,7 +7,7 @@ import { Loader } from "@plane/ui";
import { PageHeader } from "@/components/core";
import { InstanceImageConfigForm } from "./components";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks";
const InstanceImagePage = observer(() => {
// store
-19
View File
@@ -5,10 +5,6 @@ import { ThemeProvider } from "next-themes";
// lib
import { StoreProvider } from "@/lib/store-context";
import { AppWrapper } from "@/lib/wrappers";
// constants
import { SITE_NAME, SITE_DESCRIPTION, SITE_URL, TWITTER_USER_NAME, SITE_KEYWORDS, SITE_TITLE } from "@/constants/seo";
// helpers
import { ASSET_PREFIX } from "@/helpers/common.helper";
// styles
import "./globals.css";
@@ -18,21 +14,6 @@ interface RootLayoutProps {
const RootLayout = ({ children, ...pageProps }: RootLayoutProps) => (
<html lang="en">
<head>
<title>{SITE_TITLE}</title>
<meta property="og:site_name" content={SITE_NAME} />
<meta property="og:title" content={SITE_TITLE} />
<meta property="og:url" content={SITE_URL} />
<meta name="description" content={SITE_DESCRIPTION} />
<meta property="og:description" content={SITE_DESCRIPTION} />
<meta name="keywords" content={SITE_KEYWORDS} />
<meta name="twitter:site" content={`@${TWITTER_USER_NAME}`} />
<link rel="apple-touch-icon" sizes="180x180" href={`${ASSET_PREFIX}/favicon/apple-touch-icon.png`} />
<link rel="icon" type="image/png" sizes="32x32" href={`${ASSET_PREFIX}/favicon/favicon-32x32.png`} />
<link rel="icon" type="image/png" sizes="16x16" href={`${ASSET_PREFIX}/favicon/favicon-16x16.png`} />
<link rel="manifest" href={`${ASSET_PREFIX}/site.webmanifest.json`} />
<link rel="shortcut icon" href={`${ASSET_PREFIX}/favicon/favicon.ico`} />
</head>
<body className={`antialiased`}>
<StoreProvider {...pageProps}>
<ThemeProvider themes={["light", "dark"]} defaultTheme="system" enableSystem>
@@ -5,13 +5,13 @@ import { useSearchParams } from "next/navigation";
// services
import { AuthService } from "@/services/auth.service";
// ui
import { Button, Input, Spinner } from "@plane/ui";
import { Button, Input } from "@plane/ui";
// components
import { Banner } from "components/common";
// icons
import { Eye, EyeOff } from "lucide-react";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
import { API_BASE_URL, cn } from "@/helpers/common.helper";
// service initialization
const authService = new AuthService();
@@ -52,7 +52,6 @@ export const InstanceSignInForm: FC = (props) => {
const [showPassword, setShowPassword] = useState(false);
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [formData, setFormData] = useState<TFormData>(defaultFromData);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleFormChange = (key: keyof TFormData, value: string | boolean) =>
setFormData((prev) => ({ ...prev, [key]: value }));
@@ -86,40 +85,27 @@ export const InstanceSignInForm: FC = (props) => {
} else return { type: undefined, message: undefined };
}, [errorCode, errorMessage]);
const isButtonDisabled = useMemo(
() => (!isSubmitting && formData.email && formData.password ? false : true),
[formData.email, formData.password, isSubmitting]
);
const isButtonDisabled = useMemo(() => (formData.email && formData.password ? false : true), [formData]);
return (
<div className="relative w-full h-full overflow-hidden container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 py-10 flex flex-col justify-center items-center">
<div className="relative flex flex-col space-y-6">
<div className="relative w-full h-full overflow-hidden container mx-auto px-5 md:px-0 flex justify-center items-center">
<div className="w-full md:w-4/6 lg:w-3/6 xl:w-2/6 space-y-10">
<div className="text-center space-y-1">
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
Manage your Plane instance
</h3>
<p className="font-medium text-onboarding-text-400">
Configure instance-wide settings to secure your instance
</p>
<h3 className="text-3xl font-bold">Manage your Plane instance</h3>
<p className="font-medium text-custom-text-400">Configure instance-wide settings to secure your instance</p>
</div>
{errorData.type && errorData?.message && <Banner type="error" message={errorData?.message} />}
<form
className="space-y-4"
method="POST"
action={`${API_BASE_URL}/api/instances/admins/sign-in/`}
onSubmit={() => setIsSubmitting(true)}
onError={() => setIsSubmitting(false)}
>
<form className="space-y-4" method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-in/`}>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<div className="w-full space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="email">
Email <span className="text-red-500">*</span>
</label>
<Input
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
className="w-full"
id="email"
name="email"
type="email"
@@ -127,17 +113,16 @@ export const InstanceSignInForm: FC = (props) => {
placeholder="name@company.com"
value={formData.email}
onChange={(e) => handleFormChange("email", e.target.value)}
autoFocus
/>
</div>
<div className="w-full space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
Password <span className="text-red-500">*</span>
</label>
<div className="relative">
<Input
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
className={cn("w-full pr-10")}
id="password"
name="password"
type={showPassword ? "text" : "password"}
@@ -167,7 +152,7 @@ export const InstanceSignInForm: FC = (props) => {
</div>
<div className="py-2">
<Button type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Sign in"}
Sign in
</Button>
</div>
</form>
+19
View File
@@ -0,0 +1,19 @@
"use client";
import { ReactNode } from "react";
// lib
import { AuthWrapper, InstanceWrapper } from "@/lib/wrappers";
// helpers
import { EAuthenticationPageType, EInstancePageType } from "@/helpers";
interface LoginLayoutProps {
children: ReactNode;
}
const LoginLayout = ({ children }: LoginLayoutProps) => (
<InstanceWrapper pageType={EInstancePageType.POST_SETUP}>
<AuthWrapper authType={EAuthenticationPageType.NOT_AUTHENTICATED}>{children}</AuthWrapper>
</InstanceWrapper>
);
export default LoginLayout;
+18
View File
@@ -0,0 +1,18 @@
"use client";
// layouts
import { DefaultLayout } from "@/layouts";
// components
import { PageHeader } from "@/components/core";
import { InstanceSignInForm } from "./components";
const LoginPage = () => (
<>
<PageHeader title="Setup - God Mode" />
<DefaultLayout>
<InstanceSignInForm />
</DefaultLayout>
</>
);
export default LoginPage;
+14 -20
View File
@@ -1,26 +1,20 @@
"use client";
// layouts
import { DefaultLayout } from "@/layouts";
import { useEffect } from "react";
import { useRouter } from "next/navigation";
// components
import { PageHeader } from "@/components/core";
import { InstanceSignInForm } from "@/components/login";
// lib
import { AuthWrapper, InstanceWrapper } from "@/lib/wrappers";
// helpers
import { EAuthenticationPageType, EInstancePageType } from "@/helpers";
const LoginPage = () => (
<>
<PageHeader title="Login - God Mode" />
<InstanceWrapper pageType={EInstancePageType.POST_SETUP}>
<AuthWrapper authType={EAuthenticationPageType.NOT_AUTHENTICATED}>
<DefaultLayout>
<InstanceSignInForm />
</DefaultLayout>
</AuthWrapper>
</InstanceWrapper>
</>
);
const RootPage = () => {
const router = useRouter();
export default LoginPage;
useEffect(() => router.push("/login"), [router]);
return (
<>
<PageHeader title="Plane - God Mode" />
</>
);
};
export default RootPage;
+30 -94
View File
@@ -5,13 +5,13 @@ import { useSearchParams } from "next/navigation";
// services
import { AuthService } from "@/services/auth.service";
// ui
import { Button, Checkbox, Input, Spinner } from "@plane/ui";
import { Button, Checkbox, Input } from "@plane/ui";
// components
import { Banner, PasswordStrengthMeter } from "components/common";
// icons
import { Eye, EyeOff } from "lucide-react";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
import { API_BASE_URL, cn } from "@/helpers/common.helper";
import { getPasswordStrength } from "@/helpers/password.helper";
// service initialization
@@ -39,7 +39,6 @@ type TFormData = {
email: string;
company_name: string;
password: string;
confirm_password?: string;
is_telemetry_enabled: boolean;
};
@@ -67,8 +66,6 @@ export const InstanceSignUpForm: FC = (props) => {
const [showPassword, setShowPassword] = useState(false);
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const [formData, setFormData] = useState<TFormData>(defaultFromData);
const [isPasswordInputFocused, setIsPasswordInputFocused] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
const handleFormChange = (key: keyof TFormData, value: string | boolean) =>
setFormData((prev) => ({ ...prev, [key]: value }));
@@ -110,27 +107,18 @@ export const InstanceSignUpForm: FC = (props) => {
const isButtonDisabled = useMemo(
() =>
!isSubmitting &&
formData.first_name &&
formData.email &&
formData.password &&
getPasswordStrength(formData.password) >= 3 &&
formData.password === formData.confirm_password
formData.first_name && formData.email && formData.password && getPasswordStrength(formData.password) >= 3
? false
: true,
[formData.confirm_password, formData.email, formData.first_name, formData.password, isSubmitting]
[formData]
);
return (
<div className="relative w-full h-full overflow-hidden container mx-auto max-w-lg px-10 lg:max-w-md lg:px-5 flex flex-col justify-center items-center">
<div className="relative flex flex-col space-y-6">
<div className="relative w-full min-h-full h-auto overflow-hidden container mx-auto px-5 lg:px-0 flex justify-center items-center">
<div className="w-full md:w-4/6 lg:w-3/6 xl:w-2/6 space-y-10">
<div className="text-center space-y-1">
<h3 className="flex gap-4 justify-center text-3xl font-bold text-onboarding-text-100">
Setup your Plane Instance
</h3>
<p className="font-medium text-onboarding-text-400">
Post setup you will be able to manage this Plane instance.
</p>
<h3 className="text-3xl font-bold">Setup your Plane Instance</h3>
<p className="font-medium text-custom-text-400">Post setup you will be able to manage this Plane instance.</p>
</div>
{errorData.type &&
@@ -139,22 +127,16 @@ export const InstanceSignUpForm: FC = (props) => {
<Banner type="error" message={errorData?.message} />
)}
<form
className="space-y-4"
method="POST"
action={`${API_BASE_URL}/api/instances/admins/sign-up/`}
onSubmit={() => setIsSubmitting(true)}
onError={() => setIsSubmitting(false)}
>
<form className="space-y-4" method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-up/`}>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<div className="flex items-center gap-4">
<div className="w-full space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="first_name">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="first_name">
First name <span className="text-red-500">*</span>
</label>
<Input
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
className="w-full"
id="first_name"
name="first_name"
type="text"
@@ -162,15 +144,14 @@ export const InstanceSignUpForm: FC = (props) => {
placeholder="Wilber"
value={formData.first_name}
onChange={(e) => handleFormChange("first_name", e.target.value)}
autoFocus
/>
</div>
<div className="w-full space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="last_name">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="last_name">
Last name
</label>
<Input
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
className="w-full"
id="last_name"
name="last_name"
type="text"
@@ -183,11 +164,11 @@ export const InstanceSignUpForm: FC = (props) => {
</div>
<div className="w-full space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="email">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="email">
Email <span className="text-red-500">*</span>
</label>
<Input
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
className="w-full"
id="email"
name="email"
type="email"
@@ -203,11 +184,11 @@ export const InstanceSignUpForm: FC = (props) => {
</div>
<div className="w-full space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="company_name">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="company_name">
Company name
</label>
<Input
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
className="w-full"
id="company_name"
name="company_name"
type="text"
@@ -219,12 +200,12 @@ export const InstanceSignUpForm: FC = (props) => {
</div>
<div className="w-full space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="password">
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
Set a password <span className="text-red-500">*</span>
</label>
<div className="relative">
<Input
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
className={cn("w-full pr-10")}
id="password"
name="password"
type={showPassword ? "text" : "password"}
@@ -233,8 +214,6 @@ export const InstanceSignUpForm: FC = (props) => {
value={formData.password}
onChange={(e) => handleFormChange("password", e.target.value)}
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false}
onFocus={() => setIsPasswordInputFocused(true)}
onBlur={() => setIsPasswordInputFocused(false)}
/>
{showPassword ? (
<button
@@ -257,71 +236,28 @@ export const InstanceSignUpForm: FC = (props) => {
{errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD && errorData.message && (
<p className="px-1 text-xs text-red-500">{errorData.message}</p>
)}
{isPasswordInputFocused && <PasswordStrengthMeter password={formData.password} />}
</div>
<div className="w-full space-y-1">
<label className="text-sm text-onboarding-text-300 font-medium" htmlFor="confirm_password">
Confirm password
</label>
<div className="relative">
<Input
type={showPassword ? "text" : "password"}
id="confirm_password"
name="confirm_password"
inputSize="md"
value={formData.confirm_password}
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
placeholder="Confirm password"
className="w-full border border-onboarding-border-100 !bg-onboarding-background-200 pr-12 placeholder:text-onboarding-text-400"
/>
{showPassword ? (
<button
type="button"
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(false)}
>
<EyeOff className="h-4 w-4" />
</button>
) : (
<button
type="button"
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
onClick={() => setShowPassword(true)}
>
<Eye className="h-4 w-4" />
</button>
)}
</div>
{!!formData.confirm_password && formData.password !== formData.confirm_password && (
<span className="text-sm text-red-500">Passwords don{"'"}t match</span>
)}
<PasswordStrengthMeter password={formData.password} />
</div>
<div className="relative flex items-center pt-2 gap-2">
<div>
<Checkbox
id="is_telemetry_enabled"
name="is_telemetry_enabled"
value={formData.is_telemetry_enabled ? "True" : "False"}
onChange={() => handleFormChange("is_telemetry_enabled", !formData.is_telemetry_enabled)}
checked={formData.is_telemetry_enabled}
/>
</div>
<label
className="text-sm text-onboarding-text-300 font-medium cursor-pointer"
htmlFor="is_telemetry_enabled"
>
<Checkbox
id="is_telemetry_enabled"
name="is_telemetry_enabled"
value={formData.is_telemetry_enabled ? "True" : "False"}
onChange={() => handleFormChange("is_telemetry_enabled", !formData.is_telemetry_enabled)}
checked={formData.is_telemetry_enabled}
/>
<label className="text-sm text-custom-text-300 font-medium cursor-pointer" htmlFor="is_telemetry_enabled">
Allow Plane to anonymously collect usage events.
</label>
<a href="https://docs.plane.so/telemetry" className="text-sm font-medium text-blue-500 hover:text-blue-600">
<a href="#" className="text-sm font-medium text-blue-500 hover:text-blue-600">
See More
</a>
</div>
<div className="py-2">
<Button type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
Continue
</Button>
</div>
</form>
+35 -43
View File
@@ -1,13 +1,13 @@
"use client";
import { FC, useState, useRef } from "react";
import Link from "next/link";
import { observer } from "mobx-react-lite";
import { Transition } from "@headlessui/react";
import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
import Link from "next/link";
import { FileText, HelpCircle, MoveLeft } from "lucide-react";
// hooks
import { useInstance, useTheme } from "@/hooks/store";
import { useTheme } from "@/hooks";
// icons
import { DiscordIcon, GithubIcon } from "@plane/ui";
// assets
import packageJson from "package.json";
@@ -29,9 +29,7 @@ const helpOptions = [
},
];
export const HelpSection: FC = observer(() => {
// hooks
const { instance } = useInstance();
export const HelpSection: FC = () => {
// states
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
// store
@@ -39,46 +37,40 @@ export const HelpSection: FC = observer(() => {
// refs
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
const redirectionLink = `${instance?.config?.app_base_url ? `${instance?.config?.app_base_url}/create-workspace` : `/god-mode/`}`;
return (
<div
className={`flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-4 py-2 ${
isSidebarCollapsed ? "flex-col" : ""
}`}
>
<div className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full"}`}>
<Tooltip tooltipContent="Redirect to plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
<a
href={redirectionLink}
className={`relative px-2 py-1.5 flex items-center gap-2 font-medium rounded border border-custom-primary-100/20 bg-custom-primary-100/10 text-xs text-custom-primary-200 whitespace-nowrap`}
>
<ExternalLink size={14} />
{!isSidebarCollapsed && "Redirect to plane"}
</a>
</Tooltip>
<Tooltip tooltipContent="Help" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
<button
type="button"
className={`ml-auto grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
isSidebarCollapsed ? "w-full" : ""
}`}
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
>
<HelpCircle className="h-3.5 w-3.5" />
</button>
</Tooltip>
<Tooltip tooltipContent="Toggle sidebar" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
<button
type="button"
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
isSidebarCollapsed ? "w-full" : ""
}`}
onClick={() => toggleSidebar(!isSidebarCollapsed)}
>
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${isSidebarCollapsed ? "rotate-180" : ""}`} />
</button>
</Tooltip>
<div
className={`flex items-center gap-1 ${isSidebarCollapsed ? "flex-col justify-center" : "w-full justify-end"}`}
>
<button
type="button"
className={`grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 ${
isSidebarCollapsed ? "w-full" : ""
}`}
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
>
<HelpCircle className="h-3.5 w-3.5" />
</button>
<button
type="button"
className="grid place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:hidden"
onClick={() => toggleSidebar(!isSidebarCollapsed)}
>
<MoveLeft className="h-3.5 w-3.5" />
</button>
<button
type="button"
className={`hidden place-items-center rounded-md p-1.5 text-custom-text-200 outline-none hover:bg-custom-background-90 hover:text-custom-text-100 md:grid ${
isSidebarCollapsed ? "w-full" : ""
}`}
onClick={() => toggleSidebar(!isSidebarCollapsed)}
>
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${isSidebarCollapsed ? "rotate-180" : ""}`} />
</button>
</div>
<div className="relative">
@@ -131,4 +123,4 @@ export const HelpSection: FC = observer(() => {
</div>
</div>
);
});
};
+1 -1
View File
@@ -3,7 +3,7 @@
import { FC, useEffect, useRef } from "react";
import { observer } from "mobx-react-lite";
// hooks
import { useTheme } from "@/hooks/store";
import { useTheme } from "@/hooks";
import useOutsideClickDetector from "hooks/use-outside-click-detector";
// components
import { HelpSection, SidebarMenu, SidebarDropdown } from "@/components/admin-sidebar";
@@ -1,20 +1,15 @@
"use client";
import { Fragment, useEffect, useState } from "react";
import { Fragment } from "react";
import { useTheme as useNextTheme } from "next-themes";
import { observer } from "mobx-react-lite";
import { LogOut, UserCog2, Palette } from "lucide-react";
import { Menu, Transition } from "@headlessui/react";
import { Avatar } from "@plane/ui";
import { Avatar, TOAST_TYPE, setToast } from "@plane/ui";
// hooks
import { useTheme, useUser } from "@/hooks/store";
import { useTheme, useUser } from "@/hooks";
// helpers
import { API_BASE_URL, cn } from "@/helpers/common.helper";
// services
import { AuthService } from "@/services";
// service initialization
const authService = new AuthService();
import { API_BASE_URL } from "@/helpers/common.helper";
export const SidebarDropdown = observer(() => {
// store hooks
@@ -22,60 +17,22 @@ export const SidebarDropdown = observer(() => {
const { currentUser, signOut } = useUser();
// hooks
const { resolvedTheme, setTheme } = useNextTheme();
// state
const [csrfToken, setCsrfToken] = useState<string | undefined>(undefined);
const handleSignOut = async () => {
await signOut().catch(() =>
setToast({
type: TOAST_TYPE.ERROR,
title: "Error!",
message: "Failed to sign out. Please try again.",
})
);
};
const handleThemeSwitch = () => {
const newTheme = resolvedTheme === "dark" ? "light" : "dark";
setTheme(newTheme);
};
const handleSignOut = () => signOut();
const getSidebarMenuItems = () => (
<Menu.Items
className={cn(
"absolute left-0 z-20 mt-1.5 flex w-52 flex-col divide-y divide-custom-sidebar-border-100 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 text-xs shadow-lg outline-none",
{
"left-4": isSidebarCollapsed,
}
)}
>
<div className="flex flex-col gap-2.5 pb-2">
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
</div>
<div className="py-2">
<Menu.Item
as="button"
type="button"
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
onClick={handleThemeSwitch}
>
<Palette className="h-4 w-4 stroke-[1.5]" />
Switch to {resolvedTheme === "dark" ? "light" : "dark"} mode
</Menu.Item>
</div>
<div className="py-2">
<form method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-out/`} onSubmit={handleSignOut}>
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
<Menu.Item
as="button"
type="submit"
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
>
<LogOut className="h-4 w-4 stroke-[1.5]" />
Sign out
</Menu.Item>
</form>
</div>
</Menu.Items>
);
useEffect(() => {
if (csrfToken === undefined)
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
}, [csrfToken]);
return (
<div className="flex max-h-[3.75rem] items-center gap-x-5 gap-y-2 border-b border-custom-sidebar-border-200 px-4 py-3.5">
<div className="h-full w-full truncate">
@@ -84,30 +41,9 @@ export const SidebarDropdown = observer(() => {
isSidebarCollapsed ? "justify-center" : ""
}`}
>
<Menu as="div" className="flex-shrink-0">
<Menu.Button
className={cn("grid place-items-center outline-none", {
"cursor-default": !isSidebarCollapsed,
})}
>
<div className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded bg-custom-sidebar-background-80">
<UserCog2 className="h-5 w-5 text-custom-text-200" />
</div>
</Menu.Button>
{isSidebarCollapsed && (
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
{getSidebarMenuItems()}
</Transition>
)}
</Menu>
<div className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded bg-custom-sidebar-background-80">
<UserCog2 className="h-5 w-5 text-custom-text-200" />
</div>
{!isSidebarCollapsed && (
<div className="flex w-full gap-2">
@@ -138,7 +74,38 @@ export const SidebarDropdown = observer(() => {
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
{getSidebarMenuItems()}
<Menu.Items
className="absolute left-0 z-20 mt-1.5 flex w-52 flex-col divide-y
divide-custom-sidebar-border-100 rounded-md border border-custom-sidebar-border-200 bg-custom-sidebar-background-100 px-1 py-2 text-xs shadow-lg outline-none"
>
<div className="flex flex-col gap-2.5 pb-2">
<span className="px-2 text-custom-sidebar-text-200">{currentUser?.email}</span>
</div>
<div className="py-2">
<Menu.Item
as="button"
type="button"
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
onClick={handleThemeSwitch}
>
<Palette className="h-4 w-4 stroke-[1.5]" />
Switch to {resolvedTheme === "dark" ? "light" : "dark"} mode
</Menu.Item>
</div>
<div className="py-2">
<form method="POST" action={`${API_BASE_URL}/api/instances/admins/sign-out/`}>
<Menu.Item
as="button"
type="button"
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
onClick={handleSignOut}
>
<LogOut className="h-4 w-4 stroke-[1.5]" />
Sign out
</Menu.Item>
</form>
</div>
</Menu.Items>
</Transition>
</Menu>
)}
@@ -3,7 +3,7 @@
import { FC } from "react";
import { observer } from "mobx-react-lite";
// hooks
import { useTheme } from "@/hooks/store";
import { useTheme } from "@/hooks";
// icons
import { Menu } from "lucide-react";
@@ -6,7 +6,7 @@ import { observer } from "mobx-react-lite";
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
import { Tooltip } from "@plane/ui";
// hooks
import { useTheme } from "@/hooks/store";
import { useTheme } from "@/hooks";
// helpers
import { cn } from "@/helpers/common.helper";
+7 -9
View File
@@ -1,5 +1,5 @@
import { FC } from "react";
import { AlertCircle, CheckCircle2 } from "lucide-react";
import { AlertCircle, CheckCircle } from "lucide-react";
type TBanner = {
type: "success" | "error";
@@ -10,21 +10,19 @@ export const Banner: FC<TBanner> = (props) => {
const { type, message } = props;
return (
<div
className={`rounded-md p-2 w-full border ${type === "error" ? "bg-red-500/5 border-red-400" : "bg-green-500/5 border-green-400"}`}
>
<div className={`rounded-md p-4 w-full ${type === "error" ? "bg-red-50" : "bg-green-50"}`}>
<div className="flex items-center justify-center">
<div className="flex-shrink-0">
{type === "error" ? (
<span className="flex items-center justify-center h-6 w-6 rounded-full">
<AlertCircle className="h-5 w-5 text-red-600" aria-hidden="true" />
<span className="flex items-center justify-center h-6 w-6 bg-red-500 rounded-full">
<AlertCircle className="h-5 w-5 text-white" aria-hidden="true" />
</span>
) : (
<CheckCircle2 className="h-5 w-5 text-green-600" aria-hidden="true" />
<CheckCircle className="h-5 w-5 text-green-400" aria-hidden="true" />
)}
</div>
<div className="ml-1">
<p className={`text-sm font-medium ${type === "error" ? "text-red-600" : "text-green-600"}`}>{message}</p>
<div className="ml-3">
<p className={`text-sm font-medium ${type === "error" ? "text-red-800" : "text-green-800"} `}>{message}</p>
</div>
</div>
</div>
+2 -6
View File
@@ -6,8 +6,6 @@ import { Controller, Control } from "react-hook-form";
import { Input } from "@plane/ui";
// icons
import { Eye, EyeOff } from "lucide-react";
// helpers
import { cn } from "@/helpers/common.helper";
type Props = {
control: Control<any>;
@@ -53,9 +51,7 @@ export const ControllerInput: React.FC<Props> = (props) => {
ref={ref}
hasError={error}
placeholder={placeholder}
className={cn("w-full rounded-md font-medium", {
"pr-10": type === "password",
})}
className="w-full rounded-md font-medium"
/>
)}
/>
@@ -76,7 +72,7 @@ export const ControllerInput: React.FC<Props> = (props) => {
</button>
))}
</div>
{description && <p className="text-xs text-custom-text-300">{description}</p>}
{description && <p className="text-xs text-custom-text-400">{description}</p>}
</div>
);
};
-46
View File
@@ -1,46 +0,0 @@
import React from "react";
import Image from "next/image";
import { Button } from "@plane/ui";
type Props = {
title: string;
description?: React.ReactNode;
image?: any;
primaryButton?: {
icon?: any;
text: string;
onClick: () => void;
};
secondaryButton?: React.ReactNode;
disabled?: boolean;
};
export const EmptyState: React.FC<Props> = ({
title,
description,
image,
primaryButton,
secondaryButton,
disabled = false,
}) => (
<div className={`flex h-full w-full items-center justify-center`}>
<div className="flex w-full flex-col items-center text-center">
{image && <Image src={image} className="w-52 sm:w-60" alt={primaryButton?.text || "button image"} />}
<h6 className="mb-3 mt-6 text-xl font-semibold sm:mt-8">{title}</h6>
{description && <p className="mb-7 px-5 text-custom-text-300 sm:mb-8">{description}</p>}
<div className="flex items-center gap-4">
{primaryButton && (
<Button
variant="primary"
prependIcon={primaryButton.icon}
onClick={primaryButton.onClick}
disabled={disabled}
>
{primaryButton.text}
</Button>
)}
{secondaryButton}
</div>
</div>
</div>
);
-1
View File
@@ -4,4 +4,3 @@ export * from "./controller-input";
export * from "./copy-field";
export * from "./password-strength-meter";
export * from "./banner";
export * from "./empty-state";
+3 -3
View File
@@ -1,4 +1,4 @@
"use client";
import Head from "next/head";
type TPageHeader = {
title?: string;
@@ -9,9 +9,9 @@ export const PageHeader: React.FC<TPageHeader> = (props) => {
const { title = "God Mode - Plane", description = "Plane god mode" } = props;
return (
<>
<Head>
<title>{title}</title>
<meta name="description" content={description} />
</>
</Head>
);
};
@@ -2,30 +2,35 @@
import React from "react";
import { observer } from "mobx-react-lite";
import Link from "next/link";
import Image from "next/image";
import { useTheme as nextUseTheme } from "next-themes";
import { useTheme } from "next-themes";
// ui
import { Button, getButtonStyling } from "@plane/ui";
// helpers
import { resolveGeneralTheme } from "helpers/common.helper";
// hooks
import { useInstance, useTheme } from "@/hooks/store";
// icons
import TakeoffIconLight from "/public/logos/takeoff-icon-light.svg";
import TakeoffIconDark from "/public/logos/takeoff-icon-dark.svg";
export const NewUserPopup: React.FC = observer(() => {
// hooks
const { isNewUserPopup, toggleNewUserPopup } = useTheme();
const { instance } = useInstance();
type Props = {
isOpen: boolean;
onClose?: () => void;
};
export const CreateWorkspacePopup: React.FC<Props> = observer((props) => {
const { isOpen, onClose } = props;
// theme
const { resolvedTheme } = nextUseTheme();
const { resolvedTheme } = useTheme();
const redirectionLink = `${instance?.config?.app_base_url ? `${instance?.config?.app_base_url}/create-workspace` : `/god-mode/`}`;
const handleClose = () => {
onClose && onClose();
};
if (!isOpen) return null;
if (!isNewUserPopup) return <></>;
return (
<div className="absolute bottom-8 right-8 p-6 w-96 border border-custom-border-100 shadow-md rounded-lg bg-custom-background-100">
<div className="absolute bottom-8 right-6 p-6 w-96 border border-custom-border-100 shadow-md rounded-xl bg-custom-background-100 z-20">
<div className="flex gap-4">
<div className="grow">
<div className="text-base font-semibold">Create workspace</div>
@@ -34,10 +39,10 @@ export const NewUserPopup: React.FC = observer(() => {
workspace, you will need to login again.
</div>
<div className="flex items-center gap-4 pt-2">
<a href={redirectionLink} className={getButtonStyling("primary", "sm")}>
<Link href="/create-workspace" className={getButtonStyling("primary", "sm")}>
Create workspace
</a>
<Button variant="neutral-primary" size="sm" onClick={toggleNewUserPopup}>
</Link>
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
Close
</Button>
</div>
@@ -11,9 +11,9 @@ export const InstanceNotReady: FC = () => (
<div className="h-full w-full relative container px-5 mx-auto flex justify-center items-center">
<div className="w-auto max-w-2xl relative space-y-8 py-10">
<div className="relative flex flex-col justify-center items-center space-y-4">
<h1 className="text-3xl font-bold pb-3">Welcome aboard Plane!</h1>
<h1 className="text-3xl font-bold">Welcome aboard Plane!</h1>
<Image src={PlaneTakeOffImage} alt="Plane Logo" />
<p className="font-medium text-base text-onboarding-text-400">
<p className="font-medium text-base text-custom-text-400">
Get started by setting up your instance and workspace
</p>
</div>
-8
View File
@@ -1,8 +0,0 @@
export const SITE_NAME = "Plane | Simple, extensible, open-source project management tool.";
export const SITE_TITLE = "Plane | Simple, extensible, open-source project management tool.";
export const SITE_DESCRIPTION =
"Open-source project management tool to manage issues, sprints, and product roadmaps with peace of mind.";
export const SITE_KEYWORDS =
"software development, plan, ship, software, accelerate, code management, release management, project management, issue tracking, agile, scrum, kanban, collaboration";
export const SITE_URL = "https://app.plane.so/";
export const TWITTER_USER_NAME = "Plane | Simple, extensible, open-source project management tool.";
-126
View File
@@ -1,126 +0,0 @@
import { ReactNode } from "react";
export enum EPageTypes {
"PUBLIC" = "PUBLIC",
"NON_AUTHENTICATED" = "NON_AUTHENTICATED",
"ONBOARDING" = "ONBOARDING",
"AUTHENTICATED" = "AUTHENTICATED",
}
export enum EAuthModes {
SIGN_IN = "SIGN_IN",
SIGN_UP = "SIGN_UP",
}
export enum EAuthSteps {
EMAIL = "EMAIL",
PASSWORD = "PASSWORD",
UNIQUE_CODE = "UNIQUE_CODE",
}
export enum EAuthenticationErrorCodes {
INSTANCE_NOT_CONFIGURED = "5000",
// Admin
ADMIN_ALREADY_EXIST = "5029",
REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME = "5030",
INVALID_ADMIN_EMAIL = "5031",
INVALID_ADMIN_PASSWORD = "5032",
REQUIRED_ADMIN_EMAIL_PASSWORD = "5033",
ADMIN_AUTHENTICATION_FAILED = "5034",
ADMIN_USER_ALREADY_EXIST = "5035",
ADMIN_USER_DOES_NOT_EXIST = "5036",
}
export enum EErrorAlertType {
BANNER_ALERT = "BANNER_ALERT",
TOAST_ALERT = "TOAST_ALERT",
INLINE_FIRST_NAME = "INLINE_FIRST_NAME",
INLINE_EMAIL = "INLINE_EMAIL",
INLINE_PASSWORD = "INLINE_PASSWORD",
INLINE_EMAIL_CODE = "INLINE_EMAIL_CODE",
}
export type TAuthErrorInfo = {
type: EErrorAlertType;
code: EAuthenticationErrorCodes;
title: string;
message: ReactNode;
};
const errorCodeMessages: {
[key in EAuthenticationErrorCodes]: { title: string; message: (email?: string | undefined) => ReactNode };
} = {
[EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED]: {
title: "Instance not configured",
message: () => "Please contact your administrator to configure the instance.",
},
[EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST]: {
title: "Admin already exists",
message: () => "Admin already exists. Please sign in.",
},
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME]: {
title: "Required",
message: () => "Please enter email, password and first name.",
},
[EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL]: {
title: "Invalid email",
message: () => "Please enter a valid email.",
},
[EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD]: {
title: "Invalid password",
message: () => "Password must be at least 8 characters long.",
},
[EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD]: {
title: "Required",
message: () => "Please enter email and password.",
},
[EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED]: {
title: "Authentication failed",
message: () => "Please check your email and password and try again.",
},
[EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST]: {
title: "User already exists",
message: () => "User already exists. Please sign in.",
},
[EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST]: {
title: "User does not exist",
message: () => "User does not exist. Please sign up.",
},
};
export const authErrorHandler = (
errorCode: EAuthenticationErrorCodes,
email?: string | undefined
): TAuthErrorInfo | undefined => {
const toastAlertErrorCodes = [
EAuthenticationErrorCodes.ADMIN_ALREADY_EXIST,
EAuthenticationErrorCodes.INVALID_ADMIN_EMAIL,
EAuthenticationErrorCodes.INVALID_ADMIN_PASSWORD,
EAuthenticationErrorCodes.ADMIN_AUTHENTICATION_FAILED,
];
const bannerAlertErrorCodes = [
EAuthenticationErrorCodes.INSTANCE_NOT_CONFIGURED,
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD_FIRST_NAME,
EAuthenticationErrorCodes.REQUIRED_ADMIN_EMAIL_PASSWORD,
EAuthenticationErrorCodes.ADMIN_USER_ALREADY_EXIST,
EAuthenticationErrorCodes.ADMIN_USER_DOES_NOT_EXIST,
];
if (toastAlertErrorCodes.includes(errorCode))
return {
type: EErrorAlertType.TOAST_ALERT,
code: errorCode,
title: errorCodeMessages[errorCode]?.title || "Error",
message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.",
};
if (bannerAlertErrorCodes.includes(errorCode))
return {
type: EErrorAlertType.BANNER_ALERT,
code: errorCode,
title: errorCodeMessages[errorCode]?.title || "Error",
message: errorCodeMessages[errorCode]?.message(email) || "Something went wrong. Please try again.",
};
return undefined;
};
+1 -10
View File
@@ -1,16 +1,7 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL || "";
export const ADMIN_BASE_PATH = process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "";
export const SPACE_BASE_URL = process.env.NEXT_PUBLIC_SPACE_BASE_URL || "";
export const SPACE_BASE_PATH = process.env.NEXT_PUBLIC_SPACE_BASE_PATH || "";
export const WEB_BASE_URL = process.env.NEXT_PUBLIC_WEB_BASE_URL || "";
export const ASSET_PREFIX = ADMIN_BASE_PATH;
export const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ? process.env.NEXT_PUBLIC_API_BASE_URL : "";
export const cn = (...inputs: ClassValue[]) => twMerge(clsx(inputs));
+6
View File
@@ -0,0 +1,6 @@
export * from "./use-outside-click-detector";
// store-hooks
export * from "./store/use-theme";
export * from "./store/use-instance";
export * from "./store/use-user";
-3
View File
@@ -1,3 +0,0 @@
export * from "./use-theme";
export * from "./use-instance";
export * from "./use-user";
-3
View File
@@ -1,8 +1,6 @@
import { FC, ReactNode } from "react";
// components
import { InstanceSidebar } from "@/components/admin-sidebar";
import { InstanceHeader } from "@/components/auth-header";
import { NewUserPopup } from "@/components/new-user-popup";
type TAdminLayout = {
children: ReactNode;
@@ -18,7 +16,6 @@ export const AdminLayout: FC<TAdminLayout> = (props) => {
<InstanceHeader />
<div className="h-full w-full overflow-hidden">{children}</div>
</main>
<NewUserPopup />
</div>
);
};
+12 -23
View File
@@ -2,42 +2,31 @@
import { FC, ReactNode } from "react";
import Image from "next/image";
import { useTheme } from "next-themes";
// logo/ images
import PlaneBackgroundPatternDark from "public/auth/background-pattern-dark.svg";
import PlaneBackgroundPattern from "public/auth/background-pattern.svg";
import { usePathname } from "next/navigation";
// logo
import BluePlaneLogoWithoutText from "public/plane-logos/blue-without-text.png";
type TDefaultLayout = {
children: ReactNode;
withoutBackground?: boolean;
};
export const DefaultLayout: FC<TDefaultLayout> = (props) => {
const { children, withoutBackground = false } = props;
// hooks
const { resolvedTheme } = useTheme();
const { children } = props;
const pathname = usePathname();
console.log("pathname", pathname);
return (
<div className="relative">
<div className="h-screen w-full overflow-hidden overflow-y-auto flex flex-col">
<div className="container h-[110px] flex-shrink-0 mx-auto px-5 lg:px-0 flex items-center justify-between gap-5 z-50">
<div className="flex items-center gap-x-2 py-10">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" />
<div className="relative h-screen max-h-max w-full overflow-hidden overflow-y-auto flex flex-col">
<div className="flex-shrink-0 h-[120px]">
<div className="relative h-full container mx-auto px-5 lg:px-0 flex items-center justify-between gap-5 z-50">
<div className="flex items-center gap-x-2">
<Image src={BluePlaneLogoWithoutText} height={30} width={30} alt="Plane Logo" className="mr-2" />
<span className="text-2xl font-semibold sm:text-3xl">Plane</span>
</div>
</div>
{!withoutBackground && (
<div className="absolute inset-0 z-0">
<Image
src={resolvedTheme === "dark" ? PlaneBackgroundPatternDark : PlaneBackgroundPattern}
className="w-screen h-full object-cover"
alt="Plane background pattern"
/>
</div>
)}
<div className="relative z-10 mb-[110px] flex-grow">{children}</div>
</div>
<div className="w-full flex-grow">{children}</div>
</div>
);
};
+1 -1
View File
@@ -4,7 +4,7 @@ import { ReactElement, createContext } from "react";
// mobx store
import { RootStore } from "@/store/root-store";
export let rootStore = new RootStore();
let rootStore = new RootStore();
export const StoreContext = createContext<RootStore>(rootStore);
+2 -2
View File
@@ -4,11 +4,11 @@ import { FC, ReactNode, useEffect, Suspense } from "react";
import { observer } from "mobx-react-lite";
import { SWRConfig } from "swr";
// hooks
import { useTheme, useUser } from "@/hooks/store";
import { useTheme, useUser } from "@/hooks";
// ui
import { Toast } from "@plane/ui";
// constants
import { SWR_CONFIG } from "@/constants/swr-config";
import { SWR_CONFIG } from "constants/swr-config";
// helpers
import { resolveGeneralTheme } from "helpers/common.helper";
+25 -25
View File
@@ -1,14 +1,14 @@
"use client";
import { FC, ReactNode } from "react";
import { useRouter } from "next/navigation";
import { observer } from "mobx-react-lite";
import useSWR from "swr";
import { Spinner } from "@plane/ui";
// hooks
import { useInstance, useUser } from "@/hooks/store";
import { useInstance, useUser } from "@/hooks";
// helpers
import { EAuthenticationPageType } from "@/helpers";
import { EAuthenticationPageType, EUserStatus } from "@/helpers";
import { redirect } from "next/navigation";
export interface IAuthWrapper {
children: ReactNode;
@@ -16,41 +16,41 @@ export interface IAuthWrapper {
}
export const AuthWrapper: FC<IAuthWrapper> = observer((props) => {
const router = useRouter();
// props
const { children, authType = EAuthenticationPageType.AUTHENTICATED } = props;
// hooks
const { instance } = useInstance();
const { isLoading, currentUser, fetchCurrentUser } = useUser();
const { instance, fetchInstanceAdmins } = useInstance();
const { isLoading, userStatus, currentUser, fetchCurrentUser } = useUser();
const { isLoading: isSWRLoading } = useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), {
useSWR("CURRENT_USER_DETAILS", () => fetchCurrentUser(), {
shouldRetryOnError: false,
});
useSWR("INSTANCE_ADMINS", () => fetchInstanceAdmins(), {
shouldRetryOnError: false,
});
if (isSWRLoading || isLoading)
if (isLoading)
return (
<div className="relative flex h-screen w-full items-center justify-center">
<Spinner />
</div>
);
if (authType === EAuthenticationPageType.NOT_AUTHENTICATED) {
if (currentUser === undefined) return <>{children}</>;
else {
router.push("/general/");
return <></>;
}
}
if (userStatus && userStatus?.status === EUserStatus.ERROR)
return (
<div className="relative flex h-screen w-screen items-center justify-center">
Something went wrong. please try again later
</div>
);
if (authType === EAuthenticationPageType.AUTHENTICATED) {
if (currentUser) return <>{children}</>;
else {
if (instance && instance?.instance?.is_setup_done) {
router.push("/");
return <></>;
} else {
router.push("/setup/");
return <></>;
if ([EAuthenticationPageType.AUTHENTICATED, EAuthenticationPageType.NOT_AUTHENTICATED].includes(authType)) {
if (authType === EAuthenticationPageType.NOT_AUTHENTICATED) {
if (currentUser === undefined) return <>{children}</>;
else redirect("/general/");
} else {
if (currentUser) return <>{children}</>;
else {
if (instance?.instance?.is_setup_done) redirect("/login/");
else redirect("/setup/");
}
}
}
+10 -17
View File
@@ -10,10 +10,9 @@ import { DefaultLayout } from "@/layouts";
// components
import { InstanceNotReady } from "@/components/instance";
// hooks
import { useInstance } from "@/hooks/store";
import { useInstance } from "@/hooks";
// helpers
import { EInstancePageType } from "@/helpers";
import { EmptyState } from "@/components/common";
import { EInstancePageType, EInstanceStatus } from "@/helpers";
type TInstanceWrapper = {
children: ReactNode;
@@ -25,40 +24,34 @@ export const InstanceWrapper: FC<TInstanceWrapper> = observer((props) => {
const searchparams = useSearchParams();
const authEnabled = searchparams.get("auth_enabled") || "1";
// hooks
const { isLoading, instance, fetchInstanceInfo } = useInstance();
const { isLoading, instanceStatus, instance, fetchInstanceInfo } = useInstance();
const { isLoading: isSWRLoading } = useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), {
useSWR("INSTANCE_INFORMATION", () => fetchInstanceInfo(), {
revalidateOnFocus: false,
revalidateIfStale: false,
revalidateOnReconnect: false,
errorRetryCount: 0,
});
if (isSWRLoading || isLoading)
if (isLoading)
return (
<div className="relative flex h-screen w-full items-center justify-center">
<Spinner />
</div>
);
if (!instance) {
if (instanceStatus && instanceStatus?.status === EInstanceStatus.ERROR)
return (
<EmptyState
title="Your instance wasn't configured successfully."
description="Please try re-installing Plane to fix the problem. If the issue still persists please reach out to support@plane.so."
/>
<div className="relative flex h-screen w-screen items-center justify-center">
Something went wrong. please try again later
</div>
);
}
if (instance?.instance?.is_setup_done === false && authEnabled === "1")
return (
<DefaultLayout withoutBackground>
<DefaultLayout>
<InstanceNotReady />
</DefaultLayout>
);
if (instance?.instance?.is_setup_done && pageType === EInstancePageType.PRE_SETUP) redirect("/");
if (!instance?.instance?.is_setup_done && pageType === EInstancePageType.POST_SETUP) redirect("/setup");
return <>{children}</>;
+1 -1
View File
@@ -7,7 +7,7 @@ const nextConfig = {
images: {
unoptimized: true,
},
basePath: process.env.NEXT_PUBLIC_ADMIN_BASE_PATH || "",
basePath: process.env.NEXT_PUBLIC_DEPLOY_WITH_NGINX === "1" ? "/god-mode" : "",
};
module.exports = nextConfig;
+4 -5
View File
@@ -4,14 +4,13 @@
"private": true,
"scripts": {
"dev": "turbo run develop",
"develop": "next dev --port 3001",
"develop": "next dev --port 3333",
"build": "next build",
"preview": "next build && next start",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@headlessui/react": "^1.7.19",
"@plane/types": "*",
"@plane/ui": "*",
"@tailwindcss/typography": "^0.5.9",
@@ -23,11 +22,11 @@
"lucide-react": "^0.356.0",
"mobx": "^6.12.0",
"mobx-react-lite": "^4.0.5",
"next": "^14.2.3",
"next": "^14.1.0",
"next-themes": "^0.2.1",
"postcss": "8.4.23",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.0",
"swr": "^2.2.4",
"tailwindcss": "3.3.2",
Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 919 B

-11
View File
@@ -1,11 +0,0 @@
{
"name": "",
"short_name": "",
"icons": [
{ "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
-13
View File
@@ -1,13 +0,0 @@
{
"name": "Plane God Mode",
"short_name": "Plane God Mode",
"description": "Plane helps you plan your issues, cycles, and product modules.",
"start_url": ".",
"display": "standalone",
"background_color": "#f9fafb",
"theme_color": "#3f76ff",
"icons": [
{ "src": "/favicon/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" },
{ "src": "/favicon/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }
]
}
+2 -5
View File
@@ -1,6 +1,4 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from "axios";
// store
import { rootStore } from "@/lib/store-context";
export abstract class APIService {
protected baseURL: string;
@@ -20,9 +18,8 @@ export abstract class APIService {
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);
if (error.response && error.response.status === 401) window.location.href = "/login";
return Promise.reject(error.response?.data ?? error);
}
);
}
+23
View File
@@ -19,4 +19,27 @@ export class AuthService extends APIService {
throw error;
});
}
async signOut(baseUrl: string): Promise<any> {
await this.requestCSRFToken().then((data) => {
const csrfToken = data?.csrf_token;
if (!csrfToken) throw Error("CSRF token not found");
var form = document.createElement("form");
var element1 = document.createElement("input");
form.method = "POST";
form.action = `${baseUrl}/api/instances/admins/sign-out/`;
element1.value = csrfToken;
element1.name = "csrfmiddlewaretoken";
element1.type = "hidden";
form.appendChild(element1);
document.body.appendChild(form);
form.submit();
});
}
}
+2 -2
View File
@@ -1,8 +1,8 @@
import { APIService } from "services/api.service";
// types
import type { IFormattedInstanceConfiguration, IInstance, IInstanceAdmin, IInstanceConfiguration } from "@plane/types";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
import { APIService } from "@/services/api.service";
import { API_BASE_URL } from "helpers/common.helper";
export class InstanceService extends APIService {
constructor() {
-2
View File
@@ -75,8 +75,6 @@ export class InstanceStore implements IInstanceStore {
try {
if (this.instance === undefined) this.isLoading = true;
const instance = await this.instanceService.getInstanceInfo();
// handling the new user popup toggle
if (this.instance === undefined && !instance?.instance?.workspaces_exist) this.store.theme.toggleNewUserPopup();
runInAction(() => {
this.isLoading = false;
this.instance = instance;
+1 -3
View File
@@ -18,10 +18,8 @@ export class RootStore {
}
resetOnSignOut() {
localStorage.setItem("theme", "system");
this.theme = new ThemeStore(this);
this.instance = new InstanceStore(this);
this.user = new UserStore(this);
this.theme = new ThemeStore(this);
}
}
+2 -12
View File
@@ -5,41 +5,31 @@ import { RootStore } from "@/store/root-store";
type TTheme = "dark" | "light";
export interface IThemeStore {
// observables
isNewUserPopup: boolean;
theme: string | undefined;
isSidebarCollapsed: boolean | undefined;
// actions
toggleNewUserPopup: () => void;
toggleSidebar: (collapsed: boolean) => void;
setTheme: (currentTheme: TTheme) => void;
}
export class ThemeStore implements IThemeStore {
// observables
isNewUserPopup: boolean = false;
isSidebarCollapsed: boolean | undefined = undefined;
theme: string | undefined = undefined;
constructor(private store: RootStore) {
makeObservable(this, {
// observables
isNewUserPopup: observable.ref,
isSidebarCollapsed: observable.ref,
theme: observable.ref,
// action
toggleNewUserPopup: action,
toggleSidebar: action,
setTheme: action,
});
}
/**
* @description Toggle the new user popup modal
*/
toggleNewUserPopup = () => (this.isNewUserPopup = !this.isNewUserPopup);
/**
* @description Toggle the sidebar collapsed state
* Toggle the sidebar collapsed state
* @param isCollapsed
*/
toggleSidebar = (isCollapsed: boolean) => {
@@ -49,7 +39,7 @@ export class ThemeStore implements IThemeStore {
};
/**
* @description Sets the user theme and applies it to the platform
* Sets the user theme and applies it to the platform
* @param currentTheme
*/
setTheme = async (currentTheme: TTheme) => {
+13 -27
View File
@@ -3,10 +3,11 @@ import { IUser } from "@plane/types";
// helpers
import { EUserStatus, TUserStatus } from "@/helpers";
// services
import { UserService } from "@/services/user.service";
import { UserService } from "services/user.service";
// root store
import { RootStore } from "@/store/root-store";
import { AuthService } from "@/services";
import { API_BASE_URL } from "@/helpers/common.helper";
export interface IUserStore {
// observables
@@ -16,8 +17,7 @@ export interface IUserStore {
currentUser: IUser | undefined;
// fetch actions
fetchCurrentUser: () => Promise<IUser>;
reset: () => void;
signOut: () => void;
signOut: () => Promise<void>;
}
export class UserStore implements IUserStore {
@@ -29,6 +29,8 @@ export class UserStore implements IUserStore {
// services
userService;
authService;
// rootStore
rootStore;
constructor(private store: RootStore) {
makeObservable(this, {
@@ -39,11 +41,10 @@ export class UserStore implements IUserStore {
currentUser: observable,
// action
fetchCurrentUser: action,
reset: action,
signOut: action,
});
this.userService = new UserService();
this.authService = new AuthService();
this.rootStore = store;
}
/**
@@ -54,20 +55,11 @@ export class UserStore implements IUserStore {
try {
if (this.currentUser === undefined) this.isLoading = true;
const currentUser = await this.userService.currentUser();
if (currentUser) {
await this.store.instance.fetchInstanceAdmins();
runInAction(() => {
this.isUserLoggedIn = true;
this.currentUser = currentUser;
this.isLoading = false;
});
} else {
runInAction(() => {
this.isUserLoggedIn = false;
this.currentUser = undefined;
this.isLoading = false;
});
}
runInAction(() => {
this.isUserLoggedIn = true;
this.currentUser = currentUser;
this.isLoading = false;
});
return currentUser;
} catch (error: any) {
this.isLoading = false;
@@ -86,14 +78,8 @@ export class UserStore implements IUserStore {
}
};
reset = async () => {
this.isUserLoggedIn = false;
this.currentUser = undefined;
this.isLoading = false;
this.userStatus = undefined;
};
signOut = async () => {
this.store.resetOnSignOut();
await this.authService.signOut(API_BASE_URL);
this.rootStore.resetOnSignOut();
};
}
+1 -6
View File
@@ -1,7 +1,7 @@
# Backend
# Debug value for api server use it as 0 for production use
DEBUG=0
CORS_ALLOWED_ORIGINS="http://localhost"
CORS_ALLOWED_ORIGINS=""
# Error logs
SENTRY_DSN=""
@@ -44,8 +44,3 @@ WEB_URL="http://localhost"
# Gunicorn Workers
GUNICORN_WORKERS=2
# Base URLs
ADMIN_BASE_URL=
SPACE_BASE_URL=
APP_BASE_URL=
+1 -1
View File
@@ -1,4 +1,4 @@
{
"name": "plane-api",
"version": "0.19.0"
"version": "0.18.0"
}
-4
View File
@@ -1,13 +1,9 @@
# Module improts
from .base import BaseSerializer
from .issue import IssueExpandSerializer
from plane.db.models import InboxIssue
class InboxIssueSerializer(BaseSerializer):
issue_detail = IssueExpandSerializer(read_only=True, source="issue")
class Meta:
model = InboxIssue
fields = "__all__"
+37
View File
@@ -1,4 +1,6 @@
# Python imports
from urllib.parse import urlparse
import zoneinfo
# Django imports
@@ -17,6 +19,7 @@ from rest_framework.views import APIView
# Module imports
from plane.api.middleware.api_authentication import APIKeyAuthentication
from plane.api.rate_limit import ApiKeyRateThrottle
from plane.bgtasks.webhook_task import send_webhook
from plane.utils.exception_logger import log_exception
from plane.utils.paginator import BasePaginator
@@ -35,6 +38,40 @@ class TimezoneMixin:
timezone.deactivate()
class WebhookMixin:
webhook_event = None
bulk = False
def finalize_response(self, request, response, *args, **kwargs):
response = super().finalize_response(
request, response, *args, **kwargs
)
# Check for the case should webhook be sent
if (
self.webhook_event
and self.request.method in ["POST", "PATCH", "DELETE"]
and response.status_code in [200, 201, 204]
):
url = request.build_absolute_uri()
parsed_url = urlparse(url)
# Extract the scheme and netloc
scheme = parsed_url.scheme
netloc = parsed_url.netloc
# Push the object to delay
send_webhook.delay(
event=self.webhook_event,
payload=response.data,
kw=self.kwargs,
action=self.request.method,
slug=self.workspace_slug,
bulk=self.bulk,
current_site=f"{scheme}://{netloc}",
)
return response
class BaseAPIView(TimezoneMixin, APIView, BasePaginator):
authentication_classes = [
APIKeyAuthentication,
+3 -31
View File
@@ -5,7 +5,6 @@ import json
from django.core import serializers
from django.db.models import Count, F, Func, OuterRef, Q, Sum
from django.utils import timezone
from django.core.serializers.json import DjangoJSONEncoder
# Third party imports
from rest_framework import status
@@ -27,11 +26,10 @@ from plane.db.models import (
)
from plane.utils.analytics_plot import burndown_plot
from .base import BaseAPIView
from plane.bgtasks.webhook_task import model_activity
from .base import BaseAPIView, WebhookMixin
class CycleAPIEndpoint(BaseAPIView):
class CycleAPIEndpoint(WebhookMixin, BaseAPIView):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions related to cycle.
@@ -279,16 +277,6 @@ class CycleAPIEndpoint(BaseAPIView):
project_id=project_id,
owned_by=request.user,
)
# Send the model activity
model_activity.delay(
model_name="cycle",
model_id=str(serializer.data["id"]),
requested_data=request.data,
current_instance=None,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(
serializer.data, status=status.HTTP_201_CREATED
)
@@ -307,11 +295,6 @@ class CycleAPIEndpoint(BaseAPIView):
cycle = Cycle.objects.get(
workspace__slug=slug, project_id=project_id, pk=pk
)
current_instance = json.dumps(
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
)
if cycle.archived_at:
return Response(
{"error": "Archived cycle cannot be edited"},
@@ -361,17 +344,6 @@ class CycleAPIEndpoint(BaseAPIView):
status=status.HTTP_409_CONFLICT,
)
serializer.save()
# Send the model activity
model_activity.delay(
model_name="cycle",
model_id=str(serializer.data["id"]),
requested_data=request.data,
current_instance=current_instance,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -543,7 +515,7 @@ class CycleArchiveUnarchiveAPIEndpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
class CycleIssueAPIEndpoint(BaseAPIView):
class CycleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
"""
This viewset automatically provides `list`, `create`,
and `destroy` actions related to cycle issues.
+8 -10
View File
@@ -154,13 +154,6 @@ class InboxIssueAPIEndpoint(BaseAPIView):
state=state,
)
# create an inbox issue
inbox_issue = InboxIssue.objects.create(
inbox_id=inbox.id,
project_id=project_id,
issue=issue,
source=request.data.get("source", "in-app"),
)
# Create an Issue Activity
issue_activity.delay(
type="issue.activity.created",
@@ -170,7 +163,14 @@ class InboxIssueAPIEndpoint(BaseAPIView):
project_id=str(project_id),
current_instance=None,
epoch=int(timezone.now().timestamp()),
inbox=str(inbox_issue.id),
)
# create an inbox issue
inbox_issue = InboxIssue.objects.create(
inbox_id=inbox.id,
project_id=project_id,
issue=issue,
source=request.data.get("source", "in-app"),
)
serializer = InboxIssueSerializer(inbox_issue)
@@ -260,7 +260,6 @@ class InboxIssueAPIEndpoint(BaseAPIView):
cls=DjangoJSONEncoder,
),
epoch=int(timezone.now().timestamp()),
inbox=(inbox_issue.id),
)
issue_serializer.save()
else:
@@ -328,7 +327,6 @@ class InboxIssueAPIEndpoint(BaseAPIView):
epoch=int(timezone.now().timestamp()),
notification=False,
origin=request.META.get("HTTP_ORIGIN"),
inbox=str(inbox_issue.id),
)
return Response(serializer.data, status=status.HTTP_200_OK)
+12 -14
View File
@@ -32,6 +32,7 @@ from plane.api.serializers import (
LabelSerializer,
)
from plane.app.permissions import (
WorkspaceEntityPermission,
ProjectEntityPermission,
ProjectLitePermission,
ProjectMemberPermission,
@@ -48,10 +49,11 @@ from plane.db.models import (
ProjectMember,
)
from .base import BaseAPIView
from .base import BaseAPIView, WebhookMixin
class WorkspaceIssueAPIEndpoint(BaseAPIView):
class WorkspaceIssueAPIEndpoint(WebhookMixin, BaseAPIView):
"""
This viewset provides `retrieveByIssueId` on workspace level
@@ -59,9 +61,12 @@ class WorkspaceIssueAPIEndpoint(BaseAPIView):
model = Issue
webhook_event = "issue"
permission_classes = [ProjectEntityPermission]
permission_classes = [
ProjectEntityPermission
]
serializer_class = IssueSerializer
@property
def project__identifier(self):
return self.kwargs.get("project__identifier", None)
@@ -87,9 +92,7 @@ class WorkspaceIssueAPIEndpoint(BaseAPIView):
.order_by(self.kwargs.get("order_by", "-created_at"))
).distinct()
def get(
self, request, slug, project__identifier=None, issue__identifier=None
):
def get(self, request, slug, project__identifier=None, issue__identifier=None):
if issue__identifier and project__identifier:
issue = Issue.issue_objects.annotate(
sub_issues_count=Issue.issue_objects.filter(
@@ -98,11 +101,7 @@ class WorkspaceIssueAPIEndpoint(BaseAPIView):
.order_by()
.annotate(count=Func(F("id"), function="Count"))
.values("count")
).get(
workspace__slug=slug,
project__identifier=project__identifier,
sequence_id=issue__identifier,
)
).get(workspace__slug=slug, project__identifier=project__identifier, sequence_id=issue__identifier)
return Response(
IssueSerializer(
issue,
@@ -112,8 +111,7 @@ class WorkspaceIssueAPIEndpoint(BaseAPIView):
status=status.HTTP_200_OK,
)
class IssueAPIEndpoint(BaseAPIView):
class IssueAPIEndpoint(WebhookMixin, BaseAPIView):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions related to issue.
@@ -655,7 +653,7 @@ class IssueLinkAPIEndpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
class IssueCommentAPIEndpoint(BaseAPIView):
class IssueCommentAPIEndpoint(WebhookMixin, BaseAPIView):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions related to comments of the particular issue.
+3 -32
View File
@@ -5,7 +5,6 @@ import json
from django.core import serializers
from django.db.models import Count, F, Func, OuterRef, Prefetch, Q
from django.utils import timezone
from django.core.serializers.json import DjangoJSONEncoder
# Third party imports
from rest_framework import status
@@ -29,11 +28,10 @@ from plane.db.models import (
Project,
)
from .base import BaseAPIView
from plane.bgtasks.webhook_task import model_activity
from .base import BaseAPIView, WebhookMixin
class ModuleAPIEndpoint(BaseAPIView):
class ModuleAPIEndpoint(WebhookMixin, BaseAPIView):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions related to module.
@@ -165,16 +163,6 @@ class ModuleAPIEndpoint(BaseAPIView):
status=status.HTTP_409_CONFLICT,
)
serializer.save()
# Send the model activity
model_activity.delay(
model_name="module",
model_id=str(serializer.data["id"]),
requested_data=request.data,
current_instance=None,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
module = Module.objects.get(pk=serializer.data["id"])
serializer = ModuleSerializer(module)
return Response(serializer.data, status=status.HTTP_201_CREATED)
@@ -184,11 +172,6 @@ class ModuleAPIEndpoint(BaseAPIView):
module = Module.objects.get(
pk=pk, project_id=project_id, workspace__slug=slug
)
current_instance = json.dumps(
ModuleSerializer(module).data, cls=DjangoJSONEncoder
)
if module.archived_at:
return Response(
{"error": "Archived module cannot be edited"},
@@ -221,18 +204,6 @@ class ModuleAPIEndpoint(BaseAPIView):
status=status.HTTP_409_CONFLICT,
)
serializer.save()
# Send the model activity
model_activity.delay(
model_name="module",
model_id=str(serializer.data["id"]),
requested_data=request.data,
current_instance=current_instance,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -289,7 +260,7 @@ class ModuleAPIEndpoint(BaseAPIView):
return Response(status=status.HTTP_204_NO_CONTENT)
class ModuleIssueAPIEndpoint(BaseAPIView):
class ModuleIssueAPIEndpoint(WebhookMixin, BaseAPIView):
"""
This viewset automatically provides `list`, `create`, `retrieve`,
`update` and `destroy` actions related to module issues.
+4 -32
View File
@@ -1,11 +1,7 @@
# Python imports
import json
# Django imports
from django.db import IntegrityError
from django.db.models import Exists, F, Func, OuterRef, Prefetch, Q, Subquery
from django.utils import timezone
from django.core.serializers.json import DjangoJSONEncoder
# Third party imports
from rest_framework import status
@@ -27,11 +23,11 @@ from plane.db.models import (
State,
Workspace,
)
from plane.bgtasks.webhook_task import model_activity
from .base import BaseAPIView
from .base import BaseAPIView, WebhookMixin
class ProjectAPIEndpoint(BaseAPIView):
class ProjectAPIEndpoint(WebhookMixin, BaseAPIView):
"""Project Endpoints to create, update, list, retrieve and delete endpoint"""
serializer_class = ProjectSerializer
@@ -240,17 +236,6 @@ class ProjectAPIEndpoint(BaseAPIView):
.filter(pk=serializer.data["id"])
.first()
)
# Model activity
model_activity.delay(
model_name="project",
model_id=str(project.id),
requested_data=request.data,
current_instance=None,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
serializer = ProjectSerializer(project)
return Response(
serializer.data, status=status.HTTP_201_CREATED
@@ -280,9 +265,7 @@ class ProjectAPIEndpoint(BaseAPIView):
try:
workspace = Workspace.objects.get(slug=slug)
project = Project.objects.get(pk=pk)
current_instance = json.dumps(
ProjectSerializer(project).data, cls=DjangoJSONEncoder
)
if project.archived_at:
return Response(
{"error": "Archived project cannot be updated"},
@@ -320,17 +303,6 @@ class ProjectAPIEndpoint(BaseAPIView):
.filter(pk=serializer.data["id"])
.first()
)
model_activity.delay(
model_name="project",
model_id=str(project.id),
requested_data=request.data,
current_instance=current_instance,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
serializer = ProjectSerializer(project)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
+1 -2
View File
@@ -442,7 +442,7 @@ class IssueLinkSerializer(BaseSerializer):
raise serializers.ValidationError("Invalid URL format.")
# Check URL scheme
if not value.startswith(("http://", "https://")):
if not value.startswith(('http://', 'https://')):
raise serializers.ValidationError("Invalid URL scheme.")
return value
@@ -636,7 +636,6 @@ class IssueInboxSerializer(DynamicBaseSerializer):
"project_id",
"created_at",
"label_ids",
"created_by",
]
read_only_fields = fields
+1 -1
View File
@@ -29,7 +29,7 @@ from .user.base import (
)
from .base import BaseAPIView, BaseViewSet
from .base import BaseAPIView, BaseViewSet, WebhookMixin
from .workspace.base import (
WorkSpaceViewSet,
+30
View File
@@ -20,6 +20,7 @@ from rest_framework.viewsets import ModelViewSet
# Module imports
from plane.authentication.session import BaseSessionAuthentication
from plane.bgtasks.webhook_task import send_webhook
from plane.utils.exception_logger import log_exception
from plane.utils.paginator import BasePaginator
@@ -38,6 +39,35 @@ class TimezoneMixin:
timezone.deactivate()
class WebhookMixin:
webhook_event = None
bulk = False
def finalize_response(self, request, response, *args, **kwargs):
response = super().finalize_response(
request, response, *args, **kwargs
)
# Check for the case should webhook be sent
if (
self.webhook_event
and self.request.method in ["POST", "PATCH", "DELETE"]
and response.status_code in [200, 201, 204]
):
# Push the object to delay
send_webhook.delay(
event=self.webhook_event,
payload=response.data,
kw=self.kwargs,
action=self.request.method,
slug=self.workspace_slug,
bulk=self.bulk,
current_site=request.META.get("HTTP_ORIGIN"),
)
return response
class BaseViewSet(TimezoneMixin, ModelViewSet, BasePaginator):
model = None
+2 -32
View File
@@ -20,7 +20,6 @@ from django.db.models import (
)
from django.db.models.functions import Coalesce
from django.utils import timezone
from django.core.serializers.json import DjangoJSONEncoder
# Third party imports
from rest_framework import status
@@ -48,11 +47,10 @@ from plane.db.models import (
from plane.utils.analytics_plot import burndown_plot
# Module imports
from .. import BaseAPIView, BaseViewSet
from plane.bgtasks.webhook_task import model_activity
from .. import BaseAPIView, BaseViewSet, WebhookMixin
class CycleViewSet(BaseViewSet):
class CycleViewSet(WebhookMixin, BaseViewSet):
serializer_class = CycleSerializer
model = Cycle
webhook_event = "cycle"
@@ -414,17 +412,6 @@ class CycleViewSet(BaseViewSet):
)
.first()
)
# Send the model activity
model_activity.delay(
model_name="cycle",
model_id=str(cycle["id"]),
requested_data=request.data,
current_instance=None,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(cycle, status=status.HTTP_201_CREATED)
return Response(
serializer.errors, status=status.HTTP_400_BAD_REQUEST
@@ -447,11 +434,6 @@ class CycleViewSet(BaseViewSet):
{"error": "Archived cycle cannot be updated"},
status=status.HTTP_400_BAD_REQUEST,
)
current_instance = json.dumps(
CycleSerializer(cycle).data, cls=DjangoJSONEncoder
)
request_data = request.data
if (
@@ -505,18 +487,6 @@ class CycleViewSet(BaseViewSet):
"assignee_ids",
"status",
).first()
# Send the model activity
model_activity.delay(
model_name="cycle",
model_id=str(cycle["id"]),
requested_data=request.data,
current_instance=current_instance,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
return Response(cycle, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+4 -10
View File
@@ -23,7 +23,7 @@ from rest_framework.response import Response
from rest_framework import status
# Module imports
from .. import BaseViewSet
from .. import BaseViewSet, WebhookMixin
from plane.app.serializers import (
IssueSerializer,
CycleIssueSerializer,
@@ -38,9 +38,9 @@ from plane.db.models import (
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.issue_filters import issue_filters
from plane.utils.user_timezone_converter import user_timezone_converter
class CycleIssueViewSet(BaseViewSet):
class CycleIssueViewSet(WebhookMixin, BaseViewSet):
serializer_class = CycleIssueSerializer
model = CycleIssue
@@ -191,11 +191,6 @@ class CycleIssueViewSet(BaseViewSet):
"is_draft",
"archived_at",
)
datetime_fields = ["created_at", "updated_at"]
issues = user_timezone_converter(
issues, datetime_fields, request.user.user_timezone
)
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id, cycle_id):
@@ -254,7 +249,6 @@ class CycleIssueViewSet(BaseViewSet):
update_cycle_issue_activity = []
# Iterate over each cycle_issue in cycle_issues
for cycle_issue in cycle_issues:
old_cycle_id = cycle_issue.cycle_id
# Update the cycle_issue's cycle_id
cycle_issue.cycle_id = cycle_id
# Add the modified cycle_issue to the records_to_update list
@@ -262,7 +256,7 @@ class CycleIssueViewSet(BaseViewSet):
# Record the update activity
update_cycle_issue_activity.append(
{
"old_cycle_id": str(old_cycle_id),
"old_cycle_id": str(cycle_issue.cycle_id),
"new_cycle_id": str(cycle_id),
"issue_id": str(cycle_issue.issue_id),
}
+12 -33
View File
@@ -251,16 +251,6 @@ class InboxIssueViewSet(BaseViewSet):
)
if serializer.is_valid():
serializer.save()
inbox_id = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
# create an inbox issue
inbox_issue = InboxIssue.objects.create(
inbox_id=inbox_id.id,
project_id=project_id,
issue_id=serializer.data["id"],
source=request.data.get("source", "in-app"),
)
# Create an Issue Activity
issue_activity.delay(
type="issue.activity.created",
@@ -272,7 +262,16 @@ class InboxIssueViewSet(BaseViewSet):
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
inbox=str(inbox_issue.id),
)
inbox_id = Inbox.objects.filter(
workspace__slug=slug, project_id=project_id
).first()
# create an inbox issue
inbox_issue = InboxIssue.objects.create(
inbox_id=inbox_id.id,
project_id=project_id,
issue_id=serializer.data["id"],
source=request.data.get("source", "in-app"),
)
inbox_issue = (
InboxIssue.objects.select_related("issue")
@@ -340,24 +339,7 @@ class InboxIssueViewSet(BaseViewSet):
# Get issue data
issue_data = request.data.pop("issue", False)
if bool(issue_data):
issue = Issue.objects.annotate(
label_ids=Coalesce(
ArrayAgg(
"labels__id",
distinct=True,
filter=~Q(labels__id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
assignee_ids=Coalesce(
ArrayAgg(
"assignees__id",
distinct=True,
filter=~Q(assignees__id__isnull=True),
),
Value([], output_field=ArrayField(UUIDField())),
),
).get(
issue = Issue.objects.get(
pk=inbox_issue.issue_id,
workspace__slug=slug,
project_id=project_id,
@@ -397,7 +379,6 @@ class InboxIssueViewSet(BaseViewSet):
epoch=int(timezone.now().timestamp()),
notification=True,
origin=request.META.get("HTTP_ORIGIN"),
inbox=str(inbox_issue.id),
)
issue_serializer.save()
else:
@@ -463,7 +444,6 @@ class InboxIssueViewSet(BaseViewSet):
epoch=int(timezone.now().timestamp()),
notification=False,
origin=request.META.get("HTTP_ORIGIN"),
inbox=(inbox_issue.id),
)
inbox_issue = (
@@ -500,8 +480,7 @@ class InboxIssueViewSet(BaseViewSet):
output_field=ArrayField(UUIDField()),
),
),
)
.first()
).first()
)
serializer = InboxIssueDetailSerializer(inbox_issue).data
return Response(serializer, status=status.HTTP_200_OK)
+1 -6
View File
@@ -47,7 +47,7 @@ from plane.db.models import (
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.issue_filters import issue_filters
from plane.utils.user_timezone_converter import user_timezone_converter
class IssueArchiveViewSet(BaseViewSet):
permission_classes = [
@@ -239,11 +239,6 @@ class IssueArchiveViewSet(BaseViewSet):
"is_draft",
"archived_at",
)
datetime_fields = ["created_at", "updated_at"]
issues = user_timezone_converter(
issues, datetime_fields, request.user.user_timezone
)
return Response(issues, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, pk=None):
+2 -15
View File
@@ -50,10 +50,9 @@ from plane.db.models import (
Project,
)
from plane.utils.issue_filters import issue_filters
from plane.utils.user_timezone_converter import user_timezone_converter
# Module imports
from .. import BaseAPIView, BaseViewSet
from .. import BaseAPIView, BaseViewSet, WebhookMixin
class IssueListEndpoint(BaseAPIView):
@@ -242,14 +241,10 @@ class IssueListEndpoint(BaseAPIView):
"is_draft",
"archived_at",
)
datetime_fields = ["created_at", "updated_at"]
issues = user_timezone_converter(
issues, datetime_fields, request.user.user_timezone
)
return Response(issues, status=status.HTTP_200_OK)
class IssueViewSet(BaseViewSet):
class IssueViewSet(WebhookMixin, BaseViewSet):
def get_serializer_class(self):
return (
IssueCreateSerializer
@@ -445,10 +440,6 @@ class IssueViewSet(BaseViewSet):
"is_draft",
"archived_at",
)
datetime_fields = ["created_at", "updated_at"]
issues = user_timezone_converter(
issues, datetime_fields, request.user.user_timezone
)
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id):
@@ -512,10 +503,6 @@ class IssueViewSet(BaseViewSet):
)
.first()
)
datetime_fields = ["created_at", "updated_at"]
issue = user_timezone_converter(
issue, datetime_fields, request.user.user_timezone
)
return Response(issue, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+2 -2
View File
@@ -11,7 +11,7 @@ from rest_framework.response import Response
from rest_framework import status
# Module imports
from .. import BaseViewSet
from .. import BaseViewSet, WebhookMixin
from plane.app.serializers import (
IssueCommentSerializer,
CommentReactionSerializer,
@@ -25,7 +25,7 @@ from plane.db.models import (
from plane.bgtasks.issue_activites_task import issue_activity
class IssueCommentViewSet(BaseViewSet):
class IssueCommentViewSet(WebhookMixin, BaseViewSet):
serializer_class = IssueCommentSerializer
model = IssueComment
webhook_event = "issue_comment"
-5
View File
@@ -45,7 +45,6 @@ from plane.db.models import (
Project,
)
from plane.utils.issue_filters import issue_filters
from plane.utils.user_timezone_converter import user_timezone_converter
# Module imports
from .. import BaseViewSet
@@ -230,10 +229,6 @@ class IssueDraftViewSet(BaseViewSet):
"is_draft",
"archived_at",
)
datetime_fields = ["created_at", "updated_at"]
issues = user_timezone_converter(
issues, datetime_fields, request.user.user_timezone
)
return Response(issues, status=status.HTTP_200_OK)
def create(self, request, slug, project_id):
@@ -31,7 +31,6 @@ from plane.db.models import (
IssueAttachment,
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.user_timezone_converter import user_timezone_converter
from collections import defaultdict
@@ -133,10 +132,6 @@ class SubIssuesEndpoint(BaseAPIView):
"is_draft",
"archived_at",
)
datetime_fields = ["created_at", "updated_at"]
sub_issues = user_timezone_converter(
sub_issues, datetime_fields, request.user.user_timezone
)
return Response(
{
"sub_issues": sub_issues,
@@ -32,8 +32,6 @@ from plane.db.models import (
ModuleLink,
)
from plane.utils.analytics_plot import burndown_plot
from plane.utils.user_timezone_converter import user_timezone_converter
# Module imports
from .. import BaseAPIView
@@ -201,10 +199,6 @@ class ModuleArchiveUnarchiveEndpoint(BaseAPIView):
"updated_at",
"archived_at",
)
datetime_fields = ["created_at", "updated_at"]
modules = user_timezone_converter(
modules, datetime_fields, request.user.user_timezone
)
return Response(modules, status=status.HTTP_200_OK)
else:
queryset = (
+7 -45
View File
@@ -1,7 +1,6 @@
# Python imports
import json
# Django Imports
from django.contrib.postgres.aggregates import ArrayAgg
from django.contrib.postgres.fields import ArrayField
from django.db.models import (
@@ -18,14 +17,14 @@ from django.db.models import (
Value,
)
from django.db.models.functions import Coalesce
from django.core.serializers.json import DjangoJSONEncoder
# Django Imports
from django.utils import timezone
from rest_framework import status
# Third party imports
from rest_framework import status
from rest_framework.response import Response
# Module imports
from plane.app.permissions import (
ProjectEntityPermission,
ProjectLitePermission,
@@ -49,12 +48,12 @@ from plane.db.models import (
Project,
)
from plane.utils.analytics_plot import burndown_plot
from plane.utils.user_timezone_converter import user_timezone_converter
from plane.bgtasks.webhook_task import model_activity
from .. import BaseAPIView, BaseViewSet
# Module imports
from .. import BaseAPIView, BaseViewSet, WebhookMixin
class ModuleViewSet(BaseViewSet):
class ModuleViewSet(WebhookMixin, BaseViewSet):
model = Module
permission_classes = [
ProjectEntityPermission,
@@ -237,20 +236,6 @@ class ModuleViewSet(BaseViewSet):
"updated_at",
)
).first()
# Send the model activity
model_activity.delay(
model_name="module",
model_id=str(module["id"]),
requested_data=request.data,
current_instance=None,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
datetime_fields = ["created_at", "updated_at"]
module = user_timezone_converter(
module, datetime_fields, request.user.user_timezone
)
return Response(module, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
@@ -292,10 +277,6 @@ class ModuleViewSet(BaseViewSet):
"created_at",
"updated_at",
)
datetime_fields = ["created_at", "updated_at"]
modules = user_timezone_converter(
modules, datetime_fields, request.user.user_timezone
)
return Response(modules, status=status.HTTP_200_OK)
def retrieve(self, request, slug, project_id, pk):
@@ -437,9 +418,6 @@ class ModuleViewSet(BaseViewSet):
{"error": "Archived module cannot be updated"},
status=status.HTTP_400_BAD_REQUEST,
)
current_instance = json.dumps(
ModuleSerializer(module.first()).data, cls=DjangoJSONEncoder
)
serializer = ModuleWriteSerializer(
module.first(), data=request.data, partial=True
)
@@ -476,22 +454,6 @@ class ModuleViewSet(BaseViewSet):
"created_at",
"updated_at",
).first()
# Send the model activity
model_activity.delay(
model_name="module",
model_id=str(module["id"]),
requested_data=request.data,
current_instance=current_instance,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
datetime_fields = ["created_at", "updated_at"]
module = user_timezone_converter(
module, datetime_fields, request.user.user_timezone
)
return Response(module, status=status.HTTP_200_OK)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
+3 -8
View File
@@ -16,7 +16,7 @@ from rest_framework.response import Response
from rest_framework import status
# Module imports
from .. import BaseViewSet
from .. import BaseViewSet, WebhookMixin
from plane.app.serializers import (
ModuleIssueSerializer,
IssueSerializer,
@@ -31,9 +31,9 @@ from plane.db.models import (
)
from plane.bgtasks.issue_activites_task import issue_activity
from plane.utils.issue_filters import issue_filters
from plane.utils.user_timezone_converter import user_timezone_converter
class ModuleIssueViewSet(BaseViewSet):
class ModuleIssueViewSet(WebhookMixin, BaseViewSet):
serializer_class = ModuleIssueSerializer
model = ModuleIssue
webhook_event = "module_issue"
@@ -150,11 +150,6 @@ class ModuleIssueViewSet(BaseViewSet):
"is_draft",
"archived_at",
)
datetime_fields = ["created_at", "updated_at"]
issues = user_timezone_converter(
issues, datetime_fields, request.user.user_timezone
)
return Response(issues, status=status.HTTP_200_OK)
# create multiple issues inside a module
+8 -45
View File
@@ -1,6 +1,5 @@
# Python imports
import boto3
import json
# Django imports
from django.db import IntegrityError
@@ -15,7 +14,6 @@ from django.db.models import (
)
from django.conf import settings
from django.utils import timezone
from django.core.serializers.json import DjangoJSONEncoder
# Third Party imports
from rest_framework.response import Response
@@ -24,7 +22,7 @@ from rest_framework import serializers
from rest_framework.permissions import AllowAny
# Module imports
from plane.app.views.base import BaseViewSet, BaseAPIView
from plane.app.views.base import BaseViewSet, BaseAPIView, WebhookMixin
from plane.app.serializers import (
ProjectSerializer,
ProjectListSerializer,
@@ -52,10 +50,9 @@ from plane.db.models import (
Issue,
)
from plane.utils.cache import cache_response
from plane.bgtasks.webhook_task import model_activity
class ProjectViewSet(BaseViewSet):
class ProjectViewSet(WebhookMixin, BaseViewSet):
serializer_class = ProjectListSerializer
model = Project
webhook_event = "project"
@@ -196,7 +193,6 @@ class ProjectViewSet(BaseViewSet):
.annotate(
sub_issues=Issue.issue_objects.filter(
project_id=self.kwargs.get("pk"),
parent__isnull=False,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
@@ -215,7 +211,6 @@ class ProjectViewSet(BaseViewSet):
archived_sub_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"),
archived_at__isnull=False,
parent__isnull=False,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
@@ -234,7 +229,6 @@ class ProjectViewSet(BaseViewSet):
draft_sub_issues=Issue.objects.filter(
project_id=self.kwargs.get("pk"),
is_draft=True,
parent__isnull=False,
)
.order_by()
.annotate(count=Func(F("id"), function="Count"))
@@ -337,17 +331,6 @@ class ProjectViewSet(BaseViewSet):
.filter(pk=serializer.data["id"])
.first()
)
model_activity.delay(
model_name="project",
model_id=str(project.id),
requested_data=request.data,
current_instance=None,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
serializer = ProjectListSerializer(project)
return Response(
serializer.data, status=status.HTTP_201_CREATED
@@ -378,9 +361,7 @@ class ProjectViewSet(BaseViewSet):
workspace = Workspace.objects.get(slug=slug)
project = Project.objects.get(pk=pk)
current_instance = json.dumps(
ProjectSerializer(project).data, cls=DjangoJSONEncoder
)
if project.archived_at:
return Response(
{"error": "Archived projects cannot be updated"},
@@ -418,16 +399,6 @@ class ProjectViewSet(BaseViewSet):
.filter(pk=serializer.data["id"])
.first()
)
model_activity.delay(
model_name="project",
model_id=str(project.id),
requested_data=request.data,
current_instance=current_instance,
actor_id=request.user.id,
slug=slug,
origin=request.META.get("HTTP_ORIGIN"),
)
serializer = ProjectListSerializer(project)
return Response(serializer.data, status=status.HTTP_200_OK)
return Response(
@@ -602,19 +573,11 @@ class ProjectPublicCoverImagesEndpoint(BaseAPIView):
@cache_response(60 * 60 * 24, user=False)
def get(self, request):
files = []
if settings.USE_MINIO:
s3 = boto3.client(
"s3",
endpoint_url=settings.AWS_S3_ENDPOINT_URL,
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
)
else:
s3 = boto3.client(
"s3",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
)
s3 = boto3.client(
"s3",
aws_access_key_id=settings.AWS_ACCESS_KEY_ID,
aws_secret_access_key=settings.AWS_SECRET_ACCESS_KEY,
)
params = {
"Bucket": settings.AWS_STORAGE_BUCKET_NAME,
"Prefix": "static/project-cover/",
-10
View File
@@ -1,7 +1,5 @@
# Django imports
from django.db.models import Case, Count, IntegerField, Q, When
from django.contrib.auth import logout
from django.utils import timezone
# Third party imports
from rest_framework import status
@@ -28,7 +26,6 @@ from plane.db.models import (
from plane.license.models import Instance, InstanceAdmin
from plane.utils.cache import cache_response, invalidate_cache
from plane.utils.paginator import BasePaginator
from plane.authentication.utils.host import user_ip
class UserEndpoint(BaseViewSet):
@@ -169,14 +166,7 @@ class UserEndpoint(BaseViewSet):
"workspace_invite": False,
}
profile.save()
# User log out
user.last_logout_ip = user_ip(request=request)
user.last_logout_time = timezone.now()
user.save()
# Logout the user
logout(request)
return Response(status=status.HTTP_204_NO_CONTENT)
+1 -5
View File
@@ -42,7 +42,7 @@ from plane.db.models import (
IssueAttachment,
)
from plane.utils.issue_filters import issue_filters
from plane.utils.user_timezone_converter import user_timezone_converter
class GlobalViewViewSet(BaseViewSet):
serializer_class = IssueViewSerializer
@@ -255,10 +255,6 @@ class GlobalViewIssuesViewSet(BaseViewSet):
"is_draft",
"archived_at",
)
datetime_fields = ["created_at", "updated_at"]
issues = user_timezone_converter(
issues, datetime_fields, request.user.user_timezone
)
return Response(issues, status=status.HTTP_200_OK)
+1 -1
View File
@@ -27,7 +27,7 @@ class WorkspaceCyclesEndpoint(BaseAPIView):
.select_related("project")
.select_related("workspace")
.select_related("owned_by")
.filter(archived_at__isnull=True)
.filter(archived_at__isnull=False)
.annotate(
total_issues=Count(
"issue_cycle",

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