release: v1.3.0 #8835
This commit is contained in:
commit
cf696d200d
4397 changed files with 65294 additions and 42352 deletions
|
|
@ -1,7 +0,0 @@
|
||||||
[codespell]
|
|
||||||
# Ref: https://github.com/codespell-project/codespell#using-a-config-file
|
|
||||||
skip = .git*,*.svg,i18n,*-lock.yaml,*.css,.codespellrc,migrations,*.js,*.map,*.mjs
|
|
||||||
check-hidden = true
|
|
||||||
# ignore all CamelCase and camelCase
|
|
||||||
ignore-regex = \b[A-Za-z][a-z]+[A-Z][a-zA-Z]+\b
|
|
||||||
ignore-words-list = tread
|
|
||||||
2
.github/ISSUE_TEMPLATE/--bug-report.yaml
vendored
2
.github/ISSUE_TEMPLATE/--bug-report.yaml
vendored
|
|
@ -1,7 +1,7 @@
|
||||||
name: Bug report
|
name: Bug report
|
||||||
description: Create a bug report to help us improve Plane
|
description: Create a bug report to help us improve Plane
|
||||||
title: "[bug]: "
|
title: "[bug]: "
|
||||||
labels: [🐛bug]
|
labels: [🐛bug, plane]
|
||||||
assignees: [vihar, pushya22]
|
assignees: [vihar, pushya22]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
name: Feature request
|
name: Feature request
|
||||||
description: Suggest a feature to improve Plane
|
description: Suggest a feature to improve Plane
|
||||||
title: "[feature]: "
|
title: "[feature]: "
|
||||||
labels: [✨feature]
|
labels: [✨feature, plane]
|
||||||
assignees: [vihar, pushya22]
|
assignees: [vihar, pushya22]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
|
|
|
||||||
2
.github/ISSUE_TEMPLATE/config.yaml
vendored
2
.github/ISSUE_TEMPLATE/config.yaml
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
contact_links:
|
contact_links:
|
||||||
- name: Help and support
|
- name: Help and support
|
||||||
about: Reach out to us on our Discord server or GitHub discussions.
|
about: Reach out to us on our Forum or GitHub discussions.
|
||||||
- name: Dedicated support
|
- name: Dedicated support
|
||||||
url: mailto:support@plane.so
|
url: mailto:support@plane.so
|
||||||
about: Write to us if you'd like dedicated support using Plane
|
about: Write to us if you'd like dedicated support using Plane
|
||||||
|
|
|
||||||
29
.github/workflows/build-branch.yml
vendored
29
.github/workflows/build-branch.yml
vendored
|
|
@ -134,7 +134,7 @@ jobs:
|
||||||
|
|
||||||
- id: checkout_files
|
- id: checkout_files
|
||||||
name: Checkout Files
|
name: Checkout Files
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
branch_build_push_admin:
|
branch_build_push_admin:
|
||||||
name: Build-Push Admin Docker Image
|
name: Build-Push Admin Docker Image
|
||||||
|
|
@ -142,7 +142,7 @@ jobs:
|
||||||
needs: [branch_build_setup]
|
needs: [branch_build_setup]
|
||||||
steps:
|
steps:
|
||||||
- name: Admin Build and Push
|
- name: Admin Build and Push
|
||||||
uses: makeplane/actions/build-push@v1.0.0
|
uses: makeplane/actions/build-push@v1.4.0
|
||||||
with:
|
with:
|
||||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||||
|
|
@ -164,7 +164,7 @@ jobs:
|
||||||
needs: [branch_build_setup]
|
needs: [branch_build_setup]
|
||||||
steps:
|
steps:
|
||||||
- name: Web Build and Push
|
- name: Web Build and Push
|
||||||
uses: makeplane/actions/build-push@v1.0.0
|
uses: makeplane/actions/build-push@v1.4.0
|
||||||
with:
|
with:
|
||||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||||
|
|
@ -186,7 +186,7 @@ jobs:
|
||||||
needs: [branch_build_setup]
|
needs: [branch_build_setup]
|
||||||
steps:
|
steps:
|
||||||
- name: Space Build and Push
|
- name: Space Build and Push
|
||||||
uses: makeplane/actions/build-push@v1.0.0
|
uses: makeplane/actions/build-push@v1.4.0
|
||||||
with:
|
with:
|
||||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||||
|
|
@ -208,7 +208,7 @@ jobs:
|
||||||
needs: [branch_build_setup]
|
needs: [branch_build_setup]
|
||||||
steps:
|
steps:
|
||||||
- name: Live Build and Push
|
- name: Live Build and Push
|
||||||
uses: makeplane/actions/build-push@v1.0.0
|
uses: makeplane/actions/build-push@v1.4.0
|
||||||
with:
|
with:
|
||||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||||
|
|
@ -230,7 +230,7 @@ jobs:
|
||||||
needs: [branch_build_setup]
|
needs: [branch_build_setup]
|
||||||
steps:
|
steps:
|
||||||
- name: Backend Build and Push
|
- name: Backend Build and Push
|
||||||
uses: makeplane/actions/build-push@v1.0.0
|
uses: makeplane/actions/build-push@v1.4.0
|
||||||
with:
|
with:
|
||||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||||
|
|
@ -252,7 +252,7 @@ jobs:
|
||||||
needs: [branch_build_setup]
|
needs: [branch_build_setup]
|
||||||
steps:
|
steps:
|
||||||
- name: Proxy Build and Push
|
- name: Proxy Build and Push
|
||||||
uses: makeplane/actions/build-push@v1.0.0
|
uses: makeplane/actions/build-push@v1.4.0
|
||||||
with:
|
with:
|
||||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||||
|
|
@ -282,7 +282,7 @@ jobs:
|
||||||
- branch_build_push_proxy
|
- branch_build_push_proxy
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Files
|
- name: Checkout Files
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Prepare AIO Assets
|
- name: Prepare AIO Assets
|
||||||
id: prepare_aio_assets
|
id: prepare_aio_assets
|
||||||
|
|
@ -298,13 +298,13 @@ jobs:
|
||||||
echo "AIO_BUILD_VERSION=${aio_version}" >> $GITHUB_OUTPUT
|
echo "AIO_BUILD_VERSION=${aio_version}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Upload AIO Assets
|
- name: Upload AIO Assets
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
path: ./deployments/aio/community/dist
|
path: ./deployments/aio/community/dist
|
||||||
name: aio-assets-dist
|
name: aio-assets-dist
|
||||||
|
|
||||||
- name: AIO Build and Push
|
- name: AIO Build and Push
|
||||||
uses: makeplane/actions/build-push@v1.1.0
|
uses: makeplane/actions/build-push@v1.4.0
|
||||||
with:
|
with:
|
||||||
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
build-release: ${{ needs.branch_build_setup.outputs.build_release }}
|
||||||
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
build-prerelease: ${{ needs.branch_build_setup.outputs.build_prerelease }}
|
||||||
|
|
@ -337,7 +337,7 @@ jobs:
|
||||||
- branch_build_push_proxy
|
- branch_build_push_proxy
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Files
|
- name: Checkout Files
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Update Assets
|
- name: Update Assets
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -352,7 +352,7 @@ jobs:
|
||||||
# sed -i 's/APP_RELEASE=stable/APP_RELEASE='${REL_VERSION}'/g' deployments/cli/community/variables.env
|
# sed -i 's/APP_RELEASE=stable/APP_RELEASE='${REL_VERSION}'/g' deployments/cli/community/variables.env
|
||||||
|
|
||||||
- name: Upload Assets
|
- name: Upload Assets
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v7
|
||||||
with:
|
with:
|
||||||
name: community-assets
|
name: community-assets
|
||||||
path: |
|
path: |
|
||||||
|
|
@ -381,7 +381,7 @@ jobs:
|
||||||
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}
|
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Update Assets
|
- name: Update Assets
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -391,12 +391,13 @@ jobs:
|
||||||
|
|
||||||
- name: Create Release
|
- name: Create Release
|
||||||
id: create_release
|
id: create_release
|
||||||
uses: softprops/action-gh-release@v2.1.0
|
uses: softprops/action-gh-release@v2.6.1
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ env.REL_VERSION }}
|
tag_name: ${{ env.REL_VERSION }}
|
||||||
name: ${{ env.REL_VERSION }}
|
name: ${{ env.REL_VERSION }}
|
||||||
|
target_commitish: ${{ github.sha }}
|
||||||
draft: false
|
draft: false
|
||||||
prerelease: ${{ env.IS_PRERELEASE }}
|
prerelease: ${{ env.IS_PRERELEASE }}
|
||||||
generate_release_notes: true
|
generate_release_notes: true
|
||||||
|
|
|
||||||
4
.github/workflows/check-version.yml
vendored
4
.github/workflows/check-version.yml
vendored
|
|
@ -10,13 +10,13 @@ jobs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.head_ref }}
|
ref: ${{ github.head_ref }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
|
|
||||||
- name: Get PR Branch version
|
- name: Get PR Branch version
|
||||||
run: echo "PR_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
|
run: echo "PR_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
|
||||||
|
|
|
||||||
31
.github/workflows/codeql.yml
vendored
31
.github/workflows/codeql.yml
vendored
|
|
@ -20,43 +20,20 @@ jobs:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
language: ["python", "javascript"]
|
language: ["python", "javascript"]
|
||||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
|
||||||
# Use only 'java' to analyze code written in Java, Kotlin or both
|
|
||||||
# Use only 'javascript' to analyze code written in JavaScript, TypeScript or both
|
|
||||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v3
|
uses: github/codeql-action/init@v4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
|
||||||
# By default, queries listed here will override any specified in a config file.
|
|
||||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
|
||||||
|
|
||||||
# For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
|
||||||
# queries: security-extended,security-and-quality
|
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift).
|
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v3
|
uses: github/codeql-action/autobuild@v4
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
|
||||||
|
|
||||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
|
||||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
|
||||||
|
|
||||||
# - run: |
|
|
||||||
# echo "Run, Build Application using script"
|
|
||||||
# ./location_of_script_within_repo/buildscript.sh
|
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v3
|
uses: github/codeql-action/analyze@v4
|
||||||
with:
|
with:
|
||||||
category: "/language:${{matrix.language}}"
|
category: "/language:${{matrix.language}}"
|
||||||
|
|
|
||||||
25
.github/workflows/codespell.yml
vendored
25
.github/workflows/codespell.yml
vendored
|
|
@ -1,25 +0,0 @@
|
||||||
# Codespell configuration is within .codespellrc
|
|
||||||
---
|
|
||||||
name: Codespell
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [preview]
|
|
||||||
pull_request:
|
|
||||||
branches: [preview]
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
codespell:
|
|
||||||
name: Check for spelling errors
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Annotate locations with typos
|
|
||||||
uses: codespell-project/codespell-problem-matcher@v1
|
|
||||||
- name: Codespell
|
|
||||||
uses: codespell-project/actions-codespell@v2
|
|
||||||
45
.github/workflows/copyright-check.yml
vendored
Normal file
45
.github/workflows/copyright-check.yml
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
name: Copy Right Check
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- "preview"
|
||||||
|
types:
|
||||||
|
- "opened"
|
||||||
|
- "synchronize"
|
||||||
|
- "ready_for_review"
|
||||||
|
- "review_requested"
|
||||||
|
- "reopened"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
license-check:
|
||||||
|
name: Copy Right Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
go-version: "1.22"
|
||||||
|
|
||||||
|
- name: Install addlicense
|
||||||
|
run: |
|
||||||
|
go install github.com/google/addlicense@latest
|
||||||
|
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
|
||||||
|
|
||||||
|
- name: Check Copyright For Python Files
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
echo "Running copyright check..."
|
||||||
|
addlicense -check -f COPYRIGHT.txt -ignore "**/migrations/**" $(git ls-files '*.py')
|
||||||
|
echo "Copyright check passed."
|
||||||
|
|
||||||
|
- name: Check Copyright For TypeScript Files
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
echo "Running copyright check..."
|
||||||
|
addlicense -check -f COPYRIGHT.txt -ignore "**/*.config.ts" -ignore "**/*.d.ts" $(git ls-files '*.ts' '*.tsx')
|
||||||
|
echo "Copyright check passed."
|
||||||
12
.github/workflows/feature-deployment.yml
vendored
12
.github/workflows/feature-deployment.yml
vendored
|
|
@ -48,7 +48,7 @@ jobs:
|
||||||
|
|
||||||
- id: checkout_files
|
- id: checkout_files
|
||||||
name: Checkout Files
|
name: Checkout Files
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
full_build_push:
|
full_build_push:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
|
@ -63,23 +63,23 @@ jobs:
|
||||||
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
BUILDX_ENDPOINT: ${{ needs.branch_build_setup.outputs.gh_buildx_endpoint }}
|
||||||
steps:
|
steps:
|
||||||
- name: Login to Docker Hub
|
- name: Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v4
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: docker/setup-buildx-action@v4
|
||||||
with:
|
with:
|
||||||
driver: ${{ env.BUILDX_DRIVER }}
|
driver: ${{ env.BUILDX_DRIVER }}
|
||||||
version: ${{ env.BUILDX_VERSION }}
|
version: ${{ env.BUILDX_VERSION }}
|
||||||
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
endpoint: ${{ env.BUILDX_ENDPOINT }}
|
||||||
|
|
||||||
- name: Check out the repo
|
- name: Check out the repo
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Build and Push to Docker Hub
|
- name: Build and Push to Docker Hub
|
||||||
uses: docker/build-push-action@v6.9.0
|
uses: docker/build-push-action@v7.0.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./aio/Dockerfile-app
|
file: ./aio/Dockerfile-app
|
||||||
|
|
@ -112,7 +112,7 @@ jobs:
|
||||||
sudo apt-get install -y python3-pip
|
sudo apt-get install -y python3-pip
|
||||||
pip3 install awscli
|
pip3 install awscli
|
||||||
- name: Tailscale
|
- name: Tailscale
|
||||||
uses: tailscale/github-action@v2
|
uses: tailscale/github-action@v4
|
||||||
with:
|
with:
|
||||||
oauth-client-id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }}
|
oauth-client-id: ${{ secrets.TAILSCALE_OAUTH_CLIENT_ID }}
|
||||||
oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }}
|
oauth-secret: ${{ secrets.TAILSCALE_OAUTH_SECRET }}
|
||||||
|
|
|
||||||
|
|
@ -27,11 +27,13 @@ jobs:
|
||||||
github.event.pull_request.draft == false &&
|
github.event.pull_request.draft == false &&
|
||||||
github.event.pull_request.requested_reviewers != null
|
github.event.pull_request.requested_reviewers != null
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v6
|
||||||
- name: Set up Python
|
- name: Set up Python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v6
|
||||||
with:
|
with:
|
||||||
python-version: "3.12.x"
|
python-version: "3.12.x"
|
||||||
|
cache: 'pip'
|
||||||
|
cache-dependency-path: 'apps/api/requirements.txt'
|
||||||
- name: Install Pylint
|
- name: Install Pylint
|
||||||
run: python -m pip install ruff
|
run: python -m pip install ruff
|
||||||
- name: Install API Dependencies
|
- name: Install API Dependencies
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ on:
|
||||||
types:
|
types:
|
||||||
- "opened"
|
- "opened"
|
||||||
- "synchronize"
|
- "synchronize"
|
||||||
- "ready_for_review"
|
|
||||||
- "review_requested"
|
|
||||||
- "reopened"
|
- "reopened"
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
|
|
@ -17,10 +15,11 @@ concurrency:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-lint:
|
# Format check has no build dependencies - run immediately in parallel
|
||||||
name: Build and lint web apps
|
check-format:
|
||||||
|
name: check:format
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 25
|
timeout-minutes: 10
|
||||||
if: |
|
if: |
|
||||||
github.event.pull_request.draft == false &&
|
github.event.pull_request.draft == false &&
|
||||||
github.event.pull_request.requested_reviewers != null
|
github.event.pull_request.requested_reviewers != null
|
||||||
|
|
@ -29,28 +28,178 @@ jobs:
|
||||||
TURBO_SCM_HEAD: ${{ github.sha }}
|
TURBO_SCM_HEAD: ${{ github.sha }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
fetch-depth: 50
|
fetch-depth: 50
|
||||||
filter: blob:none
|
filter: blob:none
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v6
|
||||||
|
|
||||||
- name: Enable Corepack and pnpm
|
- name: Enable Corepack and pnpm
|
||||||
run: corepack enable pnpm
|
run: corepack enable pnpm
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
shell: bash
|
||||||
|
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Cache pnpm store
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: ${{ env.STORE_PATH }}
|
||||||
|
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
pnpm-store-${{ runner.os }}-
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: pnpm install --frozen-lockfile
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build Affected
|
- name: Check formatting
|
||||||
run: pnpm turbo run build --affected
|
|
||||||
|
|
||||||
- name: Lint Affected
|
|
||||||
run: pnpm turbo run check:lint --affected
|
|
||||||
|
|
||||||
- name: Check Affected format
|
|
||||||
run: pnpm turbo run check:format --affected
|
run: pnpm turbo run check:format --affected
|
||||||
|
|
||||||
- name: Check Affected types
|
# Build packages - required for lint and type checks
|
||||||
|
build:
|
||||||
|
name: Build packages
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
if: |
|
||||||
|
github.event.pull_request.draft == false &&
|
||||||
|
github.event.pull_request.requested_reviewers != null
|
||||||
|
env:
|
||||||
|
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
|
||||||
|
TURBO_SCM_HEAD: ${{ github.sha }}
|
||||||
|
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 50
|
||||||
|
filter: blob:none
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
|
||||||
|
- name: Enable Corepack and pnpm
|
||||||
|
run: corepack enable pnpm
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
shell: bash
|
||||||
|
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Cache pnpm store
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: ${{ env.STORE_PATH }}
|
||||||
|
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
pnpm-store-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Restore Turbo cache
|
||||||
|
uses: actions/cache/restore@v5
|
||||||
|
with:
|
||||||
|
path: .turbo
|
||||||
|
key: turbo-${{ runner.os }}-${{ github.event.pull_request.base.sha }}-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
turbo-${{ runner.os }}-${{ github.event.pull_request.base.sha }}-
|
||||||
|
turbo-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build packages
|
||||||
|
run: pnpm turbo run build --affected
|
||||||
|
|
||||||
|
- name: Save Turbo cache
|
||||||
|
uses: actions/cache/save@v5
|
||||||
|
with:
|
||||||
|
path: .turbo
|
||||||
|
key: turbo-${{ runner.os }}-${{ github.event.pull_request.base.sha }}-${{ github.sha }}
|
||||||
|
|
||||||
|
# Lint check - no build dependency, OxLint is a standalone Rust binary
|
||||||
|
check-lint:
|
||||||
|
name: check:lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 10
|
||||||
|
if: |
|
||||||
|
github.event.pull_request.draft == false &&
|
||||||
|
github.event.pull_request.requested_reviewers != null
|
||||||
|
env:
|
||||||
|
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
|
||||||
|
TURBO_SCM_HEAD: ${{ github.sha }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 50
|
||||||
|
filter: blob:none
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
|
||||||
|
- name: Enable Corepack and pnpm
|
||||||
|
run: corepack enable pnpm
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
shell: bash
|
||||||
|
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Cache pnpm store
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: ${{ env.STORE_PATH }}
|
||||||
|
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
pnpm-store-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run check:lint
|
||||||
|
run: pnpm turbo run check:lint --affected
|
||||||
|
|
||||||
|
# Type check depends on build artifacts
|
||||||
|
check-types:
|
||||||
|
name: check:types
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
timeout-minutes: 15
|
||||||
|
env:
|
||||||
|
TURBO_SCM_BASE: ${{ github.event.pull_request.base.sha }}
|
||||||
|
TURBO_SCM_HEAD: ${{ github.sha }}
|
||||||
|
NODE_OPTIONS: "--max-old-space-size=4096"
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
fetch-depth: 50
|
||||||
|
filter: blob:none
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
|
||||||
|
- name: Enable Corepack and pnpm
|
||||||
|
run: corepack enable pnpm
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
shell: bash
|
||||||
|
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Cache pnpm store
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: ${{ env.STORE_PATH }}
|
||||||
|
key: pnpm-store-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
pnpm-store-${{ runner.os }}-
|
||||||
|
|
||||||
|
- name: Restore Turbo cache
|
||||||
|
uses: actions/cache/restore@v5
|
||||||
|
with:
|
||||||
|
path: .turbo
|
||||||
|
key: turbo-${{ runner.os }}-${{ github.event.pull_request.base.sha }}-${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Run check:types
|
||||||
run: pnpm turbo run check:types --affected
|
run: pnpm turbo run check:types --affected
|
||||||
|
|
|
||||||
52
.github/workflows/sync-repo-pr.yml
vendored
52
.github/workflows/sync-repo-pr.yml
vendored
|
|
@ -1,52 +0,0 @@
|
||||||
name: Create PR on Sync
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- "sync/**"
|
|
||||||
|
|
||||||
env:
|
|
||||||
CURRENT_BRANCH: ${{ github.ref_name }}
|
|
||||||
TARGET_BRANCH: "preview" # The target branch that you would like to merge changes like develop
|
|
||||||
GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }} # Personal access token required to modify contents and workflows
|
|
||||||
ACCOUNT_USER_NAME: ${{ vars.ACCOUNT_USER_NAME }}
|
|
||||||
ACCOUNT_USER_EMAIL: ${{ vars.ACCOUNT_USER_EMAIL }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
create_pull_request:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0 # Fetch all history for all branches and tags
|
|
||||||
|
|
||||||
- name: Setup Git
|
|
||||||
run: |
|
|
||||||
git config user.name "$ACCOUNT_USER_NAME"
|
|
||||||
git config user.email "$ACCOUNT_USER_EMAIL"
|
|
||||||
|
|
||||||
- name: Setup GH CLI and Git Config
|
|
||||||
run: |
|
|
||||||
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
|
|
||||||
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
|
|
||||||
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
|
|
||||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install gh -y
|
|
||||||
|
|
||||||
- name: Create PR to Target Branch
|
|
||||||
run: |
|
|
||||||
# get all pull requests and check if there is already a PR
|
|
||||||
PR_EXISTS=$(gh pr list --base $TARGET_BRANCH --head $CURRENT_BRANCH --state open --json number | jq '.[] | .number')
|
|
||||||
if [ -n "$PR_EXISTS" ]; then
|
|
||||||
echo "Pull Request already exists: $PR_EXISTS"
|
|
||||||
else
|
|
||||||
echo "Creating new pull request"
|
|
||||||
PR_URL=$(gh pr create --base $TARGET_BRANCH --head $CURRENT_BRANCH --title "${{ vars.SYNC_PR_TITLE }}" --body "")
|
|
||||||
echo "Pull Request created: $PR_URL"
|
|
||||||
fi
|
|
||||||
44
.github/workflows/sync-repo.yml
vendored
44
.github/workflows/sync-repo.yml
vendored
|
|
@ -1,44 +0,0 @@
|
||||||
name: Sync Repositories
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- preview
|
|
||||||
|
|
||||||
env:
|
|
||||||
SOURCE_BRANCH_NAME: ${{ github.ref_name }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
sync_changes:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- name: Checkout Code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Setup GH CLI
|
|
||||||
run: |
|
|
||||||
type -p curl >/dev/null || (sudo apt update && sudo apt install curl -y)
|
|
||||||
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
|
|
||||||
sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg
|
|
||||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install gh -y
|
|
||||||
|
|
||||||
- name: Push Changes to Target Repo
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
|
|
||||||
run: |
|
|
||||||
TARGET_REPO="${{ vars.SYNC_TARGET_REPO }}"
|
|
||||||
TARGET_BRANCH="${{ vars.SYNC_TARGET_BRANCH_NAME }}"
|
|
||||||
SOURCE_BRANCH="${{ env.SOURCE_BRANCH_NAME }}"
|
|
||||||
|
|
||||||
git checkout $SOURCE_BRANCH
|
|
||||||
git remote add target-origin-a "https://$GH_TOKEN@github.com/$TARGET_REPO.git"
|
|
||||||
git push target-origin-a $SOURCE_BRANCH:$TARGET_BRANCH
|
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -105,10 +105,8 @@ CLAUDE.md
|
||||||
|
|
||||||
build/
|
build/
|
||||||
.react-router/
|
.react-router/
|
||||||
AGENTS.md
|
|
||||||
|
|
||||||
build/
|
build/
|
||||||
.react-router/
|
.react-router/
|
||||||
AGENTS.md
|
|
||||||
temp/
|
temp/
|
||||||
scripts/
|
scripts/
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,11 @@
|
||||||
{
|
{
|
||||||
"$schema": "https://json.schemastore.org/prettierrc",
|
"printWidth": 120,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"sortTailwindcss": {
|
||||||
|
"stylesheet": "packages/tailwind-config/index.css",
|
||||||
|
"functions": ["cn", "clsx", "cva"]
|
||||||
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"files": ["packages/codemods/**/*"],
|
"files": ["packages/codemods/**/*"],
|
||||||
|
|
@ -7,9 +13,5 @@
|
||||||
"printWidth": 80
|
"printWidth": 80
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"plugins": ["@prettier/plugin-oxc"],
|
|
||||||
"printWidth": 120,
|
|
||||||
"tabWidth": 2,
|
|
||||||
"trailingComma": "es5"
|
|
||||||
}
|
}
|
||||||
53
.oxlintrc.json
Normal file
53
.oxlintrc.json
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": ["react", "typescript", "jsx-a11y", "import", "promise", "unicorn", "oxc"],
|
||||||
|
"categories": {
|
||||||
|
"correctness": "warn",
|
||||||
|
"suspicious": "warn",
|
||||||
|
"perf": "warn"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true,
|
||||||
|
"es2024": true
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"react": {
|
||||||
|
"version": "18.3"
|
||||||
|
},
|
||||||
|
"jsx-a11y": {
|
||||||
|
"polymorphicPropName": "as"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ignorePatterns": [
|
||||||
|
".cache/**",
|
||||||
|
".next/**",
|
||||||
|
".react-router/**",
|
||||||
|
".storybook/**",
|
||||||
|
".turbo/**",
|
||||||
|
".vite/**",
|
||||||
|
"*.config.{js,mjs,cjs,ts}",
|
||||||
|
"build/**",
|
||||||
|
"coverage/**",
|
||||||
|
"dist/**",
|
||||||
|
"**/public/**",
|
||||||
|
"storybook-static/**"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"react/react-in-jsx-scope": "off",
|
||||||
|
"react/prop-types": "off",
|
||||||
|
"unicorn/filename-case": "off",
|
||||||
|
"unicorn/no-null": "off",
|
||||||
|
"unicorn/prevent-abbreviations": "off",
|
||||||
|
"no-unused-vars": [
|
||||||
|
"warn",
|
||||||
|
{
|
||||||
|
"argsIgnorePattern": "^_",
|
||||||
|
"varsIgnorePattern": "^_",
|
||||||
|
"caughtErrorsIgnorePattern": "^_",
|
||||||
|
"destructuredArrayIgnorePattern": "^_",
|
||||||
|
"ignoreRestSiblings": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
24
AGENTS.md
Normal file
24
AGENTS.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Agent Development Guide
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
- `pnpm dev` - Start all dev servers (web:3000, admin:3001)
|
||||||
|
- `pnpm build` - Build all packages and apps
|
||||||
|
- `pnpm check` - Run all checks (format, lint, types)
|
||||||
|
- `pnpm check:lint` - OxLint across all packages
|
||||||
|
- `pnpm check:types` - TypeScript type checking
|
||||||
|
- `pnpm fix` - Auto-fix format and lint issues
|
||||||
|
- `pnpm turbo run <command> --filter=<package>` - Target specific package/app
|
||||||
|
- `pnpm --filter=@plane/ui storybook` - Start Storybook on port 6006
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
- **Imports**: Use `workspace:*` for internal packages, `catalog:` for external deps
|
||||||
|
- **TypeScript**: Strict mode enabled, all files must be typed
|
||||||
|
- **Formatting**: oxfmt, run `pnpm fix:format`
|
||||||
|
- **Linting**: OxLint with shared `.oxlintrc.json` config
|
||||||
|
- **Naming**: camelCase for variables/functions, PascalCase for components/types
|
||||||
|
- **Error Handling**: Use try-catch with proper error types, log errors appropriately
|
||||||
|
- **State Management**: MobX stores in `packages/shared-state`, reactive patterns
|
||||||
|
- **Testing**: All features require unit tests, use existing test framework per package
|
||||||
|
- **Components**: Build in `@plane/ui` with Storybook for isolated development
|
||||||
|
|
@ -91,7 +91,7 @@ If you would like to _implement_ it, an issue with your proposal must be submitt
|
||||||
To ensure consistency throughout the source code, please keep these rules in mind as you are working:
|
To ensure consistency throughout the source code, please keep these rules in mind as you are working:
|
||||||
|
|
||||||
- All features or bug fixes must be tested by one or more specs (unit-tests).
|
- All features or bug fixes must be tested by one or more specs (unit-tests).
|
||||||
- We lint with [ESLint 9](https://eslint.org/docs/latest/) using the shared `eslint.config.mjs` (type-aware via `typescript-eslint`) and format with [Prettier](https://prettier.io/) using `prettier.config.cjs`.
|
- We lint with [OxLint](https://oxc.rs/docs/guide/usage/linter) using the shared `.oxlintrc.json` and format with [oxfmt](https://oxc.rs/docs/guide/usage/formatter) using `.oxfmtrc.json`.
|
||||||
|
|
||||||
## Ways to contribute
|
## Ways to contribute
|
||||||
|
|
||||||
|
|
@ -244,4 +244,4 @@ Happy translating! 🌍✨
|
||||||
|
|
||||||
## Need help? Questions and suggestions
|
## Need help? Questions and suggestions
|
||||||
|
|
||||||
Questions, suggestions, and thoughts are most welcome. We can also be reached in our [Discord Server](https://discord.com/invite/A92xrEGCge).
|
Questions, suggestions, and thoughts are most welcome. We can also be reached in our [Forum](https://forum.plane.so).
|
||||||
|
|
|
||||||
3
COPYRIGHT.txt
Normal file
3
COPYRIGHT.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
See the LICENSE file for details.
|
||||||
34
COPYRIGHT_CHECK.md
Normal file
34
COPYRIGHT_CHECK.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
## Copyright check
|
||||||
|
|
||||||
|
To verify that all tracked Python files contain the correct copyright header for **Plane Software Inc.** for the year **2023**, run this command from the repository root:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
addlicense --check -f COPYRIGHT.txt -ignore "**/migrations/**" $(git ls-files '*.py')
|
||||||
|
```
|
||||||
|
|
||||||
|
#### To Apply Changes
|
||||||
|
|
||||||
|
python files
|
||||||
|
|
||||||
|
```bash
|
||||||
|
addlicense -v -f COPYRIGHT.txt -ignore "**/migrations/**" $(git ls-files '*.py')
|
||||||
|
```
|
||||||
|
|
||||||
|
ts and tsx files in a specific app
|
||||||
|
|
||||||
|
```bash
|
||||||
|
addlicense -v -f COPYRIGHT.txt \
|
||||||
|
-ignore "**/*.config.ts" \
|
||||||
|
-ignore "**/*.d.ts" \
|
||||||
|
$(git ls-files 'packages/*.ts')
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: Please make sure ts command is running on specific folder, running it for the whole mono repo is crashing os processes.
|
||||||
|
|
||||||
|
#### Other Options
|
||||||
|
|
||||||
|
- **`addlicense -check`**: runs in check-only mode and fails if any file is missing or has an incorrect header.
|
||||||
|
- **`-c "Plane Software Inc."`**: sets the copyright holder.
|
||||||
|
- **`-f LICENSE.txt`**: uses the contents and format defined in `LICENSE.txt` as the header template.
|
||||||
|
- **`-y 2023`**: sets the year in the header.
|
||||||
|
- **`$(git ls-files '*.py')`**: restricts the check to Python files tracked in git.
|
||||||
21
README.md
21
README.md
|
|
@ -7,16 +7,9 @@
|
||||||
</p>
|
</p>
|
||||||
<p align="center"><b>Modern project management for all teams</b></p>
|
<p align="center"><b>Modern project management for all teams</b></p>
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://discord.com/invite/A92xrEGCge">
|
|
||||||
<img alt="Discord online members" src="https://img.shields.io/discord/1031547764020084846?color=5865F2&label=Discord&style=for-the-badge" />
|
|
||||||
</a>
|
|
||||||
<img alt="Commit activity per month" src="https://img.shields.io/github/commit-activity/m/makeplane/plane?style=for-the-badge" />
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://plane.so/"><b>Website</b></a> •
|
<a href="https://plane.so/"><b>Website</b></a> •
|
||||||
<a href="https://github.com/makeplane/plane/releases"><b>Releases</b></a> •
|
<a href="https://forum.plane.so"><b>Forum</b></a> •
|
||||||
<a href="https://twitter.com/planepowers"><b>Twitter</b></a> •
|
<a href="https://twitter.com/planepowers"><b>Twitter</b></a> •
|
||||||
<a href="https://docs.plane.so/"><b>Documentation</b></a>
|
<a href="https://docs.plane.so/"><b>Documentation</b></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -33,7 +26,7 @@
|
||||||
|
|
||||||
Meet [Plane](https://plane.so/), 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://plane.so/), an open-source project management tool to track issues, run ~sprints~ cycles, and manage product roadmaps without the chaos of managing the tool itself. 🧘♀️
|
||||||
|
|
||||||
> 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 evolving every day. Your suggestions, ideas, and reported bugs help us immensely. Do not hesitate to join in the conversation on [Forum](https://forum.plane.so) or raise a GitHub issue. We read everything and respond to most.
|
||||||
|
|
||||||
## 🚀 Installation
|
## 🚀 Installation
|
||||||
|
|
||||||
|
|
@ -54,7 +47,7 @@ Getting started with Plane is simple. Choose the setup that works best for you:
|
||||||
|
|
||||||
## 🌟 Features
|
## 🌟 Features
|
||||||
|
|
||||||
- **Issues**
|
- **Work Items**
|
||||||
Efficiently create and manage tasks with a robust rich text editor that supports file uploads. Enhance organization and tracking by adding sub-properties and referencing related issues.
|
Efficiently create and manage tasks with a robust rich text editor that supports file uploads. Enhance organization and tracking by adding sub-properties and referencing related issues.
|
||||||
|
|
||||||
- **Cycles**
|
- **Cycles**
|
||||||
|
|
@ -72,15 +65,13 @@ Getting started with Plane is simple. Choose the setup that works best for you:
|
||||||
- **Analytics**
|
- **Analytics**
|
||||||
Access real-time insights across all your Plane data. Visualize trends, remove blockers, and keep your projects moving forward.
|
Access real-time insights across all your Plane data. Visualize trends, remove blockers, and keep your projects moving forward.
|
||||||
|
|
||||||
- **Drive** (_coming soon_): The drive helps you share documents, images, videos, or any other files that make sense to you or your team and align on the problem/solution.
|
|
||||||
|
|
||||||
## 🛠️ Local development
|
## 🛠️ Local development
|
||||||
|
|
||||||
See [CONTRIBUTING](./CONTRIBUTING.md)
|
See [CONTRIBUTING](./CONTRIBUTING.md)
|
||||||
|
|
||||||
## ⚙️ Built with
|
## ⚙️ Built with
|
||||||
|
|
||||||
[](https://nextjs.org/)
|
[](https://reactrouter.com/)
|
||||||
[](https://www.djangoproject.com/)
|
[](https://www.djangoproject.com/)
|
||||||
[](https://nodejs.org/en)
|
[](https://nodejs.org/en)
|
||||||
|
|
||||||
|
|
@ -138,7 +129,7 @@ Explore Plane's [product documentation](https://docs.plane.so/) and [developer d
|
||||||
|
|
||||||
## ❤️ Community
|
## ❤️ Community
|
||||||
|
|
||||||
Join the Plane community on [GitHub Discussions](https://github.com/orgs/makeplane/discussions) and our [Discord server](https://discord.com/invite/A92xrEGCge). We follow a [Code of conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) in all our community channels.
|
Join the Plane community on [GitHub Discussions](https://github.com/orgs/makeplane/discussions) and our [Forum](https://forum.plane.so). We follow a [Code of conduct](https://github.com/makeplane/plane/blob/master/CODE_OF_CONDUCT.md) in all our community channels.
|
||||||
|
|
||||||
Feel free to ask questions, report bugs, participate in discussions, share ideas, request features, or showcase your projects. We’d love to hear from you!
|
Feel free to ask questions, report bugs, participate in discussions, share ideas, request features, or showcase your projects. We’d love to hear from you!
|
||||||
|
|
||||||
|
|
@ -154,7 +145,7 @@ There are many ways you can contribute to Plane:
|
||||||
|
|
||||||
- Report [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) or submit [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+).
|
- Report [bugs](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%F0%9F%90%9Bbug&projects=&template=--bug-report.yaml&title=%5Bbug%5D%3A+) or submit [feature requests](https://github.com/makeplane/plane/issues/new?assignees=srinivaspendem%2Cpushya22&labels=%E2%9C%A8feature&projects=&template=--feature-request.yaml&title=%5Bfeature%5D%3A+).
|
||||||
- Review the [documentation](https://docs.plane.so/) and submit [pull requests](https://github.com/makeplane/docs) to improve it—whether it's fixing typos or adding new content.
|
- Review the [documentation](https://docs.plane.so/) and submit [pull requests](https://github.com/makeplane/docs) to improve it—whether it's fixing typos or adding new content.
|
||||||
- Talk or write about Plane or any other ecosystem integration and [let us know](https://discord.com/invite/A92xrEGCge)!
|
- Talk or write about Plane or any other ecosystem integration and [let us know](https://forum.plane.so)!
|
||||||
- Show your support by upvoting [popular feature requests](https://github.com/makeplane/plane/issues).
|
- Show your support by upvoting [popular feature requests](https://github.com/makeplane/plane/issues).
|
||||||
|
|
||||||
Please read [CONTRIBUTING.md](https://github.com/makeplane/plane/blob/master/CONTRIBUTING.md) for details on the process for submitting pull requests to us.
|
Please read [CONTRIBUTING.md](https://github.com/makeplane/plane/blob/master/CONTRIBUTING.md) for details on the process for submitting pull requests to us.
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ RUN corepack enable pnpm
|
||||||
|
|
||||||
FROM base AS builder
|
FROM base AS builder
|
||||||
|
|
||||||
RUN pnpm add -g turbo@2.6.3
|
RUN pnpm add -g turbo@2.9.4
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Lightbulb } from "lucide-react";
|
import { Lightbulb } from "lucide-react";
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
|
|
@ -42,7 +48,7 @@ export function InstanceAIForm(props: IInstanceAIForm) {
|
||||||
<a
|
<a
|
||||||
href="https://platform.openai.com/docs/models/overview"
|
href="https://platform.openai.com/docs/models/overview"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-custom-primary-100 hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
Learn more
|
Learn more
|
||||||
|
|
@ -63,7 +69,7 @@ export function InstanceAIForm(props: IInstanceAIForm) {
|
||||||
<a
|
<a
|
||||||
href="https://platform.openai.com/api-keys"
|
href="https://platform.openai.com/api-keys"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-custom-primary-100 hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
here.
|
here.
|
||||||
|
|
@ -94,8 +100,8 @@ export function InstanceAIForm(props: IInstanceAIForm) {
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="pb-1 text-xl font-medium text-custom-text-100">OpenAI</div>
|
<div className="pb-1 text-18 font-medium text-primary">OpenAI</div>
|
||||||
<div className="text-sm font-normal text-custom-text-300">If you use ChatGPT, this is for you.</div>
|
<div className="text-13 font-regular text-tertiary">If you use ChatGPT, this is for you.</div>
|
||||||
</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-12 gap-y-8 lg:grid-cols-3">
|
||||||
{aiFormFields.map((field) => (
|
{aiFormFields.map((field) => (
|
||||||
|
|
@ -114,16 +120,16 @@ export function InstanceAIForm(props: IInstanceAIForm) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4">
|
<div className="flex flex-col items-start gap-4">
|
||||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||||
{isSubmitting ? "Saving..." : "Save changes"}
|
{isSubmitting ? "Saving" : "Save changes"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="relative inline-flex items-center gap-2 rounded border border-custom-primary-100/20 bg-custom-primary-100/10 px-4 py-2 text-xs text-custom-primary-200">
|
<div className="relative inline-flex items-center gap-1.5 rounded-sm border border-accent-subtle bg-accent-subtle px-4 py-2 text-caption-sm-regular text-accent-secondary">
|
||||||
<Lightbulb height="14" width="14" />
|
<Lightbulb className="size-4" />
|
||||||
<div>
|
<div>
|
||||||
If you have a preferred AI models vendor, please get in{" "}
|
If you have a preferred AI models vendor, please get in{" "}
|
||||||
<a className="underline font-medium" href="https://plane.so/contact">
|
<a className="font-medium underline" href="https://plane.so/contact">
|
||||||
touch with us.
|
touch with us.
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
|
// components
|
||||||
|
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useInstance } from "@/hooks/store";
|
import { useInstance } from "@/hooks/store";
|
||||||
// components
|
// types
|
||||||
import type { Route } from "./+types/page";
|
import type { Route } from "./+types/page";
|
||||||
|
// local
|
||||||
import { InstanceAIForm } from "./form";
|
import { InstanceAIForm } from "./form";
|
||||||
|
|
||||||
const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentProps) {
|
const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentProps) {
|
||||||
|
|
@ -14,30 +23,25 @@ const InstanceAIPage = observer(function InstanceAIPage(_props: Route.ComponentP
|
||||||
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageWrapper
|
||||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
header={{
|
||||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
title: "AI features for all your workspaces",
|
||||||
<div className="text-xl font-medium text-custom-text-100">AI features for all your workspaces</div>
|
description: "Configure your AI API credentials so Plane AI features are turned on for all your workspaces.",
|
||||||
<div className="text-sm font-normal text-custom-text-300">
|
}}
|
||||||
Configure your AI API credentials so Plane AI features are turned on for all your workspaces.
|
>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
|
||||||
{formattedConfig ? (
|
{formattedConfig ? (
|
||||||
<InstanceAIForm config={formattedConfig} />
|
<InstanceAIForm config={formattedConfig} />
|
||||||
) : (
|
) : (
|
||||||
<Loader className="space-y-8">
|
<Loader className="space-y-8">
|
||||||
<Loader.Item height="50px" width="40%" />
|
<Loader.Item height="50px" width="40%" />
|
||||||
<div className="w-2/3 grid grid-cols-2 gap-x-8 gap-y-4">
|
<div className="grid w-2/3 grid-cols-2 gap-x-8 gap-y-4">
|
||||||
<Loader.Item height="50px" />
|
<Loader.Item height="50px" />
|
||||||
<Loader.Item height="50px" />
|
<Loader.Item height="50px" />
|
||||||
</div>
|
</div>
|
||||||
<Loader.Item height="50px" width="20%" />
|
<Loader.Item height="50px" width="20%" />
|
||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</PageWrapper>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,25 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { isEmpty } from "lodash-es";
|
import { isEmpty } from "lodash-es";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
// plane internal packages
|
// plane internal packages
|
||||||
import { API_BASE_URL } from "@plane/constants";
|
import { API_BASE_URL } from "@plane/constants";
|
||||||
|
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
import type { IFormattedInstanceConfiguration, TInstanceGiteaAuthenticationConfigurationKeys } from "@plane/types";
|
import type { IFormattedInstanceConfiguration, TInstanceGiteaAuthenticationConfigurationKeys } from "@plane/types";
|
||||||
import { Button, getButtonStyling } from "@plane/ui";
|
|
||||||
import { cn } from "@plane/utils";
|
|
||||||
// components
|
// components
|
||||||
import { CodeBlock } from "@/components/common/code-block";
|
import { CodeBlock } from "@/components/common/code-block";
|
||||||
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
|
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
|
||||||
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
||||||
import { ControllerInput } from "@/components/common/controller-input";
|
import { ControllerInput } from "@/components/common/controller-input";
|
||||||
|
import type { TControllerSwitchFormField } from "@/components/common/controller-switch";
|
||||||
|
import { ControllerSwitch } from "@/components/common/controller-switch";
|
||||||
import type { TCopyField } from "@/components/common/copy-field";
|
import type { TCopyField } from "@/components/common/copy-field";
|
||||||
import { CopyField } from "@/components/common/copy-field";
|
import { CopyField } from "@/components/common/copy-field";
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -41,6 +48,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
GITEA_HOST: config["GITEA_HOST"] || "https://gitea.com",
|
GITEA_HOST: config["GITEA_HOST"] || "https://gitea.com",
|
||||||
GITEA_CLIENT_ID: config["GITEA_CLIENT_ID"],
|
GITEA_CLIENT_ID: config["GITEA_CLIENT_ID"],
|
||||||
GITEA_CLIENT_SECRET: config["GITEA_CLIENT_SECRET"],
|
GITEA_CLIENT_SECRET: config["GITEA_CLIENT_SECRET"],
|
||||||
|
ENABLE_GITEA_SYNC: config["ENABLE_GITEA_SYNC"] || "0",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -69,7 +77,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://gitea.com/user/settings/applications"
|
href="https://gitea.com/user/settings/applications"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-custom-primary-100 hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
Gitea OAuth application settings.
|
Gitea OAuth application settings.
|
||||||
|
|
@ -91,7 +99,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://gitea.com/user/settings/applications"
|
href="https://gitea.com/user/settings/applications"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-custom-primary-100 hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
Gitea OAuth application settings.
|
Gitea OAuth application settings.
|
||||||
|
|
@ -104,6 +112,11 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const GITEA_FORM_SWITCH_FIELD: TControllerSwitchFormField<GiteaConfigFormValues> = {
|
||||||
|
name: "ENABLE_GITEA_SYNC",
|
||||||
|
label: "Gitea",
|
||||||
|
};
|
||||||
|
|
||||||
const GITEA_SERVICE_FIELD: TCopyField[] = [
|
const GITEA_SERVICE_FIELD: TCopyField[] = [
|
||||||
{
|
{
|
||||||
key: "Callback_URI",
|
key: "Callback_URI",
|
||||||
|
|
@ -117,7 +130,7 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href={`${control._formValues.GITEA_HOST || "https://gitea.com"}/user/settings/applications`}
|
href={`${control._formValues.GITEA_HOST || "https://gitea.com"}/user/settings/applications`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-custom-primary-100 hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
here.
|
here.
|
||||||
|
|
@ -130,8 +143,8 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
const onSubmit = async (formData: GiteaConfigFormValues) => {
|
const onSubmit = async (formData: GiteaConfigFormValues) => {
|
||||||
const payload: Partial<GiteaConfigFormValues> = { ...formData };
|
const payload: Partial<GiteaConfigFormValues> = { ...formData };
|
||||||
|
|
||||||
await updateInstanceConfigurations(payload)
|
try {
|
||||||
.then((response = []) => {
|
const response = await updateInstanceConfigurations(payload);
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: "Done!",
|
title: "Done!",
|
||||||
|
|
@ -141,9 +154,11 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
GITEA_HOST: response.find((item) => item.key === "GITEA_HOST")?.value,
|
GITEA_HOST: response.find((item) => item.key === "GITEA_HOST")?.value,
|
||||||
GITEA_CLIENT_ID: response.find((item) => item.key === "GITEA_CLIENT_ID")?.value,
|
GITEA_CLIENT_ID: response.find((item) => item.key === "GITEA_CLIENT_ID")?.value,
|
||||||
GITEA_CLIENT_SECRET: response.find((item) => item.key === "GITEA_CLIENT_SECRET")?.value,
|
GITEA_CLIENT_SECRET: response.find((item) => item.key === "GITEA_CLIENT_SECRET")?.value,
|
||||||
|
ENABLE_GITEA_SYNC: response.find((item) => item.key === "ENABLE_GITEA_SYNC")?.value,
|
||||||
});
|
});
|
||||||
})
|
} catch (err) {
|
||||||
.catch((err) => console.error(err));
|
console.error(err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||||
|
|
@ -161,9 +176,9 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
handleClose={() => setIsDiscardChangesModalOpen(false)}
|
handleClose={() => setIsDiscardChangesModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
|
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
|
||||||
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
|
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
|
||||||
<div className="pt-2.5 text-xl font-medium">Gitea-provided details for Plane</div>
|
<div className="pt-2.5 text-18 font-medium">Gitea-provided details for Plane</div>
|
||||||
{GITEA_FORM_FIELDS.map((field) => (
|
{GITEA_FORM_FIELDS.map((field) => (
|
||||||
<ControllerInput
|
<ControllerInput
|
||||||
key={field.key}
|
key={field.key}
|
||||||
|
|
@ -177,24 +192,27 @@ export function InstanceGiteaConfigForm(props: Props) {
|
||||||
required={field.required}
|
required={field.required}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
<ControllerSwitch control={control} field={GITEA_FORM_SWITCH_FIELD} />
|
||||||
<div className="flex flex-col gap-1 pt-4">
|
<div className="flex flex-col gap-1 pt-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
|
<Button
|
||||||
{isSubmitting ? "Saving..." : "Save changes"}
|
variant="primary"
|
||||||
</Button>
|
size="lg"
|
||||||
<Link
|
onClick={(e) => void handleSubmit(onSubmit)(e)}
|
||||||
href="/authentication"
|
loading={isSubmitting}
|
||||||
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
|
disabled={!isDirty}
|
||||||
onClick={handleGoBack}
|
|
||||||
>
|
>
|
||||||
|
{isSubmitting ? "Saving" : "Save changes"}
|
||||||
|
</Button>
|
||||||
|
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
||||||
Go back
|
Go back
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 md:col-span-1">
|
<div className="col-span-2 md:col-span-1">
|
||||||
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-custom-background-80/60 rounded-lg">
|
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-1 px-6 pt-1.5 pb-4">
|
||||||
<div className="pt-2 text-xl font-medium">Plane-provided details for Gitea</div>
|
<div className="pt-2 text-18 font-medium">Plane-provided details for Gitea</div>
|
||||||
{GITEA_SERVICE_FIELD.map((field) => (
|
{GITEA_SERVICE_FIELD.map((field) => (
|
||||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,25 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// plane internal packages
|
// plane internal packages
|
||||||
import { setPromiseToast } from "@plane/propel/toast";
|
import { setPromiseToast } from "@plane/propel/toast";
|
||||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||||
// components
|
// assets
|
||||||
import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
|
import giteaLogo from "@/app/assets/logos/gitea-logo.svg?url";
|
||||||
|
// components
|
||||||
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
||||||
|
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useInstance } from "@/hooks/store";
|
import { useInstance } from "@/hooks/store";
|
||||||
//local components
|
// types
|
||||||
import type { Route } from "./+types/page";
|
import type { Route } from "./+types/page";
|
||||||
|
// local
|
||||||
import { InstanceGiteaConfigForm } from "./form";
|
import { InstanceGiteaConfigForm } from "./form";
|
||||||
|
|
||||||
const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthenticationPage() {
|
const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthenticationPage() {
|
||||||
|
|
@ -32,7 +41,7 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
|
||||||
const updateConfigPromise = updateInstanceConfigurations(payload);
|
const updateConfigPromise = updateInstanceConfigurations(payload);
|
||||||
|
|
||||||
setPromiseToast(updateConfigPromise, {
|
setPromiseToast(updateConfigPromise, {
|
||||||
loading: "Saving Configuration...",
|
loading: "Saving Configuration",
|
||||||
success: {
|
success: {
|
||||||
title: "Configuration saved",
|
title: "Configuration saved",
|
||||||
message: () => `Gitea authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
message: () => `Gitea authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
||||||
|
|
@ -56,9 +65,8 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
|
||||||
const isGiteaEnabled = enableGiteaConfig === "1";
|
const isGiteaEnabled = enableGiteaConfig === "1";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageWrapper
|
||||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
customHeader={
|
||||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
|
||||||
<AuthenticationMethodCard
|
<AuthenticationMethodCard
|
||||||
name="Gitea"
|
name="Gitea"
|
||||||
description="Allow members to login or sign up to plane with their Gitea accounts."
|
description="Allow members to login or sign up to plane with their Gitea accounts."
|
||||||
|
|
@ -76,8 +84,8 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
|
||||||
disabled={isSubmitting || !formattedConfig}
|
disabled={isSubmitting || !formattedConfig}
|
||||||
withBorder={false}
|
withBorder={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
}
|
||||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
>
|
||||||
{formattedConfig ? (
|
{formattedConfig ? (
|
||||||
<InstanceGiteaConfigForm config={formattedConfig} />
|
<InstanceGiteaConfigForm config={formattedConfig} />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -89,9 +97,7 @@ const InstanceGiteaAuthenticationPage = observer(function InstanceGiteaAuthentic
|
||||||
<Loader.Item height="50px" width="50%" />
|
<Loader.Item height="50px" width="50%" />
|
||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</PageWrapper>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
export const meta: Route.MetaFunction = () => [{ title: "Gitea Authentication - God Mode" }];
|
export const meta: Route.MetaFunction = () => [{ title: "Gitea Authentication - God Mode" }];
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { isEmpty } from "lodash-es";
|
import { isEmpty } from "lodash-es";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
@ -8,12 +14,12 @@ import { API_BASE_URL } from "@plane/constants";
|
||||||
import { Button, getButtonStyling } from "@plane/propel/button";
|
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
import type { IFormattedInstanceConfiguration, TInstanceGithubAuthenticationConfigurationKeys } from "@plane/types";
|
import type { IFormattedInstanceConfiguration, TInstanceGithubAuthenticationConfigurationKeys } from "@plane/types";
|
||||||
|
|
||||||
import { cn } from "@plane/utils";
|
|
||||||
// components
|
// components
|
||||||
import { CodeBlock } from "@/components/common/code-block";
|
import { CodeBlock } from "@/components/common/code-block";
|
||||||
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
|
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
|
||||||
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
||||||
|
import type { TControllerSwitchFormField } from "@/components/common/controller-switch";
|
||||||
|
import { ControllerSwitch } from "@/components/common/controller-switch";
|
||||||
import { ControllerInput } from "@/components/common/controller-input";
|
import { ControllerInput } from "@/components/common/controller-input";
|
||||||
import type { TCopyField } from "@/components/common/copy-field";
|
import type { TCopyField } from "@/components/common/copy-field";
|
||||||
import { CopyField } from "@/components/common/copy-field";
|
import { CopyField } from "@/components/common/copy-field";
|
||||||
|
|
@ -43,6 +49,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
GITHUB_CLIENT_ID: config["GITHUB_CLIENT_ID"],
|
GITHUB_CLIENT_ID: config["GITHUB_CLIENT_ID"],
|
||||||
GITHUB_CLIENT_SECRET: config["GITHUB_CLIENT_SECRET"],
|
GITHUB_CLIENT_SECRET: config["GITHUB_CLIENT_SECRET"],
|
||||||
GITHUB_ORGANIZATION_ID: config["GITHUB_ORGANIZATION_ID"],
|
GITHUB_ORGANIZATION_ID: config["GITHUB_ORGANIZATION_ID"],
|
||||||
|
ENABLE_GITHUB_SYNC: config["ENABLE_GITHUB_SYNC"] || "0",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -60,7 +67,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://github.com/settings/applications/new"
|
href="https://github.com/settings/applications/new"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-custom-primary-100 hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
GitHub OAuth application settings.
|
GitHub OAuth application settings.
|
||||||
|
|
@ -82,7 +89,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://github.com/settings/applications/new"
|
href="https://github.com/settings/applications/new"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-custom-primary-100 hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
GitHub OAuth application settings.
|
GitHub OAuth application settings.
|
||||||
|
|
@ -104,6 +111,11 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const GITHUB_FORM_SWITCH_FIELD: TControllerSwitchFormField<GithubConfigFormValues> = {
|
||||||
|
name: "ENABLE_GITHUB_SYNC",
|
||||||
|
label: "GitHub",
|
||||||
|
};
|
||||||
|
|
||||||
const GITHUB_COMMON_SERVICE_DETAILS: TCopyField[] = [
|
const GITHUB_COMMON_SERVICE_DETAILS: TCopyField[] = [
|
||||||
{
|
{
|
||||||
key: "Origin_URL",
|
key: "Origin_URL",
|
||||||
|
|
@ -116,7 +128,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://github.com/settings/applications/new"
|
href="https://github.com/settings/applications/new"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-custom-primary-100 hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
here.
|
here.
|
||||||
|
|
@ -139,7 +151,7 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://github.com/settings/applications/new"
|
href="https://github.com/settings/applications/new"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-custom-primary-100 hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
here.
|
here.
|
||||||
|
|
@ -152,8 +164,8 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
const onSubmit = async (formData: GithubConfigFormValues) => {
|
const onSubmit = async (formData: GithubConfigFormValues) => {
|
||||||
const payload: Partial<GithubConfigFormValues> = { ...formData };
|
const payload: Partial<GithubConfigFormValues> = { ...formData };
|
||||||
|
|
||||||
await updateInstanceConfigurations(payload)
|
try {
|
||||||
.then((response = []) => {
|
const response = await updateInstanceConfigurations(payload);
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: "Done!",
|
title: "Done!",
|
||||||
|
|
@ -163,9 +175,11 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
GITHUB_CLIENT_ID: response.find((item) => item.key === "GITHUB_CLIENT_ID")?.value,
|
GITHUB_CLIENT_ID: response.find((item) => item.key === "GITHUB_CLIENT_ID")?.value,
|
||||||
GITHUB_CLIENT_SECRET: response.find((item) => item.key === "GITHUB_CLIENT_SECRET")?.value,
|
GITHUB_CLIENT_SECRET: response.find((item) => item.key === "GITHUB_CLIENT_SECRET")?.value,
|
||||||
GITHUB_ORGANIZATION_ID: response.find((item) => item.key === "GITHUB_ORGANIZATION_ID")?.value,
|
GITHUB_ORGANIZATION_ID: response.find((item) => item.key === "GITHUB_ORGANIZATION_ID")?.value,
|
||||||
|
ENABLE_GITHUB_SYNC: response.find((item) => item.key === "ENABLE_GITHUB_SYNC")?.value,
|
||||||
});
|
});
|
||||||
})
|
} catch (err) {
|
||||||
.catch((err) => console.error(err));
|
console.error(err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||||
|
|
@ -183,9 +197,9 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
handleClose={() => setIsDiscardChangesModalOpen(false)}
|
handleClose={() => setIsDiscardChangesModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
|
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
|
||||||
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
|
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
|
||||||
<div className="pt-2.5 text-xl font-medium">GitHub-provided details for Plane</div>
|
<div className="pt-2.5 text-18 font-medium">GitHub-provided details for Plane</div>
|
||||||
{GITHUB_FORM_FIELDS.map((field) => (
|
{GITHUB_FORM_FIELDS.map((field) => (
|
||||||
<ControllerInput
|
<ControllerInput
|
||||||
key={field.key}
|
key={field.key}
|
||||||
|
|
@ -199,39 +213,42 @@ export function InstanceGithubConfigForm(props: Props) {
|
||||||
required={field.required}
|
required={field.required}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
<ControllerSwitch control={control} field={GITHUB_FORM_SWITCH_FIELD} />
|
||||||
<div className="flex flex-col gap-1 pt-4">
|
<div className="flex flex-col gap-1 pt-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
|
<Button
|
||||||
{isSubmitting ? "Saving..." : "Save changes"}
|
variant="primary"
|
||||||
</Button>
|
size="lg"
|
||||||
<Link
|
onClick={(e) => void handleSubmit(onSubmit)(e)}
|
||||||
href="/authentication"
|
loading={isSubmitting}
|
||||||
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
|
disabled={!isDirty}
|
||||||
onClick={handleGoBack}
|
|
||||||
>
|
>
|
||||||
|
{isSubmitting ? "Saving" : "Save changes"}
|
||||||
|
</Button>
|
||||||
|
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
||||||
Go back
|
Go back
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 md:col-span-1 flex flex-col gap-y-6">
|
<div className="col-span-2 flex flex-col gap-y-6 md:col-span-1">
|
||||||
<div className="pt-2 text-xl font-medium">Plane-provided details for GitHub</div>
|
<div className="pt-2 text-18 font-medium">Plane-provided details for GitHub</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
{/* common service details */}
|
{/* common service details */}
|
||||||
<div className="flex flex-col gap-y-4 px-6 py-4 bg-custom-background-80 rounded-lg">
|
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-1 px-6 py-4">
|
||||||
{GITHUB_COMMON_SERVICE_DETAILS.map((field) => (
|
{GITHUB_COMMON_SERVICE_DETAILS.map((field) => (
|
||||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* web service details */}
|
{/* web service details */}
|
||||||
<div className="flex flex-col rounded-lg overflow-hidden">
|
<div className="flex flex-col overflow-hidden rounded-lg">
|
||||||
<div className="px-6 py-3 bg-custom-background-80/60 font-medium text-xs uppercase flex items-center gap-x-3 text-custom-text-200">
|
<div className="flex items-center gap-x-3 bg-layer-3 px-6 py-3 text-11 font-medium text-secondary uppercase">
|
||||||
<Monitor className="w-3 h-3" />
|
<Monitor className="h-3 w-3" />
|
||||||
Web
|
Web
|
||||||
</div>
|
</div>
|
||||||
<div className="px-6 py-4 flex flex-col gap-y-4 bg-custom-background-80">
|
<div className="flex flex-col gap-y-4 bg-layer-1 px-6 py-4">
|
||||||
{GITHUB_SERVICE_DETAILS.map((field) => (
|
{GITHUB_SERVICE_DETAILS.map((field) => (
|
||||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
|
|
@ -6,15 +12,17 @@ import useSWR from "swr";
|
||||||
import { setPromiseToast } from "@plane/propel/toast";
|
import { setPromiseToast } from "@plane/propel/toast";
|
||||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||||
import { resolveGeneralTheme } from "@plane/utils";
|
import { resolveGeneralTheme } from "@plane/utils";
|
||||||
// components
|
// assets
|
||||||
import githubLightModeImage from "@/app/assets/logos/github-black.png?url";
|
import githubLightModeImage from "@/app/assets/logos/github-black.png?url";
|
||||||
import githubDarkModeImage from "@/app/assets/logos/github-white.png?url";
|
import githubDarkModeImage from "@/app/assets/logos/github-white.png?url";
|
||||||
|
// components
|
||||||
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
||||||
|
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useInstance } from "@/hooks/store";
|
import { useInstance } from "@/hooks/store";
|
||||||
// icons
|
// types
|
||||||
// local components
|
|
||||||
import type { Route } from "./+types/page";
|
import type { Route } from "./+types/page";
|
||||||
|
// local
|
||||||
import { InstanceGithubConfigForm } from "./form";
|
import { InstanceGithubConfigForm } from "./form";
|
||||||
|
|
||||||
const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthenticationPage(
|
const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthenticationPage(
|
||||||
|
|
@ -41,7 +49,7 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
|
||||||
const updateConfigPromise = updateInstanceConfigurations(payload);
|
const updateConfigPromise = updateInstanceConfigurations(payload);
|
||||||
|
|
||||||
setPromiseToast(updateConfigPromise, {
|
setPromiseToast(updateConfigPromise, {
|
||||||
loading: "Saving Configuration...",
|
loading: "Saving Configuration",
|
||||||
success: {
|
success: {
|
||||||
title: "Configuration saved",
|
title: "Configuration saved",
|
||||||
message: () => `GitHub authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
message: () => `GitHub authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
||||||
|
|
@ -65,9 +73,8 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
|
||||||
const isGithubEnabled = enableGithubConfig === "1";
|
const isGithubEnabled = enableGithubConfig === "1";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageWrapper
|
||||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
customHeader={
|
||||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
|
||||||
<AuthenticationMethodCard
|
<AuthenticationMethodCard
|
||||||
name="GitHub"
|
name="GitHub"
|
||||||
description="Allow members to login or sign up to plane with their GitHub accounts."
|
description="Allow members to login or sign up to plane with their GitHub accounts."
|
||||||
|
|
@ -92,8 +99,8 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
|
||||||
disabled={isSubmitting || !formattedConfig}
|
disabled={isSubmitting || !formattedConfig}
|
||||||
withBorder={false}
|
withBorder={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
}
|
||||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
>
|
||||||
{formattedConfig ? (
|
{formattedConfig ? (
|
||||||
<InstanceGithubConfigForm config={formattedConfig} />
|
<InstanceGithubConfigForm config={formattedConfig} />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -105,9 +112,7 @@ const InstanceGithubAuthenticationPage = observer(function InstanceGithubAuthent
|
||||||
<Loader.Item height="50px" width="50%" />
|
<Loader.Item height="50px" width="50%" />
|
||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</PageWrapper>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { isEmpty } from "lodash-es";
|
import { isEmpty } from "lodash-es";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
@ -7,11 +13,12 @@ import { API_BASE_URL } from "@plane/constants";
|
||||||
import { Button, getButtonStyling } from "@plane/propel/button";
|
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
import type { IFormattedInstanceConfiguration, TInstanceGitlabAuthenticationConfigurationKeys } from "@plane/types";
|
import type { IFormattedInstanceConfiguration, TInstanceGitlabAuthenticationConfigurationKeys } from "@plane/types";
|
||||||
import { cn } from "@plane/utils";
|
|
||||||
// components
|
// components
|
||||||
import { CodeBlock } from "@/components/common/code-block";
|
import { CodeBlock } from "@/components/common/code-block";
|
||||||
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
|
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
|
||||||
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
||||||
|
import type { TControllerSwitchFormField } from "@/components/common/controller-switch";
|
||||||
|
import { ControllerSwitch } from "@/components/common/controller-switch";
|
||||||
import { ControllerInput } from "@/components/common/controller-input";
|
import { ControllerInput } from "@/components/common/controller-input";
|
||||||
import type { TCopyField } from "@/components/common/copy-field";
|
import type { TCopyField } from "@/components/common/copy-field";
|
||||||
import { CopyField } from "@/components/common/copy-field";
|
import { CopyField } from "@/components/common/copy-field";
|
||||||
|
|
@ -41,6 +48,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
GITLAB_HOST: config["GITLAB_HOST"],
|
GITLAB_HOST: config["GITLAB_HOST"],
|
||||||
GITLAB_CLIENT_ID: config["GITLAB_CLIENT_ID"],
|
GITLAB_CLIENT_ID: config["GITLAB_CLIENT_ID"],
|
||||||
GITLAB_CLIENT_SECRET: config["GITLAB_CLIENT_SECRET"],
|
GITLAB_CLIENT_SECRET: config["GITLAB_CLIENT_SECRET"],
|
||||||
|
ENABLE_GITLAB_SYNC: config["ENABLE_GITLAB_SYNC"] || "0",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -71,7 +79,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-custom-primary-100 hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
GitLab OAuth application settings
|
GitLab OAuth application settings
|
||||||
|
|
@ -94,7 +102,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-custom-primary-100 hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
GitLab OAuth application settings
|
GitLab OAuth application settings
|
||||||
|
|
@ -108,6 +116,11 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const GITLAB_FORM_SWITCH_FIELD: TControllerSwitchFormField<GitlabConfigFormValues> = {
|
||||||
|
name: "ENABLE_GITLAB_SYNC",
|
||||||
|
label: "GitLab",
|
||||||
|
};
|
||||||
|
|
||||||
const GITLAB_SERVICE_FIELD: TCopyField[] = [
|
const GITLAB_SERVICE_FIELD: TCopyField[] = [
|
||||||
{
|
{
|
||||||
key: "Callback_URL",
|
key: "Callback_URL",
|
||||||
|
|
@ -120,7 +133,7 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
href="https://docs.gitlab.com/ee/integration/oauth_provider.html"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-custom-primary-100 hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
GitLab OAuth application
|
GitLab OAuth application
|
||||||
|
|
@ -134,8 +147,8 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
const onSubmit = async (formData: GitlabConfigFormValues) => {
|
const onSubmit = async (formData: GitlabConfigFormValues) => {
|
||||||
const payload: Partial<GitlabConfigFormValues> = { ...formData };
|
const payload: Partial<GitlabConfigFormValues> = { ...formData };
|
||||||
|
|
||||||
await updateInstanceConfigurations(payload)
|
try {
|
||||||
.then((response = []) => {
|
const response = await updateInstanceConfigurations(payload);
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: "Done!",
|
title: "Done!",
|
||||||
|
|
@ -145,9 +158,11 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
GITLAB_HOST: response.find((item) => item.key === "GITLAB_HOST")?.value,
|
GITLAB_HOST: response.find((item) => item.key === "GITLAB_HOST")?.value,
|
||||||
GITLAB_CLIENT_ID: response.find((item) => item.key === "GITLAB_CLIENT_ID")?.value,
|
GITLAB_CLIENT_ID: response.find((item) => item.key === "GITLAB_CLIENT_ID")?.value,
|
||||||
GITLAB_CLIENT_SECRET: response.find((item) => item.key === "GITLAB_CLIENT_SECRET")?.value,
|
GITLAB_CLIENT_SECRET: response.find((item) => item.key === "GITLAB_CLIENT_SECRET")?.value,
|
||||||
|
ENABLE_GITLAB_SYNC: response.find((item) => item.key === "ENABLE_GITLAB_SYNC")?.value,
|
||||||
});
|
});
|
||||||
})
|
} catch (err) {
|
||||||
.catch((err) => console.error(err));
|
console.error(err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||||
|
|
@ -165,9 +180,9 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
handleClose={() => setIsDiscardChangesModalOpen(false)}
|
handleClose={() => setIsDiscardChangesModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
|
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
|
||||||
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
|
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
|
||||||
<div className="pt-2.5 text-xl font-medium">GitLab-provided details for Plane</div>
|
<div className="pt-2.5 text-18 font-medium">GitLab-provided details for Plane</div>
|
||||||
{GITLAB_FORM_FIELDS.map((field) => (
|
{GITLAB_FORM_FIELDS.map((field) => (
|
||||||
<ControllerInput
|
<ControllerInput
|
||||||
key={field.key}
|
key={field.key}
|
||||||
|
|
@ -181,24 +196,27 @@ export function InstanceGitlabConfigForm(props: Props) {
|
||||||
required={field.required}
|
required={field.required}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
<ControllerSwitch control={control} field={GITLAB_FORM_SWITCH_FIELD} />
|
||||||
<div className="flex flex-col gap-1 pt-4">
|
<div className="flex flex-col gap-1 pt-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
|
<Button
|
||||||
{isSubmitting ? "Saving..." : "Save changes"}
|
variant="primary"
|
||||||
</Button>
|
size="lg"
|
||||||
<Link
|
onClick={(e) => void handleSubmit(onSubmit)(e)}
|
||||||
href="/authentication"
|
loading={isSubmitting}
|
||||||
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
|
disabled={!isDirty}
|
||||||
onClick={handleGoBack}
|
|
||||||
>
|
>
|
||||||
|
{isSubmitting ? "Saving" : "Save changes"}
|
||||||
|
</Button>
|
||||||
|
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
||||||
Go back
|
Go back
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 md:col-span-1">
|
<div className="col-span-2 md:col-span-1">
|
||||||
<div className="flex flex-col gap-y-4 px-6 pt-1.5 pb-4 bg-custom-background-80/60 rounded-lg">
|
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-3 px-6 pt-1.5 pb-4">
|
||||||
<div className="pt-2 text-xl font-medium">Plane-provided details for GitLab</div>
|
<div className="pt-2 text-18 font-medium">Plane-provided details for GitLab</div>
|
||||||
{GITLAB_SERVICE_FIELD.map((field) => (
|
{GITLAB_SERVICE_FIELD.map((field) => (
|
||||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,24 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { setPromiseToast } from "@plane/propel/toast";
|
import { setPromiseToast } from "@plane/propel/toast";
|
||||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||||
// components
|
// assets
|
||||||
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
|
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
|
||||||
|
// components
|
||||||
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
||||||
|
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useInstance } from "@/hooks/store";
|
import { useInstance } from "@/hooks/store";
|
||||||
// icons
|
// types
|
||||||
// local components
|
|
||||||
import type { Route } from "./+types/page";
|
import type { Route } from "./+types/page";
|
||||||
|
// local
|
||||||
import { InstanceGitlabConfigForm } from "./form";
|
import { InstanceGitlabConfigForm } from "./form";
|
||||||
|
|
||||||
const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthenticationPage(
|
const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthenticationPage(
|
||||||
|
|
@ -35,7 +43,7 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
|
||||||
const updateConfigPromise = updateInstanceConfigurations(payload);
|
const updateConfigPromise = updateInstanceConfigurations(payload);
|
||||||
|
|
||||||
setPromiseToast(updateConfigPromise, {
|
setPromiseToast(updateConfigPromise, {
|
||||||
loading: "Saving Configuration...",
|
loading: "Saving Configuration",
|
||||||
success: {
|
success: {
|
||||||
title: "Configuration saved",
|
title: "Configuration saved",
|
||||||
message: () => `GitLab authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
message: () => `GitLab authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
||||||
|
|
@ -56,9 +64,8 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<PageWrapper
|
||||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
customHeader={
|
||||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
|
||||||
<AuthenticationMethodCard
|
<AuthenticationMethodCard
|
||||||
name="GitLab"
|
name="GitLab"
|
||||||
description="Allow members to login or sign up to plane with their GitLab accounts."
|
description="Allow members to login or sign up to plane with their GitLab accounts."
|
||||||
|
|
@ -80,8 +87,8 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
|
||||||
disabled={isSubmitting || !formattedConfig}
|
disabled={isSubmitting || !formattedConfig}
|
||||||
withBorder={false}
|
withBorder={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
}
|
||||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
>
|
||||||
{formattedConfig ? (
|
{formattedConfig ? (
|
||||||
<InstanceGitlabConfigForm config={formattedConfig} />
|
<InstanceGitlabConfigForm config={formattedConfig} />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -93,9 +100,7 @@ const InstanceGitlabAuthenticationPage = observer(function InstanceGitlabAuthent
|
||||||
<Loader.Item height="50px" width="50%" />
|
<Loader.Item height="50px" width="50%" />
|
||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</PageWrapper>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { isEmpty } from "lodash-es";
|
import { isEmpty } from "lodash-es";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
@ -8,11 +14,12 @@ import { API_BASE_URL } from "@plane/constants";
|
||||||
import { Button, getButtonStyling } from "@plane/propel/button";
|
import { Button, getButtonStyling } from "@plane/propel/button";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
import type { IFormattedInstanceConfiguration, TInstanceGoogleAuthenticationConfigurationKeys } from "@plane/types";
|
import type { IFormattedInstanceConfiguration, TInstanceGoogleAuthenticationConfigurationKeys } from "@plane/types";
|
||||||
import { cn } from "@plane/utils";
|
|
||||||
// components
|
// components
|
||||||
import { CodeBlock } from "@/components/common/code-block";
|
import { CodeBlock } from "@/components/common/code-block";
|
||||||
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
|
import { ConfirmDiscardModal } from "@/components/common/confirm-discard-modal";
|
||||||
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
import type { TControllerInputFormField } from "@/components/common/controller-input";
|
||||||
|
import type { TControllerSwitchFormField } from "@/components/common/controller-switch";
|
||||||
|
import { ControllerSwitch } from "@/components/common/controller-switch";
|
||||||
import { ControllerInput } from "@/components/common/controller-input";
|
import { ControllerInput } from "@/components/common/controller-input";
|
||||||
import type { TCopyField } from "@/components/common/copy-field";
|
import type { TCopyField } from "@/components/common/copy-field";
|
||||||
import { CopyField } from "@/components/common/copy-field";
|
import { CopyField } from "@/components/common/copy-field";
|
||||||
|
|
@ -41,6 +48,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
GOOGLE_CLIENT_ID: config["GOOGLE_CLIENT_ID"],
|
GOOGLE_CLIENT_ID: config["GOOGLE_CLIENT_ID"],
|
||||||
GOOGLE_CLIENT_SECRET: config["GOOGLE_CLIENT_SECRET"],
|
GOOGLE_CLIENT_SECRET: config["GOOGLE_CLIENT_SECRET"],
|
||||||
|
ENABLE_GOOGLE_SYNC: config["ENABLE_GOOGLE_SYNC"] || "0",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -58,7 +66,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#creatingcred"
|
href="https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow#creatingcred"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-custom-primary-100 hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
Learn more
|
Learn more
|
||||||
|
|
@ -80,7 +88,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://developers.google.com/identity/oauth2/web/guides/get-google-api-clientid"
|
href="https://developers.google.com/identity/oauth2/web/guides/get-google-api-clientid"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-custom-primary-100 hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
Learn more
|
Learn more
|
||||||
|
|
@ -93,6 +101,11 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const GOOGLE_FORM_SWITCH_FIELD: TControllerSwitchFormField<GoogleConfigFormValues> = {
|
||||||
|
name: "ENABLE_GOOGLE_SYNC",
|
||||||
|
label: "Google",
|
||||||
|
};
|
||||||
|
|
||||||
const GOOGLE_COMMON_SERVICE_DETAILS: TCopyField[] = [
|
const GOOGLE_COMMON_SERVICE_DETAILS: TCopyField[] = [
|
||||||
{
|
{
|
||||||
key: "Origin_URL",
|
key: "Origin_URL",
|
||||||
|
|
@ -105,7 +118,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
<a
|
<a
|
||||||
href="https://console.cloud.google.com/apis/credentials/oauthclient"
|
href="https://console.cloud.google.com/apis/credentials/oauthclient"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-custom-primary-100 hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
here.
|
here.
|
||||||
|
|
@ -127,7 +140,7 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
<a
|
<a
|
||||||
href="https://console.cloud.google.com/apis/credentials/oauthclient"
|
href="https://console.cloud.google.com/apis/credentials/oauthclient"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-custom-primary-100 hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
here.
|
here.
|
||||||
|
|
@ -140,8 +153,8 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
const onSubmit = async (formData: GoogleConfigFormValues) => {
|
const onSubmit = async (formData: GoogleConfigFormValues) => {
|
||||||
const payload: Partial<GoogleConfigFormValues> = { ...formData };
|
const payload: Partial<GoogleConfigFormValues> = { ...formData };
|
||||||
|
|
||||||
await updateInstanceConfigurations(payload)
|
try {
|
||||||
.then((response = []) => {
|
const response = await updateInstanceConfigurations(payload);
|
||||||
setToast({
|
setToast({
|
||||||
type: TOAST_TYPE.SUCCESS,
|
type: TOAST_TYPE.SUCCESS,
|
||||||
title: "Done!",
|
title: "Done!",
|
||||||
|
|
@ -150,9 +163,11 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
reset({
|
reset({
|
||||||
GOOGLE_CLIENT_ID: response.find((item) => item.key === "GOOGLE_CLIENT_ID")?.value,
|
GOOGLE_CLIENT_ID: response.find((item) => item.key === "GOOGLE_CLIENT_ID")?.value,
|
||||||
GOOGLE_CLIENT_SECRET: response.find((item) => item.key === "GOOGLE_CLIENT_SECRET")?.value,
|
GOOGLE_CLIENT_SECRET: response.find((item) => item.key === "GOOGLE_CLIENT_SECRET")?.value,
|
||||||
|
ENABLE_GOOGLE_SYNC: response.find((item) => item.key === "ENABLE_GOOGLE_SYNC")?.value,
|
||||||
});
|
});
|
||||||
})
|
} catch (err) {
|
||||||
.catch((err) => console.error(err));
|
console.error(err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
const handleGoBack = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
|
||||||
|
|
@ -170,9 +185,9 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
handleClose={() => setIsDiscardChangesModalOpen(false)}
|
handleClose={() => setIsDiscardChangesModalOpen(false)}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
<div className="grid grid-cols-2 gap-x-12 gap-y-8 w-full">
|
<div className="grid w-full grid-cols-2 gap-x-12 gap-y-8">
|
||||||
<div className="flex flex-col gap-y-4 col-span-2 md:col-span-1 pt-1">
|
<div className="col-span-2 flex flex-col gap-y-4 pt-1 md:col-span-1">
|
||||||
<div className="pt-2.5 text-xl font-medium">Google-provided details for Plane</div>
|
<div className="pt-2.5 text-18 font-medium">Google-provided details for Plane</div>
|
||||||
{GOOGLE_FORM_FIELDS.map((field) => (
|
{GOOGLE_FORM_FIELDS.map((field) => (
|
||||||
<ControllerInput
|
<ControllerInput
|
||||||
key={field.key}
|
key={field.key}
|
||||||
|
|
@ -186,39 +201,42 @@ export function InstanceGoogleConfigForm(props: Props) {
|
||||||
required={field.required}
|
required={field.required}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
<ControllerSwitch control={control} field={GOOGLE_FORM_SWITCH_FIELD} />
|
||||||
<div className="flex flex-col gap-1 pt-4">
|
<div className="flex flex-col gap-1 pt-4">
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting} disabled={!isDirty}>
|
<Button
|
||||||
{isSubmitting ? "Saving..." : "Save changes"}
|
variant="primary"
|
||||||
</Button>
|
size="lg"
|
||||||
<Link
|
onClick={(e) => void handleSubmit(onSubmit)(e)}
|
||||||
href="/authentication"
|
loading={isSubmitting}
|
||||||
className={cn(getButtonStyling("neutral-primary", "md"), "font-medium")}
|
disabled={!isDirty}
|
||||||
onClick={handleGoBack}
|
|
||||||
>
|
>
|
||||||
|
{isSubmitting ? "Saving" : "Save changes"}
|
||||||
|
</Button>
|
||||||
|
<Link href="/authentication" className={getButtonStyling("secondary", "lg")} onClick={handleGoBack}>
|
||||||
Go back
|
Go back
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 md:col-span-1 flex flex-col gap-y-6">
|
<div className="col-span-2 flex flex-col gap-y-6 md:col-span-1">
|
||||||
<div className="pt-2 text-xl font-medium">Plane-provided details for Google</div>
|
<div className="pt-2 text-18 font-medium">Plane-provided details for Google</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-y-4">
|
<div className="flex flex-col gap-y-4">
|
||||||
{/* common service details */}
|
{/* common service details */}
|
||||||
<div className="flex flex-col gap-y-4 px-6 py-4 bg-custom-background-80 rounded-lg">
|
<div className="flex flex-col gap-y-4 rounded-lg bg-layer-1 px-6 py-4">
|
||||||
{GOOGLE_COMMON_SERVICE_DETAILS.map((field) => (
|
{GOOGLE_COMMON_SERVICE_DETAILS.map((field) => (
|
||||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* web service details */}
|
{/* web service details */}
|
||||||
<div className="flex flex-col rounded-lg overflow-hidden">
|
<div className="flex flex-col overflow-hidden rounded-lg">
|
||||||
<div className="px-6 py-3 bg-custom-background-80/60 font-medium text-xs uppercase flex items-center gap-x-3 text-custom-text-200">
|
<div className="flex items-center gap-x-3 bg-layer-3 px-6 py-3 text-11 font-medium text-secondary uppercase">
|
||||||
<Monitor className="w-3 h-3" />
|
<Monitor className="h-3 w-3" />
|
||||||
Web
|
Web
|
||||||
</div>
|
</div>
|
||||||
<div className="px-6 py-4 flex flex-col gap-y-4 bg-custom-background-80">
|
<div className="flex flex-col gap-y-4 bg-layer-1 px-6 py-4">
|
||||||
{GOOGLE_SERVICE_DETAILS.map((field) => (
|
{GOOGLE_SERVICE_DETAILS.map((field) => (
|
||||||
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
<CopyField key={field.key} label={field.label} url={field.url} description={field.description} />
|
||||||
))}
|
))}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,24 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { setPromiseToast } from "@plane/propel/toast";
|
import { setPromiseToast } from "@plane/propel/toast";
|
||||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||||
// components
|
// assets
|
||||||
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url";
|
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url";
|
||||||
|
// components
|
||||||
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
||||||
|
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useInstance } from "@/hooks/store";
|
import { useInstance } from "@/hooks/store";
|
||||||
// icons
|
// types
|
||||||
// local components
|
|
||||||
import type { Route } from "./+types/page";
|
import type { Route } from "./+types/page";
|
||||||
|
// local
|
||||||
import { InstanceGoogleConfigForm } from "./form";
|
import { InstanceGoogleConfigForm } from "./form";
|
||||||
|
|
||||||
const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthenticationPage(
|
const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthenticationPage(
|
||||||
|
|
@ -35,7 +43,7 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
|
||||||
const updateConfigPromise = updateInstanceConfigurations(payload);
|
const updateConfigPromise = updateInstanceConfigurations(payload);
|
||||||
|
|
||||||
setPromiseToast(updateConfigPromise, {
|
setPromiseToast(updateConfigPromise, {
|
||||||
loading: "Saving Configuration...",
|
loading: "Saving Configuration",
|
||||||
success: {
|
success: {
|
||||||
title: "Configuration saved",
|
title: "Configuration saved",
|
||||||
message: () => `Google authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
message: () => `Google authentication is now ${value === "1" ? "active" : "disabled"}.`,
|
||||||
|
|
@ -56,9 +64,8 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<>
|
<PageWrapper
|
||||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
customHeader={
|
||||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
|
||||||
<AuthenticationMethodCard
|
<AuthenticationMethodCard
|
||||||
name="Google"
|
name="Google"
|
||||||
description="Allow members to login or sign up to plane with their Google
|
description="Allow members to login or sign up to plane with their Google
|
||||||
|
|
@ -81,8 +88,8 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
|
||||||
disabled={isSubmitting || !formattedConfig}
|
disabled={isSubmitting || !formattedConfig}
|
||||||
withBorder={false}
|
withBorder={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
}
|
||||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
>
|
||||||
{formattedConfig ? (
|
{formattedConfig ? (
|
||||||
<InstanceGoogleConfigForm config={formattedConfig} />
|
<InstanceGoogleConfigForm config={formattedConfig} />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -94,9 +101,7 @@ const InstanceGoogleAuthenticationPage = observer(function InstanceGoogleAuthent
|
||||||
<Loader.Item height="50px" width="50%" />
|
<Loader.Item height="50px" width="50%" />
|
||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</PageWrapper>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,70 @@
|
||||||
import { useState } from "react";
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useCallback, useRef, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
import { useTheme } from "next-themes";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
// plane internal packages
|
// plane internal packages
|
||||||
import { setPromiseToast } from "@plane/propel/toast";
|
import { setPromiseToast, setToast, TOAST_TYPE } from "@plane/propel/toast";
|
||||||
import type { TInstanceConfigurationKeys } from "@plane/types";
|
import type { TInstanceConfigurationKeys, TInstanceAuthenticationModes } from "@plane/types";
|
||||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||||
import { cn } from "@plane/utils";
|
import { cn, resolveGeneralTheme } from "@plane/utils";
|
||||||
|
// components
|
||||||
|
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||||
|
import { AuthenticationMethodCard } from "@/components/authentication/authentication-method-card";
|
||||||
|
// helpers
|
||||||
|
import { canDisableAuthMethod } from "@/helpers/authentication";
|
||||||
// hooks
|
// hooks
|
||||||
|
import { useAuthenticationModes } from "@/hooks/oauth";
|
||||||
import { useInstance } from "@/hooks/store";
|
import { useInstance } from "@/hooks/store";
|
||||||
// plane admin components
|
// types
|
||||||
import { AuthenticationModes } from "@/plane-admin/components/authentication";
|
|
||||||
import type { Route } from "./+types/page";
|
import type { Route } from "./+types/page";
|
||||||
|
|
||||||
const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(_props: Route.ComponentProps) {
|
const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(_props: Route.ComponentProps) {
|
||||||
// store
|
// theme
|
||||||
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
|
const { resolvedTheme: resolvedThemeAdmin } = useTheme();
|
||||||
|
const resolvedTheme = resolveGeneralTheme(resolvedThemeAdmin);
|
||||||
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
// Ref to store authentication modes for validation (avoids circular dependency)
|
||||||
|
const authenticationModesRef = useRef<TInstanceAuthenticationModes[]>([]);
|
||||||
// state
|
// state
|
||||||
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
|
||||||
|
// store hooks
|
||||||
|
const { fetchInstanceConfigurations, formattedConfig, updateInstanceConfigurations } = useInstance();
|
||||||
// derived values
|
// derived values
|
||||||
const enableSignUpConfig = formattedConfig?.ENABLE_SIGNUP ?? "";
|
const enableSignUpConfig = formattedConfig?.ENABLE_SIGNUP ?? "";
|
||||||
|
|
||||||
const updateConfig = async (key: TInstanceConfigurationKeys, value: string) => {
|
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
||||||
|
|
||||||
|
// Create updateConfig with validation - uses authenticationModesRef for current modes
|
||||||
|
const updateConfig = useCallback(
|
||||||
|
(key: TInstanceConfigurationKeys, value: string): void => {
|
||||||
|
// Check if trying to disable (value === "0")
|
||||||
|
if (value === "0") {
|
||||||
|
// Check if this key is an authentication method key
|
||||||
|
const currentAuthModes = authenticationModesRef.current;
|
||||||
|
const isAuthMethodKey = currentAuthModes.some((method) => method.enabledConfigKey === key);
|
||||||
|
|
||||||
|
// Only validate if this is an authentication method key
|
||||||
|
if (isAuthMethodKey) {
|
||||||
|
const canDisable = canDisableAuthMethod(key, currentAuthModes, formattedConfig);
|
||||||
|
|
||||||
|
if (!canDisable) {
|
||||||
|
setToast({
|
||||||
|
type: TOAST_TYPE.ERROR,
|
||||||
|
title: "Cannot disable authentication",
|
||||||
|
message:
|
||||||
|
"At least one authentication method must remain enabled. Please enable another method before disabling this one.",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proceed with the update
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
|
|
@ -44,33 +85,43 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateConfigPromise
|
void updateConfigPromise
|
||||||
.then(() => {
|
.then(() => {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
return undefined;
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
});
|
});
|
||||||
};
|
},
|
||||||
|
[formattedConfig, updateInstanceConfigurations]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get authentication modes - this will use updateConfig which includes validation
|
||||||
|
const authenticationModes = useAuthenticationModes({
|
||||||
|
disabled: isSubmitting,
|
||||||
|
updateConfig,
|
||||||
|
resolvedTheme,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update ref with latest authentication modes
|
||||||
|
authenticationModesRef.current = authenticationModes;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageWrapper
|
||||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
header={{
|
||||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
title: "Manage authentication modes for your instance",
|
||||||
<div className="text-xl font-medium text-custom-text-100">Manage authentication modes for your instance</div>
|
description: "Configure authentication modes for your team and restrict sign-ups to be invite only.",
|
||||||
<div className="text-sm font-normal text-custom-text-300">
|
}}
|
||||||
Configure authentication modes for your team and restrict sign-ups to be invite only.
|
>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
|
||||||
{formattedConfig ? (
|
{formattedConfig ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className={cn("w-full flex items-center gap-14 rounded")}>
|
<div className={cn("flex w-full items-center gap-14 rounded-sm")}>
|
||||||
<div className="flex grow items-center gap-4">
|
<div className="flex grow items-center gap-4">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div className="text-lg font-medium pb-1">Allow anyone to sign up even without an invite</div>
|
<div className="pb-1 text-16 font-medium">Allow anyone to sign up even without an invite</div>
|
||||||
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
|
<div className={cn("text-11 leading-5 font-regular text-tertiary")}>
|
||||||
Toggling this off will only let users sign up when they are invited.
|
Toggling this off will only let users sign up when they are invited.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -92,8 +143,18 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg font-medium pt-6">Available authentication modes</div>
|
<div className="text-lg pt-6 font-medium">Available authentication modes</div>
|
||||||
<AuthenticationModes disabled={isSubmitting} updateConfig={updateConfig} />
|
{authenticationModes.map((method) => (
|
||||||
|
<AuthenticationMethodCard
|
||||||
|
key={method.key}
|
||||||
|
name={method.name}
|
||||||
|
description={method.description}
|
||||||
|
icon={method.icon}
|
||||||
|
config={method.config}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
unavailable={method.unavailable}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Loader className="space-y-10">
|
<Loader className="space-y-10">
|
||||||
|
|
@ -104,9 +165,7 @@ const InstanceAuthenticationPage = observer(function InstanceAuthenticationPage(
|
||||||
<Loader.Item height="50px" width="20%" />
|
<Loader.Item height="50px" width="20%" />
|
||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</PageWrapper>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
// types
|
// types
|
||||||
|
|
@ -157,12 +163,12 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm text-custom-text-300">Email security</h4>
|
<h4 className="text-13 text-tertiary">Email security</h4>
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
value={emailSecurityKey}
|
value={emailSecurityKey}
|
||||||
label={EMAIL_SECURITY_OPTIONS[emailSecurityKey]}
|
label={EMAIL_SECURITY_OPTIONS[emailSecurityKey]}
|
||||||
onChange={handleEmailSecurityChange}
|
onChange={handleEmailSecurityChange}
|
||||||
buttonClassName="rounded-md border-custom-border-200"
|
buttonClassName="rounded-md border-subtle"
|
||||||
input
|
input
|
||||||
>
|
>
|
||||||
{Object.entries(EMAIL_SECURITY_OPTIONS).map(([key, value]) => (
|
{Object.entries(EMAIL_SECURITY_OPTIONS).map(([key, value]) => (
|
||||||
|
|
@ -173,12 +179,12 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||||
</CustomSelect>
|
</CustomSelect>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-6 my-6 pt-4 border-t border-custom-border-100">
|
<div className="my-6 flex flex-col gap-6 border-t border-subtle pt-4">
|
||||||
<div className="flex w-full max-w-xl flex-col gap-y-10 px-1">
|
<div className="flex w-full max-w-xl flex-col gap-y-10 px-1">
|
||||||
<div className="mr-8 flex items-center gap-10 pt-4">
|
<div className="mr-8 flex items-center gap-10 pt-4">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div className="text-sm font-medium text-custom-text-100">Authentication</div>
|
<div className="text-13 font-medium text-primary">Authentication</div>
|
||||||
<div className="text-xs font-normal text-custom-text-300">
|
<div className="text-11 font-regular text-tertiary">
|
||||||
This is optional, but we recommend setting up a username and a password for your SMTP server.
|
This is optional, but we recommend setting up a username and a password for your SMTP server.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -201,17 +207,19 @@ export function InstanceEmailForm(props: IInstanceEmailForm) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex max-w-4xl items-center py-1 gap-4">
|
<div className="flex max-w-4xl items-center gap-4 py-1">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
onClick={handleSubmit(onSubmit)}
|
onClick={handleSubmit(onSubmit)}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={!isValid || !isDirty}
|
disabled={!isValid || !isDirty}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Saving..." : "Save changes"}
|
{isSubmitting ? "Saving" : "Save changes"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline-primary"
|
variant="secondary"
|
||||||
|
size="lg"
|
||||||
onClick={() => setIsSendTestEmailModalOpen(true)}
|
onClick={() => setIsSendTestEmailModalOpen(true)}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,21 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||||
|
// components
|
||||||
|
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useInstance } from "@/hooks/store";
|
import { useInstance } from "@/hooks/store";
|
||||||
// components
|
// types
|
||||||
import type { Route } from "./+types/page";
|
import type { Route } from "./+types/page";
|
||||||
|
// local
|
||||||
import { InstanceEmailForm } from "./email-config-form";
|
import { InstanceEmailForm } from "./email-config-form";
|
||||||
|
|
||||||
const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.ComponentProps) {
|
const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.ComponentProps) {
|
||||||
|
|
@ -49,29 +58,29 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp
|
||||||
}, [formattedConfig]);
|
}, [formattedConfig]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<PageWrapper
|
||||||
|
header={{
|
||||||
|
title: "Secure emails from your own instance",
|
||||||
|
description: (
|
||||||
<>
|
<>
|
||||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
|
||||||
<div className="flex items-center justify-between gap-4 border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
|
||||||
<div className="py-4 space-y-1 flex-shrink-0">
|
|
||||||
<div className="text-xl font-medium text-custom-text-100">Secure emails from your own instance</div>
|
|
||||||
<div className="text-sm font-normal text-custom-text-300">
|
|
||||||
Plane can send useful emails to you and your users from your own instance without talking to the Internet.
|
Plane can send useful emails to you and your users from your own instance without talking to the Internet.
|
||||||
<div className="text-sm font-normal text-custom-text-300">
|
<div className="text-13 font-regular text-tertiary">
|
||||||
Set it up below and please test your settings before you save them.
|
Set it up below and please test your settings before you save them.
|
||||||
<span className="text-red-400">Misconfigs can lead to email bounces and errors.</span>
|
<span className="text-danger-primary">Misconfigs can lead to email bounces and errors.</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</>
|
||||||
</div>
|
),
|
||||||
{isLoading ? (
|
actions: isLoading ? (
|
||||||
<Loader>
|
<Loader>
|
||||||
<Loader.Item width="24px" height="16px" className="rounded-full" />
|
<Loader.Item width="24px" height="16px" className="rounded-full" />
|
||||||
</Loader>
|
</Loader>
|
||||||
) : (
|
) : (
|
||||||
<ToggleSwitch value={isSMTPEnabled} onChange={handleToggle} size="sm" disabled={isSubmitting} />
|
<ToggleSwitch value={isSMTPEnabled} onChange={handleToggle} size="sm" disabled={isSubmitting} />
|
||||||
)}
|
),
|
||||||
</div>
|
}}
|
||||||
|
>
|
||||||
{isSMTPEnabled && !isLoading && (
|
{isSMTPEnabled && !isLoading && (
|
||||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
<>
|
||||||
{formattedConfig ? (
|
{formattedConfig ? (
|
||||||
<InstanceEmailForm config={formattedConfig} />
|
<InstanceEmailForm config={formattedConfig} />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -83,10 +92,9 @@ const InstanceEmailPage = observer(function InstanceEmailPage(_props: Route.Comp
|
||||||
<Loader.Item height="50px" width="20%" />
|
<Loader.Item height="50px" width="20%" />
|
||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
</PageWrapper>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useEffect, useState, Fragment } from "react";
|
import { useEffect, useState, Fragment } from "react";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
// plane imports
|
// plane imports
|
||||||
|
|
@ -72,7 +78,7 @@ export function SendTestEmailModal(props: Props) {
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
<div className="fixed inset-0 bg-backdrop transition-opacity" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
<div className="fixed inset-0 z-20 overflow-y-auto">
|
<div className="fixed inset-0 z-20 overflow-y-auto">
|
||||||
<div className="my-10 flex justify-center p-4 text-center sm:p-0 md:my-20">
|
<div className="my-10 flex justify-center p-4 text-center sm:p-0 md:my-20">
|
||||||
|
|
@ -85,8 +91,8 @@ export function SendTestEmailModal(props: Props) {
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform rounded-lg bg-custom-background-100 p-5 px-4 text-left shadow-custom-shadow-md transition-all w-full sm:max-w-xl">
|
<Dialog.Panel className="relative w-full transform rounded-lg bg-surface-1 p-5 px-4 text-left shadow-raised-200 transition-all sm:max-w-xl">
|
||||||
<h3 className="text-lg font-medium leading-6 text-custom-text-100">
|
<h3 className="text-16 leading-6 font-medium text-primary">
|
||||||
{sendEmailStep === ESendEmailSteps.SEND_EMAIL
|
{sendEmailStep === ESendEmailSteps.SEND_EMAIL
|
||||||
? "Send test email"
|
? "Send test email"
|
||||||
: sendEmailStep === ESendEmailSteps.SUCCESS
|
: sendEmailStep === ESendEmailSteps.SUCCESS
|
||||||
|
|
@ -101,12 +107,12 @@ export function SendTestEmailModal(props: Props) {
|
||||||
value={receiverEmail}
|
value={receiverEmail}
|
||||||
onChange={(e) => setReceiverEmail(e.target.value)}
|
onChange={(e) => setReceiverEmail(e.target.value)}
|
||||||
placeholder="Receiver email"
|
placeholder="Receiver email"
|
||||||
className="w-full resize-none text-lg"
|
className="w-full resize-none text-16"
|
||||||
tabIndex={1}
|
tabIndex={1}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{sendEmailStep === ESendEmailSteps.SUCCESS && (
|
{sendEmailStep === ESendEmailSteps.SUCCESS && (
|
||||||
<div className="flex flex-col gap-y-4 text-sm">
|
<div className="flex flex-col gap-y-4 text-13">
|
||||||
<p>
|
<p>
|
||||||
We have sent the test email to {receiverEmail}. Please check your spam folder if you cannot find
|
We have sent the test email to {receiverEmail}. Please check your spam folder if you cannot find
|
||||||
it.
|
it.
|
||||||
|
|
@ -114,14 +120,14 @@ export function SendTestEmailModal(props: Props) {
|
||||||
<p>If you still cannot find it, recheck your SMTP configuration and trigger a new test email.</p>
|
<p>If you still cannot find it, recheck your SMTP configuration and trigger a new test email.</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{sendEmailStep === ESendEmailSteps.FAILED && <div className="text-sm">{error}</div>}
|
{sendEmailStep === ESendEmailSteps.FAILED && <div className="text-13">{error}</div>}
|
||||||
<div className="flex items-center gap-2 justify-end mt-5">
|
<div className="mt-5 flex items-center justify-end gap-2">
|
||||||
<Button variant="neutral-primary" size="sm" onClick={handleClose} tabIndex={2}>
|
<Button variant="secondary" size="lg" onClick={handleClose} tabIndex={2}>
|
||||||
{sendEmailStep === ESendEmailSteps.SEND_EMAIL ? "Cancel" : "Close"}
|
{sendEmailStep === ESendEmailSteps.SEND_EMAIL ? "Cancel" : "Close"}
|
||||||
</Button>
|
</Button>
|
||||||
{sendEmailStep === ESendEmailSteps.SEND_EMAIL && (
|
{sendEmailStep === ESendEmailSteps.SEND_EMAIL && (
|
||||||
<Button variant="primary" size="sm" loading={isLoading} onClick={handleSubmit} tabIndex={3}>
|
<Button variant="primary" size="lg" loading={isLoading} onClick={handleSubmit} tabIndex={3}>
|
||||||
{isLoading ? "Sending email..." : "Send email"}
|
{isLoading ? "Sending email" : "Send email"}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,23 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { Telescope } from "lucide-react";
|
import { Telescope } from "lucide-react";
|
||||||
// types
|
// plane imports
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
import type { IInstance, IInstanceAdmin } from "@plane/types";
|
import type { IInstance, IInstanceAdmin } from "@plane/types";
|
||||||
// ui
|
|
||||||
import { Input, ToggleSwitch } from "@plane/ui";
|
import { Input, ToggleSwitch } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { ControllerInput } from "@/components/common/controller-input";
|
import { ControllerInput } from "@/components/common/controller-input";
|
||||||
import { useInstance } from "@/hooks/store";
|
|
||||||
import { IntercomConfig } from "./intercom";
|
|
||||||
// hooks
|
// hooks
|
||||||
|
import { useInstance } from "@/hooks/store";
|
||||||
|
// components
|
||||||
|
import { IntercomConfig } from "./intercom";
|
||||||
|
|
||||||
export interface IGeneralConfigurationForm {
|
export interface IGeneralConfigurationForm {
|
||||||
instance: IInstance;
|
instance: IInstance;
|
||||||
|
|
@ -27,8 +33,8 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
control,
|
control,
|
||||||
watch,
|
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
|
watch,
|
||||||
} = useForm<Partial<IInstance>>({
|
} = useForm<Partial<IInstance>>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
instance_name: instance?.instance_name,
|
instance_name: instance?.instance_name,
|
||||||
|
|
@ -63,8 +69,8 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="space-y-3">
|
<div className="space-y-4">
|
||||||
<div className="text-lg font-medium">Instance details</div>
|
<div className="text-16 font-medium text-primary">Instance details</div>
|
||||||
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-8 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid-col grid w-full grid-cols-1 items-center justify-between gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||||
<ControllerInput
|
<ControllerInput
|
||||||
key="instance_name"
|
key="instance_name"
|
||||||
|
|
@ -78,54 +84,52 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm text-custom-text-300">Email</h4>
|
<h4 className="text-13 text-tertiary">Email</h4>
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
value={instanceAdmins[0]?.user_detail?.email ?? ""}
|
value={instanceAdmins[0]?.user_detail?.email ?? ""}
|
||||||
placeholder="Admin email"
|
placeholder="Admin email"
|
||||||
className="w-full cursor-not-allowed !text-custom-text-400"
|
className="w-full cursor-not-allowed !text-placeholder"
|
||||||
autoComplete="on"
|
autoComplete="on"
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm text-custom-text-300">Instance ID</h4>
|
<h4 className="text-13 text-tertiary">Instance ID</h4>
|
||||||
<Input
|
<Input
|
||||||
id="instance_id"
|
id="instance_id"
|
||||||
name="instance_id"
|
name="instance_id"
|
||||||
type="text"
|
type="text"
|
||||||
value={instance.instance_id}
|
value={instance.instance_id}
|
||||||
className="w-full cursor-not-allowed rounded-md font-medium !text-custom-text-400"
|
className="w-full cursor-not-allowed rounded-md font-medium !text-placeholder"
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-6">
|
||||||
<div className="text-lg font-medium">Chat + telemetry</div>
|
<div className="border-b border-subtle pb-1.5 text-16 font-medium text-primary">Chat + telemetry</div>
|
||||||
<IntercomConfig isTelemetryEnabled={watch("is_telemetry_enabled") ?? false} />
|
<IntercomConfig isTelemetryEnabled={watch("is_telemetry_enabled") ?? false} />
|
||||||
<div className="flex items-center gap-14 px-4 py-3 border border-custom-border-200 rounded">
|
<div className="flex items-center gap-14">
|
||||||
<div className="grow flex items-center gap-4">
|
<div className="flex grow items-center gap-4">
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<div className="flex items-center justify-center w-10 h-10 bg-custom-background-80 rounded-full">
|
<div className="flex size-11 items-center justify-center rounded-lg bg-layer-1">
|
||||||
<Telescope className="w-6 h-6 text-custom-text-300/80 p-0.5" />
|
<Telescope className="size-5 text-tertiary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div className="text-sm font-medium text-custom-text-100 leading-5">
|
<div className="text-13 leading-5 font-medium text-primary">Let Plane collect anonymous usage data</div>
|
||||||
Let Plane collect anonymous usage data
|
<div className="text-11 leading-5 font-regular text-tertiary">
|
||||||
</div>
|
|
||||||
<div className="text-xs font-normal text-custom-text-300 leading-5">
|
|
||||||
No PII is collected.This anonymized data is used to understand how you use Plane and build new features
|
No PII is collected.This anonymized data is used to understand how you use Plane and build new features
|
||||||
in line with{" "}
|
in line with{" "}
|
||||||
<a
|
<a
|
||||||
href="https://developers.plane.so/self-hosting/telemetry"
|
href="https://developers.plane.so/self-hosting/telemetry"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-custom-primary-100 hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
our Telemetry Policy.
|
our Telemetry Policy.
|
||||||
|
|
@ -146,8 +150,15 @@ export const GeneralConfigurationForm = observer(function GeneralConfigurationFo
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
<Button
|
||||||
{isSubmitting ? "Saving..." : "Save changes"}
|
variant="primary"
|
||||||
|
size="lg"
|
||||||
|
onClick={() => {
|
||||||
|
void handleSubmit(onSubmit)();
|
||||||
|
}}
|
||||||
|
loading={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? "Saving" : "Save changes"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
@ -44,22 +50,22 @@ export const IntercomConfig = observer(function IntercomConfig(props: TIntercomC
|
||||||
};
|
};
|
||||||
|
|
||||||
const enableIntercomConfig = () => {
|
const enableIntercomConfig = () => {
|
||||||
submitInstanceConfigurations({ IS_INTERCOM_ENABLED: isIntercomEnabled ? "0" : "1" });
|
void submitInstanceConfigurations({ IS_INTERCOM_ENABLED: isIntercomEnabled ? "0" : "1" });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center gap-14 px-4 py-3 border border-custom-border-200 rounded">
|
<div className="flex items-center gap-14">
|
||||||
<div className="grow flex items-center gap-4">
|
<div className="flex grow items-center gap-4">
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<div className="flex items-center justify-center w-10 h-10 bg-custom-background-80 rounded-full">
|
<div className="flex size-11 items-center justify-center rounded-lg bg-layer-1">
|
||||||
<MessageSquare className="w-6 h-6 text-custom-text-300/80 p-0.5" />
|
<MessageSquare className="size-5 p-0.5 text-tertiary" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div className="text-sm font-medium text-custom-text-100 leading-5">Chat with us</div>
|
<div className="text-13 leading-5 font-medium text-primary">Chat with us</div>
|
||||||
<div className="text-xs font-normal text-custom-text-300 leading-5">
|
<div className="text-11 leading-5 font-regular text-tertiary">
|
||||||
Let your users chat with us via Intercom or another service. Toggling Telemetry off turns this off
|
Let your users chat with us via Intercom or another service. Toggling Telemetry off turns this off
|
||||||
automatically.
|
automatically.
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,32 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
|
// components
|
||||||
|
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useInstance } from "@/hooks/store";
|
import { useInstance } from "@/hooks/store";
|
||||||
// components
|
// local imports
|
||||||
import type { Route } from "./+types/page";
|
|
||||||
import { GeneralConfigurationForm } from "./form";
|
import { GeneralConfigurationForm } from "./form";
|
||||||
|
// types
|
||||||
|
import type { Route } from "./+types/page";
|
||||||
|
|
||||||
function GeneralPage() {
|
function GeneralPage() {
|
||||||
const { instance, instanceAdmins } = useInstance();
|
const { instance, instanceAdmins } = useInstance();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageWrapper
|
||||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
header={{
|
||||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
title: "General settings",
|
||||||
<div className="text-xl font-medium text-custom-text-100">General settings</div>
|
description:
|
||||||
<div className="text-sm font-normal text-custom-text-300">
|
"Change the name of your instance and instance admin e-mail addresses. Enable or disable telemetry in your instance.",
|
||||||
Change the name of your instance and instance admin e-mail addresses. Enable or disable telemetry in your
|
}}
|
||||||
instance.
|
>
|
||||||
</div>
|
{instance && instanceAdmins && <GeneralConfigurationForm instance={instance} instanceAdmins={instanceAdmins} />}
|
||||||
</div>
|
</PageWrapper>
|
||||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
|
||||||
{instance && instanceAdmins && (
|
|
||||||
<GeneralConfigurationForm instance={instance} instanceAdmins={instanceAdmins} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
|
|
@ -56,7 +62,7 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
|
||||||
<a
|
<a
|
||||||
href="https://unsplash.com/documentation#creating-a-developer-account"
|
href="https://unsplash.com/documentation#creating-a-developer-account"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-custom-primary-100 hover:underline"
|
className="text-accent-primary hover:underline"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
Learn more.
|
Learn more.
|
||||||
|
|
@ -70,8 +76,8 @@ export function InstanceImageConfigForm(props: IInstanceImageConfigForm) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button variant="primary" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
<Button variant="primary" size="lg" onClick={handleSubmit(onSubmit)} loading={isSubmitting}>
|
||||||
{isSubmitting ? "Saving..." : "Save changes"}
|
{isSubmitting ? "Saving" : "Save changes"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { Loader } from "@plane/ui";
|
import { Loader } from "@plane/ui";
|
||||||
|
// components
|
||||||
|
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||||
// hooks
|
// hooks
|
||||||
import { useInstance } from "@/hooks/store";
|
import { useInstance } from "@/hooks/store";
|
||||||
// local
|
// types
|
||||||
import type { Route } from "./+types/page";
|
import type { Route } from "./+types/page";
|
||||||
|
// local
|
||||||
import { InstanceImageConfigForm } from "./form";
|
import { InstanceImageConfigForm } from "./form";
|
||||||
|
|
||||||
const InstanceImagePage = observer(function InstanceImagePage(_props: Route.ComponentProps) {
|
const InstanceImagePage = observer(function InstanceImagePage(_props: Route.ComponentProps) {
|
||||||
|
|
@ -14,15 +23,12 @@ const InstanceImagePage = observer(function InstanceImagePage(_props: Route.Comp
|
||||||
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
useSWR("INSTANCE_CONFIGURATIONS", () => fetchInstanceConfigurations());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<PageWrapper
|
||||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
header={{
|
||||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
title: "Third-party image libraries",
|
||||||
<div className="text-xl font-medium text-custom-text-100">Third-party image libraries</div>
|
description: "Let your users search and choose images from third-party libraries",
|
||||||
<div className="text-sm font-normal text-custom-text-300">
|
}}
|
||||||
Let your users search and choose images from third-party libraries
|
>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
|
||||||
{formattedConfig ? (
|
{formattedConfig ? (
|
||||||
<InstanceImageConfigForm config={formattedConfig} />
|
<InstanceImageConfigForm config={formattedConfig} />
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -31,9 +37,7 @@ const InstanceImagePage = observer(function InstanceImagePage(_props: Route.Comp
|
||||||
<Loader.Item height="50px" width="20%" />
|
<Loader.Item height="50px" width="20%" />
|
||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</PageWrapper>
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,21 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { Outlet } from "react-router";
|
import { Outlet } from "react-router";
|
||||||
// components
|
// components
|
||||||
|
import { AdminHeader } from "@/components/common/header";
|
||||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||||
import { NewUserPopup } from "@/components/new-user-popup";
|
import { NewUserPopup } from "@/components/common/new-user-popup";
|
||||||
// hooks
|
// hooks
|
||||||
import { useUser } from "@/hooks/store";
|
import { useUser } from "@/hooks/store";
|
||||||
// local components
|
// local components
|
||||||
import type { Route } from "./+types/layout";
|
import type { Route } from "./+types/layout";
|
||||||
import { AdminHeader } from "./header";
|
|
||||||
import { AdminSidebar } from "./sidebar";
|
import { AdminSidebar } from "./sidebar";
|
||||||
|
|
||||||
function AdminLayout(_props: Route.ComponentProps) {
|
function AdminLayout(_props: Route.ComponentProps) {
|
||||||
|
|
@ -34,9 +40,9 @@ function AdminLayout(_props: Route.ComponentProps) {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex h-screen w-screen overflow-hidden">
|
<div className="relative flex h-screen w-screen overflow-hidden">
|
||||||
<AdminSidebar />
|
<AdminSidebar />
|
||||||
<main className="relative flex h-full w-full flex-col overflow-hidden bg-custom-background-100">
|
<main className="relative flex h-full w-full flex-col overflow-hidden bg-surface-1">
|
||||||
<AdminHeader />
|
<AdminHeader />
|
||||||
<div className="h-full w-full overflow-hidden">
|
<div className="vertical-scrollbar scrollbar-md h-full w-full overflow-hidden overflow-y-scroll">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { Fragment, useEffect, useState } from "react";
|
import { Fragment, useEffect, useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useTheme as useNextTheme } from "next-themes";
|
import { useTheme as useNextTheme } from "next-themes";
|
||||||
|
|
@ -33,20 +39,20 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
||||||
const getSidebarMenuItems = () => (
|
const getSidebarMenuItems = () => (
|
||||||
<Menu.Items
|
<Menu.Items
|
||||||
className={cn(
|
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",
|
"shadow-lg absolute left-0 z-20 mt-1.5 flex w-52 flex-col divide-y divide-subtle rounded-md border border-subtle bg-surface-1 px-1 py-2 text-11 outline-none",
|
||||||
{
|
{
|
||||||
"left-4": isSidebarCollapsed,
|
"left-4": isSidebarCollapsed,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col gap-2.5 pb-2">
|
<div className="flex flex-col gap-2.5 pb-2">
|
||||||
<span className="px-2 text-custom-sidebar-text-200 truncate">{currentUser?.email}</span>
|
<span className="truncate px-2 text-secondary">{currentUser?.email}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
as="button"
|
as="button"
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
className="flex w-full items-center gap-2 rounded-sm px-2 py-1 hover:bg-layer-1-hover"
|
||||||
onClick={handleThemeSwitch}
|
onClick={handleThemeSwitch}
|
||||||
>
|
>
|
||||||
<Palette className="h-4 w-4 stroke-[1.5]" />
|
<Palette className="h-4 w-4 stroke-[1.5]" />
|
||||||
|
|
@ -59,7 +65,7 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
as="button"
|
as="button"
|
||||||
type="submit"
|
type="submit"
|
||||||
className="flex w-full items-center gap-2 rounded px-2 py-1 hover:bg-custom-sidebar-background-80"
|
className="flex w-full items-center gap-2 rounded-sm px-2 py-1 hover:bg-layer-1-hover"
|
||||||
>
|
>
|
||||||
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
<LogOut className="h-4 w-4 stroke-[1.5]" />
|
||||||
Sign out
|
Sign out
|
||||||
|
|
@ -71,14 +77,14 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (csrfToken === undefined)
|
if (csrfToken === undefined)
|
||||||
authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
|
void authService.requestCSRFToken().then((data) => data?.csrf_token && setCsrfToken(data.csrf_token));
|
||||||
}, [csrfToken]);
|
}, [csrfToken]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex max-h-header items-center gap-x-5 gap-y-2 border-b border-custom-sidebar-border-200 px-4 py-3.5">
|
<div className="flex max-h-header items-center gap-x-5 gap-y-2 border-b border-subtle px-4 py-2.5">
|
||||||
<div className="h-full w-full truncate">
|
<div className="h-full w-full truncate">
|
||||||
<div
|
<div
|
||||||
className={`flex flex-grow items-center gap-x-2 truncate rounded py-1 ${
|
className={`flex flex-grow items-center gap-x-2 truncate rounded-sm ${
|
||||||
isSidebarCollapsed ? "justify-center" : ""
|
isSidebarCollapsed ? "justify-center" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
|
@ -88,8 +94,8 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
||||||
"cursor-default": !isSidebarCollapsed,
|
"cursor-default": !isSidebarCollapsed,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="flex h-7 w-7 flex-shrink-0 items-center justify-center rounded bg-custom-sidebar-background-80">
|
<div className="flex size-8 flex-shrink-0 items-center justify-center rounded-sm bg-layer-1">
|
||||||
<UserCog2 className="h-5 w-5 text-custom-text-200" />
|
<UserCog2 className="size-5 text-primary" />
|
||||||
</div>
|
</div>
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
{isSidebarCollapsed && (
|
{isSidebarCollapsed && (
|
||||||
|
|
@ -109,7 +115,7 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
||||||
|
|
||||||
{!isSidebarCollapsed && (
|
{!isSidebarCollapsed && (
|
||||||
<div className="flex w-full gap-2">
|
<div className="flex w-full gap-2">
|
||||||
<h4 className="grow truncate text-base font-medium text-custom-text-200">Instance admin</h4>
|
<h4 className="grow truncate text-body-md-medium text-primary">Instance admin</h4>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -123,7 +129,7 @@ export const AdminSidebarDropdown = observer(function AdminSidebarDropdown() {
|
||||||
src={getFileURL(currentUser.avatar_url)}
|
src={getFileURL(currentUser.avatar_url)}
|
||||||
size={24}
|
size={24}
|
||||||
shape="square"
|
shape="square"
|
||||||
className="!text-base"
|
className="!text-body-sm-medium"
|
||||||
/>
|
/>
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,23 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useState, useRef } from "react";
|
import { useState, useRef } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ExternalLink, HelpCircle, MoveLeft } from "lucide-react";
|
import { HelpCircle, MessageSquare, MoveLeft } from "lucide-react";
|
||||||
import { Transition } from "@headlessui/react";
|
import { Transition } from "@headlessui/react";
|
||||||
// plane internal packages
|
|
||||||
import { WEB_BASE_URL } from "@plane/constants";
|
import { WEB_BASE_URL } from "@plane/constants";
|
||||||
import { DiscordIcon, GithubIcon, PageIcon } from "@plane/propel/icons";
|
// plane internal packages
|
||||||
|
import { GithubIcon, NewTabIcon, PageIcon } from "@plane/propel/icons";
|
||||||
import { Tooltip } from "@plane/propel/tooltip";
|
import { Tooltip } from "@plane/propel/tooltip";
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
// hooks
|
// hooks
|
||||||
import { useTheme } from "@/hooks/store";
|
import { useInstance, useTheme } from "@/hooks/store";
|
||||||
// assets
|
// assets
|
||||||
|
|
||||||
import packageJson from "package.json";
|
|
||||||
|
|
||||||
const helpOptions = [
|
const helpOptions = [
|
||||||
{
|
{
|
||||||
name: "Documentation",
|
name: "Documentation",
|
||||||
|
|
@ -21,9 +25,9 @@ const helpOptions = [
|
||||||
Icon: PageIcon,
|
Icon: PageIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Join our Discord",
|
name: "Join our Forum",
|
||||||
href: "https://discord.com/invite/A92xrEGCge",
|
href: "https://forum.plane.so",
|
||||||
Icon: DiscordIcon,
|
Icon: MessageSquare,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Report a bug",
|
name: "Report a bug",
|
||||||
|
|
@ -36,6 +40,7 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||||
// states
|
// states
|
||||||
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
const [isNeedHelpOpen, setIsNeedHelpOpen] = useState(false);
|
||||||
// store
|
// store
|
||||||
|
const { instance } = useInstance();
|
||||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||||
// refs
|
// refs
|
||||||
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
|
const helpOptionsRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
@ -45,9 +50,9 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex w-full items-center justify-between gap-1 self-baseline border-t border-custom-border-200 bg-custom-sidebar-background-100 px-4 h-14 flex-shrink-0",
|
"flex h-14 w-full flex-shrink-0 items-center justify-between gap-1 self-baseline border-t border-subtle bg-surface-1 px-4",
|
||||||
{
|
{
|
||||||
"flex-col h-auto py-1.5": isSidebarCollapsed,
|
"h-auto flex-col py-1.5": isSidebarCollapsed,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|
@ -55,32 +60,32 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||||
<Tooltip tooltipContent="Redirect to Plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
|
<Tooltip tooltipContent="Redirect to Plane" position="right" className="ml-4" disabled={!isSidebarCollapsed}>
|
||||||
<a
|
<a
|
||||||
href={redirectionLink}
|
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`}
|
className={`relative flex items-center gap-1 rounded-sm bg-layer-1 px-2 py-1 text-body-xs-medium whitespace-nowrap text-secondary`}
|
||||||
>
|
>
|
||||||
<ExternalLink size={14} />
|
<NewTabIcon width={14} height={14} />
|
||||||
{!isSidebarCollapsed && "Redirect to Plane"}
|
{!isSidebarCollapsed && "Redirect to Plane"}
|
||||||
</a>
|
</a>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip tooltipContent="Help" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
|
<Tooltip tooltipContent="Help" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
|
||||||
<button
|
<button
|
||||||
type="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 ${
|
className={`ml-auto grid place-items-center rounded-md p-1.5 text-secondary outline-none hover:bg-layer-1-hover hover:text-primary ${
|
||||||
isSidebarCollapsed ? "w-full" : ""
|
isSidebarCollapsed ? "w-full" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
onClick={() => setIsNeedHelpOpen((prev) => !prev)}
|
||||||
>
|
>
|
||||||
<HelpCircle className="h-3.5 w-3.5" />
|
<HelpCircle className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip tooltipContent="Toggle sidebar" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
|
<Tooltip tooltipContent="Toggle sidebar" position={isSidebarCollapsed ? "right" : "top"} className="ml-4">
|
||||||
<button
|
<button
|
||||||
type="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 ${
|
className={`grid place-items-center rounded-md p-1.5 text-secondary outline-none hover:bg-layer-1-hover hover:text-primary ${
|
||||||
isSidebarCollapsed ? "w-full" : ""
|
isSidebarCollapsed ? "w-full" : ""
|
||||||
}`}
|
}`}
|
||||||
onClick={() => toggleSidebar(!isSidebarCollapsed)}
|
onClick={() => toggleSidebar(!isSidebarCollapsed)}
|
||||||
>
|
>
|
||||||
<MoveLeft className={`h-3.5 w-3.5 duration-300 ${isSidebarCollapsed ? "rotate-180" : ""}`} />
|
<MoveLeft className={`size-4 duration-300 ${isSidebarCollapsed ? "rotate-180" : ""}`} />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -96,9 +101,9 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={`absolute bottom-2 min-w-[10rem] z-[15] ${
|
className={`absolute bottom-2 z-[15] min-w-[10rem] ${
|
||||||
isSidebarCollapsed ? "left-full" : "-left-[75px]"
|
isSidebarCollapsed ? "left-full" : "-left-[75px]"
|
||||||
} divide-y divide-custom-border-200 whitespace-nowrap rounded bg-custom-background-100 p-1 shadow-custom-shadow-xs`}
|
} divide-y divide-subtle-1 rounded-sm bg-surface-1 p-1 whitespace-nowrap shadow-raised-100`}
|
||||||
ref={helpOptionsRef}
|
ref={helpOptionsRef}
|
||||||
>
|
>
|
||||||
<div className="space-y-1 pb-2">
|
<div className="space-y-1 pb-2">
|
||||||
|
|
@ -106,11 +111,11 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||||
if (href)
|
if (href)
|
||||||
return (
|
return (
|
||||||
<Link href={href} key={name} target="_blank">
|
<Link href={href} key={name} target="_blank">
|
||||||
<div className="flex items-center gap-x-2 rounded px-2 py-1 text-xs hover:bg-custom-background-80">
|
<div className="flex items-center gap-x-2 rounded-sm px-2 py-1 text-11 hover:bg-layer-1-hover">
|
||||||
<div className="grid flex-shrink-0 place-items-center">
|
<div className="grid flex-shrink-0 place-items-center">
|
||||||
<Icon className="h-3.5 w-3.5 text-custom-text-200" width={14} height={14} />
|
<Icon className="h-3.5 w-3.5 text-secondary" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs">{name}</span>
|
<span className="text-11">{name}</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
@ -119,17 +124,17 @@ export const AdminSidebarHelpSection = observer(function AdminSidebarHelpSection
|
||||||
<button
|
<button
|
||||||
key={name}
|
key={name}
|
||||||
type="button"
|
type="button"
|
||||||
className="flex w-full items-center gap-x-2 rounded px-2 py-1 text-xs hover:bg-custom-background-80"
|
className="flex w-full items-center gap-x-2 rounded-sm px-2 py-1 text-11 hover:bg-layer-1"
|
||||||
>
|
>
|
||||||
<div className="grid flex-shrink-0 place-items-center">
|
<div className="grid flex-shrink-0 place-items-center">
|
||||||
<Icon className="h-3.5 w-3.5 text-custom-text-200" />
|
<Icon className="h-3.5 w-3.5 text-secondary" />
|
||||||
</div>
|
</div>
|
||||||
<span className="text-xs">{name}</span>
|
<span className="text-11">{name}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<div className="px-2 pb-1 pt-2 text-[10px]">Version: v{packageJson.version}</div>
|
<div className="px-2 pt-2 pb-1 text-10">Version: v{instance?.current_version}</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,26 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
|
|
||||||
// plane internal packages
|
// plane internal packages
|
||||||
import { WorkspaceIcon } from "@plane/propel/icons";
|
|
||||||
import { Tooltip } from "@plane/propel/tooltip";
|
import { Tooltip } from "@plane/propel/tooltip";
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
// hooks
|
// hooks
|
||||||
import { useTheme } from "@/hooks/store";
|
import { useTheme } from "@/hooks/store";
|
||||||
|
import { useSidebarMenu } from "@/hooks/use-sidebar-menu";
|
||||||
const INSTANCE_ADMIN_LINKS = [
|
|
||||||
{
|
|
||||||
Icon: Cog,
|
|
||||||
name: "General",
|
|
||||||
description: "Identify your instances and get key details.",
|
|
||||||
href: `/general/`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Icon: WorkspaceIcon,
|
|
||||||
name: "Workspaces",
|
|
||||||
description: "Manage all workspaces on this instance.",
|
|
||||||
href: `/workspace/`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Icon: Mail,
|
|
||||||
name: "Email",
|
|
||||||
description: "Configure your SMTP controls.",
|
|
||||||
href: `/email/`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Icon: Lock,
|
|
||||||
name: "Authentication",
|
|
||||||
description: "Configure authentication modes.",
|
|
||||||
href: `/authentication/`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Icon: BrainCog,
|
|
||||||
name: "Artificial intelligence",
|
|
||||||
description: "Configure your OpenAI creds.",
|
|
||||||
href: `/ai/`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Icon: Image,
|
|
||||||
name: "Images in Plane",
|
|
||||||
description: "Allow third-party image libraries.",
|
|
||||||
href: `/image/`,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
|
export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
|
||||||
// store hooks
|
|
||||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
|
||||||
// router
|
// router
|
||||||
const pathName = usePathname();
|
const pathName = usePathname();
|
||||||
|
// store hooks
|
||||||
|
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||||
|
// derived values
|
||||||
|
const sidebarMenu = useSidebarMenu();
|
||||||
|
|
||||||
const handleItemClick = () => {
|
const handleItemClick = () => {
|
||||||
if (window.innerWidth < 768) {
|
if (window.innerWidth < 768) {
|
||||||
|
|
@ -61,41 +29,28 @@ export const AdminSidebarMenu = observer(function AdminSidebarMenu() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col gap-2.5 overflow-y-scroll vertical-scrollbar scrollbar-sm px-4 py-4">
|
<div className="vertical-scrollbar flex scrollbar-sm h-full w-full flex-col gap-2.5 overflow-y-scroll px-4 py-4">
|
||||||
{INSTANCE_ADMIN_LINKS.map((item, index) => {
|
{sidebarMenu.map((item, index) => {
|
||||||
const isActive = item.href === pathName || pathName.includes(item.href);
|
const isActive = item.href === pathName || pathName?.includes(item.href);
|
||||||
return (
|
return (
|
||||||
<Link key={index} href={item.href} onClick={handleItemClick}>
|
<Link key={index} href={item.href} onClick={handleItemClick}>
|
||||||
<div>
|
<div>
|
||||||
<Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!isSidebarCollapsed}>
|
<Tooltip tooltipContent={item.name} position="right" className="ml-2" disabled={!isSidebarCollapsed}>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
`group flex w-full items-center gap-3 rounded-md px-3 py-2 outline-none transition-colors`,
|
"group flex w-full items-center gap-3 rounded-md px-3 py-2 transition-colors outline-none",
|
||||||
isActive
|
{
|
||||||
? "bg-custom-primary-100/10 text-custom-primary-100"
|
"!bg-layer-transparent-active text-primary": isActive,
|
||||||
: "text-custom-sidebar-text-200 hover:bg-custom-sidebar-background-80 focus:bg-custom-sidebar-background-80",
|
"text-secondary hover:bg-layer-transparent-hover active:bg-layer-transparent-active": !isActive,
|
||||||
|
},
|
||||||
isSidebarCollapsed ? "justify-center" : "w-[260px]"
|
isSidebarCollapsed ? "justify-center" : "w-[260px]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{<item.Icon className="h-4 w-4 flex-shrink-0" />}
|
{<item.Icon className="h-4 w-4 flex-shrink-0" />}
|
||||||
{!isSidebarCollapsed && (
|
{!isSidebarCollapsed && (
|
||||||
<div className="w-full ">
|
<div className="w-full">
|
||||||
<div
|
<div className={cn(`text-body-xs-medium transition-colors`)}>{item.name}</div>
|
||||||
className={cn(
|
<div className={cn(`text-caption-sm-regular transition-colors`)}>{item.description}</div>
|
||||||
`text-sm font-medium transition-colors`,
|
|
||||||
isActive ? "text-custom-primary-100" : "text-custom-sidebar-text-200"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{item.name}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
`text-[10px] transition-colors`,
|
|
||||||
isActive ? "text-custom-primary-90" : "text-custom-sidebar-text-400"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{item.description}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// plane helpers
|
// plane helpers
|
||||||
|
|
@ -38,13 +44,7 @@ export const AdminSidebar = observer(function AdminSidebar() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-custom-sidebar-border-200 bg-custom-sidebar-background-100 duration-300
|
className={`fixed inset-y-0 z-20 flex h-full flex-shrink-0 flex-grow-0 flex-col border-r border-subtle bg-surface-1 duration-300 md:relative ${isSidebarCollapsed ? "-ml-[290px]" : ""} sm:${isSidebarCollapsed ? "-ml-[290px]" : ""} md:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"} lg:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"} `}
|
||||||
fixed md:relative
|
|
||||||
${isSidebarCollapsed ? "-ml-[290px]" : ""}
|
|
||||||
sm:${isSidebarCollapsed ? "-ml-[290px]" : ""}
|
|
||||||
md:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"}
|
|
||||||
lg:ml-0 ${isSidebarCollapsed ? "w-[70px]" : "w-[290px]"}
|
|
||||||
`}
|
|
||||||
>
|
>
|
||||||
<div ref={ref} className="flex h-full w-full flex-1 flex-col">
|
<div ref={ref} className="flex h-full w-full flex-1 flex-col">
|
||||||
<AdminSidebarDropdown />
|
<AdminSidebarDropdown />
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
@ -8,6 +14,7 @@ import { Button, getButtonStyling } from "@plane/propel/button";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
import { InstanceWorkspaceService } from "@plane/services";
|
import { InstanceWorkspaceService } from "@plane/services";
|
||||||
import type { IWorkspace } from "@plane/types";
|
import type { IWorkspace } from "@plane/types";
|
||||||
|
import { validateSlug, validateWorkspaceName } from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
import { CustomSelect, Input } from "@plane/ui";
|
import { CustomSelect, Input } from "@plane/ui";
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -84,20 +91,13 @@ export function WorkspaceCreateForm() {
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-start justify-between gap-x-10 gap-y-6 lg:grid-cols-2">
|
<div className="grid-col grid w-full max-w-4xl grid-cols-1 items-start justify-between gap-x-10 gap-y-6 lg:grid-cols-2">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm text-custom-text-300">Name your workspace</h4>
|
<h4 className="text-13 text-tertiary">Name your workspace</h4>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="name"
|
name="name"
|
||||||
rules={{
|
rules={{
|
||||||
required: "This is a required field.",
|
validate: (value) => validateWorkspaceName(value, true),
|
||||||
validate: (value) =>
|
|
||||||
/^[\w\s-]*$/.test(value) ||
|
|
||||||
`Workspaces names can contain only (" "), ( - ), ( _ ) and alphanumeric characters.`,
|
|
||||||
maxLength: {
|
|
||||||
value: 80,
|
|
||||||
message: "Limit your name to 80 characters.",
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
render={({ field: { value, ref, onChange } }) => (
|
render={({ field: { value, ref, onChange } }) => (
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -118,22 +118,18 @@ export function WorkspaceCreateForm() {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span className="text-xs text-red-500">{errors?.name?.message}</span>
|
<span className="text-11 text-danger-primary">{errors?.name?.message}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm text-custom-text-300">Set your workspace's URL</h4>
|
<h4 className="text-13 text-tertiary">Set your workspace's URL</h4>
|
||||||
<div className="flex gap-0.5 w-full items-center rounded-md border-[0.5px] border-custom-border-200 px-3">
|
<div className="flex w-full items-center gap-0.5 rounded-md border-[0.5px] border-subtle px-3">
|
||||||
<span className="whitespace-nowrap text-sm text-custom-text-200">{workspaceBaseURL}</span>
|
<span className="text-13 whitespace-nowrap text-secondary">{workspaceBaseURL}</span>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="slug"
|
name="slug"
|
||||||
rules={{
|
rules={{
|
||||||
required: "The URL is a required field.",
|
validate: (value) => validateSlug(value),
|
||||||
maxLength: {
|
|
||||||
value: 48,
|
|
||||||
message: "Limit your URL to 48 characters.",
|
|
||||||
},
|
|
||||||
}}
|
}}
|
||||||
render={({ field: { onChange, value, ref } }) => (
|
render={({ field: { onChange, value, ref } }) => (
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -148,19 +144,19 @@ export function WorkspaceCreateForm() {
|
||||||
ref={ref}
|
ref={ref}
|
||||||
hasError={Boolean(errors.slug)}
|
hasError={Boolean(errors.slug)}
|
||||||
placeholder="workspace-name"
|
placeholder="workspace-name"
|
||||||
className="block w-full rounded-md border-none bg-transparent !px-0 py-2 text-sm"
|
className="block w-full rounded-md border-none bg-transparent !px-0 py-2 text-13"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{slugError && <p className="text-sm text-red-500">This URL is taken. Try something else.</p>}
|
{slugError && <p className="text-13 text-danger-primary">This URL is taken. Try something else.</p>}
|
||||||
{invalidSlug && (
|
{invalidSlug && (
|
||||||
<p className="text-sm text-red-500">{`URLs can contain only ( - ), ( _ ) and alphanumeric characters.`}</p>
|
<p className="text-13 text-danger-primary">{`URLs can contain only ( - ), ( _ ) and alphanumeric characters.`}</p>
|
||||||
)}
|
)}
|
||||||
{errors.slug && <span className="text-xs text-red-500">{errors.slug.message}</span>}
|
{errors.slug && <span className="text-11 text-danger-primary">{errors.slug.message}</span>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm text-custom-text-300">How many people will use this workspace?</h4>
|
<h4 className="text-13 text-tertiary">How many people will use this workspace?</h4>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Controller
|
<Controller
|
||||||
name="organization_size"
|
name="organization_size"
|
||||||
|
|
@ -172,10 +168,10 @@ export function WorkspaceCreateForm() {
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
label={
|
label={
|
||||||
ORGANIZATION_SIZE.find((c) => c === value) ?? (
|
ORGANIZATION_SIZE.find((c) => c === value) ?? (
|
||||||
<span className="text-custom-text-400">Select a range</span>
|
<span className="text-placeholder">Select a range</span>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
buttonClassName="!border-[0.5px] !border-custom-border-200 !shadow-none"
|
buttonClassName="!border-[0.5px] !border-subtle !shadow-none"
|
||||||
input
|
input
|
||||||
>
|
>
|
||||||
{ORGANIZATION_SIZE.map((item) => (
|
{ORGANIZATION_SIZE.map((item) => (
|
||||||
|
|
@ -187,22 +183,22 @@ export function WorkspaceCreateForm() {
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{errors.organization_size && (
|
{errors.organization_size && (
|
||||||
<span className="text-sm text-red-500">{errors.organization_size.message}</span>
|
<span className="text-13 text-danger-primary">{errors.organization_size.message}</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex max-w-4xl items-center py-1 gap-4">
|
<div className="flex max-w-4xl items-center gap-4 py-1">
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
size="sm"
|
size="lg"
|
||||||
onClick={handleSubmit(handleCreateWorkspace)}
|
onClick={handleSubmit(handleCreateWorkspace)}
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
loading={isSubmitting}
|
loading={isSubmitting}
|
||||||
>
|
>
|
||||||
{isSubmitting ? "Creating workspace" : "Create workspace"}
|
{isSubmitting ? "Creating workspace" : "Create workspace"}
|
||||||
</Button>
|
</Button>
|
||||||
<Link className={getButtonStyling("neutral-primary", "sm")} href="/workspace">
|
<Link className={getButtonStyling("secondary", "lg")} href="/workspace">
|
||||||
Go back
|
Go back
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,27 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// components
|
// components
|
||||||
|
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||||
|
// types
|
||||||
import type { Route } from "./+types/page";
|
import type { Route } from "./+types/page";
|
||||||
|
// local
|
||||||
import { WorkspaceCreateForm } from "./form";
|
import { WorkspaceCreateForm } from "./form";
|
||||||
|
|
||||||
const WorkspaceCreatePage = observer(function WorkspaceCreatePage(_props: Route.ComponentProps) {
|
const WorkspaceCreatePage = observer(function WorkspaceCreatePage(_props: Route.ComponentProps) {
|
||||||
return (
|
return (
|
||||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
<PageWrapper
|
||||||
<div className="border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
header={{
|
||||||
<div className="text-xl font-medium text-custom-text-100">Create a new workspace on this instance.</div>
|
title: "Create a new workspace on this instance.",
|
||||||
<div className="text-sm font-normal text-custom-text-300">
|
description: "You will need to invite users from Workspace Settings after you create this workspace.",
|
||||||
You will need to invite users from Workspace Settings after you create this workspace.
|
}}
|
||||||
</div>
|
>
|
||||||
</div>
|
|
||||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
|
||||||
<WorkspaceCreateForm />
|
<WorkspaceCreateForm />
|
||||||
</div>
|
</PageWrapper>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
@ -8,12 +14,13 @@ import { Button, getButtonStyling } from "@plane/propel/button";
|
||||||
import { setPromiseToast } from "@plane/propel/toast";
|
import { setPromiseToast } from "@plane/propel/toast";
|
||||||
import type { TInstanceConfigurationKeys } from "@plane/types";
|
import type { TInstanceConfigurationKeys } from "@plane/types";
|
||||||
import { Loader, ToggleSwitch } from "@plane/ui";
|
import { Loader, ToggleSwitch } from "@plane/ui";
|
||||||
|
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
|
import { PageWrapper } from "@/components/common/page-wrapper";
|
||||||
import { WorkspaceListItem } from "@/components/workspace/list-item";
|
import { WorkspaceListItem } from "@/components/workspace/list-item";
|
||||||
// hooks
|
// hooks
|
||||||
import { useInstance, useWorkspace } from "@/hooks/store";
|
import { useInstance, useWorkspace } from "@/hooks/store";
|
||||||
|
// types
|
||||||
import type { Route } from "./+types/page";
|
import type { Route } from "./+types/page";
|
||||||
|
|
||||||
const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props: Route.ComponentProps) {
|
const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props: Route.ComponentProps) {
|
||||||
|
|
@ -68,25 +75,20 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative container mx-auto w-full h-full p-4 py-4 space-y-6 flex flex-col">
|
<PageWrapper
|
||||||
<div className="flex items-center justify-between gap-4 border-b border-custom-border-100 mx-4 py-4 space-y-1 flex-shrink-0">
|
header={{
|
||||||
<div className="flex flex-col gap-1">
|
title: "Workspaces on this instance",
|
||||||
<div className="text-xl font-medium text-custom-text-100">Workspaces on this instance</div>
|
description: "See all workspaces and control who can create them.",
|
||||||
<div className="text-sm font-normal text-custom-text-300">
|
}}
|
||||||
See all workspaces and control who can create them.
|
>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex-grow overflow-hidden overflow-y-scroll vertical-scrollbar scrollbar-md px-4">
|
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{formattedConfig ? (
|
{formattedConfig ? (
|
||||||
<div className={cn("w-full flex items-center gap-14 rounded")}>
|
<div className={cn("flex w-full items-center gap-14 rounded-sm")}>
|
||||||
<div className="flex grow items-center gap-4">
|
<div className="flex grow items-center gap-4">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div className="text-lg font-medium pb-1">Prevent anyone else from creating a workspace.</div>
|
<div className="pb-1 text-16 font-medium">Prevent anyone else from creating a workspace.</div>
|
||||||
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
|
<div className={cn("text-11 leading-5 font-regular text-tertiary")}>
|
||||||
Toggling this on will let only you create workspaces. You will have to invite users to new
|
Toggling this on will let only you create workspaces. You will have to invite users to new workspaces.
|
||||||
workspaces.
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -114,22 +116,21 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
|
||||||
)}
|
)}
|
||||||
{workspaceLoader !== "init-loader" ? (
|
{workspaceLoader !== "init-loader" ? (
|
||||||
<>
|
<>
|
||||||
<div className="pt-6 flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2 pt-6">
|
||||||
<div className="flex flex-col items-start gap-x-2">
|
<div className="flex flex-col items-start gap-x-2">
|
||||||
<div className="flex items-center gap-2 text-lg font-medium">
|
<div className="flex items-center gap-2 text-16 font-medium">
|
||||||
All workspaces on this instance{" "}
|
All workspaces on this instance <span className="text-tertiary">• {workspaceIds.length}</span>
|
||||||
<span className="text-custom-text-300">• {workspaceIds.length}</span>
|
|
||||||
{workspaceLoader && ["mutation", "pagination"].includes(workspaceLoader) && (
|
{workspaceLoader && ["mutation", "pagination"].includes(workspaceLoader) && (
|
||||||
<LoaderIcon className="w-4 h-4 animate-spin" />
|
<LoaderIcon className="h-4 w-4 animate-spin" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={cn("font-normal leading-5 text-custom-text-300 text-xs")}>
|
<div className={cn("text-11 leading-5 font-regular text-tertiary")}>
|
||||||
You can't yet delete workspaces and you can only go to the workspace if you are an Admin or a
|
You can't yet delete workspaces and you can only go to the workspace if you are an Admin or a
|
||||||
Member.
|
Member.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Link href="/workspace/create" className={getButtonStyling("primary", "sm")}>
|
<Link href="/workspace/create" className={getButtonStyling("primary", "base")}>
|
||||||
Create workspace
|
Create workspace
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -142,12 +143,13 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
|
||||||
{hasNextPage && (
|
{hasNextPage && (
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<Button
|
<Button
|
||||||
variant="link-primary"
|
variant="link"
|
||||||
|
size="lg"
|
||||||
onClick={() => fetchNextWorkspaces()}
|
onClick={() => fetchNextWorkspaces()}
|
||||||
disabled={workspaceLoader === "pagination"}
|
disabled={workspaceLoader === "pagination"}
|
||||||
>
|
>
|
||||||
Load more
|
Load more
|
||||||
{workspaceLoader === "pagination" && <LoaderIcon className="w-3 h-3 animate-spin" />}
|
{workspaceLoader === "pagination" && <LoaderIcon className="h-3 w-3 animate-spin" />}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -161,8 +163,7 @@ const WorkspaceManagementPage = observer(function WorkspaceManagementPage(_props
|
||||||
</Loader>
|
</Loader>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</PageWrapper>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { Info } from "lucide-react";
|
import { Info } from "lucide-react";
|
||||||
// plane constants
|
// plane constants
|
||||||
import type { TAdminAuthErrorInfo } from "@plane/constants";
|
import type { TAdminAuthErrorInfo } from "@plane/constants";
|
||||||
|
|
@ -14,16 +20,16 @@ export function AuthBanner(props: TAuthBanner) {
|
||||||
|
|
||||||
if (!bannerData) return <></>;
|
if (!bannerData) return <></>;
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center p-2 rounded-md gap-2 border border-custom-primary-100/50 bg-custom-primary-100/10">
|
<div className="relative flex items-center gap-2 rounded-md border border-accent-strong/50 bg-accent-primary/10 p-2">
|
||||||
<div className="w-4 h-4 flex-shrink-0 relative flex justify-center items-center">
|
<div className="relative flex h-4 w-4 flex-shrink-0 items-center justify-center">
|
||||||
<Info size={16} className="text-custom-primary-100" />
|
<Info size={16} className="text-accent-primary" />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full text-sm font-medium text-custom-primary-100">{bannerData?.message}</div>
|
<div className="w-full text-13 font-medium text-accent-primary">{bannerData?.message}</div>
|
||||||
<div
|
<div
|
||||||
className="relative ml-auto w-6 h-6 rounded-sm flex justify-center items-center transition-all cursor-pointer hover:bg-custom-primary-100/20 text-custom-primary-100/80"
|
className="relative ml-auto flex h-6 w-6 cursor-pointer items-center justify-center rounded-xs text-accent-primary transition-all hover:bg-accent-primary/20"
|
||||||
onClick={() => handleBannerData && handleBannerData(undefined)}
|
onClick={() => handleBannerData && handleBannerData(undefined)}
|
||||||
>
|
>
|
||||||
<CloseIcon className="w-4 h-4 flex-shrink-0" />
|
<CloseIcon className="h-4 w-4 flex-shrink-0" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { PlaneLockup } from "@plane/propel/icons";
|
import { PlaneLockup } from "@plane/propel/icons";
|
||||||
|
|
||||||
export function AuthHeader() {
|
export function AuthHeader() {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0">
|
<div className="sticky top-0 flex w-full flex-shrink-0 items-center justify-between gap-6">
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<PlaneLockup height={20} width={95} className="text-custom-text-100" />
|
<PlaneLockup height={20} width={95} className="text-primary" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,13 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { KeyRound, Mails } from "lucide-react";
|
|
||||||
// plane packages
|
// plane packages
|
||||||
import type { TAdminAuthErrorInfo } from "@plane/constants";
|
import type { TAdminAuthErrorInfo } from "@plane/constants";
|
||||||
import { SUPPORT_EMAIL, EAdminAuthErrorCodes } from "@plane/constants";
|
import { SUPPORT_EMAIL, EAdminAuthErrorCodes } from "@plane/constants";
|
||||||
import type { TGetBaseAuthenticationModeProps, TInstanceAuthenticationModes } from "@plane/types";
|
|
||||||
import { resolveGeneralTheme } from "@plane/utils";
|
|
||||||
// components
|
|
||||||
import githubLightModeImage from "@/app/assets/logos/github-black.png?url";
|
|
||||||
import githubDarkModeImage from "@/app/assets/logos/github-white.png?url";
|
|
||||||
import GitlabLogo from "@/app/assets/logos/gitlab-logo.svg?url";
|
|
||||||
import GoogleLogo from "@/app/assets/logos/google-logo.svg?url";
|
|
||||||
import { EmailCodesConfiguration } from "@/components/authentication/email-config-switch";
|
|
||||||
import { GithubConfiguration } from "@/components/authentication/github-config";
|
|
||||||
import { GitlabConfiguration } from "@/components/authentication/gitlab-config";
|
|
||||||
import { GoogleConfiguration } from "@/components/authentication/google-config";
|
|
||||||
import { PasswordLoginConfiguration } from "@/components/authentication/password-config-switch";
|
|
||||||
// images
|
|
||||||
|
|
||||||
export enum EErrorAlertType {
|
export enum EErrorAlertType {
|
||||||
BANNER_ALERT = "BANNER_ALERT",
|
BANNER_ALERT = "BANNER_ALERT",
|
||||||
|
|
@ -58,7 +50,7 @@ const errorCodeMessages: {
|
||||||
message: () => (
|
message: () => (
|
||||||
<div>
|
<div>
|
||||||
Admin user already exists.
|
Admin user already exists.
|
||||||
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
|
<Link className="font-medium underline underline-offset-4 transition-all hover:font-bold" href={`/admin`}>
|
||||||
Sign In
|
Sign In
|
||||||
</Link>
|
</Link>
|
||||||
now.
|
now.
|
||||||
|
|
@ -70,7 +62,7 @@ const errorCodeMessages: {
|
||||||
message: () => (
|
message: () => (
|
||||||
<div>
|
<div>
|
||||||
Admin user does not exist.
|
Admin user does not exist.
|
||||||
<Link className="underline underline-offset-4 font-medium hover:font-bold transition-all" href={`/admin`}>
|
<Link className="font-medium underline underline-offset-4 transition-all hover:font-bold" href={`/admin`}>
|
||||||
Sign In
|
Sign In
|
||||||
</Link>
|
</Link>
|
||||||
now.
|
now.
|
||||||
|
|
@ -106,53 +98,3 @@ export const authErrorHandler = (errorCode: EAdminAuthErrorCodes, email?: string
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getBaseAuthenticationModes: (props: TGetBaseAuthenticationModeProps) => TInstanceAuthenticationModes[] = ({
|
|
||||||
disabled,
|
|
||||||
updateConfig,
|
|
||||||
resolvedTheme,
|
|
||||||
}) => [
|
|
||||||
{
|
|
||||||
key: "unique-codes",
|
|
||||||
name: "Unique codes",
|
|
||||||
description:
|
|
||||||
"Log in or sign up for Plane using codes sent via email. You need to have set up SMTP to use this method.",
|
|
||||||
icon: <Mails className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
|
|
||||||
config: <EmailCodesConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "passwords-login",
|
|
||||||
name: "Passwords",
|
|
||||||
description: "Allow members to create accounts with passwords and use it with their email addresses to sign in.",
|
|
||||||
icon: <KeyRound className="h-6 w-6 p-0.5 text-custom-text-300/80" />,
|
|
||||||
config: <PasswordLoginConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "google",
|
|
||||||
name: "Google",
|
|
||||||
description: "Allow members to log in or sign up for Plane with their Google accounts.",
|
|
||||||
icon: <img src={GoogleLogo} height={20} width={20} alt="Google Logo" />,
|
|
||||||
config: <GoogleConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "github",
|
|
||||||
name: "GitHub",
|
|
||||||
description: "Allow members to log in or sign up for Plane with their GitHub accounts.",
|
|
||||||
icon: (
|
|
||||||
<img
|
|
||||||
src={resolveGeneralTheme(resolvedTheme) === "dark" ? githubDarkModeImage : githubLightModeImage}
|
|
||||||
height={20}
|
|
||||||
width={20}
|
|
||||||
alt="GitHub Logo"
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
config: <GithubConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: "gitlab",
|
|
||||||
name: "GitLab",
|
|
||||||
description: "Allow members to log in or sign up to plane with their GitLab accounts.",
|
|
||||||
icon: <img src={GitlabLogo} height={20} width={20} alt="GitLab Logo" />,
|
|
||||||
config: <GitlabConfiguration disabled={disabled} updateConfig={updateConfig} />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
@ -16,7 +22,7 @@ function RootLayout() {
|
||||||
}, [replace, isUserLoggedIn]);
|
}, [replace, isUserLoggedIn]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex flex-col items-center w-screen h-screen overflow-hidden overflow-y-auto pt-6 pb-10 px-8">
|
<div className="relative z-10 flex h-screen w-screen flex-col items-center overflow-hidden overflow-y-auto bg-surface-1 px-8 pt-6 pb-10">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// components
|
// components
|
||||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||||
|
|
@ -16,7 +22,7 @@ function HomePage() {
|
||||||
// if instance is not fetched, show loading
|
// if instance is not fetched, show loading
|
||||||
if (!instance && !error) {
|
if (!instance && !error) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-screen w-full">
|
<div className="flex h-screen w-full items-center justify-center">
|
||||||
<LogoSpinner />
|
<LogoSpinner />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { Eye, EyeOff } from "lucide-react";
|
import { Eye, EyeOff } from "lucide-react";
|
||||||
|
|
@ -10,7 +16,7 @@ import { Input, Spinner } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { Banner } from "@/components/common/banner";
|
import { Banner } from "@/components/common/banner";
|
||||||
// local components
|
// local components
|
||||||
import { FormHeader } from "../../../core/components/instance/form-header";
|
import { FormHeader } from "@/components/instance/form-header";
|
||||||
import { AuthBanner } from "./auth-banner";
|
import { AuthBanner } from "./auth-banner";
|
||||||
import { AuthHeader } from "./auth-header";
|
import { AuthHeader } from "./auth-header";
|
||||||
import { authErrorHandler } from "./auth-helpers";
|
import { authErrorHandler } from "./auth-helpers";
|
||||||
|
|
@ -105,8 +111,8 @@ export function InstanceSignInForm() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AuthHeader />
|
<AuthHeader />
|
||||||
<div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
|
<div className="mt-10 flex w-full flex-grow flex-col items-center justify-center py-6">
|
||||||
<div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
|
<div className="relative flex w-full max-w-[22.5rem] flex-col gap-6">
|
||||||
<FormHeader
|
<FormHeader
|
||||||
heading="Manage your Plane instance"
|
heading="Manage your Plane instance"
|
||||||
subHeading="Configure instance-wide settings to secure your instance"
|
subHeading="Configure instance-wide settings to secure your instance"
|
||||||
|
|
@ -128,11 +134,11 @@ export function InstanceSignInForm() {
|
||||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||||
|
|
||||||
<div className="w-full space-y-1">
|
<div className="w-full space-y-1">
|
||||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="email">
|
<label className="text-13 font-medium text-tertiary" htmlFor="email">
|
||||||
Email <span className="text-red-500">*</span>
|
Email <span className="text-danger-primary">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
|
|
@ -140,18 +146,18 @@ export function InstanceSignInForm() {
|
||||||
placeholder="name@company.com"
|
placeholder="name@company.com"
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={(e) => handleFormChange("email", e.target.value)}
|
onChange={(e) => handleFormChange("email", e.target.value)}
|
||||||
autoComplete="on"
|
autoComplete="off"
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full space-y-1">
|
<div className="w-full space-y-1">
|
||||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
|
<label className="text-13 font-medium text-tertiary" htmlFor="password">
|
||||||
Password <span className="text-red-500">*</span>
|
Password <span className="text-danger-primary">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type={showPassword ? "text" : "password"}
|
type={showPassword ? "text" : "password"}
|
||||||
|
|
@ -159,12 +165,12 @@ export function InstanceSignInForm() {
|
||||||
placeholder="Enter your password"
|
placeholder="Enter your password"
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={(e) => handleFormChange("password", e.target.value)}
|
onChange={(e) => handleFormChange("password", e.target.value)}
|
||||||
autoComplete="on"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
{showPassword ? (
|
{showPassword ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
|
||||||
onClick={() => setShowPassword(false)}
|
onClick={() => setShowPassword(false)}
|
||||||
>
|
>
|
||||||
<EyeOff className="h-4 w-4" />
|
<EyeOff className="h-4 w-4" />
|
||||||
|
|
@ -172,7 +178,7 @@ export function InstanceSignInForm() {
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
|
||||||
onClick={() => setShowPassword(true)}
|
onClick={() => setShowPassword(true)}
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
|
|
@ -181,7 +187,7 @@ export function InstanceSignInForm() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<Button type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
|
<Button type="submit" size="xl" className="w-full" disabled={isButtonDisabled}>
|
||||||
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Sign in"}
|
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Sign in"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<svg width="92" height="84" viewBox="0 0 92 84" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_3695_11896)">
|
|
||||||
<path d="M83.0398 32.6876C74.2901 27.2397 62.0735 23.8553 48.7013 23.8553C21.7918 23.8553 0 37.3101 0 53.9016C0 69.0898 18.1598 81.554 41.685 83.7001V74.9504C25.8364 72.9693 13.95 64.3022 13.95 53.9016C13.95 42.0977 29.4684 32.44 48.7013 32.44C58.2765 32.44 66.9436 34.8338 73.217 38.7134L64.3022 44.2439H92.1197V27.0746L83.0398 32.6876Z" fill="#CCCCCC"/>
|
|
||||||
<path d="M41.6846 8.99736V74.9504V83.7002L55.6346 74.9504V0L41.6846 8.99736Z" fill="#FF6200"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_3695_11896">
|
|
||||||
<rect width="92" height="84" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 702 B |
|
|
@ -1,17 +0,0 @@
|
||||||
<svg width="700" height="650" viewBox="0 0 700 650" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<g clip-path="url(#clip0_3262_5767)">
|
|
||||||
<mask id="mask0_3262_5767" style="mask-type:luminance" maskUnits="userSpaceOnUse" x="0" y="0" width="700" height="650">
|
|
||||||
<path d="M700 0H0V650H700V0Z" fill="white"/>
|
|
||||||
</mask>
|
|
||||||
<g mask="url(#mask0_3262_5767)">
|
|
||||||
<path d="M337.682 0L360.832 20.5C377.982 35.7 395.182 50.85 412.132 66.25C521.982 166 614.982 278.25 684.982 407.45C688.582 414.05 691.832 420.85 695.082 427.6L699.982 437.75L694.582 440.6L690.532 434.85L680.032 419.9L672.682 409.2C621.732 335.25 570.682 261.2 500.582 201.95C479.373 183.995 455.969 168.807 430.932 156.75C380.232 132.5 335.132 142.2 296.432 182C259.632 219.85 240.532 266.85 223.282 314.65C221.032 320.8 218.682 326.9 216.332 333L212.232 343.75L203.632 341C208.632 323.6 213.232 306.1 217.832 288.55C228.332 248.8 238.832 209.05 253.432 170.75C268.932 129.95 288.532 90.6 308.082 51.25C316.532 34.2 324.982 17.15 333.082 0H337.682Z" fill="#C22E33"/>
|
|
||||||
<path d="M372.382 491.1C291.082 529.6 94.3829 569.3 1.08287 559.1C-14.1671 478.8 135.482 102.5 208.982 45.5L204.232 56.4C202.115 61.531 199.813 66.5842 197.332 71.55L194.032 78C156.032 151.1 118.082 224.3 98.6329 304.5C91.6287 332.124 87.8038 360.458 87.2328 388.95C86.7328 455.95 128.432 501.55 198.082 504.4C231.582 505.75 265.432 502.25 299.232 498.7C313.932 497.2 328.582 495.65 343.232 494.5C348.632 494.1 353.932 493.45 360.832 492.55L372.382 491.15V491.1Z" fill="#C22E33"/>
|
|
||||||
<path d="M141.233 639.05C118.983 640.75 96.733 642.45 74.583 644.45C279.433 663.95 476.083 630.6 670.083 562.25C606.833 450.75 521.583 362.7 422.483 286.15C423.783 291.05 426.683 294.6 429.533 298.1L431.933 301.1C440.433 312.4 449.333 323.5 458.283 334.6C478.733 360.05 499.183 385.5 514.583 413.5C553.483 484.5 532.383 545.9 456.183 578.3C406.083 599.65 351.333 614.2 297.183 622.9C245.683 631.1 193.433 635.05 141.233 639.05Z" fill="#C22E33"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip0_3262_5767">
|
|
||||||
<rect width="700" height="650" fill="white"/>
|
|
||||||
</clipPath>
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2 KiB |
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensures that a URL has a trailing slash while preserving query parameters and fragments
|
* Ensures that a URL has a trailing slash while preserving query parameters and fragments
|
||||||
* @param url - The URL to process
|
* @param url - The URL to process
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
// Minimal shim so code using next/image compiles under React Router + Vite
|
// Minimal shim so code using next/image compiles under React Router + Vite
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link as RRLink } from "react-router";
|
import { Link as RRLink } from "react-router";
|
||||||
import { ensureTrailingSlash } from "./helper";
|
import { ensureTrailingSlash } from "./helper";
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useLocation, useNavigate, useSearchParams as useSearchParamsRR } from "react-router";
|
import { useLocation, useNavigate, useSearchParams as useSearchParamsRR } from "react-router";
|
||||||
import { ensureTrailingSlash } from "./helper";
|
import { ensureTrailingSlash } from "./helper";
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Link } from "react-router";
|
import { Link } from "react-router";
|
||||||
// ui
|
// ui
|
||||||
|
|
@ -7,22 +13,22 @@ import Image404 from "@/app/assets/images/404.svg?url";
|
||||||
|
|
||||||
function PageNotFound() {
|
function PageNotFound() {
|
||||||
return (
|
return (
|
||||||
<div className={`h-screen w-full overflow-hidden bg-custom-background-100`}>
|
<div className={`h-screen w-full overflow-hidden bg-surface-1`}>
|
||||||
<div className="grid h-full place-items-center p-4">
|
<div className="grid h-full place-items-center p-4">
|
||||||
<div className="space-y-8 text-center">
|
<div className="space-y-8 text-center">
|
||||||
<div className="relative mx-auto h-60 w-60 lg:h-80 lg:w-80">
|
<div className="relative mx-auto h-60 w-60 lg:h-80 lg:w-80">
|
||||||
<img src={Image404} alt="404 - Page not found" className="h-full w-full object-contain" />
|
<img src={Image404} alt="404 - Page not found" className="h-full w-full object-contain" />
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h3 className="text-lg font-semibold">Oops! Something went wrong.</h3>
|
<h3 className="text-16 font-semibold">Oops! Something went wrong.</h3>
|
||||||
<p className="text-sm text-custom-text-200">
|
<p className="text-13 text-secondary">
|
||||||
Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is
|
Sorry, the page you are looking for cannot be found. It may have been removed, had its name changed, or is
|
||||||
temporarily unavailable.
|
temporarily unavailable.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<Link to="/general/">
|
<Link to="/general/">
|
||||||
<span className="flex justify-center py-4">
|
<span className="flex justify-center py-4">
|
||||||
<Button variant="neutral-primary" size="md">
|
<Button variant="secondary" size="lg">
|
||||||
Go to general settings
|
Go to general settings
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,13 @@
|
||||||
import * as Sentry from "@sentry/react-router";
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { startTransition, StrictMode } from "react";
|
import { startTransition, StrictMode } from "react";
|
||||||
import { hydrateRoot } from "react-dom/client";
|
import { hydrateRoot } from "react-dom/client";
|
||||||
import { HydratedRouter } from "react-router/dom";
|
import { HydratedRouter } from "react-router/dom";
|
||||||
|
|
||||||
Sentry.init({
|
|
||||||
dsn: process.env.VITE_SENTRY_DSN,
|
|
||||||
environment: process.env.VITE_SENTRY_ENVIRONMENT,
|
|
||||||
sendDefaultPii: process.env.VITE_SENTRY_SEND_DEFAULT_PII ? process.env.VITE_SENTRY_SEND_DEFAULT_PII === "1" : false,
|
|
||||||
release: process.env.VITE_APP_VERSION,
|
|
||||||
tracesSampleRate: process.env.VITE_SENTRY_TRACES_SAMPLE_RATE
|
|
||||||
? parseFloat(process.env.VITE_SENTRY_TRACES_SAMPLE_RATE)
|
|
||||||
: 0.1,
|
|
||||||
profilesSampleRate: process.env.VITE_SENTRY_PROFILES_SAMPLE_RATE
|
|
||||||
? parseFloat(process.env.VITE_SENTRY_PROFILES_SAMPLE_RATE)
|
|
||||||
: 0.1,
|
|
||||||
replaysSessionSampleRate: process.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE
|
|
||||||
? parseFloat(process.env.VITE_SENTRY_REPLAYS_SESSION_SAMPLE_RATE)
|
|
||||||
: 0.1,
|
|
||||||
replaysOnErrorSampleRate: process.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE
|
|
||||||
? parseFloat(process.env.VITE_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE)
|
|
||||||
: 1.0,
|
|
||||||
integrations: [],
|
|
||||||
});
|
|
||||||
|
|
||||||
startTransition(() => {
|
startTransition(() => {
|
||||||
hydrateRoot(
|
hydrateRoot(
|
||||||
document,
|
document,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,10 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import * as Sentry from "@sentry/react-router";
|
|
||||||
import { Links, Meta, Outlet, Scripts } from "react-router";
|
import { Links, Meta, Outlet, Scripts } from "react-router";
|
||||||
import type { LinksFunction } from "react-router";
|
import type { LinksFunction } from "react-router";
|
||||||
import appleTouchIcon from "@/app/assets/favicon/apple-touch-icon.png?url";
|
import appleTouchIcon from "@/app/assets/favicon/apple-touch-icon.png?url";
|
||||||
|
|
@ -8,8 +13,13 @@ import favicon32 from "@/app/assets/favicon/favicon-32x32.png?url";
|
||||||
import faviconIco from "@/app/assets/favicon/favicon.ico?url";
|
import faviconIco from "@/app/assets/favicon/favicon.ico?url";
|
||||||
import { LogoSpinner } from "@/components/common/logo-spinner";
|
import { LogoSpinner } from "@/components/common/logo-spinner";
|
||||||
import globalStyles from "@/styles/globals.css?url";
|
import globalStyles from "@/styles/globals.css?url";
|
||||||
|
import { AppProviders } from "@/providers";
|
||||||
import type { Route } from "./+types/root";
|
import type { Route } from "./+types/root";
|
||||||
import { AppProviders } from "./providers";
|
// fonts
|
||||||
|
import "@fontsource-variable/inter";
|
||||||
|
import interVariableWoff2 from "@fontsource-variable/inter/files/inter-latin-wght-normal.woff2?url";
|
||||||
|
import "@fontsource/material-symbols-rounded";
|
||||||
|
import "@fontsource/ibm-plex-mono";
|
||||||
|
|
||||||
const APP_TITLE = "Plane | Simple, extensible, open-source project management tool.";
|
const APP_TITLE = "Plane | Simple, extensible, open-source project management tool.";
|
||||||
const APP_DESCRIPTION =
|
const APP_DESCRIPTION =
|
||||||
|
|
@ -22,6 +32,13 @@ export const links: LinksFunction = () => [
|
||||||
{ rel: "shortcut icon", href: faviconIco },
|
{ rel: "shortcut icon", href: faviconIco },
|
||||||
{ rel: "manifest", href: `/site.webmanifest.json` },
|
{ rel: "manifest", href: `/site.webmanifest.json` },
|
||||||
{ rel: "stylesheet", href: globalStyles },
|
{ rel: "stylesheet", href: globalStyles },
|
||||||
|
{
|
||||||
|
rel: "preload",
|
||||||
|
href: interVariableWoff2,
|
||||||
|
as: "font",
|
||||||
|
type: "font/woff2",
|
||||||
|
crossOrigin: "anonymous",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Layout({ children }: { children: ReactNode }) {
|
export function Layout({ children }: { children: ReactNode }) {
|
||||||
|
|
@ -56,7 +73,11 @@ export const meta: Route.MetaFunction = () => [
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Root() {
|
export default function Root() {
|
||||||
return <Outlet />;
|
return (
|
||||||
|
<div className="min-h-screen bg-canvas">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HydrateFallback() {
|
export function HydrateFallback() {
|
||||||
|
|
@ -67,11 +88,7 @@ export function HydrateFallback() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
export function ErrorBoundary({ error: _error }: Route.ErrorBoundaryProps) {
|
||||||
if (error) {
|
|
||||||
Sentry.captureException(error);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p>Something went wrong.</p>
|
<p>Something went wrong.</p>
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { index, layout, route } from "@react-router/dev/routes";
|
import { index, layout, route } from "@react-router/dev/routes";
|
||||||
import type { RouteConfig } from "@react-router/dev/routes";
|
import type { RouteConfig } from "@react-router/dev/routes";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "./authentication-modes";
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
export * from "./upgrade-button";
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
// icons
|
|
||||||
import { SquareArrowOutUpRight } from "lucide-react";
|
|
||||||
// plane internal packages
|
|
||||||
import { getButtonStyling } from "@plane/propel/button";
|
|
||||||
import { cn } from "@plane/utils";
|
|
||||||
|
|
||||||
export function UpgradeButton() {
|
|
||||||
return (
|
|
||||||
<a
|
|
||||||
href="https://plane.so/pricing?mode=self-hosted"
|
|
||||||
target="_blank"
|
|
||||||
className={cn(getButtonStyling("primary", "sm"))}
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
Upgrade
|
|
||||||
<SquareArrowOutUpRight className="h-3.5 w-3.5 p-0.5" />
|
|
||||||
</a>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import { enableStaticRendering } from "mobx-react";
|
|
||||||
// stores
|
|
||||||
import { CoreRootStore } from "@/store/root.store";
|
|
||||||
|
|
||||||
enableStaticRendering(typeof window === "undefined");
|
|
||||||
|
|
||||||
export class RootStore extends CoreRootStore {
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
}
|
|
||||||
|
|
||||||
hydrate(initialData: any) {
|
|
||||||
super.hydrate(initialData);
|
|
||||||
}
|
|
||||||
|
|
||||||
resetOnSignOut() {
|
|
||||||
super.resetOnSignOut();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
|
|
||||||
|
|
@ -16,8 +22,8 @@ export function AuthenticationMethodCard(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn("w-full flex items-center gap-14 rounded", {
|
className={cn("flex w-full items-center gap-14 rounded-lg bg-layer-2", {
|
||||||
"px-4 py-3 border border-custom-border-200": withBorder,
|
"border border-subtle px-4 py-3": withBorder,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
@ -26,21 +32,21 @@ export function AuthenticationMethodCard(props: Props) {
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-custom-background-80">{icon}</div>
|
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-layer-1">{icon}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div
|
<div
|
||||||
className={cn("font-medium leading-5 text-custom-text-100", {
|
className={cn("leading-5 font-medium text-primary", {
|
||||||
"text-sm": withBorder,
|
"text-13": withBorder,
|
||||||
"text-xl": !withBorder,
|
"text-18": !withBorder,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={cn("font-normal leading-5 text-custom-text-300", {
|
className={cn("leading-5 font-regular text-tertiary", {
|
||||||
"text-xs": withBorder,
|
"text-11": withBorder,
|
||||||
"text-sm": !withBorder,
|
"text-13": !withBorder,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{description}
|
{description}
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -1,11 +1,17 @@
|
||||||
import React from "react";
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
// icons
|
// icons
|
||||||
import { Settings2 } from "lucide-react";
|
import { Settings2 } from "lucide-react";
|
||||||
// plane internal packages
|
// plane internal packages
|
||||||
|
import { getButtonStyling } from "@plane/propel/button";
|
||||||
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
import type { TInstanceAuthenticationMethodKeys } from "@plane/types";
|
||||||
import { ToggleSwitch, getButtonStyling } from "@plane/ui";
|
import { ToggleSwitch } from "@plane/ui";
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
// hooks
|
// hooks
|
||||||
import { useInstance } from "@/hooks/store";
|
import { useInstance } from "@/hooks/store";
|
||||||
|
|
@ -28,7 +34,7 @@ export const GiteaConfiguration = observer(function GiteaConfiguration(props: Pr
|
||||||
<>
|
<>
|
||||||
{GiteaConfigured ? (
|
{GiteaConfigured ? (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Link href="/authentication/gitea" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
|
<Link href="/authentication/gitea" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||||
Edit
|
Edit
|
||||||
</Link>
|
</Link>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
|
|
@ -43,11 +49,8 @@ export const GiteaConfiguration = observer(function GiteaConfiguration(props: Pr
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link href="/authentication/gitea" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
||||||
href="/authentication/gitea"
|
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
|
||||||
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
|
|
||||||
>
|
|
||||||
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
|
|
||||||
Configure
|
Configure
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1,4 +1,9 @@
|
||||||
import React from "react";
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
// icons
|
// icons
|
||||||
|
|
@ -28,7 +33,7 @@ export const GithubConfiguration = observer(function GithubConfiguration(props:
|
||||||
<>
|
<>
|
||||||
{isGithubConfigured ? (
|
{isGithubConfigured ? (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Link href="/authentication/github" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
|
<Link href="/authentication/github" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||||
Edit
|
Edit
|
||||||
</Link>
|
</Link>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
|
|
@ -42,11 +47,8 @@ export const GithubConfiguration = observer(function GithubConfiguration(props:
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link href="/authentication/github" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
||||||
href="/authentication/github"
|
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
|
||||||
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
|
|
||||||
>
|
|
||||||
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
|
|
||||||
Configure
|
Configure
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
// icons
|
// icons
|
||||||
|
|
@ -27,7 +33,7 @@ export const GitlabConfiguration = observer(function GitlabConfiguration(props:
|
||||||
<>
|
<>
|
||||||
{isGitlabConfigured ? (
|
{isGitlabConfigured ? (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Link href="/authentication/gitlab" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
|
<Link href="/authentication/gitlab" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||||
Edit
|
Edit
|
||||||
</Link>
|
</Link>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
|
|
@ -41,11 +47,8 @@ export const GitlabConfiguration = observer(function GitlabConfiguration(props:
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link href="/authentication/gitlab" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
||||||
href="/authentication/gitlab"
|
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
|
||||||
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
|
|
||||||
>
|
|
||||||
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
|
|
||||||
Configure
|
Configure
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
// icons
|
// icons
|
||||||
|
|
@ -27,7 +33,7 @@ export const GoogleConfiguration = observer(function GoogleConfiguration(props:
|
||||||
<>
|
<>
|
||||||
{isGoogleConfigured ? (
|
{isGoogleConfigured ? (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<Link href="/authentication/google" className={cn(getButtonStyling("link-primary", "md"), "font-medium")}>
|
<Link href="/authentication/google" className={cn(getButtonStyling("link", "base"), "font-medium")}>
|
||||||
Edit
|
Edit
|
||||||
</Link>
|
</Link>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
|
|
@ -41,11 +47,8 @@ export const GoogleConfiguration = observer(function GoogleConfiguration(props:
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Link
|
<Link href="/authentication/google" className={cn(getButtonStyling("secondary", "base"), "text-tertiary")}>
|
||||||
href="/authentication/google"
|
<Settings2 className="h-4 w-4 p-0.5 text-tertiary" />
|
||||||
className={cn(getButtonStyling("neutral-primary", "sm"), "text-custom-text-300")}
|
|
||||||
>
|
|
||||||
<Settings2 className="h-4 w-4 p-0.5 text-custom-text-300/80" />
|
|
||||||
Configure
|
Configure
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
// hooks
|
// hooks
|
||||||
39
apps/admin/components/common/banner.tsx
Normal file
39
apps/admin/components/common/banner.tsx
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { AlertCircle, CheckCircle2 } from "lucide-react";
|
||||||
|
|
||||||
|
type TBanner = {
|
||||||
|
type: "success" | "error";
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Banner(props: TBanner) {
|
||||||
|
const { type, message } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`w-full rounded-md border p-2 ${type === "error" ? "border-danger-strong bg-danger-subtle" : "border-success-strong bg-success-subtle"}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-center">
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
{type === "error" ? (
|
||||||
|
<span className="flex h-6 w-6 items-center justify-center rounded-full">
|
||||||
|
<AlertCircle className="h-5 w-5 text-danger-primary" aria-hidden="true" />
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<CheckCircle2 className="h-5 w-5 text-success-primary" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="ml-1">
|
||||||
|
<p className={`text-13 font-medium ${type === "error" ? "text-danger-primary" : "text-success-primary"}`}>
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Tooltip } from "@plane/propel/tooltip";
|
import { Tooltip } from "@plane/propel/tooltip";
|
||||||
|
|
||||||
|
|
@ -14,19 +20,14 @@ export function BreadcrumbLink(props: Props) {
|
||||||
<li className="flex items-center space-x-2" tabIndex={-1}>
|
<li className="flex items-center space-x-2" tabIndex={-1}>
|
||||||
<div className="flex flex-wrap items-center gap-2.5">
|
<div className="flex flex-wrap items-center gap-2.5">
|
||||||
{href ? (
|
{href ? (
|
||||||
<Link
|
<Link className="flex items-center gap-1 text-13 font-medium text-tertiary hover:text-primary" href={href}>
|
||||||
className="flex items-center gap-1 text-sm font-medium text-custom-text-300 hover:text-custom-text-100"
|
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden !text-16">{icon}</div>}
|
||||||
href={href}
|
<div className="relative line-clamp-1 block max-w-[150px] truncate overflow-hidden">{label}</div>
|
||||||
>
|
|
||||||
{icon && (
|
|
||||||
<div className="flex h-5 w-5 items-center justify-center overflow-hidden !text-[1rem]">{icon}</div>
|
|
||||||
)}
|
|
||||||
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
|
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex cursor-default items-center gap-1 text-sm font-medium text-custom-text-100">
|
<div className="flex cursor-default items-center gap-1 text-13 font-medium text-primary">
|
||||||
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden">{icon}</div>}
|
{icon && <div className="flex h-5 w-5 items-center justify-center overflow-hidden">{icon}</div>}
|
||||||
<div className="relative line-clamp-1 block max-w-[150px] overflow-hidden truncate">{label}</div>
|
<div className="relative line-clamp-1 block max-w-[150px] truncate overflow-hidden">{label}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { cn } from "@plane/utils";
|
import { cn } from "@plane/utils";
|
||||||
|
|
||||||
type TProps = {
|
type TProps = {
|
||||||
|
|
@ -10,9 +16,9 @@ export function CodeBlock({ children, className, darkerShade }: TProps) {
|
||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-0.5 text-xs text-custom-text-300 bg-custom-background-90 font-semibold rounded-md border border-custom-border-100",
|
"rounded-md border border-subtle bg-surface-2 px-0.5 text-11 font-semibold text-tertiary",
|
||||||
{
|
{
|
||||||
"text-custom-text-200 bg-custom-background-80 border-custom-border-200": darkerShade,
|
"border-subtle bg-layer-1 text-secondary": darkerShade,
|
||||||
},
|
},
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
// headless ui
|
// headless ui
|
||||||
|
|
@ -26,7 +32,7 @@ export function ConfirmDiscardModal(props: Props) {
|
||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="fixed inset-0 bg-custom-backdrop transition-opacity" />
|
<div className="fixed inset-0 bg-backdrop transition-opacity" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
<div className="fixed inset-0 z-10 overflow-y-auto">
|
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||||
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-32">
|
<div className="my-10 flex items-center justify-center p-4 text-center sm:p-0 md:my-32">
|
||||||
|
|
@ -39,26 +45,26 @@ export function ConfirmDiscardModal(props: Props) {
|
||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-custom-background-100 text-left shadow-custom-shadow-md transition-all sm:my-8 sm:w-[30rem]">
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-surface-1 text-left shadow-raised-200 transition-all sm:my-8 sm:w-[30rem]">
|
||||||
<div className="px-4 pb-4 pt-5 sm:p-6 sm:pb-4">
|
<div className="px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
||||||
<div className="sm:flex sm:items-start">
|
<div className="sm:flex sm:items-start">
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
||||||
<Dialog.Title as="h3" className="text-lg font-medium leading-6 text-custom-text-300">
|
<Dialog.Title as="h3" className="text-16 leading-6 font-medium text-tertiary">
|
||||||
You have unsaved changes
|
You have unsaved changes
|
||||||
</Dialog.Title>
|
</Dialog.Title>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<p className="text-sm text-custom-text-400">
|
<p className="text-13 text-placeholder">
|
||||||
Changes you made will be lost if you go back. Do you wish to go back?
|
Changes you made will be lost if you go back. Do you wish to go back?
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end items-center p-4 sm:px-6 gap-2">
|
<div className="flex items-center justify-end gap-2 p-4 sm:px-6">
|
||||||
<Button variant="neutral-primary" size="sm" onClick={handleClose}>
|
<Button variant="secondary" size="lg" onClick={handleClose}>
|
||||||
Keep editing
|
Keep editing
|
||||||
</Button>
|
</Button>
|
||||||
<Link href={onDiscardHref} className={getButtonStyling("primary", "sm")}>
|
<Link href={onDiscardHref} className={getButtonStyling("primary", "base")}>
|
||||||
Go back
|
Go back
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import type { Control } from "react-hook-form";
|
import type { Control } from "react-hook-form";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
|
|
@ -35,7 +41,7 @@ export function ControllerInput(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm text-custom-text-300">{label}</h4>
|
<h4 className="text-13 text-tertiary">{label}</h4>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
|
|
@ -61,7 +67,7 @@ export function ControllerInput(props: Props) {
|
||||||
(showPassword ? (
|
(showPassword ? (
|
||||||
<button
|
<button
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
|
className="absolute top-2.5 right-3 flex items-center justify-center text-placeholder"
|
||||||
onClick={() => setShowPassword(false)}
|
onClick={() => setShowPassword(false)}
|
||||||
>
|
>
|
||||||
<EyeOff className="h-4 w-4" />
|
<EyeOff className="h-4 w-4" />
|
||||||
|
|
@ -69,14 +75,14 @@ export function ControllerInput(props: Props) {
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="absolute right-3 top-2.5 flex items-center justify-center text-custom-text-400"
|
className="absolute top-2.5 right-3 flex items-center justify-center text-placeholder"
|
||||||
onClick={() => setShowPassword(true)}
|
onClick={() => setShowPassword(true)}
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{description && <p className="pt-0.5 text-xs text-custom-text-300">{description}</p>}
|
{description && <p className="pt-0.5 text-11 text-tertiary">{description}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
44
apps/admin/components/common/controller-switch.tsx
Normal file
44
apps/admin/components/common/controller-switch.tsx
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { Control, FieldPath, FieldValues } from "react-hook-form";
|
||||||
|
import { Controller } from "react-hook-form";
|
||||||
|
// plane internal packages
|
||||||
|
import { ToggleSwitch } from "@plane/ui";
|
||||||
|
|
||||||
|
type Props<T extends FieldValues = FieldValues> = {
|
||||||
|
control: Control<T>;
|
||||||
|
field: TControllerSwitchFormField<T>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TControllerSwitchFormField<T extends FieldValues = FieldValues> = {
|
||||||
|
name: FieldPath<T>;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ControllerSwitch<T extends FieldValues>(props: Props<T>) {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
field: { name, label },
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-between gap-1">
|
||||||
|
<h4 className="text-sm text-custom-text-300">Refresh user attributes from {label} during sign in</h4>
|
||||||
|
<div className="relative">
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={name as FieldPath<T>}
|
||||||
|
render={({ field: { value, onChange } }) => {
|
||||||
|
const parsedValue = Number.parseInt(typeof value === "string" ? value : String(value ?? "0"), 10);
|
||||||
|
const isOn = !Number.isNaN(parsedValue) && parsedValue !== 0;
|
||||||
|
return <ToggleSwitch value={isOn} onChange={() => onChange(isOn ? "0" : "1")} size="sm" />;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
// ui
|
// ui
|
||||||
import { Copy } from "lucide-react";
|
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
|
import { CopyIcon } from "@plane/propel/icons";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
|
@ -22,9 +28,10 @@ export function CopyField(props: Props) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<h4 className="text-sm text-custom-text-200">{label}</h4>
|
<h4 className="text-13 text-secondary">{label}</h4>
|
||||||
<Button
|
<Button
|
||||||
variant="neutral-primary"
|
variant="secondary"
|
||||||
|
size="lg"
|
||||||
className="flex items-center justify-between py-2"
|
className="flex items-center justify-between py-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(url);
|
navigator.clipboard.writeText(url);
|
||||||
|
|
@ -35,10 +42,10 @@ export function CopyField(props: Props) {
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<p className="text-sm font-medium">{url}</p>
|
<p className="text-13 font-medium">{url}</p>
|
||||||
<Copy size={18} color="#B9B9B9" />
|
<CopyIcon width={18} height={18} color="#B9B9B9" />
|
||||||
</Button>
|
</Button>
|
||||||
<div className="text-xs text-custom-text-300">{description}</div>
|
<div className="text-11 text-tertiary">{description}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
|
|
||||||
|
|
@ -19,8 +25,8 @@ export function EmptyState({ title, description, image, primaryButton, secondary
|
||||||
<div className={`flex h-full w-full items-center justify-center`}>
|
<div className={`flex h-full w-full items-center justify-center`}>
|
||||||
<div className="flex w-full flex-col items-center text-center">
|
<div className="flex w-full flex-col items-center text-center">
|
||||||
{image && <img src={image} className="w-52 sm:w-60" alt={primaryButton?.text || "button image"} />}
|
{image && <img 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>
|
<h6 className="mt-6 mb-3 text-18 font-semibold sm:mt-8">{title}</h6>
|
||||||
{description && <p className="mb-7 px-5 text-custom-text-300 sm:mb-8">{description}</p>}
|
{description && <p className="mb-7 px-5 text-tertiary sm:mb-8">{description}</p>}
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{primaryButton && (
|
{primaryButton && (
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -28,6 +34,7 @@ export function EmptyState({ title, description, image, primaryButton, secondary
|
||||||
prependIcon={primaryButton.icon}
|
prependIcon={primaryButton.icon}
|
||||||
onClick={primaryButton.onClick}
|
onClick={primaryButton.onClick}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
size="lg"
|
||||||
>
|
>
|
||||||
{primaryButton.text}
|
{primaryButton.text}
|
||||||
</Button>
|
</Button>
|
||||||
19
apps/admin/components/common/header/core.ts
Normal file
19
apps/admin/components/common/header/core.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const CORE_HEADER_SEGMENT_LABELS: Record<string, string> = {
|
||||||
|
general: "General",
|
||||||
|
ai: "Artificial Intelligence",
|
||||||
|
email: "Email",
|
||||||
|
authentication: "Authentication",
|
||||||
|
image: "Image",
|
||||||
|
google: "Google",
|
||||||
|
github: "GitHub",
|
||||||
|
gitlab: "GitLab",
|
||||||
|
gitea: "Gitea",
|
||||||
|
workspace: "Workspace",
|
||||||
|
create: "Create",
|
||||||
|
};
|
||||||
7
apps/admin/components/common/header/extended.ts
Normal file
7
apps/admin/components/common/header/extended.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const EXTENDED_HEADER_SEGMENT_LABELS: Record<string, string> = {};
|
||||||
|
|
@ -1,57 +1,42 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
import { Menu, Settings } from "lucide-react";
|
import { Menu, Settings } from "lucide-react";
|
||||||
// icons
|
// icons
|
||||||
import { Breadcrumbs } from "@plane/ui";
|
import { Breadcrumbs } from "@plane/ui";
|
||||||
// components
|
// components
|
||||||
import { BreadcrumbLink } from "@/components/common/breadcrumb-link";
|
import { BreadcrumbLink } from "../breadcrumb-link";
|
||||||
// hooks
|
// hooks
|
||||||
import { useTheme } from "@/hooks/store";
|
import { useTheme } from "@/hooks/store";
|
||||||
|
// local imports
|
||||||
|
import { CORE_HEADER_SEGMENT_LABELS } from "./core";
|
||||||
|
import { EXTENDED_HEADER_SEGMENT_LABELS } from "./extended";
|
||||||
|
|
||||||
export const HamburgerToggle = observer(function HamburgerToggle() {
|
export const HamburgerToggle = observer(function HamburgerToggle() {
|
||||||
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
const { isSidebarCollapsed, toggleSidebar } = useTheme();
|
||||||
return (
|
return (
|
||||||
<div
|
<button
|
||||||
className="w-7 h-7 rounded flex justify-center items-center bg-custom-background-80 transition-all hover:bg-custom-background-90 cursor-pointer group md:hidden"
|
className="group flex size-7 cursor-pointer items-center justify-center rounded-sm bg-layer-1 transition-all hover:bg-layer-1-hover md:hidden"
|
||||||
onClick={() => toggleSidebar(!isSidebarCollapsed)}
|
onClick={() => toggleSidebar(!isSidebarCollapsed)}
|
||||||
>
|
>
|
||||||
<Menu size={14} className="text-custom-text-200 group-hover:text-custom-text-100 transition-all" />
|
<Menu size={14} className="text-secondary transition-all group-hover:text-primary" />
|
||||||
</div>
|
</button>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const HEADER_SEGMENT_LABELS = {
|
||||||
|
...CORE_HEADER_SEGMENT_LABELS,
|
||||||
|
...EXTENDED_HEADER_SEGMENT_LABELS,
|
||||||
|
};
|
||||||
|
|
||||||
export const AdminHeader = observer(function AdminHeader() {
|
export const AdminHeader = observer(function AdminHeader() {
|
||||||
const pathName = usePathname();
|
const pathName = usePathname();
|
||||||
|
|
||||||
const getHeaderTitle = (pathName: string) => {
|
|
||||||
switch (pathName) {
|
|
||||||
case "general":
|
|
||||||
return "General";
|
|
||||||
case "ai":
|
|
||||||
return "Artificial Intelligence";
|
|
||||||
case "email":
|
|
||||||
return "Email";
|
|
||||||
case "authentication":
|
|
||||||
return "Authentication";
|
|
||||||
case "image":
|
|
||||||
return "Image";
|
|
||||||
case "google":
|
|
||||||
return "Google";
|
|
||||||
case "github":
|
|
||||||
return "GitHub";
|
|
||||||
case "gitlab":
|
|
||||||
return "GitLab";
|
|
||||||
case "gitea":
|
|
||||||
return "Gitea";
|
|
||||||
case "workspace":
|
|
||||||
return "Workspace";
|
|
||||||
case "create":
|
|
||||||
return "Create";
|
|
||||||
default:
|
|
||||||
return pathName.toUpperCase();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Function to dynamically generate breadcrumb items based on pathname
|
// Function to dynamically generate breadcrumb items based on pathname
|
||||||
const generateBreadcrumbItems = (pathname: string) => {
|
const generateBreadcrumbItems = (pathname: string) => {
|
||||||
const pathSegments = pathname.split("/").slice(1); // removing the first empty string.
|
const pathSegments = pathname.split("/").slice(1); // removing the first empty string.
|
||||||
|
|
@ -61,17 +46,17 @@ export const AdminHeader = observer(function AdminHeader() {
|
||||||
const breadcrumbItems = pathSegments.map((segment) => {
|
const breadcrumbItems = pathSegments.map((segment) => {
|
||||||
currentUrl += "/" + segment;
|
currentUrl += "/" + segment;
|
||||||
return {
|
return {
|
||||||
title: getHeaderTitle(segment),
|
title: HEADER_SEGMENT_LABELS[segment] ?? segment.toUpperCase(),
|
||||||
href: currentUrl,
|
href: currentUrl,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return breadcrumbItems;
|
return breadcrumbItems;
|
||||||
};
|
};
|
||||||
|
|
||||||
const breadcrumbItems = generateBreadcrumbItems(pathName);
|
const breadcrumbItems = generateBreadcrumbItems(pathName || "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex h-header w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-custom-sidebar-border-200 bg-custom-sidebar-background-100 p-4">
|
<div className="relative z-10 flex h-header w-full flex-shrink-0 flex-row items-center justify-between gap-x-2 gap-y-4 border-b border-subtle bg-surface-1 p-4">
|
||||||
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
<div className="flex w-full flex-grow items-center gap-2 overflow-ellipsis whitespace-nowrap">
|
||||||
<HamburgerToggle />
|
<HamburgerToggle />
|
||||||
{breadcrumbItems.length >= 0 && (
|
{breadcrumbItems.length >= 0 && (
|
||||||
|
|
@ -82,7 +67,7 @@ export const AdminHeader = observer(function AdminHeader() {
|
||||||
<BreadcrumbLink
|
<BreadcrumbLink
|
||||||
href="/general/"
|
href="/general/"
|
||||||
label="Settings"
|
label="Settings"
|
||||||
icon={<Settings className="h-4 w-4 text-custom-text-300" />}
|
icon={<Settings className="h-4 w-4 text-tertiary" />}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import LogoSpinnerDark from "@/app/assets/images/logo-spinner-dark.gif?url";
|
import LogoSpinnerDark from "@/app/assets/images/logo-spinner-dark.gif?url";
|
||||||
import LogoSpinnerLight from "@/app/assets/images/logo-spinner-light.gif?url";
|
import LogoSpinnerLight from "@/app/assets/images/logo-spinner-light.gif?url";
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTheme as useNextTheme } from "next-themes";
|
import { useTheme as useNextTheme } from "next-themes";
|
||||||
|
|
@ -18,24 +24,24 @@ export const NewUserPopup = observer(function NewUserPopup() {
|
||||||
|
|
||||||
if (!isNewUserPopup) return <></>;
|
if (!isNewUserPopup) return <></>;
|
||||||
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="shadow-md absolute right-8 bottom-8 w-96 rounded-lg border border-subtle bg-surface-1 p-6">
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="grow">
|
<div className="grow">
|
||||||
<div className="text-base font-semibold">Create workspace</div>
|
<div className="text-14 font-semibold">Create workspace</div>
|
||||||
<div className="py-2 text-sm font-medium text-custom-text-300">
|
<div className="py-2 text-13 font-medium text-tertiary">
|
||||||
Instance setup done! Welcome to Plane instance portal. Start your journey with by creating your first
|
Instance setup done! Welcome to Plane instance portal. Start your journey with by creating your first
|
||||||
workspace.
|
workspace.
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4 pt-2">
|
<div className="flex items-center gap-4 pt-2">
|
||||||
<Link href="/workspace/create" className={getButtonStyling("primary", "sm")}>
|
<Link href="/workspace/create" className={getButtonStyling("primary", "lg")}>
|
||||||
Create workspace
|
Create workspace
|
||||||
</Link>
|
</Link>
|
||||||
<Button variant="neutral-primary" size="sm" onClick={toggleNewUserPopup}>
|
<Button variant="secondary" size="lg" onClick={toggleNewUserPopup}>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0 flex items-center justify-center">
|
<div className="flex shrink-0 items-center justify-center">
|
||||||
<img
|
<img
|
||||||
src={resolveGeneralTheme(resolvedTheme) === "dark" ? TakeoffIconDark : TakeoffIconLight}
|
src={resolveGeneralTheme(resolvedTheme) === "dark" ? TakeoffIconDark : TakeoffIconLight}
|
||||||
height={80}
|
height={80}
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
type TPageHeader = {
|
type TPageHeader = {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
50
apps/admin/components/common/page-wrapper.tsx
Normal file
50
apps/admin/components/common/page-wrapper.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
// plane imports
|
||||||
|
import { cn } from "@plane/utils";
|
||||||
|
|
||||||
|
type TPageWrapperProps = {
|
||||||
|
children: ReactNode;
|
||||||
|
header?: {
|
||||||
|
title: string;
|
||||||
|
description: string | ReactNode;
|
||||||
|
actions?: ReactNode;
|
||||||
|
};
|
||||||
|
customHeader?: ReactNode;
|
||||||
|
size?: "lg" | "md";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PageWrapper = (props: TPageWrapperProps) => {
|
||||||
|
const { children, header, customHeader, size = "md" } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn("mx-auto h-full w-full space-y-6 py-4", {
|
||||||
|
"max-w-[1000px] md:px-4 2xl:max-w-[1200px]": size === "md",
|
||||||
|
"px-4 lg:px-12": size === "lg",
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{customHeader ? (
|
||||||
|
<div className="mx-4 shrink-0 space-y-1 border-b border-subtle py-4">{customHeader}</div>
|
||||||
|
) : (
|
||||||
|
header && (
|
||||||
|
<div className="mx-4 flex shrink-0 items-center justify-between gap-4 space-y-1 border-b border-subtle py-4">
|
||||||
|
<div className={header.actions ? "flex flex-col gap-1" : "space-y-1"}>
|
||||||
|
<div className="text-h5-semibold text-primary">{header.title}</div>
|
||||||
|
<div className="text-body-sm-regular text-secondary">{header.description}</div>
|
||||||
|
</div>
|
||||||
|
{header.actions && <div className="shrink-0">{header.actions}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<div className="vertical-scrollbar scrollbar-sm flex-grow overflow-hidden overflow-y-scroll px-4 pb-4">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
|
|
@ -18,17 +24,17 @@ export const InstanceFailureView = observer(function InstanceFailureView() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AuthHeader />
|
<AuthHeader />
|
||||||
<div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
|
<div className="mt-10 flex w-full flex-grow flex-col items-center justify-center py-6">
|
||||||
<div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
|
<div className="relative flex w-full max-w-[22.5rem] flex-col gap-6">
|
||||||
<div className="relative flex flex-col justify-center items-center space-y-4">
|
<div className="relative flex flex-col items-center justify-center space-y-4">
|
||||||
<img src={instanceImage} alt="Instance failure illustration" />
|
<img src={instanceImage} alt="Instance failure illustration" />
|
||||||
<h3 className="font-medium text-2xl text-white text-center">Unable to fetch instance details.</h3>
|
<h3 className="text-center text-20 font-medium text-on-color">Unable to fetch instance details.</h3>
|
||||||
<p className="font-medium text-base text-center">
|
<p className="text-center text-14 font-medium">
|
||||||
We were unable to fetch the details of the instance. Fret not, it might just be a connectivity issue.
|
We were unable to fetch the details of the instance. Fret not, it might just be a connectivity issue.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<Button size="md" onClick={handleRetry}>
|
<Button size="lg" onClick={handleRetry}>
|
||||||
Retry
|
Retry
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
14
apps/admin/components/instance/form-header.tsx
Normal file
14
apps/admin/components/instance/form-header.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function FormHeader({ heading, subHeading }: { heading: string; subHeading: string }) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="text-20 leading-7 font-semibold text-primary">{heading}</span>
|
||||||
|
<span className="text-16 leading-7 font-semibold text-placeholder">{subHeading}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
apps/admin/components/instance/instance-not-ready.tsx
Normal file
32
apps/admin/components/instance/instance-not-ready.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { Button } from "@plane/propel/button";
|
||||||
|
// assets
|
||||||
|
import PlaneTakeOffImage from "@/app/assets/images/plane-takeoff.png?url";
|
||||||
|
|
||||||
|
export function InstanceNotReady() {
|
||||||
|
return (
|
||||||
|
<div className="relative container mx-auto flex h-full w-full items-center justify-center px-5">
|
||||||
|
<div className="relative w-auto max-w-2xl space-y-8 py-10">
|
||||||
|
<div className="relative flex flex-col items-center justify-center space-y-4">
|
||||||
|
<h1 className="pb-3 text-24 font-bold">Welcome aboard Plane!</h1>
|
||||||
|
<img src={PlaneTakeOffImage} alt="Plane Logo" />
|
||||||
|
<p className="text-14 font-medium text-placeholder">Get started by setting up your instance and workspace</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Link href={"/setup/?auth_enabled=0"}>
|
||||||
|
<Button size="xl" className="w-full">
|
||||||
|
Get started
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
// assets
|
// assets
|
||||||
import LogoSpinnerDark from "@/app/assets/images/logo-spinner-dark.gif?url";
|
import LogoSpinnerDark from "@/app/assets/images/logo-spinner-dark.gif?url";
|
||||||
|
|
@ -1,3 +1,9 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
// icons
|
// icons
|
||||||
|
|
@ -7,11 +13,11 @@ import { API_BASE_URL, E_PASSWORD_STRENGTH } from "@plane/constants";
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
import { AuthService } from "@plane/services";
|
import { AuthService } from "@plane/services";
|
||||||
import { Checkbox, Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
|
import { Checkbox, Input, PasswordStrengthIndicator, Spinner } from "@plane/ui";
|
||||||
import { getPasswordStrength } from "@plane/utils";
|
import { getPasswordStrength, validatePersonName, validateCompanyName } from "@plane/utils";
|
||||||
// components
|
// components
|
||||||
import { AuthHeader } from "@/app/(all)/(home)/auth-header";
|
import { AuthHeader } from "@/app/(all)/(home)/auth-header";
|
||||||
import { Banner } from "@/components/common/banner";
|
import { Banner } from "../common/banner";
|
||||||
import { FormHeader } from "@/components/instance/form-header";
|
import { FormHeader } from "./form-header";
|
||||||
|
|
||||||
// service initialization
|
// service initialization
|
||||||
const authService = new AuthService();
|
const authService = new AuthService();
|
||||||
|
|
@ -54,13 +60,13 @@ const defaultFromData: TFormData = {
|
||||||
export function InstanceSetupForm() {
|
export function InstanceSetupForm() {
|
||||||
// search params
|
// search params
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const firstNameParam = searchParams.get("first_name") || undefined;
|
const firstNameParam = searchParams?.get("first_name") || undefined;
|
||||||
const lastNameParam = searchParams.get("last_name") || undefined;
|
const lastNameParam = searchParams?.get("last_name") || undefined;
|
||||||
const companyParam = searchParams.get("company") || undefined;
|
const companyParam = searchParams?.get("company") || undefined;
|
||||||
const emailParam = searchParams.get("email") || undefined;
|
const emailParam = searchParams?.get("email") || undefined;
|
||||||
const isTelemetryEnabledParam = (searchParams.get("is_telemetry_enabled") === "True" ? true : false) || true;
|
const isTelemetryEnabledParam = (searchParams?.get("is_telemetry_enabled") === "True" ? true : false) || true;
|
||||||
const errorCode = searchParams.get("error_code") || undefined;
|
const errorCode = searchParams?.get("error_code") || undefined;
|
||||||
const errorMessage = searchParams.get("error_message") || undefined;
|
const errorMessage = searchParams?.get("error_message") || undefined;
|
||||||
// state
|
// state
|
||||||
const [showPassword, setShowPassword] = useState({
|
const [showPassword, setShowPassword] = useState({
|
||||||
password: false,
|
password: false,
|
||||||
|
|
@ -133,8 +139,8 @@ export function InstanceSetupForm() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AuthHeader />
|
<AuthHeader />
|
||||||
<div className="flex flex-col justify-center items-center flex-grow w-full py-6 mt-10">
|
<div className="mt-10 flex w-full flex-grow flex-col items-center justify-center py-6">
|
||||||
<div className="relative flex flex-col gap-6 max-w-[22.5rem] w-full">
|
<div className="relative flex w-full max-w-[22.5rem] flex-col gap-6">
|
||||||
<FormHeader
|
<FormHeader
|
||||||
heading="Setup your Plane Instance"
|
heading="Setup your Plane Instance"
|
||||||
subHeading="Post setup you will be able to manage this Plane instance."
|
subHeading="Post setup you will be able to manage this Plane instance."
|
||||||
|
|
@ -154,48 +160,60 @@ export function InstanceSetupForm() {
|
||||||
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
<input type="hidden" name="csrfmiddlewaretoken" value={csrfToken} />
|
||||||
<input type="hidden" name="is_telemetry_enabled" value={formData.is_telemetry_enabled ? "True" : "False"} />
|
<input type="hidden" name="is_telemetry_enabled" value={formData.is_telemetry_enabled ? "True" : "False"} />
|
||||||
|
|
||||||
<div className="flex flex-col sm:flex-row items-center gap-4">
|
<div className="flex flex-col items-center gap-4 sm:flex-row">
|
||||||
<div className="w-full space-y-1">
|
<div className="w-full space-y-1">
|
||||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="first_name">
|
<label className="text-13 font-medium text-tertiary" htmlFor="first_name">
|
||||||
First name <span className="text-red-500">*</span>
|
First name <span className="text-danger-primary">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||||
id="first_name"
|
id="first_name"
|
||||||
name="first_name"
|
name="first_name"
|
||||||
type="text"
|
type="text"
|
||||||
inputSize="md"
|
inputSize="md"
|
||||||
placeholder="Wilber"
|
placeholder="Wilber"
|
||||||
value={formData.first_name}
|
value={formData.first_name}
|
||||||
onChange={(e) => handleFormChange("first_name", e.target.value)}
|
onChange={(e) => {
|
||||||
autoComplete="on"
|
const validation = validatePersonName(e.target.value);
|
||||||
|
if (validation === true || e.target.value === "") {
|
||||||
|
handleFormChange("first_name", e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoComplete="off"
|
||||||
autoFocus
|
autoFocus
|
||||||
|
maxLength={50}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full space-y-1">
|
<div className="w-full space-y-1">
|
||||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="last_name">
|
<label className="text-13 font-medium text-tertiary" htmlFor="last_name">
|
||||||
Last name <span className="text-red-500">*</span>
|
Last name <span className="text-danger-primary">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||||
id="last_name"
|
id="last_name"
|
||||||
name="last_name"
|
name="last_name"
|
||||||
type="text"
|
type="text"
|
||||||
inputSize="md"
|
inputSize="md"
|
||||||
placeholder="Wright"
|
placeholder="Wright"
|
||||||
value={formData.last_name}
|
value={formData.last_name}
|
||||||
onChange={(e) => handleFormChange("last_name", e.target.value)}
|
onChange={(e) => {
|
||||||
autoComplete="on"
|
const validation = validatePersonName(e.target.value);
|
||||||
|
if (validation === true || e.target.value === "") {
|
||||||
|
handleFormChange("last_name", e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
autoComplete="off"
|
||||||
|
maxLength={50}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full space-y-1">
|
<div className="w-full space-y-1">
|
||||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="email">
|
<label className="text-13 font-medium text-tertiary" htmlFor="email">
|
||||||
Email <span className="text-red-500">*</span>
|
Email <span className="text-danger-primary">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||||
id="email"
|
id="email"
|
||||||
name="email"
|
name="email"
|
||||||
type="email"
|
type="email"
|
||||||
|
|
@ -204,53 +222,59 @@ export function InstanceSetupForm() {
|
||||||
value={formData.email}
|
value={formData.email}
|
||||||
onChange={(e) => handleFormChange("email", e.target.value)}
|
onChange={(e) => handleFormChange("email", e.target.value)}
|
||||||
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL ? true : false}
|
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL ? true : false}
|
||||||
autoComplete="on"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
{errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL && errorData.message && (
|
{errorData.type && errorData.type === EErrorCodes.INVALID_EMAIL && errorData.message && (
|
||||||
<p className="px-1 text-xs text-red-500">{errorData.message}</p>
|
<p className="px-1 text-11 text-danger-primary">{errorData.message}</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full space-y-1">
|
<div className="w-full space-y-1">
|
||||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="company_name">
|
<label className="text-13 font-medium text-tertiary" htmlFor="company_name">
|
||||||
Company name <span className="text-red-500">*</span>
|
Company name <span className="text-danger-primary">*</span>
|
||||||
</label>
|
</label>
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||||
id="company_name"
|
id="company_name"
|
||||||
name="company_name"
|
name="company_name"
|
||||||
type="text"
|
type="text"
|
||||||
inputSize="md"
|
inputSize="md"
|
||||||
placeholder="Company name"
|
placeholder="Company name"
|
||||||
value={formData.company_name}
|
value={formData.company_name}
|
||||||
onChange={(e) => handleFormChange("company_name", e.target.value)}
|
onChange={(e) => {
|
||||||
|
const validation = validateCompanyName(e.target.value, false);
|
||||||
|
if (validation === true || e.target.value === "") {
|
||||||
|
handleFormChange("company_name", e.target.value);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
maxLength={80}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full space-y-1">
|
<div className="w-full space-y-1">
|
||||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="password">
|
<label className="text-13 font-medium text-tertiary" htmlFor="password">
|
||||||
Set a password <span className="text-red-500">*</span>
|
Set a password <span className="text-danger-primary">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
className="w-full border border-custom-border-100 !bg-custom-background-100 placeholder:text-custom-text-400"
|
className="w-full border border-subtle !bg-surface-1 placeholder:text-placeholder"
|
||||||
id="password"
|
id="password"
|
||||||
name="password"
|
name="password"
|
||||||
type={showPassword.password ? "text" : "password"}
|
type={showPassword.password ? "text" : "password"}
|
||||||
inputSize="md"
|
inputSize="md"
|
||||||
placeholder="New password..."
|
placeholder="New password"
|
||||||
value={formData.password}
|
value={formData.password}
|
||||||
onChange={(e) => handleFormChange("password", e.target.value)}
|
onChange={(e) => handleFormChange("password", e.target.value)}
|
||||||
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false}
|
hasError={errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD ? true : false}
|
||||||
onFocus={() => setIsPasswordInputFocused(true)}
|
onFocus={() => setIsPasswordInputFocused(true)}
|
||||||
onBlur={() => setIsPasswordInputFocused(false)}
|
onBlur={() => setIsPasswordInputFocused(false)}
|
||||||
autoComplete="on"
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
{showPassword.password ? (
|
{showPassword.password ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
|
||||||
onClick={() => handleShowPassword("password")}
|
onClick={() => handleShowPassword("password")}
|
||||||
>
|
>
|
||||||
<EyeOff className="h-4 w-4" />
|
<EyeOff className="h-4 w-4" />
|
||||||
|
|
@ -259,7 +283,7 @@ export function InstanceSetupForm() {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
|
||||||
onClick={() => handleShowPassword("password")}
|
onClick={() => handleShowPassword("password")}
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
|
|
@ -267,14 +291,14 @@ export function InstanceSetupForm() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD && errorData.message && (
|
{errorData.type && errorData.type === EErrorCodes.INVALID_PASSWORD && errorData.message && (
|
||||||
<p className="px-1 text-xs text-red-500">{errorData.message}</p>
|
<p className="px-1 text-11 text-danger-primary">{errorData.message}</p>
|
||||||
)}
|
)}
|
||||||
<PasswordStrengthIndicator password={formData.password} isFocused={isPasswordInputFocused} />
|
<PasswordStrengthIndicator password={formData.password} isFocused={isPasswordInputFocused} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="w-full space-y-1">
|
<div className="w-full space-y-1">
|
||||||
<label className="text-sm text-custom-text-300 font-medium" htmlFor="confirm_password">
|
<label className="text-13 font-medium text-tertiary" htmlFor="confirm_password">
|
||||||
Confirm password <span className="text-red-500">*</span>
|
Confirm password <span className="text-danger-primary">*</span>
|
||||||
</label>
|
</label>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -285,15 +309,16 @@ export function InstanceSetupForm() {
|
||||||
value={formData.confirm_password}
|
value={formData.confirm_password}
|
||||||
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
|
onChange={(e) => handleFormChange("confirm_password", e.target.value)}
|
||||||
placeholder="Confirm password"
|
placeholder="Confirm password"
|
||||||
className="w-full border border-custom-border-100 !bg-custom-background-100 pr-12 placeholder:text-custom-text-400"
|
className="w-full border border-subtle !bg-surface-1 pr-12 placeholder:text-placeholder"
|
||||||
onFocus={() => setIsRetryPasswordInputFocused(true)}
|
onFocus={() => setIsRetryPasswordInputFocused(true)}
|
||||||
onBlur={() => setIsRetryPasswordInputFocused(false)}
|
onBlur={() => setIsRetryPasswordInputFocused(false)}
|
||||||
|
autoComplete="new-password"
|
||||||
/>
|
/>
|
||||||
{showPassword.retypePassword ? (
|
{showPassword.retypePassword ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
|
||||||
onClick={() => handleShowPassword("retypePassword")}
|
onClick={() => handleShowPassword("retypePassword")}
|
||||||
>
|
>
|
||||||
<EyeOff className="h-4 w-4" />
|
<EyeOff className="h-4 w-4" />
|
||||||
|
|
@ -302,7 +327,7 @@ export function InstanceSetupForm() {
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
className="absolute right-3 top-3.5 flex items-center justify-center text-custom-text-400"
|
className="absolute top-3.5 right-3 flex items-center justify-center text-placeholder"
|
||||||
onClick={() => handleShowPassword("retypePassword")}
|
onClick={() => handleShowPassword("retypePassword")}
|
||||||
>
|
>
|
||||||
<Eye className="h-4 w-4" />
|
<Eye className="h-4 w-4" />
|
||||||
|
|
@ -311,27 +336,29 @@ export function InstanceSetupForm() {
|
||||||
</div>
|
</div>
|
||||||
{!!formData.confirm_password &&
|
{!!formData.confirm_password &&
|
||||||
formData.password !== formData.confirm_password &&
|
formData.password !== formData.confirm_password &&
|
||||||
renderPasswordMatchError && <span className="text-sm text-red-500">Passwords don{"'"}t match</span>}
|
renderPasswordMatchError && (
|
||||||
|
<span className="text-13 text-danger-primary">Passwords don{"'"}t match</span>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative flex gap-2">
|
<div className="relative flex gap-2">
|
||||||
<div>
|
<div>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
className="w-4 h-4"
|
className="h-4 w-4"
|
||||||
iconClassName="w-3 h-3"
|
iconClassName="w-3 h-3"
|
||||||
id="is_telemetry_enabled"
|
id="is_telemetry_enabled"
|
||||||
onChange={() => handleFormChange("is_telemetry_enabled", !formData.is_telemetry_enabled)}
|
onChange={() => handleFormChange("is_telemetry_enabled", !formData.is_telemetry_enabled)}
|
||||||
checked={formData.is_telemetry_enabled}
|
checked={formData.is_telemetry_enabled}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<label className="text-sm text-custom-text-300 font-medium cursor-pointer" htmlFor="is_telemetry_enabled">
|
<label className="cursor-pointer text-13 font-medium text-tertiary" htmlFor="is_telemetry_enabled">
|
||||||
Allow Plane to anonymously collect usage events.{" "}
|
Allow Plane to anonymously collect usage events.{" "}
|
||||||
<a
|
<a
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
href="https://developers.plane.so/self-hosting/telemetry"
|
href="https://developers.plane.so/self-hosting/telemetry"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="text-sm font-medium text-blue-500 hover:text-blue-600 flex-shrink-0"
|
className="text-blue-500 hover:text-blue-600 flex-shrink-0 text-13 font-medium"
|
||||||
>
|
>
|
||||||
See More
|
See More
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -339,7 +366,7 @@ export function InstanceSetupForm() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="py-2">
|
<div className="py-2">
|
||||||
<Button type="submit" size="lg" className="w-full" disabled={isButtonDisabled}>
|
<Button type="submit" size="xl" className="w-full" disabled={isButtonDisabled}>
|
||||||
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
|
{isSubmitting ? <Spinner height="20px" width="20px" /> : "Continue"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) 2023-present Plane Software, Inc. and contributors
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
* See the LICENSE file for details.
|
||||||
|
*/
|
||||||
|
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { ExternalLink } from "lucide-react";
|
|
||||||
// plane internal packages
|
// plane internal packages
|
||||||
import { WEB_BASE_URL } from "@plane/constants";
|
import { WEB_BASE_URL } from "@plane/constants";
|
||||||
|
import { NewTabIcon } from "@plane/propel/icons";
|
||||||
import { Tooltip } from "@plane/propel/tooltip";
|
import { Tooltip } from "@plane/propel/tooltip";
|
||||||
import { getFileURL } from "@plane/utils";
|
import { getFileURL } from "@plane/utils";
|
||||||
// hooks
|
// hooks
|
||||||
|
|
@ -23,19 +30,19 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
|
||||||
key={workspaceId}
|
key={workspaceId}
|
||||||
href={`${WEB_BASE_URL}/${encodeURIComponent(workspace.slug)}`}
|
href={`${WEB_BASE_URL}/${encodeURIComponent(workspace.slug)}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="group flex items-center justify-between p-4 gap-2.5 truncate border border-custom-border-200/70 hover:border-custom-border-200 hover:bg-custom-background-90 rounded-md"
|
className="group flex items-center justify-between gap-2.5 truncate rounded-lg border border-subtle bg-layer-1 p-3 hover:border-subtle-1 hover:bg-layer-1-hover hover:shadow-raised-100"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
>
|
>
|
||||||
<div className="flex items-start gap-4">
|
<div className="flex items-start gap-4">
|
||||||
<span
|
<span
|
||||||
className={`relative flex h-8 w-8 flex-shrink-0 items-center justify-center p-2 mt-1 text-xs uppercase ${
|
className={`relative mt-1 flex h-8 w-8 flex-shrink-0 items-center justify-center p-2 text-11 uppercase ${
|
||||||
!workspace?.logo_url && "rounded bg-custom-primary-500 text-white"
|
!workspace?.logo_url && "rounded-lg bg-accent-primary text-on-color"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{workspace?.logo_url && workspace.logo_url !== "" ? (
|
{workspace?.logo_url && workspace.logo_url !== "" ? (
|
||||||
<img
|
<img
|
||||||
src={getFileURL(workspace.logo_url)}
|
src={getFileURL(workspace.logo_url)}
|
||||||
className="absolute left-0 top-0 h-full w-full rounded object-cover"
|
className="absolute top-0 left-0 h-full w-full rounded-sm object-cover"
|
||||||
alt="Workspace Logo"
|
alt="Workspace Logo"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -43,31 +50,31 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-col items-start gap-1">
|
<div className="flex flex-col items-start gap-1">
|
||||||
<div className="flex flex-wrap w-full items-center gap-2.5">
|
<div className="flex w-full flex-wrap items-center gap-2.5">
|
||||||
<h3 className={`text-base font-medium capitalize`}>{workspace.name}</h3>/
|
<h3 className={`text-14 font-medium capitalize`}>{workspace.name}</h3>/
|
||||||
<Tooltip tooltipContent="The unique URL of your workspace">
|
<Tooltip tooltipContent="The unique URL of your workspace">
|
||||||
<h4 className="text-sm text-custom-text-300">[{workspace.slug}]</h4>
|
<h4 className="text-13 text-tertiary">[{workspace.slug}]</h4>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
{workspace.owner.email && (
|
{workspace.owner.email && (
|
||||||
<div className="flex items-center gap-1 text-xs">
|
<div className="flex items-center gap-1 text-11">
|
||||||
<h3 className="text-custom-text-200 font-medium">Owned by:</h3>
|
<h3 className="font-medium text-secondary">Owned by:</h3>
|
||||||
<h4 className="text-custom-text-300">{workspace.owner.email}</h4>
|
<h4 className="text-tertiary">{workspace.owner.email}</h4>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-2.5 text-xs">
|
<div className="flex items-center gap-2.5 text-11">
|
||||||
{workspace.total_projects !== null && (
|
{workspace.total_projects !== null && (
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<h3 className="text-custom-text-200 font-medium">Total projects:</h3>
|
<h3 className="font-medium text-secondary">Total projects:</h3>
|
||||||
<h4 className="text-custom-text-300">{workspace.total_projects}</h4>
|
<h4 className="text-tertiary">{workspace.total_projects}</h4>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{workspace.total_members !== null && (
|
{workspace.total_members !== null && (
|
||||||
<>
|
<>
|
||||||
•
|
•
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<h3 className="text-custom-text-200 font-medium">Total members:</h3>
|
<h3 className="font-medium text-secondary">Total members:</h3>
|
||||||
<h4 className="text-custom-text-300">{workspace.total_members}</h4>
|
<h4 className="text-tertiary">{workspace.total_members}</h4>
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
@ -75,7 +82,7 @@ export const WorkspaceListItem = observer(function WorkspaceListItem({ workspace
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<ExternalLink size={14} className="text-custom-text-400 group-hover:text-custom-text-200" />
|
<NewTabIcon width={14} height={16} className="text-placeholder group-hover:text-secondary" />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import { AlertCircle, CheckCircle2 } from "lucide-react";
|
|
||||||
|
|
||||||
type TBanner = {
|
|
||||||
type: "success" | "error";
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Banner(props: TBanner) {
|
|
||||||
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="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>
|
|
||||||
) : (
|
|
||||||
<CheckCircle2 className="h-5 w-5 text-green-600" 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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
export function FormHeader({ heading, subHeading }: { heading: string; subHeading: string }) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<span className="text-2xl font-semibold text-custom-text-100 leading-7">{heading}</span>
|
|
||||||
<span className="text-lg font-semibold text-custom-text-400 leading-7">{subHeading}</span>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue