Compare 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
|
||||
|
||||
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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.",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
@@ -31,11 +31,11 @@
|
||||
"lucide-react": "^0.469.0",
|
||||
"mobx": "^6.12.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"next": "14.2.32",
|
||||
"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": "^0.33.5",
|
||||
"swr": "^2.2.4",
|
||||
@@ -45,11 +45,11 @@
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -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
|
||||
),
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
]
|
||||
@@ -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,9 +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"
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"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",
|
||||
@@ -58,8 +58,8 @@
|
||||
"concurrently": "^9.0.1",
|
||||
"nodemon": "^3.1.7",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsup": "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[] = [
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// plane editor
|
||||
import {
|
||||
getAllDocumentFormatsFromDocumentEditorBinaryData,
|
||||
getAllDocumentFormatsFromRichTextEditorBinaryData,
|
||||
getBinaryDataFromDocumentEditorHTMLString,
|
||||
getBinaryDataFromRichTextEditorHTMLString,
|
||||
} from "@plane/editor";
|
||||
// plane types
|
||||
import { TDocumentPayload } from "@plane/types";
|
||||
|
||||
type TArgs = {
|
||||
document_html: string;
|
||||
variant: "rich" | "document";
|
||||
};
|
||||
|
||||
export const convertHTMLDocumentToAllFormats = (args: TArgs): TDocumentPayload => {
|
||||
const { document_html, variant } = args;
|
||||
|
||||
let allFormats: TDocumentPayload;
|
||||
|
||||
if (variant === "rich") {
|
||||
const contentBinary = getBinaryDataFromRichTextEditorHTMLString(document_html);
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } =
|
||||
getAllDocumentFormatsFromRichTextEditorBinaryData(contentBinary);
|
||||
allFormats = {
|
||||
description: contentJSON,
|
||||
description_html: contentHTML,
|
||||
description_binary: contentBinaryEncoded,
|
||||
};
|
||||
} else if (variant === "document") {
|
||||
const contentBinary = getBinaryDataFromDocumentEditorHTMLString(document_html);
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } =
|
||||
getAllDocumentFormatsFromDocumentEditorBinaryData(contentBinary);
|
||||
allFormats = {
|
||||
description: contentJSON,
|
||||
description_html: contentHTML,
|
||||
description_binary: contentBinaryEncoded,
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Invalid variant provided: ${variant}`);
|
||||
}
|
||||
|
||||
return allFormats;
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import { getSchema } from "@tiptap/core";
|
||||
import { generateHTML, generateJSON } from "@tiptap/html";
|
||||
import { prosemirrorJSONToYDoc, yXmlFragmentToProseMirrorRootNode } from "y-prosemirror";
|
||||
import * as Y from "yjs";
|
||||
// plane editor
|
||||
import { CoreEditorExtensionsWithoutProps, DocumentEditorExtensionsWithoutProps } from "@plane/editor/lib";
|
||||
|
||||
const DOCUMENT_EDITOR_EXTENSIONS = [...CoreEditorExtensionsWithoutProps, ...DocumentEditorExtensionsWithoutProps];
|
||||
const documentEditorSchema = getSchema(DOCUMENT_EDITOR_EXTENSIONS);
|
||||
|
||||
export const getAllDocumentFormatsFromBinaryData = (
|
||||
description: Uint8Array
|
||||
): {
|
||||
contentBinaryEncoded: string;
|
||||
contentJSON: object;
|
||||
contentHTML: string;
|
||||
} => {
|
||||
// encode binary description data
|
||||
const base64Data = Buffer.from(description).toString("base64");
|
||||
const yDoc = new Y.Doc();
|
||||
Y.applyUpdate(yDoc, description);
|
||||
// convert to JSON
|
||||
const type = yDoc.getXmlFragment("default");
|
||||
const contentJSON = yXmlFragmentToProseMirrorRootNode(type, documentEditorSchema).toJSON();
|
||||
// convert to HTML
|
||||
const contentHTML = generateHTML(contentJSON, DOCUMENT_EDITOR_EXTENSIONS);
|
||||
|
||||
return {
|
||||
contentBinaryEncoded: base64Data,
|
||||
contentJSON,
|
||||
contentHTML,
|
||||
};
|
||||
};
|
||||
|
||||
export const getBinaryDataFromHTMLString = (
|
||||
descriptionHTML: string
|
||||
): {
|
||||
contentBinary: Uint8Array;
|
||||
} => {
|
||||
// convert HTML to JSON
|
||||
const contentJSON = generateJSON(descriptionHTML ?? "<p></p>", DOCUMENT_EDITOR_EXTENSIONS);
|
||||
// convert JSON to Y.Doc format
|
||||
const transformedData = prosemirrorJSONToYDoc(documentEditorSchema, contentJSON, "default");
|
||||
// convert Y.Doc to Uint8Array format
|
||||
const encodedData = Y.encodeStateAsUpdate(transformedData);
|
||||
|
||||
return {
|
||||
contentBinary: encodedData,
|
||||
};
|
||||
};
|
||||
@@ -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,5 +1,5 @@
|
||||
// helpers
|
||||
import { getAllDocumentFormatsFromBinaryData, getBinaryDataFromHTMLString } from "@plane/editor/lib";
|
||||
import { getAllDocumentFormatsFromBinaryData, getBinaryDataFromHTMLString } from "@/core/helpers/page.js";
|
||||
// services
|
||||
import { PageService } from "@/core/services/page.service.js";
|
||||
import { manualLogger } from "../helpers/logger.js";
|
||||
@@ -19,9 +19,7 @@ export const updatePageDescription = async (
|
||||
const projectId = params.get("projectId")?.toString();
|
||||
if (!workspaceSlug || !projectId || !cookie) return;
|
||||
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } = getAllDocumentFormatsFromBinaryData({
|
||||
descriptionBinary: updatedDescription,
|
||||
});
|
||||
const { contentBinaryEncoded, contentHTML, contentJSON } = getAllDocumentFormatsFromBinaryData(updatedDescription);
|
||||
try {
|
||||
const payload = {
|
||||
description_binary: contentBinaryEncoded,
|
||||
@@ -46,9 +44,7 @@ const fetchDescriptionHTMLAndTransform = async (
|
||||
|
||||
try {
|
||||
const pageDetails = await pageService.fetchDetails(workspaceSlug, projectId, pageId, cookie);
|
||||
const contentBinary = getBinaryDataFromHTMLString({
|
||||
descriptionHTML: pageDetails.description_html ?? "<p></p>",
|
||||
});
|
||||
const { contentBinary } = getBinaryDataFromHTMLString(pageDetails.description_html ?? "<p></p>");
|
||||
return contentBinary;
|
||||
} catch (error) {
|
||||
manualLogger.error("Error while transforming from HTML to Uint8Array", error);
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
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
|
||||
// helpers
|
||||
import { logger, manualLogger } from "@/core/helpers/logger.js";
|
||||
import { getHocusPocusServer } from "@/core/hocuspocus-server.js";
|
||||
// helpers
|
||||
import { convertHTMLDocumentToAllFormats } from "@/core/helpers/convert-document.js";
|
||||
import { logger, manualLogger } from "@/core/helpers/logger.js";
|
||||
// types
|
||||
import { TConvertDocumentRequestBody } from "@/core/types/common.js";
|
||||
import { getAllDocumentFormatsFromHTMLString } from "@plane/editor/lib";
|
||||
|
||||
export class Server {
|
||||
private app: any;
|
||||
@@ -72,8 +72,9 @@ export class Server {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const { description, description_binary } = getAllDocumentFormatsFromHTMLString({
|
||||
descriptionHTML: description_html,
|
||||
const { description, description_binary } = convertHTMLDocumentToAllFormats({
|
||||
document_html: description_html,
|
||||
variant,
|
||||
});
|
||||
res.status(200).json({
|
||||
description,
|
||||
|
||||
@@ -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"]
|
||||
@@ -58,7 +58,7 @@ export const RichTextEditor = forwardRef<EditorRefApi, RichTextEditorWrapperProp
|
||||
flaggedExtensions={richTextEditorExtensions.flagged}
|
||||
{...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" }}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "space",
|
||||
"version": "1.0.0",
|
||||
"version": "0.28.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@@ -41,11 +41,11 @@
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"mobx-utils": "^6.0.8",
|
||||
"next": "14.2.32",
|
||||
"next": "14.2.30",
|
||||
"next-themes": "^0.2.1",
|
||||
"nprogress": "^0.2.0",
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "7.51.5",
|
||||
"react-popper": "^2.3.0",
|
||||
@@ -58,12 +58,13 @@
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "18.14.1",
|
||||
"@types/nprogress": "^0.2.0",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/uuid": "^9.0.1",
|
||||
"typescript": "catalog:"
|
||||
"@typescript-eslint/eslint-plugin": "^8.36.0",
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,4 @@
|
||||
.next/*
|
||||
out/*
|
||||
public/*
|
||||
core/local-db/worker/wa-sqlite/src/*
|
||||
dist/*
|
||||
node_modules/*
|
||||
.turbo/*
|
||||
.env*
|
||||
.env
|
||||
.env.local
|
||||
.env.development
|
||||
.env.production
|
||||
.env.test
|
||||
core/local-db/worker/wa-sqlite/src/*
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/next.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -1,13 +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 3000
|
||||
VOLUME [ "/app/node_modules", "/app/web/node_modules" ]
|
||||
CMD ["pnpm", "dev", "--filter=web"]
|
||||
@@ -1,120 +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
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
ARG TURBO_VERSION=2.5.6
|
||||
RUN corepack enable pnpm && pnpm add -g turbo@${TURBO_VERSION}
|
||||
COPY . .
|
||||
|
||||
RUN turbo prune --scope=web --docker
|
||||
|
||||
# *****************************************************************************
|
||||
# STAGE 2: Install dependencies & build the project
|
||||
# *****************************************************************************
|
||||
# Add lockfile and package.json's of isolated subworkspace
|
||||
FROM base AS installer
|
||||
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
# First install the dependencies (as they change less often)
|
||||
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
|
||||
|
||||
# Build the project
|
||||
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_LIVE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_LIVE_BASE_URL=$NEXT_PUBLIC_LIVE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_LIVE_BASE_PATH="/live"
|
||||
ENV NEXT_PUBLIC_LIVE_BASE_PATH=$NEXT_PUBLIC_LIVE_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=web
|
||||
|
||||
# *****************************************************************************
|
||||
# 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/web/.next/standalone ./
|
||||
COPY --from=installer /app/apps/web/.next/static ./apps/web/.next/static
|
||||
COPY --from=installer /app/apps/web/public ./apps/web/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_LIVE_BASE_URL=""
|
||||
ENV NEXT_PUBLIC_LIVE_BASE_URL=$NEXT_PUBLIC_LIVE_BASE_URL
|
||||
|
||||
ARG NEXT_PUBLIC_LIVE_BASE_PATH="/live"
|
||||
ENV NEXT_PUBLIC_LIVE_BASE_PATH=$NEXT_PUBLIC_LIVE_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/web/server.js"]
|
||||
@@ -1,24 +1,36 @@
|
||||
import { FC } from "react";
|
||||
import { FC, useEffect, useRef } from "react";
|
||||
import isEmpty from "lodash/isEmpty";
|
||||
import { observer } from "mobx-react";
|
||||
// plane helpers
|
||||
import { EUserPermissions, EUserPermissionsLevel } from "@plane/constants";
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
// components
|
||||
import { SidebarWrapper } from "@/components/sidebar/sidebar-wrapper";
|
||||
import { AppSidebarToggleButton } from "@/components/sidebar/sidebar-toggle-button";
|
||||
import { SidebarDropdown } from "@/components/workspace/sidebar/dropdown";
|
||||
import { SidebarFavoritesMenu } from "@/components/workspace/sidebar/favorites/favorites-menu";
|
||||
import { HelpMenu } from "@/components/workspace/sidebar/help-menu";
|
||||
import { SidebarProjectsList } from "@/components/workspace/sidebar/projects-list";
|
||||
import { SidebarQuickActions } from "@/components/workspace/sidebar/quick-actions";
|
||||
import { SidebarMenuItems } from "@/components/workspace/sidebar/sidebar-menu-items";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store/use-app-theme";
|
||||
import { useFavorite } from "@/hooks/store/use-favorite";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useAppRail } from "@/hooks/use-app-rail";
|
||||
import useSize from "@/hooks/use-window-size";
|
||||
// plane web components
|
||||
import { WorkspaceEditionBadge } from "@/plane-web/components/workspace/edition-badge";
|
||||
import { SidebarTeamsList } from "@/plane-web/components/workspace/sidebar/teams-sidebar-list";
|
||||
|
||||
export const AppSidebar: FC = observer(() => {
|
||||
// store hooks
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
|
||||
const { shouldRenderAppRail, isEnabled: isAppRailEnabled } = useAppRail();
|
||||
const { groupedFavorites } = useFavorite();
|
||||
const windowSize = useSize();
|
||||
// refs
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
// derived values
|
||||
const canPerformWorkspaceMemberActions = allowPermissions(
|
||||
@@ -26,17 +38,55 @@ export const AppSidebar: FC = observer(() => {
|
||||
EUserPermissionsLevel.WORKSPACE
|
||||
);
|
||||
|
||||
useOutsideClickDetector(ref, () => {
|
||||
if (sidebarCollapsed === false) {
|
||||
if (window.innerWidth < 768) {
|
||||
toggleSidebar();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (windowSize[0] < 768 && !sidebarCollapsed) toggleSidebar();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [windowSize]);
|
||||
|
||||
const isFavoriteEmpty = isEmpty(groupedFavorites);
|
||||
|
||||
return (
|
||||
<SidebarWrapper title="Projects" quickActions={<SidebarQuickActions />}>
|
||||
<SidebarMenuItems />
|
||||
{/* Favorites Menu */}
|
||||
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && <SidebarFavoritesMenu />}
|
||||
{/* Teams List */}
|
||||
<SidebarTeamsList />
|
||||
{/* Projects List */}
|
||||
<SidebarProjectsList />
|
||||
</SidebarWrapper>
|
||||
<>
|
||||
<div className="flex flex-col gap-3 px-3">
|
||||
{/* Workspace switcher and settings */}
|
||||
{!shouldRenderAppRail && <SidebarDropdown />}
|
||||
|
||||
{isAppRailEnabled && (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-md text-custom-text-200 font-medium pt-1">Projects</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<AppSidebarToggleButton />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Quick actions */}
|
||||
<SidebarQuickActions />
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 overflow-x-hidden scrollbar-sm h-full w-full overflow-y-auto vertical-scrollbar px-3 pt-3 pb-0.5">
|
||||
<SidebarMenuItems />
|
||||
{/* Favorites Menu */}
|
||||
{canPerformWorkspaceMemberActions && !isFavoriteEmpty && <SidebarFavoritesMenu />}
|
||||
{/* Teams List */}
|
||||
<SidebarTeamsList />
|
||||
{/* Projects List */}
|
||||
<SidebarProjectsList />
|
||||
</div>
|
||||
{/* Help Section */}
|
||||
<div className="flex items-center justify-between p-3 border-t border-custom-border-200 bg-custom-sidebar-background-100 h-12">
|
||||
<WorkspaceEditionBadge />
|
||||
<div className="flex items-center gap-2">
|
||||
{!shouldRenderAppRail && <HelpMenu />}
|
||||
{!isAppRailEnabled && <AppSidebarToggleButton />}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -240,7 +240,7 @@ export const ListGroup = observer((props: Props) => {
|
||||
isWorkflowDropDisabled,
|
||||
]);
|
||||
|
||||
const isDragAllowed = group_by ? DRAG_ALLOWED_GROUPS.includes(group_by) : true;
|
||||
const isDragAllowed = !!group_by && DRAG_ALLOWED_GROUPS.includes(group_by);
|
||||
const canOverlayBeVisible = isWorkflowDropDisabled || orderBy !== "sort_order" || !!group.isDropDisabled;
|
||||
const isDropDisabled = isWorkflowDropDisabled || !!group.isDropDisabled;
|
||||
|
||||
|
||||
@@ -521,7 +521,7 @@ export const handleGroupDragDrop = async (
|
||||
subGroupBy: TIssueGroupByOptions | undefined,
|
||||
shouldAddIssueAtTop = false
|
||||
) => {
|
||||
if (!source.id || (subGroupBy && (!source.subGroupId || !destination.subGroupId))) return;
|
||||
if (!source.id || !groupBy || (subGroupBy && (!source.subGroupId || !destination.subGroupId))) return;
|
||||
|
||||
let updatedIssue: Partial<TIssue> = {};
|
||||
const issueUpdates: IssueUpdates = {};
|
||||
@@ -549,7 +549,7 @@ export const handleGroupDragDrop = async (
|
||||
};
|
||||
|
||||
// update updatedIssue values based on the source and destination groupIds
|
||||
if (source.groupId && destination.groupId && source.groupId !== destination.groupId && groupBy) {
|
||||
if (source.groupId && destination.groupId && source.groupId !== destination.groupId) {
|
||||
const groupKey = ISSUE_FILTER_DEFAULT_DATA[groupBy];
|
||||
let groupValue: any = clone(sourceIssue[groupKey]);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ export type TBasePaidPlanCardProps = {
|
||||
extraFeatures?: string | React.ReactNode;
|
||||
renderPriceContent: (price: TSubscriptionPrice) => React.ReactNode;
|
||||
renderActionButton: (price: TSubscriptionPrice) => React.ReactNode;
|
||||
isSelfHosted: boolean;
|
||||
};
|
||||
|
||||
export const BasePaidPlanCard: FC<TBasePaidPlanCardProps> = observer((props) => {
|
||||
@@ -30,10 +31,11 @@ export const BasePaidPlanCard: FC<TBasePaidPlanCardProps> = observer((props) =>
|
||||
extraFeatures,
|
||||
renderPriceContent,
|
||||
renderActionButton,
|
||||
isSelfHosted,
|
||||
} = props;
|
||||
// states
|
||||
const [selectedPlan, setSelectedPlan] = useState<TBillingFrequency>("month");
|
||||
const basePlan = getBaseSubscriptionName(planVariant);
|
||||
const basePlan = getBaseSubscriptionName(planVariant, isSelfHosted);
|
||||
const upgradeCardVariantStyle = getUpgradeCardVariantStyle(planVariant);
|
||||
// Plane details
|
||||
const planeName = getSubscriptionName(planVariant);
|
||||
|
||||
@@ -107,6 +107,7 @@ export const PlanUpgradeCard: FC<PlanUpgradeCardProps> = observer((props) => {
|
||||
isTrialAllowed={isTrialAllowed}
|
||||
/>
|
||||
)}
|
||||
isSelfHosted={isSelfHosted}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -107,6 +107,7 @@ export const TalkToSalesCard: FC<TalkToSalesCardProps> = observer((props) => {
|
||||
extraFeatures={extraFeatures}
|
||||
renderPriceContent={renderPriceContent}
|
||||
renderActionButton={renderActionButton}
|
||||
isSelfHosted={isSelfHosted}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
import Image from "next/image";
|
||||
// plane imports
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import {
|
||||
EUserPermissionsLevel,
|
||||
EPageAccess,
|
||||
PROJECT_PAGE_TRACKER_ELEMENTS,
|
||||
PROJECT_PAGE_TRACKER_EVENTS,
|
||||
} from "@plane/constants";
|
||||
import { EUserPermissionsLevel, EPageAccess, PROJECT_PAGE_TRACKER_ELEMENTS } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { EUserProjectRoles, TPage, TPageNavigationTabs } from "@plane/types";
|
||||
import { EUserProjectRoles, TPageNavigationTabs } from "@plane/types";
|
||||
// components
|
||||
import { setToast, TOAST_TYPE } from "@plane/ui";
|
||||
import { DetailedEmptyState } from "@/components/empty-state/detailed-empty-state-root";
|
||||
import { PageLoader } from "@/components/pages/loaders/page-loader";
|
||||
import { captureClick, captureError, captureSuccess } from "@/helpers/event-tracker.helper";
|
||||
import { useProject } from "@/hooks/store/use-project";
|
||||
import { captureClick } from "@/helpers/event-tracker.helper";
|
||||
import { useCommandPalette } from "@/hooks/store/use-command-palette";
|
||||
import { useUserPermissions } from "@/hooks/store/user";
|
||||
import { useResolvedAssetPath } from "@/hooks/use-resolved-asset-path";
|
||||
// plane web hooks
|
||||
@@ -34,16 +25,10 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
|
||||
// plane hooks
|
||||
const { t } = useTranslation();
|
||||
// store hooks
|
||||
const { currentProjectDetails, loader } = useProject();
|
||||
const { isAnyPageAvailable, getCurrentProjectFilteredPageIdsByTab, getCurrentProjectPageIdsByTab, filters } =
|
||||
const { loader, isAnyPageAvailable, getCurrentProjectFilteredPageIdsByTab, getCurrentProjectPageIdsByTab, filters } =
|
||||
usePageStore(storeType);
|
||||
const { toggleCreatePageModal } = useCommandPalette();
|
||||
const { allowPermissions } = useUserPermissions();
|
||||
const { createPage } = usePageStore(EPageStoreType.PROJECT);
|
||||
// states
|
||||
const [isCreatingPage, setIsCreatingPage] = useState(false);
|
||||
// router
|
||||
const router = useRouter();
|
||||
const { workspaceSlug } = useParams();
|
||||
// derived values
|
||||
const pageIds = getCurrentProjectPageIdsByTab(pageType);
|
||||
const filteredPageIds = getCurrentProjectFilteredPageIdsByTab(pageType);
|
||||
@@ -69,42 +54,6 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
|
||||
extension: "svg",
|
||||
});
|
||||
|
||||
// handle page create
|
||||
const handleCreatePage = async () => {
|
||||
setIsCreatingPage(true);
|
||||
|
||||
const payload: Partial<TPage> = {
|
||||
access: pageType === "private" ? EPageAccess.PRIVATE : EPageAccess.PUBLIC,
|
||||
};
|
||||
|
||||
await createPage(payload)
|
||||
.then((res) => {
|
||||
captureSuccess({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.create,
|
||||
payload: {
|
||||
id: res?.id,
|
||||
state: "SUCCESS",
|
||||
},
|
||||
});
|
||||
const pageId = `/${workspaceSlug}/projects/${currentProjectDetails?.id}/pages/${res?.id}`;
|
||||
router.push(pageId);
|
||||
})
|
||||
.catch((err) => {
|
||||
captureError({
|
||||
eventName: PROJECT_PAGE_TRACKER_EVENTS.create,
|
||||
payload: {
|
||||
state: "ERROR",
|
||||
},
|
||||
});
|
||||
setToast({
|
||||
type: TOAST_TYPE.ERROR,
|
||||
title: "Error!",
|
||||
message: err?.data?.error || "Page could not be created. Please try again.",
|
||||
});
|
||||
})
|
||||
.finally(() => setIsCreatingPage(false));
|
||||
};
|
||||
|
||||
if (loader === "init-loader") return <PageLoader />;
|
||||
// if no pages exist in the active page type
|
||||
if (!isAnyPageAvailable || pageIds?.length === 0) {
|
||||
@@ -115,12 +64,12 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
|
||||
description={t("project_page.empty_state.general.description")}
|
||||
assetPath={generalPageResolvedPath}
|
||||
primaryButton={{
|
||||
text: isCreatingPage ? t("creating") : t("project_page.empty_state.general.primary_button.text"),
|
||||
text: t("project_page.empty_state.general.primary_button.text"),
|
||||
onClick: () => {
|
||||
handleCreatePage();
|
||||
toggleCreatePageModal({ isOpen: true });
|
||||
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions || isCreatingPage,
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -132,12 +81,12 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
|
||||
description={t("project_page.empty_state.public.description")}
|
||||
assetPath={publicPageResolvedPath}
|
||||
primaryButton={{
|
||||
text: isCreatingPage ? t("creating") : t("project_page.empty_state.public.primary_button.text"),
|
||||
text: t("project_page.empty_state.public.primary_button.text"),
|
||||
onClick: () => {
|
||||
handleCreatePage();
|
||||
toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PUBLIC });
|
||||
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions || isCreatingPage,
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -148,12 +97,12 @@ export const PagesListMainContent: React.FC<Props> = observer((props) => {
|
||||
description={t("project_page.empty_state.private.description")}
|
||||
assetPath={privatePageResolvedPath}
|
||||
primaryButton={{
|
||||
text: isCreatingPage ? t("creating") : t("project_page.empty_state.private.primary_button.text"),
|
||||
text: t("project_page.empty_state.private.primary_button.text"),
|
||||
onClick: () => {
|
||||
handleCreatePage();
|
||||
toggleCreatePageModal({ isOpen: true, pageAccess: EPageAccess.PRIVATE });
|
||||
captureClick({ elementName: PROJECT_PAGE_TRACKER_ELEMENTS.EMPTY_STATE_CREATE_BUTTON });
|
||||
},
|
||||
disabled: !canPerformEmptyStateActions || isCreatingPage,
|
||||
disabled: !canPerformEmptyStateActions,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { FC, useEffect, useRef } from "react";
|
||||
import { observer } from "mobx-react";
|
||||
// plane helpers
|
||||
import { useOutsideClickDetector } from "@plane/hooks";
|
||||
// components
|
||||
import { AppSidebarToggleButton } from "@/components/sidebar/sidebar-toggle-button";
|
||||
import { SidebarDropdown } from "@/components/workspace/sidebar/dropdown";
|
||||
import { HelpMenu } from "@/components/workspace/sidebar/help-menu";
|
||||
// hooks
|
||||
import { useAppTheme } from "@/hooks/store/use-app-theme";
|
||||
import { useAppRail } from "@/hooks/use-app-rail";
|
||||
import useSize from "@/hooks/use-window-size";
|
||||
// plane web components
|
||||
import { WorkspaceEditionBadge } from "@/plane-web/components/workspace/edition-badge";
|
||||
|
||||
type TSidebarWrapperProps = {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
quickActions?: React.ReactNode;
|
||||
};
|
||||
|
||||
export const SidebarWrapper: FC<TSidebarWrapperProps> = observer((props) => {
|
||||
const { children, title, quickActions } = props;
|
||||
// store hooks
|
||||
const { toggleSidebar, sidebarCollapsed } = useAppTheme();
|
||||
const { shouldRenderAppRail, isEnabled: isAppRailEnabled } = useAppRail();
|
||||
const windowSize = useSize();
|
||||
// refs
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useOutsideClickDetector(ref, () => {
|
||||
if (sidebarCollapsed === false && window.innerWidth < 768) {
|
||||
toggleSidebar();
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (windowSize[0] < 768 && !sidebarCollapsed) toggleSidebar();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [windowSize]);
|
||||
|
||||
return (
|
||||
<div ref={ref} className="flex flex-col h-full w-full">
|
||||
<div className="flex flex-col gap-3 px-3">
|
||||
{/* Workspace switcher and settings */}
|
||||
{!shouldRenderAppRail && <SidebarDropdown />}
|
||||
|
||||
{isAppRailEnabled && (
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-md text-custom-text-200 font-medium pt-1">{title}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<AppSidebarToggleButton />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{/* Quick actions */}
|
||||
{quickActions}
|
||||
</div>
|
||||
<div className="flex flex-col gap-3 overflow-x-hidden scrollbar-sm h-full w-full overflow-y-auto vertical-scrollbar px-3 pt-3 pb-0.5">
|
||||
{children}
|
||||
</div>
|
||||
{/* Help Section */}
|
||||
<div className="flex items-center justify-between p-3 border-t border-custom-border-200 bg-custom-sidebar-background-100 h-12">
|
||||
<WorkspaceEditionBadge />
|
||||
<div className="flex items-center gap-2">
|
||||
{!shouldRenderAppRail && <HelpMenu />}
|
||||
{!isAppRailEnabled && <AppSidebarToggleButton />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -7,7 +7,6 @@ import { useParams, usePathname } from "next/navigation";
|
||||
// plane imports
|
||||
import { EUserPermissionsLevel, IWorkspaceSidebarNavigationItem } from "@plane/constants";
|
||||
import { useTranslation } from "@plane/i18n";
|
||||
import { joinUrlPath } from "@plane/utils";
|
||||
// components
|
||||
import { SidebarNavItem } from "@/components/sidebar/sidebar-navigation";
|
||||
import { NotificationAppSidebarOption } from "@/components/workspace-notifications/notification-app-sidebar-option";
|
||||
@@ -48,13 +47,13 @@ export const SidebarItemBase: FC<Props> = observer(({ item, additionalRender, ad
|
||||
const isPinned = sidebarPreference?.[item.key]?.is_pinned;
|
||||
if (!isPinned && !staticItems.includes(item.key)) return null;
|
||||
|
||||
const itemHref =
|
||||
item.key === "your_work" && data?.id ? joinUrlPath(slug, item.href, data?.id) : joinUrlPath(slug, item.href);
|
||||
const itemHref = item.key === "your_work" ? `/${slug}${item.href}${data?.id}/` : `/${slug}${item.href}`;
|
||||
const isActive = itemHref === pathname;
|
||||
const icon = getSidebarNavigationItemIcon(item.key);
|
||||
|
||||
return (
|
||||
<Link href={itemHref} onClick={handleLinkClick}>
|
||||
<SidebarNavItem isActive={item.highlight(pathname, itemHref)}>
|
||||
<SidebarNavItem isActive={isActive}>
|
||||
<div className="flex items-center gap-1.5 py-[1px]">
|
||||
{icon}
|
||||
<p className="text-sm leading-5 font-medium">{t(item.labelTranslationKey)}</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
// plane editor
|
||||
import { type EditorRefApi, getBinaryDataFromHTMLString } from "@plane/editor";
|
||||
import { type EditorRefApi, getBinaryDataFromDocumentEditorHTMLString } from "@plane/editor";
|
||||
// plane types
|
||||
import { TDocumentPayload } from "@plane/types";
|
||||
// hooks
|
||||
@@ -27,9 +27,7 @@ export const usePageFallback = (args: TArgs) => {
|
||||
if (latestEncodedDescription && latestEncodedDescription.byteLength > 0) {
|
||||
latestDecodedDescription = new Uint8Array(latestEncodedDescription);
|
||||
} else {
|
||||
latestDecodedDescription = getBinaryDataFromHTMLString({
|
||||
descriptionHTML: "<p></p>",
|
||||
});
|
||||
latestDecodedDescription = getBinaryDataFromDocumentEditorHTMLString("<p></p>");
|
||||
}
|
||||
|
||||
editor.setProviderDocument(latestDecodedDescription);
|
||||
|
||||
@@ -23,12 +23,8 @@ export class ProjectPageService extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async fetchById(workspaceSlug: string, projectId: string, pageId: string, trackVisit: boolean): Promise<TPage> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`, {
|
||||
params: {
|
||||
track_visit: trackVisit,
|
||||
},
|
||||
})
|
||||
async fetchById(workspaceSlug: string, projectId: string, pageId: string): Promise<TPage> {
|
||||
return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/pages/${pageId}/`)
|
||||
.then((response) => response?.data)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
|
||||
@@ -49,12 +49,7 @@ export interface IProjectPageStore {
|
||||
projectId: string,
|
||||
pageType?: TPageNavigationTabs
|
||||
) => Promise<TPage[] | undefined>;
|
||||
fetchPageDetails: (
|
||||
workspaceSlug: string,
|
||||
projectId: string,
|
||||
pageId: string,
|
||||
options?: { trackVisit?: boolean }
|
||||
) => Promise<TPage | undefined>;
|
||||
fetchPageDetails: (workspaceSlug: string, projectId: string, pageId: string) => Promise<TPage | undefined>;
|
||||
createPage: (pageData: Partial<TPage>) => Promise<TPage | undefined>;
|
||||
removePage: (pageId: string) => Promise<void>;
|
||||
movePage: (workspaceSlug: string, projectId: string, pageId: string, newProjectId: string) => Promise<void>;
|
||||
@@ -244,9 +239,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
* @description fetch the details of a page
|
||||
* @param {string} pageId
|
||||
*/
|
||||
fetchPageDetails = async (...args: Parameters<IProjectPageStore["fetchPageDetails"]>) => {
|
||||
const [workspaceSlug, projectId, pageId, options] = args;
|
||||
const { trackVisit } = options || {};
|
||||
fetchPageDetails = async (workspaceSlug: string, projectId: string, pageId: string) => {
|
||||
try {
|
||||
if (!workspaceSlug || !projectId || !pageId) return undefined;
|
||||
|
||||
@@ -256,7 +249,7 @@ export class ProjectPageStore implements IProjectPageStore {
|
||||
this.error = undefined;
|
||||
});
|
||||
|
||||
const page = await this.service.fetchById(workspaceSlug, projectId, pageId, trackVisit ?? true);
|
||||
const page = await this.service.fetchById(workspaceSlug, projectId, pageId);
|
||||
|
||||
runInAction(() => {
|
||||
if (page?.id) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "1.0.0",
|
||||
"version": "0.28.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
@@ -47,13 +47,13 @@
|
||||
"mobx": "^6.10.0",
|
||||
"mobx-react": "^9.1.1",
|
||||
"mobx-utils": "^6.0.8",
|
||||
"next": "14.2.32",
|
||||
"next": "14.2.30",
|
||||
"next-themes": "^0.2.1",
|
||||
"posthog-js": "^1.131.3",
|
||||
"react": "catalog:",
|
||||
"react": "^18.3.1",
|
||||
"react-color": "^2.19.3",
|
||||
"react-day-picker": "9.5.0",
|
||||
"react-dom": "catalog:",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "7.51.5",
|
||||
"react-markdown": "^8.0.7",
|
||||
@@ -72,13 +72,13 @@
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/lodash": "4.17.20",
|
||||
"@types/lodash": "^4.17.6",
|
||||
"@types/node": "18.16.1",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-color": "^3.0.6",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"@types/uuid": "^8.3.4",
|
||||
"prettier": "^3.2.5",
|
||||
"typescript": "catalog:"
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 6.8 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 7.9 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 8.5 KiB After Width: | Height: | Size: 25 KiB |
@@ -97,7 +97,7 @@ priority=20
|
||||
|
||||
[program:live]
|
||||
directory=/app/live
|
||||
command=sh -c "node live/server.js"
|
||||
command=sh -c "node /app/live/apps/live/dist/server.js"
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/app/logs/access/live.log
|
||||
@@ -106,7 +106,7 @@ stderr_logfile=/app/logs/error/live.err.log
|
||||
# stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=50MB
|
||||
stderr_logfile_backups=5
|
||||
environment=PORT=3005,HOSTNAME=0.0.0.0
|
||||
environment=PORT=3005,HOSTNAME=0.0.0.0,LIVE_BASE_PATH=/live
|
||||
priority=20
|
||||
|
||||
|
||||
@@ -121,4 +121,4 @@ stderr_logfile=/app/logs/error/proxy.err.log
|
||||
# stderr_logfile=/dev/stderr
|
||||
stderr_logfile_maxbytes=50MB
|
||||
stderr_logfile_backups=5
|
||||
priority=20
|
||||
priority=20
|
||||
|
||||
@@ -3,19 +3,25 @@ services:
|
||||
image: ${DOCKERHUB_USER:-local}/plane-frontend:${APP_RELEASE:-latest}
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: apps/web/Dockerfile.web
|
||||
dockerfile: Dockerfile.node
|
||||
args:
|
||||
APP_SCOPE: web
|
||||
|
||||
space:
|
||||
image: ${DOCKERHUB_USER:-local}/plane-space:${APP_RELEASE:-latest}
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: apps/space/Dockerfile.space
|
||||
dockerfile: Dockerfile.node
|
||||
args:
|
||||
APP_SCOPE: space
|
||||
|
||||
admin:
|
||||
image: ${DOCKERHUB_USER:-local}/plane-admin:${APP_RELEASE:-latest}
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: apps/admin/Dockerfile.admin
|
||||
dockerfile: Dockerfile.node
|
||||
args:
|
||||
APP_SCOPE: admin
|
||||
|
||||
live:
|
||||
image: ${DOCKERHUB_USER:-local}/plane-live:${APP_RELEASE:-latest}
|
||||
@@ -26,7 +32,7 @@ services:
|
||||
api:
|
||||
image: ${DOCKERHUB_USER:-local}/plane-backend:${APP_RELEASE:-latest}
|
||||
build:
|
||||
context: ../../apps/api
|
||||
context: ../../
|
||||
dockerfile: Dockerfile.api
|
||||
|
||||
proxy:
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# docker-bake.hcl
|
||||
#
|
||||
# Build all runtime images for Plane services and the AIO assembler image.
|
||||
# Uses unified Dockerfiles under plane/ for Node (Next.js) apps and API.
|
||||
#
|
||||
# Usage examples:
|
||||
# # Build all individual runtime images (no push)
|
||||
# docker buildx bake
|
||||
#
|
||||
# # Build everything including AIO
|
||||
# docker buildx bake all
|
||||
#
|
||||
# # Build and push with custom tag and registry/prefix
|
||||
# docker buildx bake all --set *.tags=myrepo/plane-{{.target}}:1.2.3 --push
|
||||
#
|
||||
# # Or set variables:
|
||||
# docker buildx bake all --set TAG=1.2.3 --set IMAGE_PREFIX=myrepo/ --push
|
||||
#
|
||||
# Notes:
|
||||
# - The "AIO" image composes from the individual images built here.
|
||||
# - "live" currently uses its app-specific Dockerfile (non-Next.js runtime).
|
||||
|
||||
group "default" {
|
||||
targets = ["web", "space", "admin", "live", "api", "proxy"]
|
||||
}
|
||||
|
||||
group "all" {
|
||||
targets = ["web", "space", "admin", "live", "api", "proxy", "aio"]
|
||||
}
|
||||
|
||||
group "frontend" {
|
||||
targets = ["web", "space", "admin"]
|
||||
}
|
||||
|
||||
variable "TAG" {
|
||||
# Global tag to apply to images
|
||||
default = "latest"
|
||||
}
|
||||
|
||||
variable "IMAGE_PREFIX" {
|
||||
# Optional prefix/registry for image tags, e.g. "ghcr.io/makeplane/" or "yourrepo/"
|
||||
# Leave empty to tag locally (e.g. "plane-web:latest")
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "PLATFORMS" {
|
||||
# List of platforms (e.g., ["linux/amd64", "linux/arm64"])
|
||||
default = ["linux/amd64"]
|
||||
}
|
||||
|
||||
# Common cache configuration for faster CI builds
|
||||
target "with-cache" {
|
||||
cache-from = ["type=gha"]
|
||||
cache-to = ["type=gha,mode=max"]
|
||||
}
|
||||
|
||||
# Common base for Next.js apps using the unified Dockerfile
|
||||
target "common-node" {
|
||||
inherits = ["with-cache"]
|
||||
dockerfile = "Dockerfile.node"
|
||||
context = "."
|
||||
platforms = "${PLATFORMS}"
|
||||
}
|
||||
|
||||
# Frontend apps (Next.js standalone runtime)
|
||||
target "web" {
|
||||
inherits = ["common-node"]
|
||||
target = "runtime"
|
||||
args = { APP_SCOPE = "web" }
|
||||
tags = ["${IMAGE_PREFIX}plane-web:${TAG}"]
|
||||
}
|
||||
|
||||
target "space" {
|
||||
inherits = ["common-node"]
|
||||
target = "runtime"
|
||||
args = { APP_SCOPE = "space" }
|
||||
tags = ["${IMAGE_PREFIX}plane-space:${TAG}"]
|
||||
}
|
||||
|
||||
target "admin" {
|
||||
inherits = ["common-node"]
|
||||
target = "runtime"
|
||||
args = { APP_SCOPE = "admin" }
|
||||
tags = ["${IMAGE_PREFIX}plane-admin:${TAG}"]
|
||||
}
|
||||
|
||||
# Live app (Node service; not Next.js standalone)
|
||||
# Keeps its dedicated Dockerfile to match current build/run layout
|
||||
target "live" {
|
||||
inherits = ["with-cache"]
|
||||
dockerfile = "apps/live/Dockerfile.live"
|
||||
context = "."
|
||||
platforms = "${PLATFORMS}"
|
||||
tags = ["${IMAGE_PREFIX}plane-live:${TAG}"]
|
||||
}
|
||||
|
||||
# Python API (unified Dockerfile)
|
||||
target "api" {
|
||||
inherits = ["with-cache"]
|
||||
dockerfile = "Dockerfile.api"
|
||||
context = "."
|
||||
target = "runtime"
|
||||
platforms = "${PLATFORMS}"
|
||||
tags = ["${IMAGE_PREFIX}plane-api:${TAG}"]
|
||||
}
|
||||
|
||||
# Proxy (Caddy with plugins)
|
||||
target "proxy" {
|
||||
inherits = ["with-cache"]
|
||||
dockerfile = "Dockerfile.ce"
|
||||
context = "apps/proxy"
|
||||
platforms = "${PLATFORMS}"
|
||||
tags = ["${IMAGE_PREFIX}plane-proxy:${TAG}"]
|
||||
}
|
||||
|
||||
# All-in-one assembler image
|
||||
# Composes from previously built runtime images; override args if you use different tags.
|
||||
target "aio" {
|
||||
inherits = ["with-cache"]
|
||||
dockerfile = "Dockerfile.aio"
|
||||
context = "."
|
||||
platforms = "${PLATFORMS}"
|
||||
contexts = {
|
||||
web_ctx = "target:web"
|
||||
space_ctx = "target:space"
|
||||
admin_ctx = "target:admin"
|
||||
live_ctx = "target:live"
|
||||
api_ctx = "target:api"
|
||||
proxy_ctx = "target:proxy"
|
||||
}
|
||||
tags = ["${IMAGE_PREFIX}plane-aio:${TAG}"]
|
||||
}
|
||||
@@ -1,9 +1,134 @@
|
||||
# Local development stack with hot reload for web/space/admin/live and Django API.
|
||||
# Uses unified dev targets in plane/Dockerfile.node and plane/Dockerfile.api.
|
||||
# Infra services (Postgres, Redis, RabbitMQ, MinIO) are included.
|
||||
|
||||
services:
|
||||
# Django API in dev mode (bind mounts for instant reload)
|
||||
api:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.api
|
||||
target: dev
|
||||
restart: unless-stopped
|
||||
working_dir: /code
|
||||
command: ./bin/docker-entrypoint-api-local.sh
|
||||
env_file:
|
||||
- ./apps/api/.env
|
||||
environment:
|
||||
USE_MINIO: "1"
|
||||
AWS_S3_ENDPOINT_URL: http://plane-minio:9000
|
||||
MINIO_ENDPOINT_URL: http://plane-minio:9000
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-minioadmin}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-minioadmin}
|
||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
AWS_REGION: ${AWS_REGION:-us-east-1}
|
||||
WEB_URL: ${WEB_URL:-http://localhost:3001}
|
||||
DJANGO_DEBUG_TOOLBAR: ${DJANGO_DEBUG_TOOLBAR:-0}
|
||||
volumes:
|
||||
- ./apps/api:/code
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
- plane-mq
|
||||
- plane-minio
|
||||
ports:
|
||||
- "8000:8000"
|
||||
|
||||
worker:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.api
|
||||
target: dev
|
||||
restart: unless-stopped
|
||||
working_dir: /code
|
||||
command: ./bin/docker-entrypoint-worker.sh
|
||||
env_file:
|
||||
- ./apps/api/.env
|
||||
environment:
|
||||
USE_MINIO: "1"
|
||||
AWS_S3_ENDPOINT_URL: http://plane-minio:9000
|
||||
MINIO_ENDPOINT_URL: http://plane-minio:9000
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-minioadmin}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-minioadmin}
|
||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
AWS_REGION: ${AWS_REGION:-us-east-1}
|
||||
volumes:
|
||||
- ./apps/api:/code
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
- plane-mq
|
||||
|
||||
beat-worker:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.api
|
||||
target: dev
|
||||
restart: unless-stopped
|
||||
working_dir: /code
|
||||
command: ./bin/docker-entrypoint-beat.sh
|
||||
env_file:
|
||||
- ./apps/api/.env
|
||||
environment:
|
||||
USE_MINIO: "1"
|
||||
AWS_S3_ENDPOINT_URL: http://plane-minio:9000
|
||||
MINIO_ENDPOINT_URL: http://plane-minio:9000
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-minioadmin}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-minioadmin}
|
||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
AWS_REGION: ${AWS_REGION:-us-east-1}
|
||||
volumes:
|
||||
- ./apps/api:/code
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
- plane-mq
|
||||
|
||||
migrator:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.api
|
||||
target: dev
|
||||
restart: "no"
|
||||
working_dir: /code
|
||||
command: ./bin/docker-entrypoint-migrator.sh --settings=plane.settings.local
|
||||
env_file:
|
||||
- ./apps/api/.env
|
||||
environment:
|
||||
USE_MINIO: "1"
|
||||
AWS_S3_ENDPOINT_URL: http://plane-minio:9000
|
||||
MINIO_ENDPOINT_URL: http://plane-minio:9000
|
||||
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-minioadmin}
|
||||
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-minioadmin}
|
||||
AWS_S3_BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
AWS_REGION: ${AWS_REGION:-us-east-1}
|
||||
volumes:
|
||||
- ./apps/api:/code
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
- plane-minio
|
||||
|
||||
# Infra
|
||||
plane-db:
|
||||
image: postgres:15.7-alpine
|
||||
restart: unless-stopped
|
||||
command: "postgres -c 'max_connections=1000'"
|
||||
environment:
|
||||
POSTGRES_USER: ${POSTGRES_USER:-plane}
|
||||
POSTGRES_DB: ${POSTGRES_DB:-plane}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-plane}
|
||||
PGDATA: /var/lib/postgresql/data
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
plane-redis:
|
||||
image: valkey/valkey:7.2.5-alpine
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- redisdata:/data
|
||||
ports:
|
||||
@@ -12,218 +137,43 @@ services:
|
||||
plane-mq:
|
||||
image: rabbitmq:3.13.6-management-alpine
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER:-plane}
|
||||
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD:-plane}
|
||||
RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST:-plane}
|
||||
volumes:
|
||||
- rabbitmq_data:/var/lib/rabbitmq
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
RABBITMQ_DEFAULT_USER: ${RABBITMQ_USER}
|
||||
RABBITMQ_DEFAULT_PASS: ${RABBITMQ_PASSWORD}
|
||||
RABBITMQ_DEFAULT_VHOST: ${RABBITMQ_VHOST}
|
||||
ports:
|
||||
- "15672:15672"
|
||||
|
||||
plane-minio:
|
||||
image: minio/minio
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
# Bootstrap bucket on first run; suitable for local smoke/dev only
|
||||
entrypoint: >
|
||||
/bin/sh -c "
|
||||
mkdir -p /export/${AWS_S3_BUCKET_NAME} &&
|
||||
minio server /export --console-address ':9090' &
|
||||
sleep 5 &&
|
||||
mc alias set myminio http://localhost:9000 ${AWS_ACCESS_KEY_ID} ${AWS_SECRET_ACCESS_KEY} &&
|
||||
mc mb myminio/${AWS_S3_BUCKET_NAME} -p || true
|
||||
&& tail -f /dev/null
|
||||
set -e
|
||||
mkdir -p /export/${AWS_S3_BUCKET_NAME:-uploads};
|
||||
minio server /export --console-address ':9090' & pid=$!;
|
||||
sleep 5;
|
||||
mc alias set myminio http://plane-minio:9000 ${AWS_ACCESS_KEY_ID:-minioadmin} ${AWS_SECRET_ACCESS_KEY:-minioadmin};
|
||||
mc mb myminio/${AWS_S3_BUCKET_NAME:-uploads} -p || true;
|
||||
wait $pid
|
||||
"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID:-minioadmin}
|
||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY:-minioadmin}
|
||||
volumes:
|
||||
- uploads:/export
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${AWS_ACCESS_KEY_ID}
|
||||
MINIO_ROOT_PASSWORD: ${AWS_SECRET_ACCESS_KEY}
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9090:9090"
|
||||
|
||||
plane-db:
|
||||
image: postgres:15.7-alpine
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
command: postgres -c 'max_connections=1000'
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
PGDATA: /var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
# web:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: ./web/Dockerfile.dev
|
||||
# restart: unless-stopped
|
||||
# networks:
|
||||
# - dev_env
|
||||
# volumes:
|
||||
# - ./web:/app/web
|
||||
# env_file:
|
||||
# - ./web/.env
|
||||
# depends_on:
|
||||
# - api
|
||||
# - worker
|
||||
|
||||
# space:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: ./space/Dockerfile.dev
|
||||
# restart: unless-stopped
|
||||
# networks:
|
||||
# - dev_env
|
||||
# volumes:
|
||||
# - ./space:/app/space
|
||||
# depends_on:
|
||||
# - api
|
||||
# - worker
|
||||
# - web
|
||||
|
||||
# admin:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: ./admin/Dockerfile.dev
|
||||
# restart: unless-stopped
|
||||
# networks:
|
||||
# - dev_env
|
||||
# volumes:
|
||||
# - ./admin:/app/admin
|
||||
# depends_on:
|
||||
# - api
|
||||
# - worker
|
||||
# - web
|
||||
|
||||
# live:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: ./live/Dockerfile.dev
|
||||
# restart: unless-stopped
|
||||
# networks:
|
||||
# - dev_env
|
||||
# volumes:
|
||||
# - ./live:/app/live
|
||||
# depends_on:
|
||||
# - api
|
||||
# - worker
|
||||
# - web
|
||||
|
||||
api:
|
||||
build:
|
||||
context: ./apps/api
|
||||
dockerfile: Dockerfile.dev
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./apps/api:/code
|
||||
command: ./bin/docker-entrypoint-api-local.sh
|
||||
env_file:
|
||||
- ./apps/api/.env
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
- plane-mq
|
||||
ports:
|
||||
- "8000:8000"
|
||||
|
||||
worker:
|
||||
build:
|
||||
context: ./apps/api
|
||||
dockerfile: Dockerfile.dev
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./apps/api:/code
|
||||
command: ./bin/docker-entrypoint-worker.sh
|
||||
env_file:
|
||||
- ./apps/api/.env
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
beat-worker:
|
||||
build:
|
||||
context: ./apps/api
|
||||
dockerfile: Dockerfile.dev
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./apps/api:/code
|
||||
command: ./bin/docker-entrypoint-beat.sh
|
||||
env_file:
|
||||
- ./apps/api/.env
|
||||
depends_on:
|
||||
- api
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
migrator:
|
||||
build:
|
||||
context: ./apps/api
|
||||
dockerfile: Dockerfile.dev
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: "no"
|
||||
networks:
|
||||
- dev_env
|
||||
volumes:
|
||||
- ./apps/api:/code
|
||||
command: ./bin/docker-entrypoint-migrator.sh --settings=plane.settings.local
|
||||
env_file:
|
||||
- ./apps/api/.env
|
||||
depends_on:
|
||||
- plane-db
|
||||
- plane-redis
|
||||
|
||||
# proxy:
|
||||
# build:
|
||||
# context: ./apps/proxy
|
||||
# dockerfile: Dockerfile.ce
|
||||
# restart: unless-stopped
|
||||
# networks:
|
||||
# - dev_env
|
||||
# ports:
|
||||
# - ${LISTEN_HTTP_PORT}:80
|
||||
# - ${LISTEN_HTTPS_PORT}:443
|
||||
# env_file:
|
||||
# - .env
|
||||
# environment:
|
||||
# FILE_SIZE_LIMIT: ${FILE_SIZE_LIMIT:-5242880}
|
||||
# BUCKET_NAME: ${AWS_S3_BUCKET_NAME:-uploads}
|
||||
# depends_on:
|
||||
# - api
|
||||
# - web
|
||||
# - space
|
||||
# - admin
|
||||
|
||||
volumes:
|
||||
redisdata:
|
||||
uploads:
|
||||
pgdata:
|
||||
rabbitmq_data:
|
||||
# Node/pnpm caches for speedy dev
|
||||
|
||||
networks:
|
||||
dev_env:
|
||||
driver: bridge
|
||||
# Infra data
|
||||
pgdata:
|
||||
redisdata:
|
||||
rabbitmq_data:
|
||||
uploads:
|
||||
|
||||
@@ -3,9 +3,10 @@ services:
|
||||
container_name: web
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./apps/web/Dockerfile.web
|
||||
dockerfile: ./Dockerfile.node
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
APP_SCOPE: web
|
||||
restart: always
|
||||
depends_on:
|
||||
- api
|
||||
@@ -14,9 +15,10 @@ services:
|
||||
container_name: admin
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./apps/admin/Dockerfile.admin
|
||||
dockerfile: ./Dockerfile.node
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
APP_SCOPE: admin
|
||||
restart: always
|
||||
depends_on:
|
||||
- api
|
||||
@@ -26,9 +28,10 @@ services:
|
||||
container_name: space
|
||||
build:
|
||||
context: .
|
||||
dockerfile: ./apps/space/Dockerfile.space
|
||||
dockerfile: ./Dockerfile.node
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
APP_SCOPE: space
|
||||
restart: always
|
||||
depends_on:
|
||||
- api
|
||||
@@ -37,8 +40,8 @@ services:
|
||||
api:
|
||||
container_name: api
|
||||
build:
|
||||
context: ./apps/api
|
||||
dockerfile: Dockerfile.api
|
||||
context: .
|
||||
dockerfile: ./Dockerfile.api
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: always
|
||||
@@ -52,8 +55,8 @@ services:
|
||||
worker:
|
||||
container_name: bgworker
|
||||
build:
|
||||
context: ./apps/api
|
||||
dockerfile: Dockerfile.api
|
||||
context: .
|
||||
dockerfile: ./Dockerfile.api
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: always
|
||||
@@ -68,8 +71,8 @@ services:
|
||||
beat-worker:
|
||||
container_name: beatworker
|
||||
build:
|
||||
context: ./apps/api
|
||||
dockerfile: Dockerfile.api
|
||||
context: .
|
||||
dockerfile: ./Dockerfile.api
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: always
|
||||
@@ -84,8 +87,8 @@ services:
|
||||
migrator:
|
||||
container_name: plane-migrator
|
||||
build:
|
||||
context: ./apps/api
|
||||
dockerfile: Dockerfile.api
|
||||
context: .
|
||||
dockerfile: ./Dockerfile.api
|
||||
args:
|
||||
DOCKER_BUILDKIT: 1
|
||||
restart: no
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Repo-wide tool versions managed by mise
|
||||
# See: https://mise.jdx.dev
|
||||
|
||||
[tools]
|
||||
# Node LTS "Jod" to match .nvmrc and package.json engines
|
||||
node = "22.18.0"
|
||||
|
||||
# Python version to match Dockerfile.api
|
||||
python = "3.12.10"
|
||||
|
||||
# Global CLI utilities installed via pipx
|
||||
# This provides the `ruff` command used by CI and local checks
|
||||
pipx = "latest"
|
||||
"pipx:ruff" = "0.9.7"
|
||||
@@ -2,9 +2,13 @@
|
||||
"name": "plane",
|
||||
"description": "Open-source project management that unlocks customer value",
|
||||
"repository": "https://github.com/makeplane/plane.git",
|
||||
"version": "1.0.0",
|
||||
"version": "0.28.0",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "turbo run build",
|
||||
"dev": "turbo run dev --concurrency=18",
|
||||
@@ -32,7 +36,7 @@
|
||||
"tar-fs": "3.0.9",
|
||||
"prosemirror-view": "1.40.0",
|
||||
"@types/express": "4.17.23",
|
||||
"typescript": "catalog:",
|
||||
"typescript": "5.8.3",
|
||||
"sharp": "0.33.5"
|
||||
}
|
||||
},
|
||||
@@ -40,4 +44,4 @@
|
||||
"engines": {
|
||||
"node": ">=22.18.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
build/*
|
||||
dist/*
|
||||
out/*
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/constants",
|
||||
"version": "1.0.0",
|
||||
"version": "0.28.0",
|
||||
"private": true,
|
||||
"license": "AGPL-3.0",
|
||||
"files": [
|
||||
@@ -33,8 +33,8 @@
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/node": "^22.5.4",
|
||||
"@types/react": "catalog:",
|
||||
"tsup": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
"@types/react": "^18.3.11",
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,7 +244,6 @@ export interface IWorkspaceSidebarNavigationItem {
|
||||
labelTranslationKey: string;
|
||||
href: string;
|
||||
access: EUserWorkspaceRoles[];
|
||||
highlight: (pathname: string, url: string) => boolean;
|
||||
}
|
||||
|
||||
export const WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS: Record<string, IWorkspaceSidebarNavigationItem> = {
|
||||
@@ -253,28 +252,24 @@ export const WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS: Record<string, IWorkspa
|
||||
labelTranslationKey: "views",
|
||||
href: `/workspace-views/all-issues/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
|
||||
highlight: (pathname: string, url: string) => pathname === url,
|
||||
},
|
||||
analytics: {
|
||||
key: "analytics",
|
||||
labelTranslationKey: "analytics",
|
||||
href: `/analytics/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
|
||||
highlight: (pathname: string, url: string) => pathname.includes(url),
|
||||
},
|
||||
drafts: {
|
||||
key: "drafts",
|
||||
labelTranslationKey: "drafts",
|
||||
href: `/drafts/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
|
||||
highlight: (pathname: string, url: string) => pathname.includes(url),
|
||||
},
|
||||
archives: {
|
||||
key: "archives",
|
||||
labelTranslationKey: "archives",
|
||||
href: `/projects/archives/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
|
||||
highlight: (pathname: string, url: string) => pathname.includes(url),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -291,28 +286,24 @@ export const WORKSPACE_SIDEBAR_STATIC_NAVIGATION_ITEMS: Record<string, IWorkspac
|
||||
labelTranslationKey: "home.title",
|
||||
href: `/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
|
||||
highlight: (pathname: string, url: string) => pathname === url,
|
||||
},
|
||||
inbox: {
|
||||
key: "inbox",
|
||||
labelTranslationKey: "notification.label",
|
||||
href: `/notifications/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
|
||||
highlight: (pathname: string, url: string) => pathname.includes(url),
|
||||
},
|
||||
"your-work": {
|
||||
key: "your_work",
|
||||
labelTranslationKey: "your_work",
|
||||
href: `/profile/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER],
|
||||
highlight: (pathname: string, url: string) => pathname.includes(url),
|
||||
},
|
||||
projects: {
|
||||
key: "projects",
|
||||
labelTranslationKey: "projects",
|
||||
href: `/projects/`,
|
||||
access: [EUserWorkspaceRoles.ADMIN, EUserWorkspaceRoles.MEMBER, EUserWorkspaceRoles.GUEST],
|
||||
highlight: (pathname: string, url: string) => pathname === url,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
/** @type {import("eslint").Linter.Config} */
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "es5"
|
||||
}
|
||||
@@ -52,7 +52,11 @@ userController.registerRoutes(router);
|
||||
### WebSocket Controller
|
||||
|
||||
```typescript
|
||||
import { Controller, WebSocket, BaseWebSocketController } from "@plane/decorators";
|
||||
import {
|
||||
Controller,
|
||||
WebSocket,
|
||||
BaseWebSocketController,
|
||||
} from "@plane/decorators";
|
||||
import { Request } from "express";
|
||||
import { WebSocket as WS } from "ws";
|
||||
|
||||
|
||||
@@ -11,23 +11,35 @@
|
||||
"dist/**"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc --noEmit && tsup --minify",
|
||||
"dev": "tsup --watch",
|
||||
"check:lint": "eslint . --max-warnings 1",
|
||||
"build": "tsup src/index.ts --format esm,cjs --dts --external express,ws",
|
||||
"dev": "tsup src/index.ts --format esm,cjs --watch --dts --external express,ws",
|
||||
"check:lint": "eslint . --max-warnings 0",
|
||||
"check:types": "tsc --noEmit",
|
||||
"check:format": "prettier --check \"**/*.{ts,tsx,md,json,css,scss}\"",
|
||||
"fix:lint": "eslint . --fix",
|
||||
"fix:format": "prettier --write \"**/*.{ts,tsx,md,json,css,scss}\"",
|
||||
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.21.2",
|
||||
"reflect-metadata": "^0.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@plane/eslint-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.14.9",
|
||||
"@types/ws": "^8.5.10",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"tsup": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"express": ">=4.21.2",
|
||||
"ws": ">=8.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import type { RequestHandler, Router, Request } from "express";
|
||||
import type { WebSocket } from "ws";
|
||||
|
||||
import { RequestHandler, Router } from "express";
|
||||
import "reflect-metadata";
|
||||
|
||||
type HttpMethod = "get" | "post" | "put" | "delete" | "patch" | "options" | "head" | "ws";
|
||||
type HttpMethod =
|
||||
| "get"
|
||||
| "post"
|
||||
| "put"
|
||||
| "delete"
|
||||
| "patch"
|
||||
| "options"
|
||||
| "head"
|
||||
| "ws";
|
||||
|
||||
interface ControllerInstance {
|
||||
[key: string]: unknown;
|
||||
@@ -16,85 +22,40 @@ interface ControllerConstructor {
|
||||
|
||||
export function registerControllers(
|
||||
router: Router,
|
||||
controllers: ControllerConstructor[],
|
||||
dependencies: any[] = []
|
||||
Controller: ControllerConstructor,
|
||||
): void {
|
||||
controllers.forEach((Controller) => {
|
||||
// Create the controller instance with dependencies
|
||||
const instance = new Controller(...dependencies);
|
||||
|
||||
// Determine if it's a WebSocket controller or REST controller by checking
|
||||
// if it has any methods with the "ws" method metadata
|
||||
const isWebsocket = Object.getOwnPropertyNames(Controller.prototype).some((methodName) => {
|
||||
if (methodName === "constructor") return false;
|
||||
return Reflect.getMetadata("method", instance, methodName) === "ws";
|
||||
});
|
||||
|
||||
if (isWebsocket) {
|
||||
// Register as WebSocket controller
|
||||
// Pass the existing instance with dependencies to avoid creating a new instance without them
|
||||
registerWebSocketController(router, Controller, instance);
|
||||
} else {
|
||||
// Register as REST controller - doesn't accept an instance parameter
|
||||
registerRestController(router, Controller);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registerRestController(router: Router, Controller: ControllerConstructor): void {
|
||||
const instance = new Controller();
|
||||
const baseRoute = Reflect.getMetadata("baseRoute", Controller) as string;
|
||||
|
||||
Object.getOwnPropertyNames(Controller.prototype).forEach((methodName) => {
|
||||
if (methodName === "constructor") return; // Skip the constructor
|
||||
|
||||
const method = Reflect.getMetadata("method", instance, methodName) as HttpMethod;
|
||||
const method = Reflect.getMetadata(
|
||||
"method",
|
||||
instance,
|
||||
methodName,
|
||||
) as HttpMethod;
|
||||
const route = Reflect.getMetadata("route", instance, methodName) as string;
|
||||
const middlewares = (Reflect.getMetadata("middlewares", instance, methodName) as RequestHandler[]) || [];
|
||||
const middlewares =
|
||||
(Reflect.getMetadata(
|
||||
"middlewares",
|
||||
instance,
|
||||
methodName,
|
||||
) as RequestHandler[]) || [];
|
||||
|
||||
if (method && route) {
|
||||
const handler = instance[methodName] as unknown;
|
||||
|
||||
if (typeof handler === "function") {
|
||||
if (method !== "ws") {
|
||||
(router[method] as (path: string, ...handlers: RequestHandler[]) => void)(
|
||||
`${baseRoute}${route}`,
|
||||
...middlewares,
|
||||
handler.bind(instance)
|
||||
);
|
||||
(
|
||||
router[method] as (
|
||||
path: string,
|
||||
...handlers: RequestHandler[]
|
||||
) => void
|
||||
)(`${baseRoute}${route}`, ...middlewares, handler.bind(instance));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function registerWebSocketController(
|
||||
router: Router,
|
||||
Controller: ControllerConstructor,
|
||||
existingInstance?: ControllerInstance
|
||||
): void {
|
||||
const instance = existingInstance || new Controller();
|
||||
const baseRoute = Reflect.getMetadata("baseRoute", Controller) as string;
|
||||
|
||||
Object.getOwnPropertyNames(Controller.prototype).forEach((methodName) => {
|
||||
if (methodName === "constructor") return; // Skip the constructor
|
||||
|
||||
const method = Reflect.getMetadata("method", instance, methodName) as string;
|
||||
const route = Reflect.getMetadata("route", instance, methodName) as string;
|
||||
|
||||
if (method === "ws" && route) {
|
||||
const handler = instance[methodName] as unknown;
|
||||
|
||||
if (typeof handler === "function" && "ws" in router && typeof router.ws === "function") {
|
||||
router.ws(`${baseRoute}${route}`, (ws: WebSocket, req: Request) => {
|
||||
try {
|
||||
handler.call(instance, ws, req);
|
||||
} catch (error) {
|
||||
console.error(`WebSocket error in ${Controller.name}.${methodName}`, error);
|
||||
ws.close(1011, error instanceof Error ? error.message : "Internal server error");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ export { Controller, Middleware } from "./rest";
|
||||
export { Get, Post, Put, Patch, Delete } from "./rest";
|
||||
export { WebSocket } from "./websocket";
|
||||
export { registerControllers } from "./controller";
|
||||
export { registerWebSocketControllers } from "./websocket-controller";
|
||||
|
||||
// Also provide namespaced exports for better organization
|
||||
import * as RestDecorators from "./rest";
|
||||
|
||||
@@ -21,7 +21,9 @@ export function Controller(baseRoute: string = ""): ClassDecorator {
|
||||
* @param method HTTP method to handle
|
||||
* @returns Method decorator
|
||||
*/
|
||||
function createHttpMethodDecorator(method: RestMethod): (route: string) => MethodDecorator {
|
||||
function createHttpMethodDecorator(
|
||||
method: RestMethod,
|
||||
): (route: string) => MethodDecorator {
|
||||
return function (route: string): MethodDecorator {
|
||||
return function (target: object, propertyKey: string | symbol) {
|
||||
Reflect.defineMetadata("method", method, target, propertyKey);
|
||||
@@ -44,7 +46,8 @@ export const Delete = createHttpMethodDecorator("delete");
|
||||
*/
|
||||
export function Middleware(middleware: RequestHandler): MethodDecorator {
|
||||
return function (target: object, propertyKey: string | symbol) {
|
||||
const middlewares = Reflect.getMetadata("middlewares", target, propertyKey) || [];
|
||||
const middlewares =
|
||||
Reflect.getMetadata("middlewares", target, propertyKey) || [];
|
||||
middlewares.push(middleware);
|
||||
Reflect.defineMetadata("middlewares", middlewares, target, propertyKey);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Router, Request } from "express";
|
||||
import type { WebSocket } from "ws";
|
||||
import "reflect-metadata";
|
||||
|
||||
interface ControllerInstance {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface ControllerConstructor {
|
||||
new (...args: unknown[]): ControllerInstance;
|
||||
prototype: ControllerInstance;
|
||||
}
|
||||
|
||||
export function registerWebSocketControllers(
|
||||
router: Router,
|
||||
Controller: ControllerConstructor,
|
||||
existingInstance?: ControllerInstance,
|
||||
): void {
|
||||
const instance = existingInstance || new Controller();
|
||||
const baseRoute = Reflect.getMetadata("baseRoute", Controller) as string;
|
||||
|
||||
Object.getOwnPropertyNames(Controller.prototype).forEach((methodName) => {
|
||||
if (methodName === "constructor") return; // Skip the constructor
|
||||
|
||||
const method = Reflect.getMetadata(
|
||||
"method",
|
||||
instance,
|
||||
methodName,
|
||||
) as string;
|
||||
const route = Reflect.getMetadata("route", instance, methodName) as string;
|
||||
|
||||
if (method === "ws" && route) {
|
||||
const handler = instance[methodName] as unknown;
|
||||
|
||||
if (
|
||||
typeof handler === "function" &&
|
||||
"ws" in router &&
|
||||
typeof router.ws === "function"
|
||||
) {
|
||||
router.ws(`${baseRoute}${route}`, (ws: WebSocket, req: Request) => {
|
||||
try {
|
||||
handler.call(instance, ws, req);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`WebSocket error in ${Controller.name}.${methodName}`,
|
||||
error,
|
||||
);
|
||||
ws.close(
|
||||
1011,
|
||||
error instanceof Error ? error.message : "Internal server error",
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Base controller class for WebSocket endpoints
|
||||
*/
|
||||
export abstract class BaseWebSocketController {
|
||||
protected router: Router;
|
||||
|
||||
constructor() {
|
||||
this.router = Router();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base route for this controller
|
||||
*/
|
||||
protected getBaseRoute(): string {
|
||||
return Reflect.getMetadata("baseRoute", this.constructor) || "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Abstract method to handle WebSocket connections
|
||||
* Implement this in your derived class
|
||||
*/
|
||||
abstract handleConnection(ws: WebSocket, req: Request): void;
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"lib": ["ES2020"],
|
||||
"rootDir": ".",
|
||||
"baseUrl": ".",
|
||||
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
node_modules
|
||||
build/*
|
||||
dist/*
|
||||
out/*
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: ["@plane/eslint-config/library.js"],
|
||||
parser: "@typescript-eslint/parser",
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@plane/editor",
|
||||
"version": "1.0.0",
|
||||
"version": "0.28.0",
|
||||
"description": "Core Editor that powers Plane",
|
||||
"license": "AGPL-3.0",
|
||||
"private": true,
|
||||
@@ -32,8 +32,8 @@
|
||||
"clean": "rm -rf .turbo && rm -rf .next && rm -rf node_modules && rm -rf dist"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "catalog:",
|
||||
"react-dom": "catalog:"
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "18.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^1.7.1",
|
||||
@@ -85,11 +85,11 @@
|
||||
"@plane/tailwind-config": "workspace:*",
|
||||
"@plane/typescript-config": "workspace:*",
|
||||
"@types/node": "18.15.3",
|
||||
"@types/react": "catalog:",
|
||||
"@types/react-dom": "catalog:",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.2.18",
|
||||
"postcss": "^8.4.38",
|
||||
"tsup": "catalog:",
|
||||
"typescript": "catalog:"
|
||||
"tsup": "8.4.0",
|
||||
"typescript": "5.8.3"
|
||||
},
|
||||
"keywords": [
|
||||
"editor",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
import { Extensions } from "@tiptap/core";
|
||||
|
||||
export const CoreEditorAdditionalExtensionsWithoutProps: Extensions = [];
|
||||
@@ -1,3 +0,0 @@
|
||||
import { Extensions } from "@tiptap/core";
|
||||
|
||||
export const PARSER_KIT_ADDITIONAL_EXTENSIONS: Extensions = [];
|
||||
@@ -1,10 +1,13 @@
|
||||
import React from "react";
|
||||
import type { Extensions } from "@tiptap/core";
|
||||
import React, { useMemo } from "react";
|
||||
// plane imports
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
import { PageRenderer } from "@/components/editors";
|
||||
// constants
|
||||
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
||||
// extensions
|
||||
import { WorkItemEmbedExtension } from "@/extensions";
|
||||
// helpers
|
||||
import { getEditorClassNames } from "@/helpers/common";
|
||||
// hooks
|
||||
@@ -18,7 +21,7 @@ const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> =
|
||||
bubbleMenuEnabled = true,
|
||||
containerClassName,
|
||||
documentLoaderClassName,
|
||||
extensions,
|
||||
extensions: externalExtensions = [],
|
||||
disabledExtensions,
|
||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editable,
|
||||
@@ -44,6 +47,20 @@ const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> =
|
||||
user,
|
||||
} = props;
|
||||
|
||||
const extensions: Extensions = useMemo(() => {
|
||||
const allExtensions = [...externalExtensions];
|
||||
|
||||
if (embedHandler?.issue) {
|
||||
allExtensions.push(
|
||||
WorkItemEmbedExtension({
|
||||
widgetCallback: embedHandler.issue.widgetCallback,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return allExtensions;
|
||||
}, [externalExtensions, embedHandler.issue]);
|
||||
|
||||
// use document editor
|
||||
const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeEditor({
|
||||
disabledExtensions,
|
||||
|
||||
@@ -7,7 +7,7 @@ import { PageRenderer } from "@/components/editors";
|
||||
// constants
|
||||
import { DEFAULT_DISPLAY_CONFIG } from "@/constants/config";
|
||||
// extensions
|
||||
import { HeadingListExtension, SideMenuExtension } from "@/extensions";
|
||||
import { HeadingListExtension, WorkItemEmbedExtension, SideMenuExtension } from "@/extensions";
|
||||
// helpers
|
||||
import { getEditorClassNames } from "@/helpers/common";
|
||||
// hooks
|
||||
@@ -39,6 +39,13 @@ const DocumentEditor = (props: IDocumentEditorProps) => {
|
||||
} = props;
|
||||
const extensions: Extensions = useMemo(() => {
|
||||
const additionalExtensions: Extensions = [];
|
||||
if (embedHandler?.issue) {
|
||||
additionalExtensions.push(
|
||||
WorkItemEmbedExtension({
|
||||
widgetCallback: embedHandler.issue.widgetCallback,
|
||||
})
|
||||
);
|
||||
}
|
||||
additionalExtensions.push(
|
||||
SideMenuExtension({
|
||||
aiEnabled: !disabledExtensions?.includes("ai"),
|
||||
|
||||
@@ -6,7 +6,7 @@ import StarterKit from "@tiptap/starter-kit";
|
||||
// helpers
|
||||
import { isValidHttpUrl } from "@/helpers/common";
|
||||
// plane editor imports
|
||||
import { PARSER_KIT_ADDITIONAL_EXTENSIONS } from "@/plane-editor/extensions/parser-kit";
|
||||
import { CoreEditorAdditionalExtensionsWithoutProps } from "@/plane-editor/extensions/core/without-props";
|
||||
// extensions
|
||||
import { CustomCalloutExtensionConfig } from "./callout/extension-config";
|
||||
import { CustomCodeBlockExtensionWithoutProps } from "./code/without-props";
|
||||
@@ -21,8 +21,9 @@ import { CustomMentionExtensionConfig } from "./mentions/extension-config";
|
||||
import { CustomQuoteExtension } from "./quote";
|
||||
import { TableHeader, TableCell, TableRow, Table } from "./table";
|
||||
import { CustomTextAlignExtension } from "./text-align";
|
||||
import { WorkItemEmbedExtensionConfig } from "./work-item-embed/extension-config";
|
||||
|
||||
export const PARSER_KIT = [
|
||||
export const CoreEditorExtensionsWithoutProps = [
|
||||
StarterKit.configure({
|
||||
bulletList: {
|
||||
HTMLAttributes: {
|
||||
@@ -98,5 +99,7 @@ export const PARSER_KIT = [
|
||||
CustomTextAlignExtension,
|
||||
CustomCalloutExtensionConfig,
|
||||
CustomColorExtension,
|
||||
...PARSER_KIT_ADDITIONAL_EXTENSIONS,
|
||||
...CoreEditorAdditionalExtensionsWithoutProps,
|
||||
];
|
||||
|
||||
export const DocumentEditorExtensionsWithoutProps = [WorkItemEmbedExtensionConfig];
|
||||