Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9aa92da633 |
@@ -139,7 +139,8 @@ jobs:
|
||||
branch_build_push_admin:
|
||||
name: Build-Push Admin Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [branch_build_setup]
|
||||
needs:
|
||||
- branch_build_setup
|
||||
steps:
|
||||
- name: Admin Build and Push
|
||||
uses: makeplane/actions/build-push@v1.0.0
|
||||
@@ -152,7 +153,9 @@ jobs:
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_admin }}
|
||||
build-context: .
|
||||
dockerfile-path: ./apps/admin/Dockerfile.admin
|
||||
dockerfile-path: ./Dockerfile.node
|
||||
build-args: |
|
||||
APP_SCOPE=admin
|
||||
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
@@ -161,7 +164,8 @@ jobs:
|
||||
branch_build_push_web:
|
||||
name: Build-Push Web Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [branch_build_setup]
|
||||
needs:
|
||||
- branch_build_setup
|
||||
steps:
|
||||
- name: Web Build and Push
|
||||
uses: makeplane/actions/build-push@v1.0.0
|
||||
@@ -174,7 +178,9 @@ jobs:
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_web }}
|
||||
build-context: .
|
||||
dockerfile-path: ./apps/web/Dockerfile.web
|
||||
dockerfile-path: ./Dockerfile.node
|
||||
build-args: |
|
||||
APP_SCOPE=web
|
||||
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
@@ -183,7 +189,8 @@ jobs:
|
||||
branch_build_push_space:
|
||||
name: Build-Push Space Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [branch_build_setup]
|
||||
needs:
|
||||
- branch_build_setup
|
||||
steps:
|
||||
- name: Space Build and Push
|
||||
uses: makeplane/actions/build-push@v1.0.0
|
||||
@@ -196,7 +203,9 @@ jobs:
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_space }}
|
||||
build-context: .
|
||||
dockerfile-path: ./apps/space/Dockerfile.space
|
||||
dockerfile-path: ./Dockerfile.node
|
||||
build-args: |
|
||||
APP_SCOPE=space
|
||||
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
@@ -205,7 +214,8 @@ jobs:
|
||||
branch_build_push_live:
|
||||
name: Build-Push Live Collaboration Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [branch_build_setup]
|
||||
needs:
|
||||
- branch_build_setup
|
||||
steps:
|
||||
- name: Live Build and Push
|
||||
uses: makeplane/actions/build-push@v1.0.0
|
||||
@@ -227,7 +237,8 @@ jobs:
|
||||
branch_build_push_api:
|
||||
name: Build-Push API Server Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [branch_build_setup]
|
||||
needs:
|
||||
- branch_build_setup
|
||||
steps:
|
||||
- name: Backend Build and Push
|
||||
uses: makeplane/actions/build-push@v1.0.0
|
||||
@@ -239,8 +250,8 @@ jobs:
|
||||
dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
docker-image-owner: makeplane
|
||||
docker-image-name: ${{ needs.branch_build_setup.outputs.dh_img_backend }}
|
||||
build-context: ./apps/api
|
||||
dockerfile-path: ./apps/api/Dockerfile.api
|
||||
build-context: .
|
||||
dockerfile-path: ./Dockerfile.api
|
||||
buildx-driver: ${{ needs.branch_build_setup.outputs.gh_buildx_driver }}
|
||||
buildx-version: ${{ needs.branch_build_setup.outputs.gh_buildx_version }}
|
||||
buildx-platforms: ${{ needs.branch_build_setup.outputs.gh_buildx_platforms }}
|
||||
@@ -249,7 +260,8 @@ jobs:
|
||||
branch_build_push_proxy:
|
||||
name: Build-Push Proxy Docker Image
|
||||
runs-on: ubuntu-22.04
|
||||
needs: [branch_build_setup]
|
||||
needs:
|
||||
- branch_build_setup
|
||||
steps:
|
||||
- name: Proxy Build and Push
|
||||
uses: makeplane/actions/build-push@v1.0.0
|
||||
@@ -368,15 +380,13 @@ jobs:
|
||||
name: Build Release
|
||||
runs-on: ubuntu-22.04
|
||||
needs:
|
||||
[
|
||||
branch_build_setup,
|
||||
branch_build_push_admin,
|
||||
branch_build_push_web,
|
||||
branch_build_push_space,
|
||||
branch_build_push_live,
|
||||
branch_build_push_api,
|
||||
branch_build_push_proxy,
|
||||
]
|
||||
- branch_build_setup
|
||||
- branch_build_push_admin
|
||||
- branch_build_push_web
|
||||
- branch_build_push_space
|
||||
- branch_build_push_live
|
||||
- branch_build_push_api
|
||||
- branch_build_push_proxy
|
||||
env:
|
||||
REL_VERSION: ${{ needs.branch_build_setup.outputs.release_version }}
|
||||
steps:
|
||||
|
||||
@@ -17,6 +17,8 @@ jobs:
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '18'
|
||||
|
||||
- name: Get PR Branch version
|
||||
run: echo "PR_VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_ENV
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
name: Docker AIO build and smoke test
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- "preview"
|
||||
paths:
|
||||
- "apps/web/**"
|
||||
- "apps/space/**"
|
||||
- "apps/admin/**"
|
||||
- "apps/live/**"
|
||||
- "packages/**"
|
||||
- "turbo.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "Dockerfile.node"
|
||||
- "Dockerfile.api"
|
||||
- "Dockerfile.aio"
|
||||
- "docker-bake.hcl"
|
||||
- ".github/workflows/docker-smoke-aio.yml"
|
||||
push:
|
||||
branches:
|
||||
- "preview"
|
||||
paths:
|
||||
- "apps/web/**"
|
||||
- "apps/space/**"
|
||||
- "apps/admin/**"
|
||||
- "apps/live/**"
|
||||
- "packages/**"
|
||||
- "turbo.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "Dockerfile.node"
|
||||
- "Dockerfile.api"
|
||||
- "Dockerfile.aio"
|
||||
- "docker-bake.hcl"
|
||||
- ".github/workflows/docker-smoke-aio.yml"
|
||||
|
||||
concurrency:
|
||||
group: aio-${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
determine-aio:
|
||||
name: Determine if AIO needed
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
aio_needed: ${{ steps.build-flag.outputs.aio_needed }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Detect changed paths
|
||||
id: changes
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
web:
|
||||
- 'apps/web/**'
|
||||
space:
|
||||
- 'apps/space/**'
|
||||
admin:
|
||||
- 'apps/admin/**'
|
||||
live:
|
||||
- 'apps/live/**'
|
||||
common:
|
||||
- 'packages/**'
|
||||
- 'turbo.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- 'Dockerfile.node'
|
||||
- 'Dockerfile.api'
|
||||
- 'Dockerfile.aio'
|
||||
- 'docker-bake.hcl'
|
||||
- '.github/workflows/docker-smoke-aio.yml'
|
||||
|
||||
- name: Compute AIO flag
|
||||
id: build-flag
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const anyCommon = '${{ steps.changes.outputs.common }}' === 'true';
|
||||
const changedWeb = '${{ steps.changes.outputs.web }}' === 'true';
|
||||
const changedSpace = '${{ steps.changes.outputs.space }}' === 'true';
|
||||
const changedAdmin = '${{ steps.changes.outputs.admin }}' === 'true';
|
||||
const changedLive = '${{ steps.changes.outputs.live }}' === 'true';
|
||||
const aioNeeded = anyCommon || changedWeb || changedSpace || changedAdmin || changedLive;
|
||||
core.setOutput('aio_needed', String(aioNeeded));
|
||||
|
||||
aio_smoke:
|
||||
name: Build and smoke test AIO
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine-aio
|
||||
if: ${{ needs.determine-aio.outputs.aio_needed == 'true' }}
|
||||
timeout-minutes: 30
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Show Docker version
|
||||
run: |
|
||||
docker version
|
||||
docker info
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build AIO image with bake (load into local daemon)
|
||||
run: |
|
||||
docker buildx bake -f "./docker-bake.hcl" --load aio
|
||||
|
||||
- name: Run AIO smoke script
|
||||
run: |
|
||||
chmod +x "scripts/smoke-aio.sh"
|
||||
"scripts/smoke-aio.sh"
|
||||
@@ -0,0 +1,171 @@
|
||||
name: Docker build and smoke test for apps
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- "preview"
|
||||
paths:
|
||||
- "apps/web/**"
|
||||
- "apps/space/**"
|
||||
- "apps/admin/**"
|
||||
- "apps/live/**"
|
||||
- "packages/**"
|
||||
- "turbo.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "Dockerfile.node"
|
||||
- "Dockerfile.api"
|
||||
- "Dockerfile.aio"
|
||||
- "docker-bake.hcl"
|
||||
|
||||
- ".github/workflows/docker-smoke.yml"
|
||||
push:
|
||||
branches:
|
||||
- "preview"
|
||||
paths:
|
||||
- "apps/web/**"
|
||||
- "apps/space/**"
|
||||
- "apps/admin/**"
|
||||
- "apps/live/**"
|
||||
- "packages/**"
|
||||
- "turbo.json"
|
||||
- "pnpm-lock.yaml"
|
||||
- "pnpm-workspace.yaml"
|
||||
- "Dockerfile.node"
|
||||
- "Dockerfile.api"
|
||||
- "Dockerfile.aio"
|
||||
- "docker-bake.hcl"
|
||||
|
||||
- ".github/workflows/docker-smoke.yml"
|
||||
|
||||
jobs:
|
||||
determine-matrix:
|
||||
name: Determine matrix
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.build-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Detect changed paths
|
||||
id: changes
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
web:
|
||||
- 'apps/web/**'
|
||||
space:
|
||||
- 'apps/space/**'
|
||||
admin:
|
||||
- 'apps/admin/**'
|
||||
live:
|
||||
- 'apps/live/**'
|
||||
common:
|
||||
- 'packages/**'
|
||||
- 'turbo.json'
|
||||
- 'pnpm-lock.yaml'
|
||||
- 'pnpm-workspace.yaml'
|
||||
- 'Dockerfile.node'
|
||||
- 'Dockerfile.api'
|
||||
- 'Dockerfile.aio'
|
||||
- 'docker-bake.hcl'
|
||||
- '.github/workflows/docker-smoke.yml'
|
||||
|
||||
- name: Build matrix
|
||||
id: build-matrix
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const include = [];
|
||||
const anyCommon = '${{ steps.changes.outputs.common }}' === 'true';
|
||||
const changed = {
|
||||
web: '${{ steps.changes.outputs.web }}' === 'true',
|
||||
space: '${{ steps.changes.outputs.space }}' === 'true',
|
||||
admin: '${{ steps.changes.outputs.admin }}' === 'true',
|
||||
live: '${{ steps.changes.outputs.live }}' === 'true',
|
||||
};
|
||||
const add = (name, bake_target, image, container, port, path, env_flags = "") =>
|
||||
include.push({ name, bake_target, image, container, host_port: port, path, env_flags });
|
||||
|
||||
const buildAll = anyCommon || changed.web || changed.space || changed.admin || changed.live;
|
||||
if (buildAll || changed.web) add('web', 'web', 'plane-web:ci-smoke', 'plane-web-ci', 3001, '/');
|
||||
if (buildAll || changed.space) add('space', 'space', 'plane-space:ci-smoke', 'plane-space-ci', 3002, '/spaces');
|
||||
if (buildAll || changed.admin) add('admin', 'admin', 'plane-admin:ci-smoke', 'plane-admin-ci', 3003, '/god-mode');
|
||||
if (buildAll || changed.live) add('live', 'live', 'plane-live:ci-smoke', 'plane-live-ci', 3005, '/live/health', '-e NODE_ENV=production -e LIVE_BASE_PATH=/live');
|
||||
|
||||
if (include.length === 0) {
|
||||
// Default to web to keep job non-empty
|
||||
add('web', 'web', 'plane-web:ci-smoke', 'plane-web-ci', 3001, '/');
|
||||
}
|
||||
core.setOutput('matrix', JSON.stringify({ include }));
|
||||
|
||||
smoke:
|
||||
name: Build and smoke test ${{ matrix.name }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: determine-matrix
|
||||
timeout-minutes: 25
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJSON(needs.determine-matrix.outputs.matrix) }}
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Prepare build environment
|
||||
run: echo "Using docker build (no buildx bake)"
|
||||
|
||||
- name: Show Docker version
|
||||
run: |
|
||||
docker version
|
||||
docker info
|
||||
|
||||
- name: Build image (${{ matrix.name }})
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
name="${{ matrix.name }}"
|
||||
tag="${{ matrix.image }}"
|
||||
if [ "$name" = "live" ]; then
|
||||
docker build -f "apps/live/Dockerfile.live" -t "$tag" "."
|
||||
else
|
||||
docker build -f "Dockerfile.node" --target runtime --build-arg APP_SCOPE="$name" -t "$tag" "."
|
||||
fi
|
||||
|
||||
- name: Run container (${{ matrix.name }})
|
||||
run: |
|
||||
docker run -d --name ${{ matrix.container }} -p ${{ matrix.host_port }}:3000 ${{ matrix.env_flags }} ${{ matrix.image }}
|
||||
docker ps -a
|
||||
|
||||
- name: Smoke test HTTP endpoint (${{ matrix.name }})
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
URL="http://localhost:${{ matrix.host_port }}${{ matrix.path }}"
|
||||
echo "Probing $URL ..."
|
||||
for i in {1..60}; do
|
||||
STATUS="$(curl -sS -o /dev/null -w "%{http_code}" -L "${URL}" || true)"
|
||||
if [ "${STATUS}" = "200" ]; then
|
||||
echo "Success: HTTP ${STATUS} from ${URL}"
|
||||
exit 0
|
||||
fi
|
||||
echo "Attempt ${i}: HTTP ${STATUS} (waiting 2s)"
|
||||
sleep 2
|
||||
done
|
||||
echo "Failed to get HTTP 200 from ${URL}"
|
||||
echo "::group::Container logs (${{ matrix.container }})"
|
||||
docker logs ${{ matrix.container }} || true
|
||||
echo "::endgroup::"
|
||||
exit 1
|
||||
|
||||
- name: Cleanup container (${{ matrix.name }})
|
||||
if: always()
|
||||
run: |
|
||||
docker rm -f ${{ matrix.container }} || true
|
||||
@@ -36,6 +36,8 @@ jobs:
|
||||
|
||||
- name: Set up Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version-file: ".nvmrc"
|
||||
|
||||
- name: Enable Corepack and pnpm
|
||||
run: corepack enable pnpm
|
||||
|
||||
@@ -14,7 +14,7 @@ strict-peer-dependencies=false
|
||||
# Turbo occasionally performs postinstall tasks for optimal performance
|
||||
# moved to pnpm-workspace.yaml: onlyBuiltDependencies (e.g., allow turbo)
|
||||
|
||||
public-hoist-pattern[]=*eslint*
|
||||
public-hoist-pattern[]=eslint
|
||||
public-hoist-pattern[]=prettier
|
||||
public-hoist-pattern[]=typescript
|
||||
|
||||
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# All-in-one assembler image that composes Plane runtime from per-app images
|
||||
# and the community supervisor/Caddy configuration.
|
||||
#
|
||||
# Build requirements:
|
||||
# - Build and tag the per-app images first (or override args below):
|
||||
# plane-web:latest, plane-space:latest, plane-admin:latest, plane-live:latest,
|
||||
# plane-api:latest, plane-proxy:latest
|
||||
#
|
||||
# Example:
|
||||
# docker build \
|
||||
# -f plane/Dockerfile.aio \
|
||||
# --build-arg WEB_IMG=plane-web:latest \
|
||||
# --build-arg SPACE_IMG=plane-space:latest \
|
||||
# --build-arg ADMIN_IMG=plane-admin:latest \
|
||||
# --build-arg LIVE_IMG=plane-live:latest \
|
||||
# --build-arg API_IMG=plane-api:latest \
|
||||
# --build-arg PROXY_IMG=plane-proxy:latest \
|
||||
# -t plane-aio:latest .
|
||||
#
|
||||
# Run:
|
||||
# docker run --rm -it -p 80:80 plane-aio:latest
|
||||
#
|
||||
# Provide required env vars; see deployments/aio/community/README.md.
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Arguments to reference locally-built component images
|
||||
# ------------------------------------------------------------------------------
|
||||
# Build contexts are used for component images:
|
||||
# --build-context web_ctx=target:web
|
||||
# --build-context space_ctx=target:space
|
||||
# --build-context admin_ctx=target:admin
|
||||
# --build-context live_ctx=target:live
|
||||
# --build-context api_ctx=target:api
|
||||
# --build-context proxy_ctx=target:proxy
|
||||
ARG NODE_VERSION=22-alpine
|
||||
ARG PY_VERSION=3.12.10-alpine
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Source stages: pull artifacts from pre-built images
|
||||
# ------------------------------------------------------------------------------
|
||||
FROM node:${NODE_VERSION} AS node
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Final runner: Python as base (Supervisor + API deps live here)
|
||||
# ------------------------------------------------------------------------------
|
||||
FROM python:${PY_VERSION} AS runner
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# Base system libs for API/Caddy/Node runtime
|
||||
RUN apk add --no-cache \
|
||||
libpq \
|
||||
libxslt \
|
||||
xmlsec \
|
||||
nss-tools \
|
||||
bash \
|
||||
curl \
|
||||
ca-certificates \
|
||||
openssl
|
||||
|
||||
# Install supervisor
|
||||
RUN pip install --no-cache-dir supervisor && mkdir -p /etc/supervisor/conf.d
|
||||
|
||||
# Copy Node runtime into the AIO container for Next standalone apps
|
||||
COPY --from=node /usr/lib /usr/lib
|
||||
COPY --from=node /usr/local/lib /usr/local/lib
|
||||
COPY --from=node /usr/local/include /usr/local/include
|
||||
COPY --from=node /usr/local/bin /usr/local/bin
|
||||
|
||||
# Copy Next.js standalone app artifacts (namespaced under /app/<app>)
|
||||
COPY --from=web_ctx /app /app/web
|
||||
COPY --from=space_ctx /app /app/space
|
||||
COPY --from=admin_ctx /app /app/admin
|
||||
COPY --from=live_ctx /app /app/live
|
||||
|
||||
# Clean potential Next caches (optional)
|
||||
RUN rm -rf /app/web/apps/web/.next/cache || true \
|
||||
&& rm -rf /app/space/apps/space/.next/cache || true \
|
||||
&& rm -rf /app/admin/apps/admin/.next/cache || true
|
||||
|
||||
# Copy Python backend code and installed packages/binaries
|
||||
COPY --from=api_ctx /code /app/backend
|
||||
# Match CPython version path (PY_VERSION=3.12.x-alpine)
|
||||
COPY --from=api_ctx /usr/local/lib/python3.12/site-packages/ /usr/local/lib/python3.12/site-packages/
|
||||
COPY --from=api_ctx /usr/local/bin/ /usr/local/bin/
|
||||
|
||||
# Caddy binary from proxy image
|
||||
COPY --from=proxy_ctx /usr/bin/caddy /usr/bin/caddy
|
||||
|
||||
# Community supervisor config and startup script
|
||||
COPY deployments/aio/community/start.sh /app/start.sh
|
||||
COPY deployments/aio/community/supervisor.conf /etc/supervisor/conf.d/supervisor.conf
|
||||
|
||||
# Provide a default plane.env from variables template; start.sh will update it
|
||||
COPY deployments/aio/community/variables.env /app/plane.env
|
||||
|
||||
# Prepare Caddy configuration:
|
||||
# - Start from the repository Caddyfile.ce
|
||||
# - Update upstreams to localhost-bound ports managed by supervisor
|
||||
RUN mkdir -p /app/proxy
|
||||
COPY apps/proxy/Caddyfile.ce /app/proxy/Caddyfile
|
||||
RUN set -eux; \
|
||||
sed -i 's|web:3000|localhost:3001|g' /app/proxy/Caddyfile; \
|
||||
sed -i 's|space:3000|localhost:3002|g' /app/proxy/Caddyfile; \
|
||||
sed -i 's|admin:3000|localhost:3003|g' /app/proxy/Caddyfile; \
|
||||
sed -i 's|api:8000|localhost:3004|g' /app/proxy/Caddyfile; \
|
||||
sed -i 's|live:3000|localhost:3005|g' /app/proxy/Caddyfile; \
|
||||
sed -i '/plane-minio:9000/d' /app/proxy/Caddyfile
|
||||
|
||||
# Folders and permissions
|
||||
RUN mkdir -p /app/logs/access /app/logs/error /app/data \
|
||||
&& chmod +x /app/start.sh
|
||||
|
||||
VOLUME ["/app/data", "/app/logs"]
|
||||
|
||||
EXPOSE 80 443
|
||||
|
||||
CMD ["/app/start.sh"]
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# Unified multi-stage Dockerfile for the Python API
|
||||
# Targets:
|
||||
# - dev: local development with hot reload and full build toolchain
|
||||
# - runtime: production runtime using prebuilt wheels
|
||||
#
|
||||
# Example builds (from repo root):
|
||||
# docker build -f plane/Dockerfile.api --target dev -t plane-api:dev .
|
||||
# docker build -f plane/Dockerfile.api --target runtime -t plane-api:latest .
|
||||
|
||||
ARG PY_VERSION=python:3.12.10-alpine
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# base: common runtime base (no build toolchain)
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM ${PY_VERSION} AS base
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||
INSTANCE_CHANGELOG_URL=https://sites.plane.so/pages/691ef037bcfe416a902e48cb55f59891/
|
||||
|
||||
# Runtime libraries required by the app
|
||||
RUN apk add --no-cache \
|
||||
libpq \
|
||||
libxslt \
|
||||
xmlsec \
|
||||
ca-certificates \
|
||||
openssl
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# builder: build Python wheels for all dependencies
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM ${PY_VERSION} AS builder
|
||||
|
||||
# Full build toolchain to compile deps to wheels
|
||||
RUN apk add --no-cache \
|
||||
bash~=5.2 \
|
||||
g++ \
|
||||
gcc \
|
||||
cargo \
|
||||
git \
|
||||
make \
|
||||
postgresql-dev \
|
||||
libc-dev \
|
||||
linux-headers \
|
||||
libffi-dev \
|
||||
libxml2-dev \
|
||||
libxslt-dev \
|
||||
openssl-dev \
|
||||
xmlsec-dev
|
||||
|
||||
WORKDIR /w
|
||||
|
||||
# Copy requirements (relative to repo root)
|
||||
COPY apps/api/requirements.txt /w/requirements.txt
|
||||
COPY apps/api/requirements /w/requirements
|
||||
|
||||
# Build wheels into /wheels to reuse in runtime
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
pip wheel -r /w/requirements.txt --wheel-dir /wheels
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# dev: local development image (bind mount source into /code)
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM ${PY_VERSION} AS dev
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_DISABLE_PIP_VERSION_CHECK=1 \
|
||||
INSTANCE_CHANGELOG_URL=https://sites.plane.so/pages/691ef037bcfe416a902e48cb55f59891/
|
||||
|
||||
# Match prior dev environment: runtime + build deps + node
|
||||
RUN apk add --no-cache \
|
||||
libpq \
|
||||
libxslt \
|
||||
xmlsec \
|
||||
nodejs-current \
|
||||
bash~=5.2 \
|
||||
g++ \
|
||||
gcc \
|
||||
cargo \
|
||||
git \
|
||||
make \
|
||||
postgresql-dev \
|
||||
libc-dev \
|
||||
linux-headers \
|
||||
libffi-dev
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
# Copy and install local dev requirements
|
||||
COPY apps/api/requirements.txt ./requirements.txt
|
||||
COPY apps/api/requirements ./requirements
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
pip install -r requirements/local.txt --no-cache-dir
|
||||
|
||||
# Bring in the API source
|
||||
COPY apps/api/ ./
|
||||
|
||||
# Permissions similar to existing Dockerfiles
|
||||
RUN mkdir -p /code/plane/logs \
|
||||
&& chmod -R +x /code/bin \
|
||||
&& chmod -R 777 /code
|
||||
|
||||
EXPOSE 8000
|
||||
CMD ["./bin/docker-entrypoint-api-local.sh"]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# runtime: production image using wheels from builder
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM base AS runtime
|
||||
|
||||
# Bash needed for entrypoint scripts
|
||||
RUN apk add --no-cache bash~=5.2
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
# Install from wheels for reproducible, fast builds
|
||||
COPY --from=builder /wheels /wheels
|
||||
COPY apps/api/requirements.txt ./requirements.txt
|
||||
COPY apps/api/requirements ./requirements
|
||||
RUN --mount=type=cache,target=/root/.cache/pip \
|
||||
pip install --no-cache-dir --no-index --find-links=/wheels -r requirements.txt
|
||||
|
||||
# Copy only what is required to run
|
||||
COPY apps/api/manage.py ./manage.py
|
||||
COPY apps/api/plane ./plane
|
||||
COPY apps/api/templates ./templates
|
||||
COPY apps/api/package.json ./package.json
|
||||
COPY apps/api/bin ./bin
|
||||
|
||||
# Create unprivileged user and set secure permissions
|
||||
RUN addgroup -S plane && adduser -S -G plane -h /code -s /sbin/nologin plane \
|
||||
&& chmod +x ./bin/* \
|
||||
&& mkdir -p /code/plane/logs \
|
||||
&& chown -R plane:plane /code \
|
||||
&& chmod -R 755 /code \
|
||||
&& chmod 775 /code/plane/logs
|
||||
|
||||
ENV GUNICORN_WORKERS=3 \
|
||||
PORT=8000
|
||||
|
||||
USER plane
|
||||
EXPOSE 8000
|
||||
|
||||
# Default: API server; override command for worker/beat/migrator as needed
|
||||
CMD ["./bin/docker-entrypoint-api.sh"]
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
#
|
||||
# Unified multi-stage Dockerfile for Next.js apps in this monorepo.
|
||||
# Supports:
|
||||
# - target=dev (hot-reload, expects bind-mounted repo)
|
||||
# - target=runtime (production image using Next.js standalone output)
|
||||
#
|
||||
# Usage examples:
|
||||
# - Build dev image for web: docker build --target dev --build-arg APP_SCOPE=web -t plane-web:dev .
|
||||
# - Run dev (with compose): mount the repo into /repo and override command/ports as needed
|
||||
# - Build prod image for web: docker build --target runtime --build-arg APP_SCOPE=web -t plane-web:latest .
|
||||
#
|
||||
# APP_SCOPE must be one of: web, space, admin
|
||||
|
||||
ARG NODE_VERSION=22-alpine
|
||||
ARG TURBO_VERSION=2.5.6
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Base: Node + pnpm (corepack)
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM node:${NODE_VERSION} AS base
|
||||
|
||||
ENV PNPM_HOME=/pnpm
|
||||
ENV PATH=$PNPM_HOME:$PATH
|
||||
RUN corepack enable && apk add --no-cache libc6-compat
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# builder: prune workspace using turbo for the selected app scope
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM base AS builder
|
||||
ARG APP_SCOPE
|
||||
WORKDIR /repo
|
||||
|
||||
# Full context is required for turbo prune
|
||||
COPY . .
|
||||
RUN pnpm add -g turbo@${TURBO_VERSION}
|
||||
RUN turbo prune --scope=${APP_SCOPE} --docker
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# installer: install deps (offline) and build the selected scope
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM base AS installer
|
||||
WORKDIR /repo
|
||||
|
||||
# Seed minimal files for reproducible installs and good caching
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /repo/out/json/ .
|
||||
COPY --from=builder /repo/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
|
||||
# Fetch dependencies into a cached store layer
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
|
||||
pnpm fetch --store-dir=/pnpm/store
|
||||
|
||||
# Bring in only the pruned workspace for fast installs/builds
|
||||
COPY --from=builder /repo/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
|
||||
# Offline, frozen lockfile install from cached store
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \
|
||||
pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store
|
||||
|
||||
# Build-time environment (safe to pass through; only NEXT_PUBLIC_* are embedded)
|
||||
# Keep parity with existing app Dockerfiles
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
# Build only the selected scope
|
||||
ARG APP_SCOPE
|
||||
RUN pnpm turbo run build --filter=${APP_SCOPE}
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# dev: for local development with hot reload (bind-mount the repo to /repo)
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM base AS dev
|
||||
WORKDIR /repo
|
||||
|
||||
# Helpful global tool for selective builds/dev
|
||||
RUN pnpm add -g turbo@${TURBO_VERSION}
|
||||
|
||||
ENV NODE_ENV=development \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
# Select which app to run in dev (web|space|admin)
|
||||
ARG APP_SCOPE
|
||||
ENV APP_SCOPE=${APP_SCOPE}
|
||||
|
||||
EXPOSE 3000
|
||||
# Expect the source to be bind-mounted; install deps and start dev server
|
||||
CMD ["sh", "-lc", "pnpm install && pnpm dev --filter=${APP_SCOPE}"]
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# runtime: minimal Next.js standalone runner for the selected scope
|
||||
# -----------------------------------------------------------------------------
|
||||
FROM node:${NODE_VERSION} AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
NEXT_TELEMETRY_DISABLED=1 \
|
||||
TURBO_TELEMETRY_DISABLED=1 \
|
||||
HOSTNAME=0.0.0.0 \
|
||||
PORT=3000
|
||||
|
||||
# Drop privileges
|
||||
RUN addgroup -S -g 1001 nodejs \
|
||||
&& adduser -S -u 1001 -G nodejs -h /home/nextjs -D nextjs \
|
||||
&& mkdir -p /home/nextjs \
|
||||
&& chown -R nextjs:nodejs /home/nextjs
|
||||
USER nextjs
|
||||
|
||||
# Copy only the built output for the selected scope
|
||||
ARG APP_SCOPE
|
||||
ENV APP_SCOPE=${APP_SCOPE}
|
||||
# Next.js standalone layout
|
||||
COPY --from=installer /repo/apps/${APP_SCOPE}/.next/standalone ./
|
||||
COPY --from=installer /repo/apps/${APP_SCOPE}/.next/static ./apps/${APP_SCOPE}/.next/static
|
||||
COPY --from=installer /repo/apps/${APP_SCOPE}/public ./apps/${APP_SCOPE}/public
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["sh", "-lc", "set -e; : \"${APP_SCOPE:?APP_SCOPE env is required}\"; SERVER=\"apps/${APP_SCOPE}/server.js\"; if [ ! -f \"$SERVER\" ]; then echo \"Error: $SERVER not found.\"; ls -la \"apps/${APP_SCOPE}\" || true; echo \"Known apps:\"; ls -1 apps || true; exit 1; fi; exec node \"$SERVER\""]
|
||||
@@ -1,12 +0,0 @@
|
||||
.next/*
|
||||
out/*
|
||||
public/*
|
||||
dist/*
|
||||
node_modules/*
|
||||
.turbo/*
|
||||
.env*
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.test
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/next.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
# Setup pnpm package manager with corepack and configure global bin directory for caching
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 1: Build the project
|
||||
# *****************************************************************************
|
||||
FROM base AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
ARG TURBO_VERSION=2.5.6
|
||||
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune --scope=admin --docker
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 2: Install dependencies & build the project
|
||||
# *****************************************************************************
|
||||
FROM base AS installer
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
RUN corepack enable pnpm
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store
|
||||
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN pnpm turbo run build --filter=admin
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 3: Copy the project and start it
|
||||
# *****************************************************************************
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# Don't run production as root
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
USER nextjs
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=installer /app/apps/admin/.next/standalone ./
|
||||
COPY --from=installer /app/apps/admin/.next/static ./apps/admin/.next/static
|
||||
COPY --from=installer /app/apps/admin/public ./apps/admin/public
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "apps/admin/server.js"]
|
||||
@@ -1,17 +0,0 @@
|
||||
FROM node:22-alpine
|
||||
RUN apk add --no-cache libc6-compat
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN corepack enable pnpm && pnpm add -g turbo
|
||||
RUN pnpm install
|
||||
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
VOLUME [ "/app/node_modules", "/app/admin/node_modules" ]
|
||||
|
||||
CMD ["pnpm", "dev", "--filter=admin"]
|
||||
@@ -9,7 +9,7 @@ import { useInstance } from "@/hooks/store";
|
||||
// components
|
||||
import { InstanceEmailForm } from "./email-config-form";
|
||||
|
||||
const InstanceEmailPage: React.FC = observer(() => {
|
||||
const InstanceEmailPage = observer(() => {
|
||||
// store
|
||||
const { fetchInstanceConfigurations, formattedConfig, disableEmail } = useInstance();
|
||||
|
||||
@@ -29,7 +29,7 @@ const InstanceEmailPage: React.FC = observer(() => {
|
||||
message: "Email feature has been disabled",
|
||||
type: TOAST_TYPE.SUCCESS,
|
||||
});
|
||||
} catch (_error) {
|
||||
} catch (error) {
|
||||
setToast({
|
||||
title: "Error disabling email",
|
||||
message: "Failed to disable email feature. Please try again.",
|
||||
|
||||
@@ -7,8 +7,7 @@ import { ExternalLink, FileText, HelpCircle, MoveLeft } from "lucide-react";
|
||||
import { Transition } from "@headlessui/react";
|
||||
// plane internal packages
|
||||
import { WEB_BASE_URL } from "@plane/constants";
|
||||
import { DiscordIcon, GithubIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { DiscordIcon, GithubIcon, Tooltip } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks/store";
|
||||
|
||||
@@ -5,8 +5,7 @@ import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Image, BrainCog, Cog, Lock, Mail } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { WorkspaceIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { Tooltip, WorkspaceIcon } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// hooks
|
||||
import { useTheme } from "@/hooks/store";
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { PlaneLockup } from "@plane/propel/icons";
|
||||
import { PlaneLockup } from "@plane/ui";
|
||||
|
||||
export const AuthHeader = () => (
|
||||
<div className="flex items-center justify-between gap-6 w-full flex-shrink-0 sticky top-0">
|
||||
|
||||
@@ -25,8 +25,9 @@ export const EmailCodesConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableMagicLogin))}
|
||||
onChange={() => {
|
||||
const newEnableMagicLogin = Boolean(parseInt(enableMagicLogin)) === true ? "0" : "1";
|
||||
updateConfig("ENABLE_MAGIC_LINK_LOGIN", newEnableMagicLogin);
|
||||
Boolean(parseInt(enableMagicLogin)) === true
|
||||
? updateConfig("ENABLE_MAGIC_LINK_LOGIN", "0")
|
||||
: updateConfig("ENABLE_MAGIC_LINK_LOGIN", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -35,8 +35,9 @@ export const GithubConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGithubConfig))}
|
||||
onChange={() => {
|
||||
const newEnableGithubConfig = Boolean(parseInt(enableGithubConfig)) === true ? "0" : "1";
|
||||
updateConfig("IS_GITHUB_ENABLED", newEnableGithubConfig);
|
||||
Boolean(parseInt(enableGithubConfig)) === true
|
||||
? updateConfig("IS_GITHUB_ENABLED", "0")
|
||||
: updateConfig("IS_GITHUB_ENABLED", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -35,8 +35,9 @@ export const GitlabConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGitlabConfig))}
|
||||
onChange={() => {
|
||||
const newEnableGitlabConfig = Boolean(parseInt(enableGitlabConfig)) === true ? "0" : "1";
|
||||
updateConfig("IS_GITLAB_ENABLED", newEnableGitlabConfig);
|
||||
Boolean(parseInt(enableGitlabConfig)) === true
|
||||
? updateConfig("IS_GITLAB_ENABLED", "0")
|
||||
: updateConfig("IS_GITLAB_ENABLED", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -35,8 +35,9 @@ export const GoogleConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableGoogleConfig))}
|
||||
onChange={() => {
|
||||
const newEnableGoogleConfig = Boolean(parseInt(enableGoogleConfig)) === true ? "0" : "1";
|
||||
updateConfig("IS_GOOGLE_ENABLED", newEnableGoogleConfig);
|
||||
Boolean(parseInt(enableGoogleConfig)) === true
|
||||
? updateConfig("IS_GOOGLE_ENABLED", "0")
|
||||
: updateConfig("IS_GOOGLE_ENABLED", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -25,8 +25,9 @@ export const PasswordLoginConfiguration: React.FC<Props> = observer((props) => {
|
||||
<ToggleSwitch
|
||||
value={Boolean(parseInt(enableEmailPassword))}
|
||||
onChange={() => {
|
||||
const newEnableEmailPassword = Boolean(parseInt(enableEmailPassword)) === true ? "0" : "1";
|
||||
updateConfig("ENABLE_EMAIL_PASSWORD", newEnableEmailPassword);
|
||||
Boolean(parseInt(enableEmailPassword)) === true
|
||||
? updateConfig("ENABLE_EMAIL_PASSWORD", "0")
|
||||
: updateConfig("ENABLE_EMAIL_PASSWORD", "1");
|
||||
}}
|
||||
size="sm"
|
||||
disabled={disabled}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
label?: string;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { observer } from "mobx-react";
|
||||
import { ExternalLink } from "lucide-react";
|
||||
// plane internal packages
|
||||
import { WEB_BASE_URL } from "@plane/constants";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { getFileURL } from "@plane/utils";
|
||||
// hooks
|
||||
import { useWorkspace } from "@/hooks/store";
|
||||
|
||||
@@ -209,7 +209,7 @@ export class InstanceStore implements IInstanceStore {
|
||||
});
|
||||
});
|
||||
await this.instanceService.disableEmail();
|
||||
} catch (_error) {
|
||||
} catch (error) {
|
||||
console.error("Error disabling the email");
|
||||
this.instanceConfigurations = instanceConfigurations;
|
||||
}
|
||||
|
||||
+16
-16
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"description": "Admin UI for Plane",
|
||||
"version": "1.0.0",
|
||||
"version": "0.28.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -26,30 +26,30 @@
|
||||
"@plane/ui": "workspace:*",
|
||||
"@plane/utils": "workspace:*",
|
||||
"autoprefixer": "10.4.14",
|
||||
"axios": "catalog:",
|
||||
"lodash": "catalog:",
|
||||
"lucide-react": "catalog:",
|
||||
"mobx": "catalog:",
|
||||
"mobx-react": "catalog:",
|
||||
"next": "catalog:",
|
||||
"axios": "1.11.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.469.0",
|
||||
"mobx": "^6.12.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"next": "14.2.30",
|
||||
"next-themes": "^0.2.1",
|
||||
"postcss": "^8.4.49",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-hook-form": "7.51.5",
|
||||
"sharp": "catalog:",
|
||||
"swr": "catalog:",
|
||||
"uuid": "catalog:"
|
||||
"sharp": "^0.33.5",
|
||||
"swr": "^2.2.4",
|
||||
"uuid": "^9.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/lodash": "catalog:",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "18.16.1",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/uuid": "^9.0.8",
|
||||
"typescript": "catalog:"
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
FROM python:3.12.10-alpine
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE=1
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
ENV INSTANCE_CHANGELOG_URL=https://sites.plane.so/pages/691ef037bcfe416a902e48cb55f59891/
|
||||
|
||||
# Update system packages for security
|
||||
RUN apk update && apk upgrade
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
RUN apk add --no-cache --upgrade \
|
||||
"libpq" \
|
||||
"libxslt" \
|
||||
"xmlsec" \
|
||||
"ca-certificates" \
|
||||
"openssl"
|
||||
|
||||
COPY requirements.txt ./
|
||||
COPY requirements ./requirements
|
||||
RUN apk add --no-cache libffi-dev
|
||||
RUN apk add --no-cache --virtual .build-deps \
|
||||
"bash~=5.2" \
|
||||
"g++" \
|
||||
"gcc" \
|
||||
"cargo" \
|
||||
"git" \
|
||||
"make" \
|
||||
"postgresql-dev" \
|
||||
"libc-dev" \
|
||||
"linux-headers" \
|
||||
&& \
|
||||
pip install -r requirements.txt --compile --no-cache-dir \
|
||||
&& \
|
||||
apk del .build-deps \
|
||||
&& \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
|
||||
# Add in Django deps and generate Django's static files
|
||||
COPY manage.py manage.py
|
||||
COPY plane plane/
|
||||
COPY templates templates/
|
||||
COPY package.json package.json
|
||||
|
||||
RUN apk --no-cache add "bash~=5.2"
|
||||
COPY ./bin ./bin/
|
||||
|
||||
RUN mkdir -p /code/plane/logs
|
||||
RUN chmod +x ./bin/*
|
||||
RUN chmod -R 777 /code
|
||||
|
||||
# Expose container port and run entry point script
|
||||
EXPOSE 8000
|
||||
|
||||
CMD ["./bin/docker-entrypoint-api.sh"]
|
||||
@@ -1,46 +0,0 @@
|
||||
FROM python:3.12.5-alpine AS backend
|
||||
|
||||
# set environment variables
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
ENV INSTANCE_CHANGELOG_URL https://sites.plane.so/pages/691ef037bcfe416a902e48cb55f59891/
|
||||
|
||||
RUN apk --no-cache add \
|
||||
"bash~=5.2" \
|
||||
"libpq" \
|
||||
"libxslt" \
|
||||
"nodejs-current" \
|
||||
"xmlsec" \
|
||||
"libffi-dev" \
|
||||
"bash~=5.2" \
|
||||
"g++" \
|
||||
"gcc" \
|
||||
"cargo" \
|
||||
"git" \
|
||||
"make" \
|
||||
"postgresql-dev" \
|
||||
"libc-dev" \
|
||||
"linux-headers"
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
COPY requirements.txt ./requirements.txt
|
||||
ADD requirements ./requirements
|
||||
|
||||
# Install the local development settings
|
||||
RUN pip install -r requirements/local.txt --compile --no-cache-dir
|
||||
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN mkdir -p /code/plane/logs
|
||||
RUN chmod -R +x /code/bin
|
||||
RUN chmod -R 777 /code
|
||||
|
||||
|
||||
# Expose container port and run entry point script
|
||||
EXPOSE 8000
|
||||
|
||||
CMD [ "./bin/docker-entrypoint-api-local.sh" ]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plane-api",
|
||||
"version": "1.0.0",
|
||||
"version": "0.28.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"description": "API server powering Plane's backend"
|
||||
|
||||
@@ -91,7 +91,6 @@ class BaseSerializer(serializers.ModelSerializer):
|
||||
"project_lead": UserLiteSerializer,
|
||||
"state": StateLiteSerializer,
|
||||
"created_by": UserLiteSerializer,
|
||||
"updated_by": UserLiteSerializer,
|
||||
"issue": IssueSerializer,
|
||||
"actor": UserLiteSerializer,
|
||||
"owned_by": UserLiteSerializer,
|
||||
|
||||
@@ -39,31 +39,13 @@ def allow_permission(allowed_roles, level="PROJECT", creator=False, model=None):
|
||||
).exists():
|
||||
return view_func(instance, request, *args, **kwargs)
|
||||
else:
|
||||
is_user_has_allowed_role = ProjectMember.objects.filter(
|
||||
if ProjectMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=kwargs["slug"],
|
||||
project_id=kwargs["project_id"],
|
||||
role__in=allowed_role_values,
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
# Return if the user has the allowed role else if they are workspace admin and part of the project regardless of the role
|
||||
if is_user_has_allowed_role:
|
||||
return view_func(instance, request, *args, **kwargs)
|
||||
elif (
|
||||
ProjectMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=kwargs["slug"],
|
||||
project_id=kwargs["project_id"],
|
||||
is_active=True,
|
||||
).exists()
|
||||
and WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=kwargs["slug"],
|
||||
role=ROLE.ADMIN.value,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
).exists():
|
||||
return view_func(instance, request, *args, **kwargs)
|
||||
|
||||
# Return permission denied if no conditions are met
|
||||
|
||||
@@ -3,7 +3,11 @@ from rest_framework.permissions import SAFE_METHODS, BasePermission
|
||||
|
||||
# Module import
|
||||
from plane.db.models import ProjectMember, WorkspaceMember
|
||||
from plane.db.models.project import ROLE
|
||||
|
||||
# Permission Mappings
|
||||
Admin = 20
|
||||
Member = 15
|
||||
Guest = 5
|
||||
|
||||
|
||||
class ProjectBasePermission(BasePermission):
|
||||
@@ -22,31 +26,18 @@ class ProjectBasePermission(BasePermission):
|
||||
return WorkspaceMember.objects.filter(
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
|
||||
role__in=[Admin, Member],
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
project_member_qs = ProjectMember.objects.filter(
|
||||
## Only Project Admins can update project attributes
|
||||
return ProjectMember.objects.filter(
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
role=Admin,
|
||||
project_id=view.project_id,
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
## Only project admins or workspace admin who is part of the project can access
|
||||
|
||||
if project_member_qs.filter(role=ROLE.ADMIN.value).exists():
|
||||
return True
|
||||
else:
|
||||
return (
|
||||
project_member_qs.exists()
|
||||
and WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=view.workspace_slug,
|
||||
role=ROLE.ADMIN.value,
|
||||
is_active=True,
|
||||
).exists()
|
||||
)
|
||||
).exists()
|
||||
|
||||
|
||||
class ProjectMemberPermission(BasePermission):
|
||||
@@ -64,7 +55,7 @@ class ProjectMemberPermission(BasePermission):
|
||||
return WorkspaceMember.objects.filter(
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
|
||||
role__in=[Admin, Member],
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
@@ -72,7 +63,7 @@ class ProjectMemberPermission(BasePermission):
|
||||
return ProjectMember.objects.filter(
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
|
||||
role__in=[Admin, Member],
|
||||
project_id=view.project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
@@ -106,7 +97,7 @@ class ProjectEntityPermission(BasePermission):
|
||||
return ProjectMember.objects.filter(
|
||||
workspace__slug=view.workspace_slug,
|
||||
member=request.user,
|
||||
role__in=[ROLE.ADMIN.value, ROLE.MEMBER.value],
|
||||
role__in=[Admin, Member],
|
||||
project_id=view.project_id,
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
@@ -667,33 +667,16 @@ class IssueReactionSerializer(BaseSerializer):
|
||||
|
||||
|
||||
class IssueReactionLiteSerializer(DynamicBaseSerializer):
|
||||
display_name = serializers.CharField(source="actor.display_name", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = IssueReaction
|
||||
fields = ["id", "actor", "issue", "reaction", "display_name"]
|
||||
fields = ["id", "actor", "issue", "reaction"]
|
||||
|
||||
|
||||
class CommentReactionSerializer(BaseSerializer):
|
||||
display_name = serializers.CharField(source="actor.display_name", read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = CommentReaction
|
||||
fields = [
|
||||
"id",
|
||||
"actor",
|
||||
"comment",
|
||||
"reaction",
|
||||
"display_name",
|
||||
"deleted_at",
|
||||
"workspace",
|
||||
"project",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
]
|
||||
read_only_fields = ["workspace", "project", "comment", "actor", "deleted_at", "created_by", "updated_by"]
|
||||
fields = "__all__"
|
||||
read_only_fields = ["workspace", "project", "comment", "actor", "deleted_at"]
|
||||
|
||||
|
||||
class IssueVoteSerializer(BaseSerializer):
|
||||
|
||||
@@ -15,6 +15,7 @@ from plane.db.models import (
|
||||
)
|
||||
from plane.utils.content_validator import (
|
||||
validate_html_content,
|
||||
validate_binary_data,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -441,11 +441,7 @@ class WorkspaceFileAssetEndpoint(BaseAPIView):
|
||||
# Get the presigned URL
|
||||
storage = S3Storage(request=request)
|
||||
# Generate a presigned URL to share an S3 object
|
||||
signed_url = storage.generate_presigned_url(
|
||||
object_name=asset.asset.name,
|
||||
disposition="attachment",
|
||||
filename=asset.attributes.get("name"),
|
||||
)
|
||||
signed_url = storage.generate_presigned_url(object_name=asset.asset.name)
|
||||
# Redirect to the signed URL
|
||||
return HttpResponseRedirect(signed_url)
|
||||
|
||||
@@ -645,11 +641,7 @@ class ProjectAssetEndpoint(BaseAPIView):
|
||||
# Get the presigned URL
|
||||
storage = S3Storage(request=request)
|
||||
# Generate a presigned URL to share an S3 object
|
||||
signed_url = storage.generate_presigned_url(
|
||||
object_name=asset.asset.name,
|
||||
disposition="attachment",
|
||||
filename=asset.attributes.get("name"),
|
||||
)
|
||||
signed_url = storage.generate_presigned_url(object_name=asset.asset.name)
|
||||
# Redirect to the signed URL
|
||||
return HttpResponseRedirect(signed_url)
|
||||
|
||||
|
||||
@@ -356,9 +356,9 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
is_active=True,
|
||||
)
|
||||
# Only project members admins and created_by users can access this endpoint
|
||||
if (project_member and project_member.role <= 5) and str(
|
||||
intake_issue.created_by_id
|
||||
) != str(request.user.id):
|
||||
if project_member.role <= 5 and str(intake_issue.created_by_id) != str(
|
||||
request.user.id
|
||||
):
|
||||
return Response(
|
||||
{"error": "You cannot edit intake issues"},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
@@ -392,7 +392,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
),
|
||||
).get(pk=intake_issue.issue_id, workspace__slug=slug, project_id=project_id)
|
||||
# Only allow guests to edit name and description
|
||||
if project_member and project_member.role <= 5:
|
||||
if project_member.role <= 5:
|
||||
issue_data = {
|
||||
"name": issue_data.get("name", issue.name),
|
||||
"description_html": issue_data.get(
|
||||
@@ -437,7 +437,7 @@ class IntakeIssueViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
# Only project admins and members can edit intake issue attributes
|
||||
if project_member and project_member.role > 15:
|
||||
if project_member.role > 15:
|
||||
serializer = IntakeIssueSerializer(
|
||||
intake_issue, data=request.data, partial=True
|
||||
)
|
||||
|
||||
@@ -198,7 +198,6 @@ class PageViewSet(BaseViewSet):
|
||||
def retrieve(self, request, slug, project_id, pk=None):
|
||||
page = self.get_queryset().filter(pk=pk).first()
|
||||
project = Project.objects.get(pk=project_id)
|
||||
track_visit = request.query_params.get("track_visit", "true").lower() == "true"
|
||||
|
||||
"""
|
||||
if the role is guest and guest_view_all_features is false and owned by is not
|
||||
@@ -231,14 +230,13 @@ class PageViewSet(BaseViewSet):
|
||||
).values_list("entity_identifier", flat=True)
|
||||
data = PageDetailSerializer(page).data
|
||||
data["issue_ids"] = issue_ids
|
||||
if track_visit:
|
||||
recent_visited_task.delay(
|
||||
slug=slug,
|
||||
entity_name="page",
|
||||
entity_identifier=pk,
|
||||
user_id=request.user.id,
|
||||
project_id=project_id,
|
||||
)
|
||||
recent_visited_task.delay(
|
||||
slug=slug,
|
||||
entity_name="page",
|
||||
entity_identifier=pk,
|
||||
user_id=request.user.id,
|
||||
project_id=project_id,
|
||||
)
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
@allow_permission([ROLE.ADMIN], model=Page, creator=True)
|
||||
|
||||
@@ -5,12 +5,13 @@ from django.utils import timezone
|
||||
import json
|
||||
|
||||
# Django imports
|
||||
from django.db import IntegrityError
|
||||
from django.db.models import Exists, F, OuterRef, Prefetch, Q, Subquery
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
|
||||
# Third Party imports
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
from rest_framework import serializers, status
|
||||
from rest_framework.permissions import AllowAny
|
||||
|
||||
# Module imports
|
||||
@@ -105,10 +106,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
fields = [field for field in request.GET.get("fields", "").split(",") if field]
|
||||
projects = self.get_queryset().order_by("sort_order", "name")
|
||||
if WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
role=ROLE.GUEST.value,
|
||||
member=request.user, workspace__slug=slug, is_active=True, role=5
|
||||
).exists():
|
||||
projects = projects.filter(
|
||||
project_projectmember__member=self.request.user,
|
||||
@@ -116,10 +114,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
if WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
role=ROLE.MEMBER.value,
|
||||
member=request.user, workspace__slug=slug, is_active=True, role=15
|
||||
).exists():
|
||||
projects = projects.filter(
|
||||
Q(
|
||||
@@ -194,10 +189,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
if WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
role=ROLE.GUEST.value,
|
||||
member=request.user, workspace__slug=slug, is_active=True, role=5
|
||||
).exists():
|
||||
projects = projects.filter(
|
||||
project_projectmember__member=self.request.user,
|
||||
@@ -205,10 +197,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
)
|
||||
|
||||
if WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
role=ROLE.MEMBER.value,
|
||||
member=request.user, workspace__slug=slug, is_active=True, role=15
|
||||
).exists():
|
||||
projects = projects.filter(
|
||||
Q(
|
||||
@@ -261,9 +250,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
|
||||
# Add the user as Administrator to the project
|
||||
_ = ProjectMember.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
member=request.user,
|
||||
role=ROLE.ADMIN.value,
|
||||
project_id=serializer.data["id"], member=request.user, role=20
|
||||
)
|
||||
# Also create the issue property for the user
|
||||
_ = IssueUserProperty.objects.create(
|
||||
@@ -276,7 +263,7 @@ class ProjectViewSet(BaseViewSet):
|
||||
ProjectMember.objects.create(
|
||||
project_id=serializer.data["id"],
|
||||
member_id=serializer.data["project_lead"],
|
||||
role=ROLE.ADMIN.value,
|
||||
role=20,
|
||||
)
|
||||
# Also create the issue property for the user
|
||||
IssueUserProperty.objects.create(
|
||||
@@ -354,23 +341,13 @@ class ProjectViewSet(BaseViewSet):
|
||||
|
||||
def partial_update(self, request, slug, pk=None):
|
||||
# try:
|
||||
is_workspace_admin = WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
role=ROLE.ADMIN.value,
|
||||
).exists()
|
||||
|
||||
is_project_admin = ProjectMember.objects.filter(
|
||||
if not ProjectMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
project_id=pk,
|
||||
role=ROLE.ADMIN.value,
|
||||
role=20,
|
||||
is_active=True,
|
||||
).exists()
|
||||
|
||||
# Return error for if the user is neither workspace admin nor project admin
|
||||
if not is_project_admin and not is_workspace_admin:
|
||||
).exists():
|
||||
return Response(
|
||||
{"error": "You don't have the required permissions."},
|
||||
status=status.HTTP_403_FORBIDDEN,
|
||||
@@ -425,16 +402,13 @@ class ProjectViewSet(BaseViewSet):
|
||||
def destroy(self, request, slug, pk):
|
||||
if (
|
||||
WorkspaceMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
is_active=True,
|
||||
role=ROLE.ADMIN.value,
|
||||
member=request.user, workspace__slug=slug, is_active=True, role=20
|
||||
).exists()
|
||||
or ProjectMember.objects.filter(
|
||||
member=request.user,
|
||||
workspace__slug=slug,
|
||||
project_id=pk,
|
||||
role=ROLE.ADMIN.value,
|
||||
role=20,
|
||||
is_active=True,
|
||||
).exists()
|
||||
):
|
||||
|
||||
@@ -172,14 +172,12 @@ class WorkspaceDraftIssueViewSet(BaseViewSet):
|
||||
{"error": "Issue not found"}, status=status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
project_id = request.data.get("project_id", issue.project_id)
|
||||
|
||||
serializer = DraftIssueCreateSerializer(
|
||||
issue,
|
||||
data=request.data,
|
||||
partial=True,
|
||||
context={
|
||||
"project_id": project_id,
|
||||
"project_id": request.data.get("project_id", None),
|
||||
"cycle_id": request.data.get("cycle_id", "not_provided"),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -7,6 +7,7 @@ from plane.app.serializers import StateSerializer
|
||||
from plane.app.views.base import BaseAPIView
|
||||
from plane.db.models import State
|
||||
from plane.app.permissions import WorkspaceEntityPermission
|
||||
from plane.utils.cache import cache_response
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
@@ -14,6 +15,7 @@ class WorkspaceStatesEndpoint(BaseAPIView):
|
||||
permission_classes = [WorkspaceEntityPermission]
|
||||
use_read_replica = True
|
||||
|
||||
@cache_response(60 * 60 * 2)
|
||||
def get(self, request, slug):
|
||||
states = State.objects.filter(
|
||||
workspace__slug=slug,
|
||||
|
||||
@@ -107,8 +107,7 @@ class MagicSignInEndpoint(View):
|
||||
# Login the user and record his device info
|
||||
user_login(request=request, user=user, is_app=True)
|
||||
if user.is_password_autoset and profile.is_onboarded:
|
||||
# Redirect to the home page
|
||||
path = "/"
|
||||
path = "accounts/set-password"
|
||||
else:
|
||||
# Get the redirection path
|
||||
path = (
|
||||
|
||||
@@ -67,7 +67,7 @@ def flush_to_mongo_and_delete(
|
||||
mongo_archival_failed = False
|
||||
|
||||
# Try to insert into MongoDB if available
|
||||
if mongo_collection is not None and mongo_available:
|
||||
if mongo_collection and mongo_available:
|
||||
try:
|
||||
mongo_collection.bulk_write([InsertOne(doc) for doc in buffer])
|
||||
except BulkWriteError as bwe:
|
||||
@@ -166,9 +166,9 @@ def process_cleanup_task(
|
||||
def transform_api_log(record: Dict) -> Dict:
|
||||
"""Transform API activity log record."""
|
||||
return {
|
||||
"id": str(record["id"]),
|
||||
"id": record["id"],
|
||||
"created_at": str(record["created_at"]) if record.get("created_at") else None,
|
||||
"token_identifier": str(record["token_identifier"]),
|
||||
"token_identifier": record["token_identifier"],
|
||||
"path": record["path"],
|
||||
"method": record["method"],
|
||||
"query_params": record.get("query_params"),
|
||||
@@ -178,18 +178,18 @@ def transform_api_log(record: Dict) -> Dict:
|
||||
"response_body": record["response_body"],
|
||||
"ip_address": record["ip_address"],
|
||||
"user_agent": record["user_agent"],
|
||||
"created_by_id": str(record["created_by_id"]),
|
||||
"created_by_id": record["created_by_id"],
|
||||
}
|
||||
|
||||
|
||||
def transform_email_log(record: Dict) -> Dict:
|
||||
"""Transform email notification log record."""
|
||||
return {
|
||||
"id": str(record["id"]),
|
||||
"id": record["id"],
|
||||
"created_at": str(record["created_at"]) if record.get("created_at") else None,
|
||||
"receiver_id": str(record["receiver_id"]),
|
||||
"triggered_by_id": str(record["triggered_by_id"]),
|
||||
"entity_identifier": str(record["entity_identifier"]),
|
||||
"receiver_id": record["receiver_id"],
|
||||
"triggered_by_id": record["triggered_by_id"],
|
||||
"entity_identifier": record["entity_identifier"],
|
||||
"entity_name": record["entity_name"],
|
||||
"data": record["data"],
|
||||
"processed_at": (
|
||||
@@ -197,27 +197,27 @@ def transform_email_log(record: Dict) -> Dict:
|
||||
),
|
||||
"sent_at": str(record["sent_at"]) if record.get("sent_at") else None,
|
||||
"entity": record["entity"],
|
||||
"old_value": str(record["old_value"]),
|
||||
"new_value": str(record["new_value"]),
|
||||
"created_by_id": str(record["created_by_id"]),
|
||||
"old_value": record["old_value"],
|
||||
"new_value": record["new_value"],
|
||||
"created_by_id": record["created_by_id"],
|
||||
}
|
||||
|
||||
|
||||
def transform_page_version(record: Dict) -> Dict:
|
||||
"""Transform page version record."""
|
||||
return {
|
||||
"id": str(record["id"]),
|
||||
"id": record["id"],
|
||||
"created_at": str(record["created_at"]) if record.get("created_at") else None,
|
||||
"page_id": str(record["page_id"]),
|
||||
"workspace_id": str(record["workspace_id"]),
|
||||
"owned_by_id": str(record["owned_by_id"]),
|
||||
"page_id": record["page_id"],
|
||||
"workspace_id": record["workspace_id"],
|
||||
"owned_by_id": record["owned_by_id"],
|
||||
"description_html": record["description_html"],
|
||||
"description_binary": record["description_binary"],
|
||||
"description_stripped": record["description_stripped"],
|
||||
"description_json": record["description_json"],
|
||||
"sub_pages_data": record["sub_pages_data"],
|
||||
"created_by_id": str(record["created_by_id"]),
|
||||
"updated_by_id": str(record["updated_by_id"]),
|
||||
"created_by_id": record["created_by_id"],
|
||||
"updated_by_id": record["updated_by_id"],
|
||||
"deleted_at": str(record["deleted_at"]) if record.get("deleted_at") else None,
|
||||
"last_saved_at": (
|
||||
str(record["last_saved_at"]) if record.get("last_saved_at") else None
|
||||
@@ -228,14 +228,14 @@ def transform_page_version(record: Dict) -> Dict:
|
||||
def transform_issue_description_version(record: Dict) -> Dict:
|
||||
"""Transform issue description version record."""
|
||||
return {
|
||||
"id": str(record["id"]),
|
||||
"id": record["id"],
|
||||
"created_at": str(record["created_at"]) if record.get("created_at") else None,
|
||||
"issue_id": str(record["issue_id"]),
|
||||
"workspace_id": str(record["workspace_id"]),
|
||||
"project_id": str(record["project_id"]),
|
||||
"created_by_id": str(record["created_by_id"]),
|
||||
"updated_by_id": str(record["updated_by_id"]),
|
||||
"owned_by_id": str(record["owned_by_id"]),
|
||||
"issue_id": record["issue_id"],
|
||||
"workspace_id": record["workspace_id"],
|
||||
"project_id": record["project_id"],
|
||||
"created_by_id": record["created_by_id"],
|
||||
"updated_by_id": record["updated_by_id"],
|
||||
"owned_by_id": record["owned_by_id"],
|
||||
"last_saved_at": (
|
||||
str(record["last_saved_at"]) if record.get("last_saved_at") else None
|
||||
),
|
||||
|
||||
@@ -30,8 +30,6 @@ def page_version(page_id, existing_instance, user_id):
|
||||
description_binary=page.description_binary,
|
||||
owned_by_id=user_id,
|
||||
last_saved_at=page.updated_at,
|
||||
description_json=page.description,
|
||||
description_stripped=page.description_stripped,
|
||||
)
|
||||
|
||||
# If page versions are greater than 20 delete the oldest one
|
||||
|
||||
@@ -92,10 +92,6 @@ def create_project_and_member(workspace: Workspace) -> Dict[int, uuid.UUID]:
|
||||
name=workspace.name, # Use workspace name
|
||||
identifier=project_identifier,
|
||||
created_by_id=workspace.created_by_id,
|
||||
# Enable all views in seed data
|
||||
cycle_view=True,
|
||||
module_view=True,
|
||||
issue_views_view=True,
|
||||
)
|
||||
|
||||
# Create project members
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# Generated by Django 4.2.22 on 2025-08-29 11:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("db", "0101_description_descriptionversion"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="page",
|
||||
name="sort_order",
|
||||
field=models.FloatField(default=65535),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="pagelog",
|
||||
name="entity_type",
|
||||
field=models.CharField(
|
||||
blank=True, max_length=30, null=True, verbose_name="Entity Type"
|
||||
),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="pagelog",
|
||||
name="entity_identifier",
|
||||
field=models.UUIDField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,75 +0,0 @@
|
||||
# Generated by Django 4.2.22 on 2025-09-01 14:33
|
||||
|
||||
from django.db import migrations, models
|
||||
from django.contrib.postgres.operations import AddIndexConcurrently
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
atomic = False
|
||||
|
||||
dependencies = [
|
||||
('db', '0102_page_sort_order_pagelog_entity_type_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
AddIndexConcurrently(
|
||||
model_name='fileasset',
|
||||
index=models.Index(fields=['entity_type'], name='asset_entity_type_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='fileasset',
|
||||
index=models.Index(fields=['entity_identifier'], name='asset_entity_identifier_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='fileasset',
|
||||
index=models.Index(fields=['entity_type', 'entity_identifier'], name='asset_entity_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='notification',
|
||||
index=models.Index(fields=['entity_identifier'], name='notif_entity_identifier_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='notification',
|
||||
index=models.Index(fields=['entity_name'], name='notif_entity_name_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='notification',
|
||||
index=models.Index(fields=['read_at'], name='notif_read_at_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='notification',
|
||||
index=models.Index(fields=['receiver', 'read_at'], name='notif_entity_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='pagelog',
|
||||
index=models.Index(fields=['entity_type'], name='pagelog_entity_type_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='pagelog',
|
||||
index=models.Index(fields=['entity_identifier'], name='pagelog_entity_id_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='pagelog',
|
||||
index=models.Index(fields=['entity_name'], name='pagelog_entity_name_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='pagelog',
|
||||
index=models.Index(fields=['entity_type', 'entity_identifier'], name='pagelog_type_id_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='pagelog',
|
||||
index=models.Index(fields=['entity_name', 'entity_identifier'], name='pagelog_name_id_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='userfavorite',
|
||||
index=models.Index(fields=['entity_type'], name='fav_entity_type_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='userfavorite',
|
||||
index=models.Index(fields=['entity_identifier'], name='fav_entity_identifier_idx'),
|
||||
),
|
||||
AddIndexConcurrently(
|
||||
model_name='userfavorite',
|
||||
index=models.Index(fields=['entity_type', 'entity_identifier'], name='fav_entity_idx'),
|
||||
),
|
||||
]
|
||||
@@ -1,43 +0,0 @@
|
||||
# Generated by Django 4.2.22 on 2025-09-03 05:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('db', '0103_fileasset_asset_entity_type_idx_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='cycleuserproperties',
|
||||
name='rich_filters',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='exporterhistory',
|
||||
name='rich_filters',
|
||||
field=models.JSONField(blank=True, default=dict, null=True),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issueuserproperty',
|
||||
name='rich_filters',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='issueview',
|
||||
name='rich_filters',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='moduleuserproperties',
|
||||
name='rich_filters',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='workspaceuserproperties',
|
||||
name='rich_filters',
|
||||
field=models.JSONField(default=dict),
|
||||
),
|
||||
]
|
||||
@@ -1,33 +0,0 @@
|
||||
# Generated by Django 4.2.22 on 2025-09-10 09:54
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("db", "0104_cycleuserproperties_rich_filters_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name="project",
|
||||
name="cycle_view",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="project",
|
||||
name="issue_views_view",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="project",
|
||||
name="module_view",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name="session",
|
||||
name="user_id",
|
||||
field=models.CharField(db_index=True, max_length=50, null=True),
|
||||
),
|
||||
]
|
||||
@@ -76,15 +76,6 @@ class FileAsset(BaseModel):
|
||||
verbose_name_plural = "File Assets"
|
||||
db_table = "file_assets"
|
||||
ordering = ("-created_at",)
|
||||
indexes = [
|
||||
models.Index(fields=["entity_type"], name="asset_entity_type_idx"),
|
||||
models.Index(
|
||||
fields=["entity_identifier"], name="asset_entity_identifier_idx"
|
||||
),
|
||||
models.Index(
|
||||
fields=["entity_type", "entity_identifier"], name="asset_entity_idx"
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return str(self.asset)
|
||||
|
||||
@@ -139,7 +139,6 @@ class CycleUserProperties(ProjectBaseModel):
|
||||
filters = models.JSONField(default=get_default_filters)
|
||||
display_filters = models.JSONField(default=get_default_display_filters)
|
||||
display_properties = models.JSONField(default=get_default_display_properties)
|
||||
rich_filters = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["cycle", "user", "deleted_at"]
|
||||
|
||||
@@ -56,7 +56,6 @@ class ExporterHistory(BaseModel):
|
||||
related_name="workspace_exporters",
|
||||
)
|
||||
filters = models.JSONField(blank=True, null=True)
|
||||
rich_filters = models.JSONField(default=dict, blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Exporter"
|
||||
|
||||
@@ -41,15 +41,6 @@ class UserFavorite(WorkspaceBaseModel):
|
||||
verbose_name_plural = "User Favorites"
|
||||
db_table = "user_favorites"
|
||||
ordering = ("-created_at",)
|
||||
indexes = [
|
||||
models.Index(fields=["entity_type"], name="fav_entity_type_idx"),
|
||||
models.Index(
|
||||
fields=["entity_identifier"], name="fav_entity_identifier_idx"
|
||||
),
|
||||
models.Index(
|
||||
fields=["entity_type", "entity_identifier"], name="fav_entity_idx"
|
||||
),
|
||||
]
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self._state.adding:
|
||||
|
||||
@@ -509,7 +509,6 @@ class IssueUserProperty(ProjectBaseModel):
|
||||
filters = models.JSONField(default=get_default_filters)
|
||||
display_filters = models.JSONField(default=get_default_display_filters)
|
||||
display_properties = models.JSONField(default=get_default_display_properties)
|
||||
rich_filters = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Issue User Property"
|
||||
|
||||
@@ -207,7 +207,6 @@ class ModuleUserProperties(ProjectBaseModel):
|
||||
filters = models.JSONField(default=get_default_filters)
|
||||
display_filters = models.JSONField(default=get_default_display_filters)
|
||||
display_properties = models.JSONField(default=get_default_display_properties)
|
||||
rich_filters = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["module", "user", "deleted_at"]
|
||||
|
||||
@@ -39,14 +39,6 @@ class Notification(BaseModel):
|
||||
verbose_name_plural = "Notifications"
|
||||
db_table = "notifications"
|
||||
ordering = ("-created_at",)
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["entity_identifier"], name="notif_entity_identifier_idx"
|
||||
),
|
||||
models.Index(fields=["entity_name"], name="notif_entity_name_idx"),
|
||||
models.Index(fields=["read_at"], name="notif_read_at_idx"),
|
||||
models.Index(fields=["receiver", "read_at"], name="notif_entity_idx"),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
"""Return name of the notifications"""
|
||||
|
||||
@@ -57,7 +57,6 @@ class Page(BaseModel):
|
||||
)
|
||||
moved_to_page = models.UUIDField(null=True, blank=True)
|
||||
moved_to_project = models.UUIDField(null=True, blank=True)
|
||||
sort_order = models.FloatField(default=65535)
|
||||
|
||||
external_id = models.CharField(max_length=255, null=True, blank=True)
|
||||
external_source = models.CharField(max_length=255, null=True, blank=True)
|
||||
@@ -99,11 +98,8 @@ class PageLog(BaseModel):
|
||||
)
|
||||
transaction = models.UUIDField(default=uuid.uuid4)
|
||||
page = models.ForeignKey(Page, related_name="page_log", on_delete=models.CASCADE)
|
||||
entity_identifier = models.UUIDField(null=True, blank=True)
|
||||
entity_identifier = models.UUIDField(null=True)
|
||||
entity_name = models.CharField(max_length=30, verbose_name="Transaction Type")
|
||||
entity_type = models.CharField(
|
||||
max_length=30, verbose_name="Entity Type", null=True, blank=True
|
||||
)
|
||||
workspace = models.ForeignKey(
|
||||
"db.Workspace", on_delete=models.CASCADE, related_name="workspace_page_log"
|
||||
)
|
||||
@@ -114,17 +110,6 @@ class PageLog(BaseModel):
|
||||
verbose_name_plural = "Page Logs"
|
||||
db_table = "page_logs"
|
||||
ordering = ("-created_at",)
|
||||
indexes = [
|
||||
models.Index(fields=["entity_type"], name="pagelog_entity_type_idx"),
|
||||
models.Index(fields=["entity_identifier"], name="pagelog_entity_id_idx"),
|
||||
models.Index(fields=["entity_name"], name="pagelog_entity_name_idx"),
|
||||
models.Index(
|
||||
fields=["entity_type", "entity_identifier"], name="pagelog_type_id_idx"
|
||||
),
|
||||
models.Index(
|
||||
fields=["entity_name", "entity_identifier"], name="pagelog_name_id_idx"
|
||||
),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.page.name} {self.entity_name}"
|
||||
|
||||
@@ -18,12 +18,6 @@ from .base import BaseModel
|
||||
ROLE_CHOICES = ((20, "Admin"), (15, "Member"), (5, "Guest"))
|
||||
|
||||
|
||||
class ROLE(Enum):
|
||||
ADMIN = 20
|
||||
MEMBER = 15
|
||||
GUEST = 5
|
||||
|
||||
|
||||
class ProjectNetwork(Enum):
|
||||
SECRET = 0
|
||||
PUBLIC = 2
|
||||
@@ -95,9 +89,9 @@ class Project(BaseModel):
|
||||
)
|
||||
emoji = models.CharField(max_length=255, null=True, blank=True)
|
||||
icon_prop = models.JSONField(null=True)
|
||||
module_view = models.BooleanField(default=False)
|
||||
cycle_view = models.BooleanField(default=False)
|
||||
issue_views_view = models.BooleanField(default=False)
|
||||
module_view = models.BooleanField(default=True)
|
||||
cycle_view = models.BooleanField(default=True)
|
||||
issue_views_view = models.BooleanField(default=True)
|
||||
page_view = models.BooleanField(default=True)
|
||||
intake_view = models.BooleanField(default=False)
|
||||
is_time_tracking_enabled = models.BooleanField(default=False)
|
||||
|
||||
@@ -13,7 +13,7 @@ VALID_KEY_CHARS = string.ascii_lowercase + string.digits
|
||||
class Session(AbstractBaseSession):
|
||||
device_info = models.JSONField(null=True, blank=True, default=None)
|
||||
session_key = models.CharField(max_length=128, primary_key=True)
|
||||
user_id = models.CharField(null=True, max_length=50, db_index=True)
|
||||
user_id = models.CharField(null=True, max_length=50)
|
||||
|
||||
@classmethod
|
||||
def get_session_store_class(cls):
|
||||
|
||||
@@ -58,7 +58,6 @@ class IssueView(WorkspaceBaseModel):
|
||||
filters = models.JSONField(default=dict)
|
||||
display_filters = models.JSONField(default=get_default_display_filters)
|
||||
display_properties = models.JSONField(default=get_default_display_properties)
|
||||
rich_filters = models.JSONField(default=dict)
|
||||
access = models.PositiveSmallIntegerField(
|
||||
default=1, choices=((0, "Private"), (1, "Public"))
|
||||
)
|
||||
|
||||
@@ -332,7 +332,6 @@ class WorkspaceUserProperties(BaseModel):
|
||||
filters = models.JSONField(default=get_default_filters)
|
||||
display_filters = models.JSONField(default=get_default_display_filters)
|
||||
display_properties = models.JSONField(default=get_default_display_properties)
|
||||
rich_filters = models.JSONField(default=dict)
|
||||
|
||||
class Meta:
|
||||
unique_together = ["workspace", "user", "deleted_at"]
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 4.2.22 on 2025-09-11 08:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("license", "0005_rename_product_instance_edition_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="instance",
|
||||
name="is_current_version_deprecated",
|
||||
field=models.BooleanField(default=False),
|
||||
),
|
||||
]
|
||||
@@ -38,8 +38,6 @@ class Instance(BaseModel):
|
||||
is_signup_screen_visited = models.BooleanField(default=False)
|
||||
is_verified = models.BooleanField(default=False)
|
||||
is_test = models.BooleanField(default=False)
|
||||
# field for validating if the current version is deprecated
|
||||
is_current_version_deprecated = models.BooleanField(default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Instance"
|
||||
|
||||
@@ -465,7 +465,3 @@ if ENABLE_DRF_SPECTACULAR:
|
||||
REST_FRAMEWORK["DEFAULT_SCHEMA_CLASS"] = "drf_spectacular.openapi.AutoSchema"
|
||||
INSTALLED_APPS.append("drf_spectacular")
|
||||
from .openapi import SPECTACULAR_SETTINGS # noqa: F401
|
||||
|
||||
# MongoDB Settings
|
||||
MONGO_DB_URL = os.environ.get("MONGO_DB_URL", False)
|
||||
MONGO_DB_DATABASE = os.environ.get("MONGO_DB_DATABASE", False)
|
||||
|
||||
@@ -118,7 +118,4 @@ class MongoConnection:
|
||||
Returns:
|
||||
bool: True if MongoDB is configured and connected, False otherwise
|
||||
"""
|
||||
|
||||
if cls._client is None:
|
||||
cls._instance = cls()
|
||||
return cls._client is not None and cls._db is not None
|
||||
|
||||
@@ -2,9 +2,6 @@
|
||||
import base64
|
||||
import nh3
|
||||
from plane.utils.exception_logger import log_exception
|
||||
from bs4 import BeautifulSoup
|
||||
from collections import defaultdict
|
||||
|
||||
|
||||
# Maximum allowed size for binary data (10MB)
|
||||
MAX_SIZE = 10 * 1024 * 1024
|
||||
@@ -22,8 +19,7 @@ SUSPICIOUS_BINARY_PATTERNS = [
|
||||
|
||||
def validate_binary_data(data):
|
||||
"""
|
||||
Validate that binary data appears to be a valid document format
|
||||
and doesn't contain malicious content.
|
||||
Validate that binary data appears to be valid document format and doesn't contain malicious content.
|
||||
|
||||
Args:
|
||||
data (bytes or str): The binary data to validate, or base64-encoded string
|
||||
@@ -64,147 +60,6 @@ def validate_binary_data(data):
|
||||
return True, None
|
||||
|
||||
|
||||
# Combine custom components and editor-specific nodes into a single set of tags
|
||||
CUSTOM_TAGS = {
|
||||
# editor node/tag names
|
||||
"mention-component",
|
||||
"label",
|
||||
"input",
|
||||
"image-component",
|
||||
}
|
||||
ALLOWED_TAGS = nh3.ALLOWED_TAGS | CUSTOM_TAGS
|
||||
|
||||
# Merge nh3 defaults with all attributes used across our custom components
|
||||
ATTRIBUTES = {
|
||||
"*": {
|
||||
"class",
|
||||
"id",
|
||||
"title",
|
||||
"role",
|
||||
"aria-label",
|
||||
"aria-hidden",
|
||||
"style",
|
||||
"start",
|
||||
"type",
|
||||
# common editor data-* attributes seen in stored HTML
|
||||
# (wildcards like data-* are NOT supported by nh3; we add known keys
|
||||
# here and dynamically include all data-* seen in the input below)
|
||||
"data-tight",
|
||||
"data-node-type",
|
||||
"data-type",
|
||||
"data-checked",
|
||||
"data-background-color",
|
||||
"data-text-color",
|
||||
"data-name",
|
||||
# callout attributes
|
||||
"data-icon-name",
|
||||
"data-icon-color",
|
||||
"data-background",
|
||||
"data-emoji-unicode",
|
||||
"data-emoji-url",
|
||||
"data-logo-in-use",
|
||||
"data-block-type",
|
||||
},
|
||||
"a": {"href", "target"},
|
||||
# editor node/tag attributes
|
||||
"image-component": {
|
||||
"id",
|
||||
"width",
|
||||
"height",
|
||||
"aspectRatio",
|
||||
"aspectratio",
|
||||
"src",
|
||||
"alignment",
|
||||
},
|
||||
"img": {
|
||||
"width",
|
||||
"height",
|
||||
"aspectRatio",
|
||||
"aspectratio",
|
||||
"alignment",
|
||||
"src",
|
||||
"alt",
|
||||
"title",
|
||||
},
|
||||
"mention-component": {"id", "entity_identifier", "entity_name"},
|
||||
"th": {
|
||||
"colspan",
|
||||
"rowspan",
|
||||
"colwidth",
|
||||
"background",
|
||||
"hideContent",
|
||||
"hidecontent",
|
||||
"style",
|
||||
},
|
||||
"td": {
|
||||
"colspan",
|
||||
"rowspan",
|
||||
"colwidth",
|
||||
"background",
|
||||
"textColor",
|
||||
"textcolor",
|
||||
"hideContent",
|
||||
"hidecontent",
|
||||
"style",
|
||||
},
|
||||
"tr": {"background", "textColor", "textcolor", "style"},
|
||||
"pre": {"language"},
|
||||
"code": {"language", "spellcheck"},
|
||||
"input": {"type", "checked"},
|
||||
}
|
||||
|
||||
SAFE_PROTOCOLS = {"http", "https", "mailto", "tel"}
|
||||
|
||||
|
||||
def _compute_html_sanitization_diff(before_html: str, after_html: str):
|
||||
"""
|
||||
Compute a coarse diff between original and sanitized HTML.
|
||||
|
||||
Returns a dict with:
|
||||
- removed_tags: mapping[tag] -> removed_count
|
||||
- removed_attributes: mapping[tag] -> sorted list of attribute names removed
|
||||
"""
|
||||
try:
|
||||
|
||||
def collect(soup):
|
||||
tag_counts = defaultdict(int)
|
||||
attrs_by_tag = defaultdict(set)
|
||||
for el in soup.find_all(True):
|
||||
tag_name = (el.name or "").lower()
|
||||
if not tag_name:
|
||||
continue
|
||||
tag_counts[tag_name] += 1
|
||||
for attr_name in list(el.attrs.keys()):
|
||||
if isinstance(attr_name, str) and attr_name:
|
||||
attrs_by_tag[tag_name].add(attr_name.lower())
|
||||
return tag_counts, attrs_by_tag
|
||||
|
||||
soup_before = BeautifulSoup(before_html or "", "html.parser")
|
||||
soup_after = BeautifulSoup(after_html or "", "html.parser")
|
||||
|
||||
counts_before, attrs_before = collect(soup_before)
|
||||
counts_after, attrs_after = collect(soup_after)
|
||||
|
||||
removed_tags = {}
|
||||
for tag, cnt_before in counts_before.items():
|
||||
cnt_after = counts_after.get(tag, 0)
|
||||
if cnt_after < cnt_before:
|
||||
removed = cnt_before - cnt_after
|
||||
removed_tags[tag] = removed
|
||||
|
||||
removed_attributes = {}
|
||||
for tag, before_set in attrs_before.items():
|
||||
after_set = attrs_after.get(tag, set())
|
||||
removed = before_set - after_set
|
||||
if removed:
|
||||
removed_attributes[tag] = sorted(list(removed))
|
||||
|
||||
return {"removed_tags": removed_tags, "removed_attributes": removed_attributes}
|
||||
except Exception:
|
||||
# Best-effort only; if diffing fails we don't block the request
|
||||
return {"removed_tags": {}, "removed_attributes": {}}
|
||||
|
||||
|
||||
def validate_html_content(html_content: str):
|
||||
"""
|
||||
Sanitize HTML content using nh3.
|
||||
@@ -218,25 +73,7 @@ def validate_html_content(html_content: str):
|
||||
return False, "HTML content exceeds maximum size limit (10MB)", None
|
||||
|
||||
try:
|
||||
clean_html = nh3.clean(
|
||||
html_content,
|
||||
tags=ALLOWED_TAGS,
|
||||
attributes=ATTRIBUTES,
|
||||
url_schemes=SAFE_PROTOCOLS,
|
||||
)
|
||||
# Report removals to logger (Sentry) if anything was stripped
|
||||
diff = _compute_html_sanitization_diff(html_content, clean_html)
|
||||
if diff.get("removed_tags") or diff.get("removed_attributes"):
|
||||
try:
|
||||
import json
|
||||
|
||||
summary = json.dumps(diff)
|
||||
except Exception:
|
||||
summary = str(diff)
|
||||
log_exception(
|
||||
f"HTML sanitization removals: {summary}",
|
||||
warning=True,
|
||||
)
|
||||
clean_html = nh3.clean(html_content)
|
||||
return True, None, clean_html
|
||||
except Exception as e:
|
||||
log_exception(e)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Django imports
|
||||
from django.contrib.postgres.aggregates import ArrayAgg
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db.models import Q, UUIDField, Value, QuerySet, OuterRef, Subquery
|
||||
from django.db.models import Q, UUIDField, Value, QuerySet
|
||||
from django.db.models.functions import Coalesce
|
||||
|
||||
# Module imports
|
||||
@@ -14,9 +14,6 @@ from plane.db.models import (
|
||||
ProjectMember,
|
||||
State,
|
||||
WorkspaceMember,
|
||||
IssueAssignee,
|
||||
ModuleIssue,
|
||||
IssueLabel,
|
||||
)
|
||||
from typing import Optional, Dict, Tuple, Any, Union, List
|
||||
|
||||
@@ -42,52 +39,33 @@ def issue_queryset_grouper(
|
||||
if group_key in GROUP_FILTER_MAPPER:
|
||||
queryset = queryset.filter(GROUP_FILTER_MAPPER[group_key])
|
||||
|
||||
issue_assignee_subquery = Subquery(
|
||||
IssueAssignee.objects.filter(
|
||||
issue_id=OuterRef("pk"),
|
||||
deleted_at__isnull=True,
|
||||
)
|
||||
.values("issue_id")
|
||||
.annotate(arr=ArrayAgg("assignee_id", distinct=True))
|
||||
.values("arr")
|
||||
)
|
||||
|
||||
issue_module_subquery = Subquery(
|
||||
ModuleIssue.objects.filter(
|
||||
issue_id=OuterRef("pk"),
|
||||
deleted_at__isnull=True,
|
||||
module__archived_at__isnull=True,
|
||||
)
|
||||
.values("issue_id")
|
||||
.annotate(arr=ArrayAgg("module_id", distinct=True))
|
||||
.values("arr")
|
||||
)
|
||||
|
||||
issue_label_subquery = Subquery(
|
||||
IssueLabel.objects.filter(issue_id=OuterRef("pk"), deleted_at__isnull=True)
|
||||
.values("issue_id")
|
||||
.annotate(arr=ArrayAgg("label_id", distinct=True))
|
||||
.values("arr")
|
||||
)
|
||||
|
||||
annotations_map: Dict[str, Tuple[str, Q]] = {
|
||||
"assignee_ids": Coalesce(
|
||||
issue_assignee_subquery, Value([], output_field=ArrayField(UUIDField()))
|
||||
"assignee_ids": (
|
||||
"assignees__id",
|
||||
~Q(assignees__id__isnull=True) & Q(issue_assignee__deleted_at__isnull=True),
|
||||
),
|
||||
"label_ids": Coalesce(
|
||||
issue_label_subquery, Value([], output_field=ArrayField(UUIDField()))
|
||||
"label_ids": (
|
||||
"labels__id",
|
||||
~Q(labels__id__isnull=True) & Q(label_issue__deleted_at__isnull=True),
|
||||
),
|
||||
"module_ids": Coalesce(
|
||||
issue_module_subquery, Value([], output_field=ArrayField(UUIDField()))
|
||||
"module_ids": (
|
||||
"issue_module__module_id",
|
||||
(
|
||||
~Q(issue_module__module_id__isnull=True)
|
||||
& Q(issue_module__module__archived_at__isnull=True)
|
||||
& Q(issue_module__deleted_at__isnull=True)
|
||||
),
|
||||
),
|
||||
}
|
||||
|
||||
default_annotations: Dict[str, Any] = {}
|
||||
|
||||
for key, expression in annotations_map.items():
|
||||
if FIELD_MAPPER.get(key) in {group_by, sub_group_by}:
|
||||
continue
|
||||
default_annotations[key] = expression
|
||||
default_annotations: Dict[str, Any] = {
|
||||
key: Coalesce(
|
||||
ArrayAgg(field, distinct=True, filter=condition),
|
||||
Value([], output_field=ArrayField(UUIDField())),
|
||||
)
|
||||
for key, (field, condition) in annotations_map.items()
|
||||
if FIELD_MAPPER.get(key) != group_by or FIELD_MAPPER.get(key) != sub_group_by
|
||||
}
|
||||
|
||||
return queryset.annotate(**default_annotations)
|
||||
|
||||
|
||||
@@ -476,8 +476,6 @@ def filter_subscribed_issues(params, issue_filter, method, prefix=""):
|
||||
issue_filter[f"{prefix}issue_subscribers__subscriber_id__in"] = params.get(
|
||||
"subscriber"
|
||||
)
|
||||
issue_filter[f"{prefix}issue_subscribers__deleted_at__isnull"] = True
|
||||
|
||||
return issue_filter
|
||||
|
||||
|
||||
|
||||
@@ -3,20 +3,18 @@ from urllib.parse import urlparse
|
||||
|
||||
|
||||
def validate_next_path(next_path: str) -> str:
|
||||
"""Validates that next_path is a safe relative path for redirection."""
|
||||
# Browsers interpret backslashes as forward slashes. Remove all backslashes.
|
||||
next_path = next_path.replace("\\", "")
|
||||
"""Validates that next_path is a valid path and extracts only the path component."""
|
||||
parsed_url = urlparse(next_path)
|
||||
|
||||
# Block absolute URLs or anything with scheme/netloc
|
||||
# Ensure next_path is not an absolute URL
|
||||
if parsed_url.scheme or parsed_url.netloc:
|
||||
next_path = parsed_url.path # Extract only the path component
|
||||
|
||||
# Must start with a forward slash and not be empty
|
||||
if not next_path or not next_path.startswith("/"):
|
||||
# Ensure it starts with a forward slash (indicating a valid relative path)
|
||||
if not next_path.startswith("/"):
|
||||
return ""
|
||||
|
||||
# Prevent path traversal
|
||||
# Ensure it does not contain dangerous path traversal sequences
|
||||
if ".." in next_path:
|
||||
return ""
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# base requirements
|
||||
|
||||
# django
|
||||
Django==4.2.24
|
||||
Django==4.2.22
|
||||
# rest framework
|
||||
djangorestframework==3.15.2
|
||||
# postgres
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/server.js"],
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"root": true,
|
||||
"extends": ["@plane/eslint-config/server.js"],
|
||||
"parser": "@typescript-eslint/parser"
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "live",
|
||||
"version": "1.0.0",
|
||||
"version": "0.28.0",
|
||||
"license": "AGPL-3.0",
|
||||
"description": "A realtime collaborative server powers Plane's rich text editor",
|
||||
"main": "./src/server.ts",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsdown",
|
||||
"dev": "tsdown --watch",
|
||||
"dev": "tsup --watch --onSuccess 'node --env-file=.env dist/server.js'",
|
||||
"build": "tsc --noEmit && tsup",
|
||||
"start": "node --env-file=.env dist/server.js",
|
||||
"check:lint": "eslint . --max-warnings 10",
|
||||
"check:types": "tsc --noEmit",
|
||||
@@ -28,7 +28,7 @@
|
||||
"@plane/types": "workspace:*",
|
||||
"@tiptap/core": "^2.22.3",
|
||||
"@tiptap/html": "^2.22.3",
|
||||
"axios": "catalog:",
|
||||
"axios": "1.11.0",
|
||||
"compression": "1.8.1",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.4.5",
|
||||
@@ -36,11 +36,11 @@
|
||||
"express-ws": "^5.0.2",
|
||||
"helmet": "^7.1.0",
|
||||
"ioredis": "^5.4.1",
|
||||
"lodash": "catalog:",
|
||||
"lodash": "^4.17.21",
|
||||
"morgan": "1.10.1",
|
||||
"pino-http": "^10.3.0",
|
||||
"pino-pretty": "^11.2.2",
|
||||
"uuid": "catalog:",
|
||||
"uuid": "^10.0.0",
|
||||
"y-prosemirror": "^1.2.15",
|
||||
"y-protocols": "^1.0.6",
|
||||
"yjs": "^13.6.20"
|
||||
@@ -58,8 +58,8 @@
|
||||
"concurrently": "^9.0.1",
|
||||
"nodemon": "^3.1.7",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsdown": "catalog:",
|
||||
"typescript": "catalog:",
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "5.8.3",
|
||||
"ws": "^8.18.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
// Third-party libraries
|
||||
import { Redis } from "ioredis";
|
||||
// Hocuspocus extensions and core
|
||||
import { Database } from "@hocuspocus/extension-database";
|
||||
import { Extension } from "@hocuspocus/server";
|
||||
import { Logger } from "@hocuspocus/extension-logger";
|
||||
import { Redis as HocusPocusRedis } from "@hocuspocus/extension-redis";
|
||||
import { Extension } from "@hocuspocus/server";
|
||||
import { Redis } from "ioredis";
|
||||
// core helpers and utilities
|
||||
import { manualLogger } from "@/core/helpers/logger.js";
|
||||
import { getRedisUrl } from "@/core/lib/utils/redis-url.js";
|
||||
// core libraries
|
||||
import { fetchPageDescriptionBinary, updatePageDescription } from "@/core/lib/page.js";
|
||||
import { getRedisUrl } from "@/core/lib/utils/redis-url.js";
|
||||
import { type HocusPocusServerContext, type TDocumentTypes } from "@/core/types/common.js";
|
||||
// plane live libraries
|
||||
import { fetchDocument } from "@/plane-live/lib/fetch-document.js";
|
||||
import { updateDocument } from "@/plane-live/lib/update-document.js";
|
||||
// types
|
||||
import { type HocusPocusServerContext, type TDocumentTypes } from "@/core/types/common.js";
|
||||
|
||||
export const getExtensions: () => Promise<Extension[]> = async () => {
|
||||
const extensions: Extension[] = [
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { Server } from "@hocuspocus/server";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// editor types
|
||||
import { TUserDetails } from "@plane/editor";
|
||||
import { DocumentCollaborativeEvents, TDocumentEventsServer } from "@plane/editor/lib";
|
||||
// extensions
|
||||
import { getExtensions } from "@/core/extensions/index.js";
|
||||
// lib
|
||||
import { handleAuthentication } from "@/core/lib/authentication.js";
|
||||
// extensions
|
||||
import { getExtensions } from "@/core/extensions/index.js";
|
||||
import { DocumentCollaborativeEvents, TDocumentEventsServer } from "@plane/editor/lib";
|
||||
// editor types
|
||||
import { TUserDetails } from "@plane/editor";
|
||||
// types
|
||||
import { type HocusPocusServerContext } from "@/core/types/common.js";
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// core helpers
|
||||
import { manualLogger } from "@/core/helpers/logger.js";
|
||||
// services
|
||||
import { UserService } from "@/core/services/user.service.js";
|
||||
// core helpers
|
||||
import { manualLogger } from "@/core/helpers/logger.js";
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import compression from "compression";
|
||||
import cors from "cors";
|
||||
import express, { Request, Response } from "express";
|
||||
import expressWs from "express-ws";
|
||||
import express, { Request, Response } from "express";
|
||||
import helmet from "helmet";
|
||||
// hocuspocus server
|
||||
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
|
||||
// helpers
|
||||
import { convertHTMLDocumentToAllFormats } from "@/core/helpers/convert-document.js";
|
||||
import { logger, manualLogger } from "@/core/helpers/logger.js";
|
||||
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
|
||||
// types
|
||||
import { TConvertDocumentRequestBody } from "@/core/types/common.js";
|
||||
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"emitDecoratorMetadata": true,
|
||||
"sourceRoot": "/"
|
||||
},
|
||||
"include": ["src/**/*.ts", "tsdown.config.ts"],
|
||||
"include": ["src/**/*.ts", "tsup.config.ts"],
|
||||
"exclude": ["./dist", "./build", "./node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
import { defineConfig } from "tsdown";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/server.ts"],
|
||||
outDir: "dist",
|
||||
format: ["esm", "cjs"],
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from "tsup";
|
||||
|
||||
export default defineConfig({
|
||||
entry: ["src/server.ts"],
|
||||
format: ["esm", "cjs"],
|
||||
dts: true,
|
||||
splitting: false,
|
||||
sourcemap: true,
|
||||
minify: false,
|
||||
target: "node18",
|
||||
outDir: "dist",
|
||||
env: {
|
||||
NODE_ENV: process.env.NODE_ENV || "development",
|
||||
},
|
||||
});
|
||||
@@ -1,12 +0,0 @@
|
||||
.next/*
|
||||
out/*
|
||||
public/*
|
||||
dist/*
|
||||
node_modules/*
|
||||
.turbo/*
|
||||
.env*
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.test
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/next.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
FROM node:22-alpine
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN corepack enable pnpm && pnpm add -g turbo
|
||||
RUN pnpm install
|
||||
|
||||
EXPOSE 3002
|
||||
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
|
||||
VOLUME [ "/app/node_modules", "/app/apps/space/node_modules"]
|
||||
|
||||
CMD ["pnpm", "dev", "--filter=space"]
|
||||
@@ -1,103 +0,0 @@
|
||||
# syntax=docker/dockerfile:1.7
|
||||
FROM node:22-alpine AS base
|
||||
|
||||
# Setup pnpm package manager with corepack and configure global bin directory for caching
|
||||
ENV PNPM_HOME="/pnpm"
|
||||
ENV PATH="$PNPM_HOME:$PATH"
|
||||
RUN corepack enable
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 1: Build the project
|
||||
# *****************************************************************************
|
||||
FROM base AS builder
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
ARG TURBO_VERSION=2.5.6
|
||||
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune --scope=space --docker
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 2: Install dependencies & build the project
|
||||
# *****************************************************************************
|
||||
FROM base AS installer
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
COPY .gitignore .gitignore
|
||||
COPY --from=builder /app/out/json/ .
|
||||
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
|
||||
RUN corepack enable pnpm
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm fetch --store-dir=/pnpm/store
|
||||
|
||||
COPY --from=builder /app/out/full/ .
|
||||
COPY turbo.json turbo.json
|
||||
RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store pnpm install --offline --frozen-lockfile --store-dir=/pnpm/store
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN pnpm turbo run build --filter=space
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 3: Copy the project and start it
|
||||
# *****************************************************************************
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
||||
# Don't run production as root
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
USER nextjs
|
||||
|
||||
# Automatically leverage output traces to reduce image size
|
||||
# https://nextjs.org/docs/advanced-features/output-file-tracing
|
||||
COPY --from=installer /app/apps/space/.next/standalone ./
|
||||
COPY --from=installer /app/apps/space/.next/static ./apps/space/.next/static
|
||||
COPY --from=installer /app/apps/space/public ./apps/space/public
|
||||
|
||||
ARG NEXT_PUBLIC_API_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_API_BASE_URL=$NEXT_PUBLIC_API_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_URL=$NEXT_PUBLIC_ADMIN_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_ADMIN_BASE_PATH="/god-mode"
|
||||
ENV NEXT_PUBLIC_ADMIN_BASE_PATH=$NEXT_PUBLIC_ADMIN_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_URL=$NEXT_PUBLIC_SPACE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_SPACE_BASE_PATH="/spaces"
|
||||
ENV NEXT_PUBLIC_SPACE_BASE_PATH=$NEXT_PUBLIC_SPACE_BASE_PATH
|
||||
|
||||
ARG NEXT_PUBLIC_WEB_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_WEB_BASE_URL=$NEXT_PUBLIC_WEB_BASE_URL
|
||||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
ENV TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
EXPOSE 3000
|
||||
|
||||
CMD ["node", "apps/space/server.js"]
|
||||
@@ -13,11 +13,6 @@ export async function generateMetadata({ params }: Props) {
|
||||
const { anchor } = params;
|
||||
const DEFAULT_TITLE = "Plane";
|
||||
const DEFAULT_DESCRIPTION = "Made with Plane, an AI-powered work management platform with publishing capabilities.";
|
||||
// Validate anchor before using in request (only allow alphanumeric, -, _)
|
||||
const ANCHOR_REGEX = /^[a-zA-Z0-9_-]+$/;
|
||||
if (!ANCHOR_REGEX.test(anchor)) {
|
||||
return { title: DEFAULT_TITLE, description: DEFAULT_DESCRIPTION };
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${process.env.NEXT_PUBLIC_API_BASE_URL}/api/public/anchor/${anchor}/meta/`);
|
||||
const data = await response.json();
|
||||
|
||||
@@ -172,7 +172,7 @@ export const AuthRoot: FC = observer(() => {
|
||||
text: `${content} with GitHub`,
|
||||
icon: (
|
||||
<Image
|
||||
src={resolvedTheme === "dark" ? GithubLightLogo : GithubDarkLogo}
|
||||
src={resolvedTheme === "dark" ? GithubDarkLogo : GithubLightLogo}
|
||||
height={18}
|
||||
width={18}
|
||||
alt="GitHub Logo"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
import { PlaneLockup } from "@plane/propel/icons";
|
||||
import { PlaneLockup } from "@plane/ui";
|
||||
// components
|
||||
import { PoweredBy } from "@/components/common/powered-by";
|
||||
import { UserAvatar } from "@/components/issues/navbar/user-avatar";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { FC } from "react";
|
||||
import { WEBSITE_URL } from "@plane/constants";
|
||||
// assets
|
||||
import { PlaneLogo } from "@plane/propel/icons";
|
||||
import { PlaneLogo } from "@plane/ui";
|
||||
|
||||
type TPoweredBy = {
|
||||
disabled?: boolean;
|
||||
|
||||
@@ -2,16 +2,17 @@ import React from "react";
|
||||
// plane imports
|
||||
import { type EditorRefApi, type ILiteTextEditorProps, LiteTextEditorWithRef, type TFileHandler } from "@plane/editor";
|
||||
import type { MakeOptional } from "@plane/types";
|
||||
import { cn, isCommentEmpty } from "@plane/utils";
|
||||
import { cn } from "@plane/utils";
|
||||
// helpers
|
||||
import { getEditorFileHandlers } from "@/helpers/editor.helper";
|
||||
import { isCommentEmpty } from "@/helpers/string.helper";
|
||||
import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
// local imports
|
||||
import { EditorMentionsRoot } from "./embeds/mentions";
|
||||
import { IssueCommentToolbar } from "./toolbar";
|
||||
|
||||
type LiteTextEditorWrapperProps = MakeOptional<
|
||||
Omit<ILiteTextEditorProps, "fileHandler" | "mentionHandler" | "extendedEditorProps">,
|
||||
Omit<ILiteTextEditorProps, "fileHandler" | "mentionHandler">,
|
||||
"disabledExtensions" | "flaggedExtensions"
|
||||
> & {
|
||||
anchor: string;
|
||||
@@ -62,7 +63,6 @@ export const LiteTextEditor = React.forwardRef<EditorRefApi, LiteTextEditorWrapp
|
||||
mentionHandler={{
|
||||
renderComponent: (props) => <EditorMentionsRoot {...props} />,
|
||||
}}
|
||||
extendedEditorProps={{}}
|
||||
{...rest}
|
||||
// overriding the containerClassName to add relative class passed
|
||||
containerClassName={cn(containerClassName, "relative")}
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useEditorFlagging } from "@/plane-web/hooks/use-editor-flagging";
|
||||
import { EditorMentionsRoot } from "./embeds/mentions";
|
||||
|
||||
type RichTextEditorWrapperProps = MakeOptional<
|
||||
Omit<IRichTextEditorProps, "editable" | "fileHandler" | "mentionHandler" | "extendedEditorProps">,
|
||||
Omit<IRichTextEditorProps, "editable" | "fileHandler" | "mentionHandler">,
|
||||
"disabledExtensions" | "flaggedExtensions"
|
||||
> & {
|
||||
anchor: string;
|
||||
@@ -56,10 +56,9 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
||||
workspaceId,
|
||||
})}
|
||||
flaggedExtensions={richTextEditorExtensions.flagged}
|
||||
extendedEditorProps={{}}
|
||||
{...rest}
|
||||
containerClassName={containerClassName}
|
||||
editorClassName="min-h-[100px] py-2 overflow-hidden"
|
||||
editorClassName="min-h-[100px] max-h-[200px] border-[0.5px] border-custom-border-300 rounded-md pl-3 py-2 overflow-hidden"
|
||||
displayConfig={{ fontSize: "large-font" }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,8 +3,7 @@
|
||||
import React, { useEffect, useState, useCallback } from "react";
|
||||
// plane imports
|
||||
import { TOOLBAR_ITEMS, type ToolbarMenuItem, type EditorRefApi } from "@plane/editor";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { Button } from "@plane/ui";
|
||||
import { Button, Tooltip } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
|
||||
type Props = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { X } from "lucide-react";
|
||||
import { PriorityIcon, type TIssuePriorities } from "@plane/propel/icons";
|
||||
import { PriorityIcon, type TIssuePriorities } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
handleRemove: (val: string) => void;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { observer } from "mobx-react";
|
||||
import { X } from "lucide-react";
|
||||
// plane imports
|
||||
import { EIconSize } from "@plane/constants";
|
||||
import { StateGroupIcon } from "@plane/propel/icons";
|
||||
import { StateGroupIcon } from "@plane/ui";
|
||||
// hooks
|
||||
import { useStates } from "@/hooks/store/use-state";
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { observer } from "mobx-react";
|
||||
// plane imports
|
||||
import { ISSUE_PRIORITY_FILTERS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { PriorityIcon } from "@plane/propel/icons";
|
||||
import { PriorityIcon } from "@plane/ui";
|
||||
// local imports
|
||||
import { FilterHeader } from "./helpers/filter-header";
|
||||
import { FilterOption } from "./helpers/filter-option";
|
||||
|
||||
@@ -4,8 +4,7 @@ import React, { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// ui
|
||||
import { EIconSize } from "@plane/constants";
|
||||
import { StateGroupIcon } from "@plane/propel/icons";
|
||||
import { Loader } from "@plane/ui";
|
||||
import { Loader, StateGroupIcon } from "@plane/ui";
|
||||
// hooks
|
||||
import { useStates } from "@/hooks/store/use-state";
|
||||
// local imports
|
||||
|
||||
@@ -5,9 +5,9 @@ import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
// plane types
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { IIssueDisplayProperties } from "@plane/types";
|
||||
// plane ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
|
||||
@@ -5,9 +5,9 @@ import { observer } from "mobx-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, useSearchParams } from "next/navigation";
|
||||
// plane types
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { IIssueDisplayProperties } from "@plane/types";
|
||||
// plane ui
|
||||
import { Tooltip } from "@plane/ui";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
// helpers
|
||||
@@ -75,7 +75,7 @@ export const IssueBlock = observer((props: IssueBlockProps) => {
|
||||
onClick={handleIssuePeekOverview}
|
||||
className="w-full truncate cursor-pointer text-sm text-custom-text-100"
|
||||
>
|
||||
<Tooltip tooltipContent={issue.name} position="top-start">
|
||||
<Tooltip tooltipContent={issue.name} position="top-left">
|
||||
<p className="truncate">{issue.name}</p>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { observer } from "mobx-react";
|
||||
import { Layers, Link, Paperclip } from "lucide-react";
|
||||
// plane imports
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import type { IIssueDisplayProperties } from "@plane/types";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { WithDisplayPropertiesHOC } from "@/components/issues/issue-layouts/with-display-properties-HOC";
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
// plane ui
|
||||
import { ContrastIcon } from "@plane/propel/icons";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { ContrastIcon, Tooltip } from "@plane/ui";
|
||||
// plane utils
|
||||
import { cn } from "@plane/utils";
|
||||
//hooks
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { observer } from "mobx-react";
|
||||
import { CalendarCheck2 } from "lucide-react";
|
||||
import { Tooltip } from "@plane/propel/tooltip";
|
||||
import { Tooltip } from "@plane/ui";
|
||||
import { cn } from "@plane/utils";
|
||||
// helpers
|
||||
import { renderFormattedDate } from "@/helpers/date-time.helper";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user