diff --git a/.github/workflows/backend-tests.yml b/.github/workflows/backend-tests.yml index b691a8233c1..55ea64b90c0 100644 --- a/.github/workflows/backend-tests.yml +++ b/.github/workflows/backend-tests.yml @@ -27,7 +27,8 @@ jobs: strategy: fail-fast: false matrix: - node: [">=20.0.0 <21.0.0", ">=22.0.0 <23.0.0", ">=24.0.0 <25.0.0"] + # PRs: test on latest Node only. Push to develop: full matrix. + node: ${{ github.event_name == 'pull_request' && fromJSON('[">=24.0.0 <25.0.0"]') || fromJSON('[">=20.0.0 <21.0.0", ">=22.0.0 <23.0.0", ">=24.0.0 <25.0.0"]') }} steps: - name: Checkout repository @@ -83,7 +84,7 @@ jobs: strategy: fail-fast: false matrix: - node: [">=20.0.0 <21.0.0", ">=22.0.0 <23.0.0", ">=24.0.0 <25.0.0"] + node: ${{ github.event_name == 'pull_request' && fromJSON('[">=24.0.0 <25.0.0"]') || fromJSON('[">=20.0.0 <21.0.0", ">=22.0.0 <23.0.0", ">=24.0.0 <25.0.0"]') }} steps: - name: Checkout repository @@ -124,14 +125,13 @@ jobs: ep_author_hover ep_cursortrace ep_font_size - ep_hash_auth ep_headings2 ep_markdown ep_readonly_guest ep_set_title_on_pad ep_spellcheck ep_subscript_and_superscript - ep_table_of_contents --runtimeVersion="${{ matrix.node }}" + ep_table_of_contents - name: Run the backend tests run: gnpm test --runtimeVersion="${{ matrix.node }}" @@ -139,14 +139,12 @@ jobs: working-directory: src run: gnpm run test:vitest --runtimeVersion="${{ matrix.node }}" + # Windows tests only run on push to develop/master, not on PRs withoutpluginsWindows: env: PNPM_HOME: ~\\.pnpm-store - # run on pushes to any branch - # run on PRs from external forks if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + github.event_name != 'pull_request' strategy: fail-fast: false matrix: @@ -194,11 +192,8 @@ jobs: withpluginsWindows: env: PNPM_HOME: ~\\.pnpm-store - # run on pushes to any branch - # run on PRs from external forks if: | - (github.event_name != 'pull_request') - || (github.event.pull_request.head.repo.id != github.event.pull_request.base.repo.id) + github.event_name != 'pull_request' strategy: fail-fast: false matrix: @@ -232,22 +227,19 @@ jobs: run: gnpm build --runtimeVersion="${{ matrix.node }}" - name: Install Etherpad plugins - # The --legacy-peer-deps flag is required to work around a bug in npm - # v7: https://github.com/npm/cli/issues/2199 run: > gnpm install --workspace-root ep_align ep_author_hover ep_cursortrace ep_font_size - ep_hash_auth ep_headings2 ep_markdown ep_readonly_guest ep_set_title_on_pad ep_spellcheck ep_subscript_and_superscript - ep_table_of_contents --runtimeVersion="${{ matrix.node }}" + ep_table_of_contents # Etherpad core dependencies must be installed after installing the # plugin's dependencies, otherwise npm will try to hoist common # dependencies by removing them from src/node_modules and installing them diff --git a/.github/workflows/build-and-deploy-docs.yml b/.github/workflows/build-and-deploy-docs.yml index b0b87697a27..0d89eec6485 100644 --- a/.github/workflows/build-and-deploy-docs.yml +++ b/.github/workflows/build-and-deploy-docs.yml @@ -50,7 +50,7 @@ jobs: with: version: 0.0.12 - name: Setup Pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@v6 - name: Install dependencies run: gnpm install - name: Build app @@ -65,4 +65,4 @@ jobs: path: './doc/.vitepress/dist' - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 9cc63ebb3f3..3f668384868 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -30,13 +30,13 @@ jobs: - name: Set up QEMU if: github.event_name == 'push' - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Build and export to Docker - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: ./etherpad target: production @@ -82,7 +82,7 @@ jobs: name: Docker meta if: github.event_name == 'push' id: meta - uses: docker/metadata-action@v5 + uses: docker/metadata-action@v6 with: images: etherpad/etherpad tags: | @@ -93,7 +93,7 @@ jobs: - name: Log in to Docker Hub if: github.event_name == 'push' - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -101,7 +101,7 @@ jobs: name: Build and push id: build-docker if: github.event_name == 'push' - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: ./etherpad target: production diff --git a/.github/workflows/frontend-admin-tests.yml b/.github/workflows/frontend-admin-tests.yml index 7c980bab3dc..f31f442f889 100644 --- a/.github/workflows/frontend-admin-tests.yml +++ b/.github/workflows/frontend-admin-tests.yml @@ -1,13 +1,15 @@ -# Leave the powered by Sauce Labs bit in as this means we get additional concurrency name: "Frontend admin tests" on: push: paths-ignore: - 'doc/**' + pull_request: + paths-ignore: + - 'doc/**' permissions: - contents: read # to fetch code (actions/checkout) + contents: read jobs: withplugins: @@ -19,15 +21,10 @@ jobs: strategy: fail-fast: false matrix: - node: [20, 22, 24] + # PRs: single Node version. Push: full matrix. + node: ${{ github.event_name == 'pull_request' && fromJSON('[24]') || fromJSON('[20, 22, 24]') }} steps: - - - name: Generate Sauce Labs strings - id: sauce_strings - run: | - printf %s\\n '::set-output name=name::${{ github.workflow }} - ${{ github.job }} - Node ${{ matrix.node }}' - printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}-node${{ matrix.node }}' - name: Checkout repository uses: actions/checkout@v6 @@ -47,47 +44,26 @@ jobs: uses: SamTV12345/gnpm-setup@main with: version: 0.0.12 - - name: Cache playwright binaries - uses: actions/cache@v5 - id: playwright-cache - with: - path: | - ~/.cache/ms-playwright - key: ${{ runner.os }}-playwright-${{ env.PLAYWRIGHT_VERSION }} - #- - # name: Install etherpad plugins - # # We intentionally install an old ep_align version to test upgrades to - # # the minor version number. The --legacy-peer-deps flag is required to - # # work around a bug in npm v7: https://github.com/npm/cli/issues/2199 - # run: pnpm install --workspace-root ep_align@0.2.27 - # Etherpad core dependencies must be installed after installing the - # plugin's dependencies, otherwise npm will try to hoist common - # dependencies by removing them from src/node_modules and installing them - # in the top-level node_modules. As of v6.14.10, npm's hoist logic appears - # to be buggy, because it sometimes removes dependencies from - # src/node_modules but fails to add them to the top-level node_modules. - # Even if npm correctly hoists the dependencies, the hoisting seems to - # confuse tools such as `npm outdated`, `npm update`, and some ESLint - # rules. - name: Install all dependencies and symlink for ep_etherpad-lite run: gnpm i --runtimeVersion="${{ matrix.node }}" - #- - # name: Install etherpad plugins - # run: rm -Rf node_modules/ep_align/static/tests/* - - - name: export GIT_HASH to env - id: environment - run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" + - name: Cache Playwright browsers + uses: actions/cache@v5 + id: playwright-cache + with: + path: ~/.cache/ms-playwright + key: ${{ runner.os }}-playwright-${{ hashFiles('src/package.json') }} + - name: Install Playwright browsers + if: steps.playwright-cache.outputs.cache-hit != 'true' + run: cd src && npx playwright install + - name: Install Playwright system dependencies + run: cd src && npx playwright install-deps - name: Create settings.json run: cp settings.json.template settings.json - name: Write custom settings.json that enables the Admin UI tests run: "sed -i 's/\"enableAdminUITests\": false/\"enableAdminUITests\": true,\\n\"users\":{\"admin\":{\"password\":\"changeme1\",\"is_admin\":true}}/' settings.json" - - - name: increase maxHttpBufferSize - run: "sed -i 's/\"maxHttpBufferSize\": 50000/\"maxHttpBufferSize\": 10000000/' settings.json" - name: Disable import/export rate limiting run: | @@ -96,37 +72,10 @@ jobs: working-directory: admin run: | gnpm run build --runtimeVersion="${{ matrix.node }}" - # name: Run the frontend admin tests - # shell: bash - # env: - # SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} - # SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} - # SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }} - # TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }} - # GIT_HASH: ${{ steps.environment.outputs.sha_short }} - # run: | - # src/tests/frontend/travis/adminrunner.sh - #- - # uses: saucelabs/sauce-connect-action@v2.3.6 - # with: - # username: ${{ secrets.SAUCE_USERNAME }} - # accessKey: ${{ secrets.SAUCE_ACCESS_KEY }} - # tunnelIdentifier: ${{ steps.sauce_strings.outputs.tunnel_id }} - #- - # name: Run the frontend admin tests - # shell: bash - # env: - # SAUCE_USERNAME: ${{ secrets.SAUCE_USERNAME }} - # SAUCE_ACCESS_KEY: ${{ secrets.SAUCE_ACCESS_KEY }} - # SAUCE_NAME: ${{ steps.sauce_strings.outputs.name }} - # TRAVIS_JOB_NUMBER: ${{ steps.sauce_strings.outputs.tunnel_id }} - # GIT_HASH: ${{ steps.environment.outputs.sha_short }} - # run: | - # src/tests/frontend/travis/adminrunner.sh - name: Run the frontend admin tests shell: bash run: | - gnpm run prod --runtimeVersion="${{ matrix.node }}" & + gnpm run prod --runtimeVersion="${{ matrix.node }}" > /tmp/etherpad-server.log 2>&1 & connected=false can_connect() { curl -sSfo /dev/null http://localhost:9001/ || return 1 @@ -138,10 +87,15 @@ jobs: sleep 1 done cd src - gnpm exec playwright install --runtimeVersion="${{ matrix.node }}" - gnpm exec playwright install-deps --runtimeVersion="${{ matrix.node }}" gnpm run test-admin --runtimeVersion="${{ matrix.node }}" - - uses: actions/upload-artifact@v6 + - name: Upload server log on failure + uses: actions/upload-artifact@v7 + if: failure() + with: + name: server-log-admin-${{ matrix.node }} + path: /tmp/etherpad-server.log + retention-days: 7 + - uses: actions/upload-artifact@v7 if: always() with: name: playwright-report-${{ matrix.node }} diff --git a/.github/workflows/frontend-tests.yml b/.github/workflows/frontend-tests.yml index 0b95070f4ef..da5ce0a3be8 100644 --- a/.github/workflows/frontend-tests.yml +++ b/.github/workflows/frontend-tests.yml @@ -1,13 +1,15 @@ -# Leave the powered by Sauce Labs bit in as this means we get additional concurrency -name: "Frontend tests powered by Sauce Labs" +name: "Frontend tests" on: push: paths-ignore: - 'doc/**' + pull_request: + paths-ignore: + - 'doc/**' permissions: - contents: read # to fetch code (actions/checkout) + contents: read jobs: playwright-chrome: @@ -16,12 +18,6 @@ jobs: name: Playwright Chrome runs-on: ubuntu-latest steps: - - - name: Generate Sauce Labs strings - id: sauce_strings - run: | - printf %s\\n '::set-output name=name::${{ github.workflow }} - ${{ github.job }}' - printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}' - name: Checkout repository uses: actions/checkout@v6 @@ -45,17 +41,13 @@ jobs: - name: Install all dependencies and symlink for ep_etherpad-lite run: gnpm install --frozen-lockfile - - - name: export GIT_HASH to env - id: environment - run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" - name: Create settings.json run: cp ./src/tests/settings.json settings.json - name: Run the frontend tests shell: bash run: | - gnpm run prod & + gnpm run prod > /tmp/etherpad-server.log 2>&1 & connected=false can_connect() { curl -sSfo /dev/null http://localhost:9001/ || return 1 @@ -69,10 +61,17 @@ jobs: cd src gnpm exec playwright install chromium --with-deps gnpm run test-ui --project=chromium - - uses: actions/upload-artifact@v6 + - name: Upload server log on failure + uses: actions/upload-artifact@v7 + if: failure() + with: + name: server-log-chrome + path: /tmp/etherpad-server.log + retention-days: 7 + - uses: actions/upload-artifact@v7 if: always() with: - name: playwright-report-${{ matrix.node }}-chrome + name: playwright-report-chrome path: src/playwright-report/ retention-days: 30 playwright-firefox: @@ -81,11 +80,6 @@ jobs: name: Playwright Firefox runs-on: ubuntu-latest steps: - - name: Generate Sauce Labs strings - id: sauce_strings - run: | - printf %s\\n '::set-output name=name::${{ github.workflow }} - ${{ github.job }}' - printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}' - name: Checkout repository uses: actions/checkout@v6 - uses: actions/cache@v5 @@ -107,15 +101,12 @@ jobs: version: 0.0.12 - name: Install all dependencies and symlink for ep_etherpad-lite run: gnpm install --frozen-lockfile - - name: export GIT_HASH to env - id: environment - run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" - name: Create settings.json run: cp ./src/tests/settings.json settings.json - name: Run the frontend tests shell: bash run: | - gnpm run prod & + gnpm run prod > /tmp/etherpad-server.log 2>&1 & connected=false can_connect() { curl -sSfo /dev/null http://localhost:9001/ || return 1 @@ -129,76 +120,16 @@ jobs: cd src gnpm exec playwright install firefox --with-deps gnpm run test-ui --project=firefox - - uses: actions/upload-artifact@v6 - if: always() - with: - name: playwright-report-${{ matrix.node }}-firefox - path: src/playwright-report/ - retention-days: 30 - playwright-webkit: - name: Playwright Webkit - runs-on: ubuntu-latest - env: - PNPM_HOME: ~/.pnpm-store - steps: - - - name: Generate Sauce Labs strings - id: sauce_strings - run: | - printf %s\\n '::set-output name=name::${{ github.workflow }} - ${{ github.job }}' - printf %s\\n '::set-output name=tunnel_id::${{ github.run_id }}-${{ github.run_number }}-${{ github.job }}' - - - name: Checkout repository - uses: actions/checkout@v6 - - uses: actions/cache@v5 - name: Setup gnpm cache - if: always() - with: - path: | - ${{ env.PNPM_HOME }} - ~/.local/share/gnpm - ~/.cache/ms-playwright - /usr/local/bin/gnpm - /usr/local/bin/gnpm-0.0.12 - key: ${{ runner.os }}-gnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - restore-keys: ${{ runner.os }}-gnpm-store- - - name: Setup gnpm - uses: SamTV12345/gnpm-setup@main + - name: Upload server log on failure + uses: actions/upload-artifact@v7 + if: failure() with: - version: 0.0.12 - - - name: Install all dependencies and symlink for ep_etherpad-lite - run: gnpm install --frozen-lockfile - - - name: export GIT_HASH to env - id: environment - run: echo "::set-output name=sha_short::$(git rev-parse --short ${{ github.sha }})" - - - name: Create settings.json - run: cp ./src/tests/settings.json settings.json - - name: Run the frontend tests - shell: bash - run: | - gnpm run prod & - connected=false - can_connect() { - curl -sSfo /dev/null http://localhost:9001/ || return 1 - connected=true - } - now() { date +%s; } - start=$(now) - while [ $(($(now) - $start)) -le 15 ] && ! can_connect; do - sleep 1 - done - cd src - gnpm exec playwright install webkit --with-deps - gnpm run test-ui --project=webkit || true - - uses: actions/upload-artifact@v6 + name: server-log-firefox + path: /tmp/etherpad-server.log + retention-days: 7 + - uses: actions/upload-artifact@v7 if: always() with: - name: playwright-report-${{ matrix.node }}-webkit + name: playwright-report-firefox path: src/playwright-report/ retention-days: 30 - - - diff --git a/.github/workflows/load-test.yml b/.github/workflows/load-test.yml index 12bacdcf8e3..46a960aab59 100644 --- a/.github/workflows/load-test.yml +++ b/.github/workflows/load-test.yml @@ -1,13 +1,9 @@ name: "Loadtest" -# any branch is useful for testing before a PR is submitted on: - push: - paths-ignore: - - "doc/**" - pull_request: - paths-ignore: - - "doc/**" + schedule: + - cron: '0 8 * * *' # Daily at 08:00 UTC + workflow_dispatch: # Allow manual trigger permissions: contents: read diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e769b550829..d77f94641cb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -75,7 +75,7 @@ jobs: with: ruby-version: 2.7 - - uses: reitzig/actions-asciidoctor@v2.0.3 + - uses: reitzig/actions-asciidoctor@v2.0.4 with: version: 2.0.18 - name: Prepare release diff --git a/.github/workflows/update-plugins.yml b/.github/workflows/update-plugins.yml new file mode 100644 index 00000000000..2944877a120 --- /dev/null +++ b/.github/workflows/update-plugins.yml @@ -0,0 +1,79 @@ +name: Update Plugins + +on: + schedule: + - cron: '0 6 * * *' # Daily at 06:00 UTC + workflow_dispatch: # Allow manual trigger + +jobs: + update-plugins: + runs-on: ubuntu-latest + steps: + - name: Check out etherpad-lite + uses: actions/checkout@v6 + + - uses: pnpm/action-setup@v5 + name: Install pnpm + with: + version: 10 + run_install: false + + - name: Use Node.js + uses: actions/setup-node@v6 + with: + node-version: 22 + + - name: Install bin dependencies + working-directory: ./bin + run: pnpm install + + - name: Configure git + run: | + git config --global user.name 'github-actions[bot]' + git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com' + + - name: Clone and update all plugins + env: + GH_TOKEN: ${{ secrets.PLUGINS_PAT }} + run: | + # Configure git to use the PAT for all ether/ repos + git config --global url."https://x-access-token:${GH_TOKEN}@github.com/ether/".insteadOf "https://github.com/ether/" + + cd .. + # List all ep_* repos from ether org + plugins=$(gh repo list ether --limit 200 --json name --jq '.[] | select(.name | startswith("ep_")) | .name') + + failed="" + succeeded="" + skipped="" + + for plugin in $plugins; do + echo "============================================================" + echo "Processing $plugin" + echo "============================================================" + + # Clone if not present + if [ ! -d "$plugin" ]; then + git clone "https://github.com/ether/${plugin}.git" "$plugin" || { echo "SKIP: clone failed"; skipped="$skipped $plugin"; continue; } + fi + + # Pull latest + (cd "$plugin" && git pull --ff-only) || { echo "SKIP: pull failed"; skipped="$skipped $plugin"; continue; } + + # Run checkPlugin with autopush — continue on failure + if cd etherpad-lite/bin && pnpm run checkPlugin "$plugin" autopush 2>&1; then + succeeded="$succeeded $plugin" + else + echo "WARN: checkPlugin failed for $plugin" + failed="$failed $plugin" + fi + cd ../.. + done + + echo "" + echo "============================================================" + echo "SUMMARY" + echo "============================================================" + echo "Succeeded:$(echo $succeeded | wc -w) -$succeeded" + echo "Failed:$(echo $failed | wc -w) -$failed" + echo "Skipped:$(echo $skipped | wc -w) -$skipped" diff --git a/.github/workflows/upgrade-from-latest-release.yml b/.github/workflows/upgrade-from-latest-release.yml index eb480c25624..c60af163a0a 100644 --- a/.github/workflows/upgrade-from-latest-release.yml +++ b/.github/workflows/upgrade-from-latest-release.yml @@ -27,7 +27,8 @@ jobs: strategy: fail-fast: false matrix: - node: [20, 22, 24] + # PRs: single Node version. Push: full matrix. + node: ${{ github.event_name == 'pull_request' && fromJSON('[24]') || fromJSON('[20, 22, 24]') }} steps: - name: Check out latest release @@ -56,12 +57,6 @@ jobs: with: packages: libreoffice libreoffice-pdfimport version: 1.0 - - - name: Install libreoffice - uses: awalsh128/cache-apt-pkgs-action@v1.6.0 - with: - packages: libreoffice libreoffice-pdfimport - version: 1.0 - name: Install all dependencies and symlink for ep_etherpad-lite run: gnpm install --frozen-lockfile --runtimeVersion="${{ matrix.node }}" diff --git a/.npmrc b/.npmrc deleted file mode 100644 index f301fedf982..00000000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -auto-install-peers=false diff --git a/.pr_agent.toml b/.pr_agent.toml new file mode 100644 index 00000000000..93e106ea25a --- /dev/null +++ b/.pr_agent.toml @@ -0,0 +1,5 @@ +[pr_reviewer] +run_on_pr_sync = true + +[pr_description] +run_on_pr_sync = true diff --git a/AGENTS.MD b/AGENTS.MD new file mode 100644 index 00000000000..491c7655070 --- /dev/null +++ b/AGENTS.MD @@ -0,0 +1,207 @@ +# Agent Guide - Etherpad + +Welcome to the Etherpad project. This guide provides essential context and instructions for AI agents and developers to effectively contribute to the codebase. + +## Project Overview +Etherpad is a real-time collaborative editor designed to be lightweight, scalable, and highly extensible via plugins. + +## Technical Stack +- **Runtime:** Node.js >= 20.0.0 +- **Package Manager:** pnpm (>= 8.3.0) +- **Languages:** TypeScript (primary for new code), JavaScript (legacy), CSS, HTML +- **Backend:** Express.js 5, Socket.io 4 +- **Frontend:** Legacy core (`src/static`), Modern React UI (`ui/`), Admin UI (`admin/`) +- **Database:** ueberdb2 abstraction (supports dirtyDB, MySQL, PostgreSQL, Redis) +- **Build Tools:** Vite (for `ui` and `admin`), esbuild, tsx +- **Testing:** Mocha (backend), Playwright (frontend E2E), Vitest (unit) +- **Auth:** JWT (jose library), OIDC provider + +## Directory Structure +- `src/node/` - Backend logic, API handlers, database models, hooks +- `src/static/` - Core frontend logic (legacy jQuery-based editor) +- `src/static/js/pluginfw/` - Plugin framework (installer, hook system) +- `src/tests/` - Test suites (backend, frontend, container) +- `ui/` - Modern React OIDC login UI (Vite + TypeScript) +- `admin/` - Modern React admin panel (Vite + TypeScript + Radix UI) +- `bin/` - CLI utilities, build scripts, plugin management tools +- `bin/plugins/` - Plugin maintenance scripts (checkPlugin.ts, updateCorePlugins.sh) +- `doc/` - Documentation (VitePress + Markdown/AsciiDoc) +- `local_plugins/` - Directory for developing and testing plugins locally +- `var/` - Runtime data (logs, dirtyDB, etc. - ignored by git) + +## Quick Start + +```bash +pnpm install # Install all dependencies +pnpm run build:etherpad # Build admin UI and static assets +pnpm --filter ep_etherpad-lite run dev # Start dev server (port 9001) +pnpm --filter ep_etherpad-lite run prod # Start production server +``` + +## Core Mandates & Conventions + +### Coding Style +- **Indentation:** 2 spaces for all files (JS/TS/CSS/HTML). No tabs. +- **TypeScript:** All new code should be TypeScript. Strict mode is enabled. +- **Comments:** Provide clear comments for complex logic only. +- **Backward Compatibility:** Always ensure compatibility with older versions of the database and configuration files. + +### Development Workflow +- **Branching:** Work in feature branches. Issue PRs against the `develop` branch. Never PR directly to `master`. +- **Commits:** Maintain a linear history (no merge commits). Use meaningful messages in the format: `submodule: description`. +- **Feature Flags:** New features should be placed behind feature flags and disabled by default. +- **Deprecation:** Never remove features abruptly; deprecate them first with a `WARN` log. +- **Forks:** For etherpad-lite changes, commit to `johnmclear/etherpad-lite` fork on a new branch, then PR to `ether/etherpad-lite`. For plugins (`ep_*` repos), committing directly is acceptable. + +### Testing & Validation +- **Requirement:** Every bug fix MUST include a regression test in the same commit. +- **Always run tests locally before pushing to CI.** +- **Linting:** `pnpm run lint` +- **Type Check:** `pnpm --filter ep_etherpad-lite run ts-check` +- **Build:** `pnpm run build:etherpad` before production deployment + +#### Running Backend Tests Locally + +Backend tests use Mocha with tsx and run against a real server instance (started automatically by the test harness). No separate server process is needed. + +```bash +# Run ALL backend tests (includes plugin tests) +pnpm --filter ep_etherpad-lite run test + +# Run only utility tests (faster, ~5s timeout) +pnpm --filter ep_etherpad-lite run test-utils + +# Run a single test file directly +cd src && cross-env NODE_ENV=production npx mocha --import=tsx --timeout 120000 tests/backend/specs/YOUR_TEST.ts + +# Run unit tests (Vitest) +cd src && npx vitest +``` + +- Tests run with `NODE_ENV=production`. +- Default timeout is 120 seconds per test. +- Test files live in `src/tests/backend/specs/`. +- Plugin backend tests live in `node_modules/ep_*/static/tests/backend/specs/` (at repo root) and are included automatically by the test script. + +#### Running Frontend E2E Tests Locally + +Frontend tests use Playwright. **You must have a running Etherpad server** before launching them — the Playwright config does not auto-start the server. + +**Before running frontend or admin tests, ensure Playwright browsers are installed.** Check and install if needed: + +```bash +# Check which browsers are installed +cd src && npx playwright install --dry-run + +# Install all browsers and their system dependencies (must run from src/) +cd src && npx playwright install +cd src && sudo npx playwright install-deps +``` + +If `sudo` is unavailable, install system dependencies for webkit manually: +```bash +# Check which system libraries are missing for webkit +ldd ~/.cache/ms-playwright/webkit-*/minibrowser-wpe/MiniBrowser 2>&1 | grep "not found" +``` + +If browsers or system dependencies are missing, tests will fail silently or timeout — **always verify browser installation before debugging test failures.** + +```bash +# 1. Start the dev server in a separate terminal +pnpm --filter ep_etherpad-lite run dev + +# 2. Run frontend E2E tests +pnpm --filter ep_etherpad-lite run test-ui + +# 3. Run with interactive Playwright UI (useful for debugging) +pnpm --filter ep_etherpad-lite run test-ui:ui + +# Run a single test file +cd src && cross-env NODE_ENV=production npx playwright test tests/frontend-new/specs/YOUR_TEST.spec.ts +``` + +- Tests expect the server at `localhost:9001`. +- Test files live in `src/tests/frontend-new/specs/`. +- Runs against chromium and firefox by default (webkit is disabled). +- Playwright config is at `src/playwright.config.ts`. + +#### Running Admin Panel Tests Locally + +```bash +# Requires a running server and Playwright browsers installed (same as frontend tests) +pnpm --filter ep_etherpad-lite run test-admin + +# Interactive UI mode +pnpm --filter ep_etherpad-lite run test-admin:ui +``` + +- Admin tests run with `--workers 1` (sequential) on chromium and firefox only. +- Test files live in `src/tests/frontend-new/admin-spec/`. + +### Backend Test Auth +Tests use JWT authentication, not API keys. Pattern: +```typescript +import * as common from 'ep_etherpad-lite/tests/backend/common'; + +const agent = await common.init(); // Starts server, returns supertest agent +const token = await common.generateJWTToken(); +agent.get('/api/1/endpoint').set('authorization', token); +``` +Do not use `APIKEY.txt` — it may not exist in the test environment. + +## Key Concepts + +### Easysync +The real-time synchronization engine. It is complex; refer to `doc/public/easysync/` before modifying core synchronization logic. + +### Plugin Framework +Most functionality should be implemented as plugins (`ep_*`). Avoid modifying the core unless absolutely necessary. + +**Plugin structure:** +``` +ep_myplugin/ +├── ep.json # Hook declarations (server_hooks, client_hooks) +├── index.js # Server-side hook implementations +├── package.json +├── static/ +│ ├── js/ # Client-side code +│ ├── css/ +│ └── tests/ +│ ├── backend/specs/ # Backend tests (Mocha) +│ └── frontend-new/ # Frontend tests (Playwright) +├── templates/ # EJS templates +└── locales/ # i18n files +``` + +**Plugin management:** +```bash +pnpm run plugins i ep_plugin_name # Install from npm +pnpm run plugins i --path ../plugin # Install from local path +pnpm run plugins rm ep_plugin_name # Remove +pnpm run plugins ls # List installed +``` + +**Plugin installation internals:** Plugins are installed to `src/plugin_packages/` via `live-plugin-manager`, which stores them at `src/plugin_packages/.versions/ep_name@version/`. Symlinks are created: `src/node_modules/ep_name` → `src/plugin_packages/ep_name` → `.versions/ep_name@ver/`. + +### Plugin Repositories +- **Monorepo:** `ether/ether-plugins` contains 80+ plugins with shared CI/publishing +- **Standalone repos:** Individual `ether/ep_*` repos still exist for many plugins +- **Plugin CI templates:** `bin/plugins/lib/` contains workflow templates pushed to standalone plugin repos via `checkPlugin.ts` +- **Shared pipelines:** `ether/ether-pipelines` contains reusable GitHub Actions workflows for plugin CI + +### Settings +Configured via `settings.json`. A template is available at `settings.json.template`. Environment variables can override any setting using `"${ENV_VAR}"` or `"${ENV_VAR:default_value}"`. + +## Monorepo Structure + +This project uses pnpm workspaces. The workspaces are: +- `src/` - Core Etherpad (package: `ep_etherpad-lite`) +- `bin/` - CLI tools and plugin scripts +- `ui/` - Login UI +- `admin/` - Admin panel +- `doc/` - Documentation + +Root-level commands operate across all workspaces. Use `pnpm --filter ` to target specific workspaces. + +## AI-Specific Guidance +AI/Agent contributions are explicitly welcomed by the maintainers, provided they strictly adhere to the guidelines in `CONTRIBUTING.md` and this guide. Always prioritize stability, readability, and compatibility. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 57ff5740306..2a6f384fc4f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,8 @@ # Contributor Guidelines (Please talk to people on the mailing list before you change this page, see our section on [how to get in touch](https://github.com/ether/etherpad-lite#get-in-touch)) +**We have decided that LLM/Agent/AI contributions are fine as long as they are within the instructions set out by this document.** + ## Pull requests * the commit series in the PR should be _linear_ (it **should not contain merge commits**). This is necessary because we want to be able to [bisect](https://en.wikipedia.org/wiki/Bisection_(software_engineering)) bugs easily. Rewrite history/perform a rebase if necessary @@ -81,7 +83,7 @@ Also, keep it maintainable. We don't wanna end up as the monster Etherpad was! ## Coding style * Do write comments. (You don't have to comment every line, but if you come up with something that's a bit complex/weird, just leave a comment. Bear in mind that you will probably leave the project at some point and that other people will read your code. Undocumented huge amounts of code are worthless!) * Never ever use tabs -* Indentation: JS/CSS: 2 spaces; HTML: 4 spaces +* Indentation: 2 spaces * Don't overengineer. Don't try to solve any possible problem in one step, but try to solve problems as easy as possible and improve the solution over time! * Do generalize sooner or later! (if an old solution, quickly hacked together, poses more problems than it solves today, refactor it!) * Keep it compatible. Do not introduce changes to the public API, db schema or configurations too lightly. Don't make incompatible changes without good reasons! diff --git a/Dockerfile b/Dockerfile index ee0d15a2c40..d3374296672 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,8 @@ # https://github.com/ether/etherpad-lite # # Author: muxator +# Set to "copy" for builds without git metadata (source tarballs, some CI): +# docker build --build-arg BUILD_ENV=copy . ARG BUILD_ENV=git ARG PnpmVersion=10.28.2 @@ -125,8 +127,13 @@ COPY --chown=etherpad:etherpad ./pnpm-workspace.yaml ./package.json ./ FROM build AS build_git -ONBUILD COPY --chown=etherpad:etherpad ./.git/HEA[D] ./.git/HEAD -ONBUILD COPY --chown=etherpad:etherpad ./.git/ref[s] ./.git/refs +# When checked out as a git submodule, .git is a file (gitlink) instead of a +# directory, so .git/HEAD and .git/refs do not exist. Copy the whole .git +# entry (the .dockerignore already strips the heavy objects) and normalise it +# with a shell step so the build succeeds in both cases and across builders +# (Docker, buildah, podman). See #6663 and containers/buildah#5742. +ONBUILD COPY --chown=etherpad:etherpad ./.git ./.git +ONBUILD RUN if [ -f .git ]; then rm .git; fi FROM build AS build_copy diff --git a/README.md b/README.md index 3719ecda70a..492ad015d3c 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ volumes: ### Requirements -[Node.js](https://nodejs.org/) >= **18.18.2**. +[Node.js](https://nodejs.org/). ### Windows, macOS, Linux diff --git a/admin/package.json b/admin/package.json index 7be9eac6fee..dd0da030849 100644 --- a/admin/package.json +++ b/admin/package.json @@ -18,27 +18,27 @@ "@radix-ui/react-toast": "^1.2.15", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@typescript-eslint/eslint-plugin": "^8.55.0", - "@typescript-eslint/parser": "^8.55.0", - "@vitejs/plugin-react": "^5.1.4", + "@typescript-eslint/eslint-plugin": "^8.58.0", + "@typescript-eslint/parser": "^8.58.0", + "@vitejs/plugin-react": "^6.0.1", "babel-plugin-react-compiler": "19.1.0-rc.3", - "eslint": "^10.0.0", + "eslint": "^10.2.0", "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.5.0", - "i18next": "^25.8.8", + "eslint-plugin-react-refresh": "^0.5.2", + "i18next": "^26.0.3", "i18next-browser-languagedetector": "^8.2.1", - "lucide-react": "^0.564.0", + "lucide-react": "^1.7.0", "react": "^19.2.4", "react-dom": "^19.2.4", - "react-hook-form": "^7.71.1", - "react-i18next": "^16.5.4", - "react-router-dom": "^7.13.0", + "react-hook-form": "^7.72.1", + "react-i18next": "^17.0.2", + "react-router-dom": "^7.14.0", "socket.io-client": "^4.8.3", - "typescript": "^5.9.3", + "typescript": "^6.0.2", "vite": "npm:rolldown-vite@7.2.10", - "vite-plugin-babel": "^1.5.1", - "vite-plugin-static-copy": "^3.2.0", - "zustand": "^5.0.11" + "vite-plugin-babel": "^1.6.0", + "vite-plugin-static-copy": "^4.0.1", + "zustand": "^5.0.12" }, "overrides": { "vite": "npm:rolldown-vite@7.2.10" diff --git a/admin/src/components/IconButton.tsx b/admin/src/components/IconButton.tsx index a93c9877c8f..b73396c827d 100644 --- a/admin/src/components/IconButton.tsx +++ b/admin/src/components/IconButton.tsx @@ -1,17 +1,17 @@ import {FC, JSX, ReactElement} from "react"; export type IconButtonProps = { - style?: React.CSSProperties, - icon: JSX.Element, - title: string|ReactElement, - onClick: ()=>void, - className?: string, - disabled?: boolean + style?: React.CSSProperties, + icon: JSX.Element, + title: string|ReactElement, + onClick: ()=>void, + className?: string, + disabled?: boolean } export const IconButton:FC = ({icon,className,onClick,title, disabled, style})=>{ - return + return } diff --git a/admin/src/components/SearchField.tsx b/admin/src/components/SearchField.tsx index 62a965d403a..1d2110005a6 100644 --- a/admin/src/components/SearchField.tsx +++ b/admin/src/components/SearchField.tsx @@ -1,14 +1,14 @@ import {ChangeEventHandler, FC} from "react"; import {Search} from 'lucide-react' export type SearchFieldProps = { - value: string, - onChange: ChangeEventHandler, - placeholder?: string + value: string, + onChange: ChangeEventHandler, + placeholder?: string } export const SearchField:FC = ({onChange,value, placeholder})=>{ - return - - - + return + + + } diff --git a/admin/src/components/ShoutType.ts b/admin/src/components/ShoutType.ts index f7e8b1df3dc..f4b94b9cbae 100644 --- a/admin/src/components/ShoutType.ts +++ b/admin/src/components/ShoutType.ts @@ -1,13 +1,13 @@ export type ShoutType = { + type: string, + data:{ type: string, - data:{ - type: string, - payload: { - message: { - message: string, - sticky: boolean - }, - timestamp: number - } + payload: { + message: { + message: string, + sticky: boolean + }, + timestamp: number } + } } diff --git a/admin/src/localization/i18n.ts b/admin/src/localization/i18n.ts index 67ae140e711..8c97eca7594 100644 --- a/admin/src/localization/i18n.ts +++ b/admin/src/localization/i18n.ts @@ -6,52 +6,50 @@ import LanguageDetector from 'i18next-browser-languagedetector' import { BackendModule } from 'i18next'; const LazyImportPlugin: BackendModule = { - type: 'backend', - init: function () { - }, - read: async function (language, namespace, callback) { - - let baseURL = import.meta.env.BASE_URL - if(namespace === "translation") { - // If default we load the translation file - baseURL+=`/locales/${language}.json` - } else { - // Else we load the former plugin translation file - baseURL+=`/${namespace}/${language}.json` - } - - const localeJSON = await fetch(baseURL, { - cache: "force-cache" - }) - let json; - - try { - json = JSON.parse(await localeJSON.text()) - } catch(e) { - callback(new Error("Error loading"), null); - } - - - callback(null, json); - }, - - save: function () { - }, - - create: function () { - /* save the missing translation */ - }, + type: 'backend', + init: function () { + }, + read: async function (language, namespace, callback) { + + let baseURL = import.meta.env.BASE_URL + if(namespace === "translation") { + // If default we load the translation file + baseURL+=`/locales/${language}.json` + } else { + // Else we load the former plugin translation file + baseURL+=`/${namespace}/${language}.json` + } + + const localeJSON = await fetch(baseURL) + let json; + + try { + json = JSON.parse(await localeJSON.text()) + } catch(e) { + callback(new Error("Error loading"), null); + } + + + callback(null, json); + }, + + save: function () { + }, + + create: function () { + /* save the missing translation */ + }, }; i18n - .use(LanguageDetector) - .use(LazyImportPlugin) - .use(initReactI18next) - .init( - { - ns: ['translation','ep_admin_pads'], - fallbackLng: 'en' - } - ) + .use(LanguageDetector) + .use(LazyImportPlugin) + .use(initReactI18next) + .init( + { + ns: ['translation','ep_admin_pads'], + fallbackLng: 'en' + } + ) export default i18n diff --git a/admin/src/pages/HelpPage.tsx b/admin/src/pages/HelpPage.tsx index dd9695b0a4a..13454742096 100644 --- a/admin/src/pages/HelpPage.tsx +++ b/admin/src/pages/HelpPage.tsx @@ -4,67 +4,67 @@ import {useEffect, useState} from "react"; import {HelpObj} from "./Plugin.ts"; export const HelpPage = () => { - const settingsSocket = useStore(state=>state.settingsSocket) - const [helpData, setHelpData] = useState(); + const settingsSocket = useStore(state=>state.settingsSocket) + const [helpData, setHelpData] = useState(); - useEffect(() => { - if(!settingsSocket) return; - settingsSocket?.on('reply:help', (data) => { - setHelpData(data) - }); + useEffect(() => { + if(!settingsSocket) return; + settingsSocket?.on('reply:help', (data) => { + setHelpData(data) + }); - settingsSocket?.emit('help'); - }, [settingsSocket]); + settingsSocket?.emit('help'); + }, [settingsSocket]); - const renderHooks = (hooks:Record>) => { - return Object.keys(hooks).map((hookName, i) => { - return
-

{hookName}

-
    - {Object.keys(hooks[hookName]).map((hook, i) =>
  • {hook} -
      - {Object.keys(hooks[hookName][hook]).map((subHook, i) =>
    • {subHook}
    • )} -
    -
  • )} -
-
- }) - } + const renderHooks = (hooks:Record>) => { + return Object.keys(hooks).map((hookName, i) => { + return
+

{hookName}

+
    + {Object.keys(hooks[hookName]).map((hook, i) =>
  • {hook} +
      + {Object.keys(hooks[hookName][hook]).map((subHook, i) =>
    • {subHook}
    • )} +
    +
  • )} +
+
+ }) + } - if (!helpData) return
+ if (!helpData) return
- return
-

-
-
-
{helpData?.epVersion}
-
-
{helpData.latestVersion}
-
Git sha
-
{helpData.gitCommit}
-
-

-
    - {helpData.installedPlugins.map((plugin, i) =>
  • {plugin}
  • )} -
+ return
+

+
+
+
{helpData?.epVersion}
+
+
{helpData.latestVersion}
+
Git sha
+
{helpData.gitCommit}
+
+

+
    + {helpData.installedPlugins.map((plugin, i) =>
  • {plugin}
  • )} +
-

-
    - {helpData.installedParts.map((part, i) =>
  • {part}
  • )} -
+

+
    + {helpData.installedParts.map((part, i) =>
  • {part}
  • )} +
-

- { - renderHooks(helpData.installedServerHooks) - } +

+ { + renderHooks(helpData.installedServerHooks) + } -

- - { - renderHooks(helpData.installedClientHooks) - } -

+

+ + { + renderHooks(helpData.installedClientHooks) + } +

-
+
} diff --git a/admin/src/pages/LoginScreen.tsx b/admin/src/pages/LoginScreen.tsx index 61ac8993e7e..a8094540771 100644 --- a/admin/src/pages/LoginScreen.tsx +++ b/admin/src/pages/LoginScreen.tsx @@ -5,57 +5,57 @@ import {Eye, EyeOff} from "lucide-react"; import {useState} from "react"; type Inputs = { - username: string - password: string + username: string + password: string } export const LoginScreen = ()=>{ - const navigate = useNavigate() - const [passwordVisible, setPasswordVisible] = useState(false) + const navigate = useNavigate() + const [passwordVisible, setPasswordVisible] = useState(false) - const { - register, - handleSubmit} = useForm() + const { + register, + handleSubmit} = useForm() - const login: SubmitHandler = ({username,password})=>{ - fetch('/admin-auth/', { - method: 'POST', - headers:{ - Authorization: `Basic ${btoa(`${username}:${password}`)}` - } - }).then(r=>{ - if(!r.ok) { - useStore.getState().setToastState({ - open: true, - title: "Login failed", - success: false - }) - } else { - navigate('/') - } - }).catch(e=>{ - console.error(e) + const login: SubmitHandler = ({username,password})=>{ + fetch('/admin-auth/', { + method: 'POST', + headers:{ + Authorization: `Basic ${btoa(`${username}:${password}`)}` + } + }).then(r=>{ + if(!r.ok) { + useStore.getState().setToastState({ + open: true, + title: "Login failed", + success: false }) - } + } else { + navigate('/') + } + }).catch(e=>{ + console.error(e) + }) + } - return
-
-

Etherpad

-
-
Username
- -
Password
- - - {passwordVisible? setPasswordVisible(!passwordVisible)}/> : - setPasswordVisible(!passwordVisible)}/>} - - -
-
+ return
+
+

Etherpad

+
+
Username
+ +
Password
+ + + {passwordVisible? setPasswordVisible(!passwordVisible)}/> : + setPasswordVisible(!passwordVisible)}/>} + + +
+
} diff --git a/admin/src/pages/PadPage.tsx b/admin/src/pages/PadPage.tsx index bfcbb9a14d1..cedef1157f5 100644 --- a/admin/src/pages/PadPage.tsx +++ b/admin/src/pages/PadPage.tsx @@ -11,274 +11,274 @@ import {SearchField} from "../components/SearchField.tsx"; import {useForm} from "react-hook-form"; type PadCreateProps = { - padName: string + padName: string } export const PadPage = ()=>{ - const settingsSocket = useStore(state=>state.settingsSocket) - const [searchParams, setSearchParams] = useState({ - offset: 0, - limit: 12, - pattern: '', - sortBy: 'padName', - ascending: true - }) - const {t} = useTranslation() - const [searchTerm, setSearchTerm] = useState('') - const pads = useStore(state=>state.pads) - const [currentPage, setCurrentPage] = useState(0) - const [deleteDialog, setDeleteDialog] = useState(false) - const [errorText, setErrorText] = useState(null) - const [padToDelete, setPadToDelete] = useState('') - const [createPadDialogOpen, setCreatePadDialogOpen] = useState(false) - const {register, handleSubmit} = useForm() - const pages = useMemo(()=>{ - if(!pads){ - return 0; - } + const settingsSocket = useStore(state=>state.settingsSocket) + const [searchParams, setSearchParams] = useState({ + offset: 0, + limit: 12, + pattern: '', + sortBy: 'padName', + ascending: true + }) + const {t} = useTranslation() + const [searchTerm, setSearchTerm] = useState('') + const pads = useStore(state=>state.pads) + const [currentPage, setCurrentPage] = useState(0) + const [deleteDialog, setDeleteDialog] = useState(false) + const [errorText, setErrorText] = useState(null) + const [padToDelete, setPadToDelete] = useState('') + const [createPadDialogOpen, setCreatePadDialogOpen] = useState(false) + const {register, handleSubmit} = useForm() + const pages = useMemo(()=>{ + if(!pads){ + return 0; + } - return Math.ceil(pads!.total / searchParams.limit) - },[pads, searchParams.limit]) + return Math.ceil(pads!.total / searchParams.limit) + },[pads, searchParams.limit]) - useDebounce(()=>{ - setSearchParams({ - ...searchParams, - pattern: searchTerm - }) + useDebounce(()=>{ + setSearchParams({ + ...searchParams, + pattern: searchTerm + }) - }, 500, [searchTerm]) + }, 500, [searchTerm]) - useEffect(() => { - if(!settingsSocket){ - return - } + useEffect(() => { + if(!settingsSocket){ + return + } - settingsSocket.emit('padLoad', searchParams) + settingsSocket.emit('padLoad', searchParams) - }, [settingsSocket, searchParams]); + }, [settingsSocket, searchParams]); - useEffect(() => { - if(!settingsSocket){ - return - } + useEffect(() => { + if(!settingsSocket){ + return + } - settingsSocket.on('results:padLoad', (data: PadSearchResult)=>{ - useStore.getState().setPads(data); - }) + settingsSocket.on('results:padLoad', (data: PadSearchResult)=>{ + useStore.getState().setPads(data); + }) - settingsSocket.on('results:deletePad', (padID: string)=>{ - const newPads = useStore.getState().pads?.results?.filter((pad)=>{ - return pad.padName !== padID - }) - useStore.getState().setPads({ - total: useStore.getState().pads!.total-1, - results: newPads - }) - }) + settingsSocket.on('results:deletePad', (padID: string)=>{ + const newPads = useStore.getState().pads?.results?.filter((pad)=>{ + return pad.padName !== padID + }) + useStore.getState().setPads({ + total: useStore.getState().pads!.total-1, + results: newPads + }) + }) - type SettingsSocketCreateReponse = { - error: string - } | { - success: string - } + type SettingsSocketCreateReponse = { + error: string + } | { + success: string + } - settingsSocket.on('results:createPad', (rep: SettingsSocketCreateReponse)=>{ - if ('error' in rep) { - useStore.getState().setToastState({ - open: true, - title: rep.error, - success: false - }) - } else { - useStore.getState().setToastState({ - open: true, - title: rep.success, - success: true - }) - setCreatePadDialogOpen(false) - // reload pads - settingsSocket.emit('padLoad', searchParams) - } - }) + settingsSocket.on('results:createPad', (rep: SettingsSocketCreateReponse)=>{ + if ('error' in rep) { + useStore.getState().setToastState({ + open: true, + title: rep.error, + success: false + }) + } else { + useStore.getState().setToastState({ + open: true, + title: rep.success, + success: true + }) + setCreatePadDialogOpen(false) + // reload pads + settingsSocket.emit('padLoad', searchParams) + } + }) - settingsSocket.on('results:cleanupPadRevisions', (data)=>{ - const newPads = useStore.getState().pads?.results ?? [] + settingsSocket.on('results:cleanupPadRevisions', (data)=>{ + const newPads = useStore.getState().pads?.results ?? [] - if (data.error) { - setErrorText(data.error) - return - } + if (data.error) { + setErrorText(data.error) + return + } - newPads.forEach((pad)=>{ - if (pad.padName === data.padId) { - pad.revisionNumber = data.keepRevisions - } - }) + newPads.forEach((pad)=>{ + if (pad.padName === data.padId) { + pad.revisionNumber = data.keepRevisions + } + }) - useStore.getState().setPads({ - results: newPads, - total: useStore.getState().pads!.total - }) - }) - }, [settingsSocket, pads]); + useStore.getState().setPads({ + results: newPads, + total: useStore.getState().pads!.total + }) + }) + }, [settingsSocket, pads]); - const deletePad = (padID: string)=>{ - settingsSocket?.emit('deletePad', padID) - } + const deletePad = (padID: string)=>{ + settingsSocket?.emit('deletePad', padID) + } - const cleanupPad = (padID: string)=>{ - settingsSocket?.emit('cleanupPadRevisions', padID) - } + const cleanupPad = (padID: string)=>{ + settingsSocket?.emit('cleanupPadRevisions', padID) + } - const onPadCreate = (data: PadCreateProps)=>{ - settingsSocket?.emit('createPad', { - padName: data.padName - }) - } + const onPadCreate = (data: PadCreateProps)=>{ + settingsSocket?.emit('createPad', { + padName: data.padName + }) + } - return
- - - -
-
-
- {t("ep_admin_pads:ep_adminpads2_confirm", { - padID: padToDelete, - })} -
-
- - -
-
-
-
-
- - - - -
-
Error occured: {errorText}
-
- -
-
-
-
-
- - - - - -
- -
- - -
- -
-
-
-
- -

- } title={} onClick={()=>{ - setCreatePadDialogOpen(true) - }}/> -
- setSearchTerm(v.target.value)} placeholder={t('ep_admin_pads:ep_adminpads2_search-heading')}/> - - - - - - - - - - - - { - pads?.results?.map((pad)=>{ - return - - - - - - - }) - } - -
{ - setSearchParams({ - ...searchParams, - sortBy: 'padName', - ascending: !searchParams.ascending - }) - }}>{ - setSearchParams({ - ...searchParams, - sortBy: 'userCount', - ascending: !searchParams.ascending - }) - }}>{ - setSearchParams({ - ...searchParams, - sortBy: 'lastEdited', - ascending: !searchParams.ascending - }) - }}>{ - setSearchParams({ - ...searchParams, - sortBy: 'revisionNumber', - ascending: !searchParams.ascending - }) - }}>Revision number
{pad.padName}{pad.userCount}{new Date(pad.lastEdited).toLocaleString()}{pad.revisionNumber} -
- } title={} onClick={()=>{ - setPadToDelete(pad.padName) - setDeleteDialog(true) - }}/> - } title={} onClick={()=>{ - cleanupPad(pad.padName) - }}/> - } title={} onClick={()=>window.open(`../../p/${pad.padName}`, '_blank')}/> -
-
-
- - {currentPage+1} out of {pages} - + return
+ + + +
+
+
+ {t("ep_admin_pads:ep_adminpads2_confirm", { + padID: padToDelete, + })} +
+
+ + +
+
+
+
+
+ + + + +
+
Error occured: {errorText}
+
+
+
+
+
+
+ + + + + +
+ +
+ + +
+ +
+
+
+
+ +

+ } title={} onClick={()=>{ + setCreatePadDialogOpen(true) + }}/> +
+ setSearchTerm(v.target.value)} placeholder={t('ep_admin_pads:ep_adminpads2_search-heading')}/> + + + + + + + + + + + + { + pads?.results?.map((pad)=>{ + return + + + + + + + }) + } + +
{ + setSearchParams({ + ...searchParams, + sortBy: 'padName', + ascending: !searchParams.ascending + }) + }}>{ + setSearchParams({ + ...searchParams, + sortBy: 'userCount', + ascending: !searchParams.ascending + }) + }}>{ + setSearchParams({ + ...searchParams, + sortBy: 'lastEdited', + ascending: !searchParams.ascending + }) + }}>{ + setSearchParams({ + ...searchParams, + sortBy: 'revisionNumber', + ascending: !searchParams.ascending + }) + }}>Revision number
{pad.padName}{pad.userCount}{new Date(pad.lastEdited).toLocaleString()}{pad.revisionNumber} +
+ } title={} onClick={()=>{ + setPadToDelete(pad.padName) + setDeleteDialog(true) + }}/> + } title={} onClick={()=>{ + cleanupPad(pad.padName) + }}/> + } title={} onClick={()=>window.open(`../../p/${pad.padName}`, '_blank')}/> +
+
+
+ + {currentPage+1} out of {pages} +
+
} diff --git a/admin/src/pages/Plugin.ts b/admin/src/pages/Plugin.ts index f5563863b53..7e556d8a665 100644 --- a/admin/src/pages/Plugin.ts +++ b/admin/src/pages/Plugin.ts @@ -1,36 +1,36 @@ export type PluginDef = { - name: string, - description: string, - version: string, - time: string, - official: boolean, + name: string, + description: string, + version: string, + time: string, + official: boolean, } export type InstalledPlugin = { - name: string, - path: string, - realPath: string, - version:string, - updatable?: boolean + name: string, + path: string, + realPath: string, + version:string, + updatable?: boolean } export type SearchParams = { - searchTerm: string, - offset: number, - limit: number, - sortBy: 'name'|'version'|'last-updated', - sortDir: 'asc'|'desc' + searchTerm: string, + offset: number, + limit: number, + sortBy: 'name'|'version'|'last-updated', + sortDir: 'asc'|'desc' } export type HelpObj = { - epVersion: string - gitCommit: string - installedClientHooks: Record>, - installedParts: string[], - installedPlugins: string[], - installedServerHooks: Record, - latestVersion: string + epVersion: string + gitCommit: string + installedClientHooks: Record>, + installedParts: string[], + installedPlugins: string[], + installedServerHooks: Record, + latestVersion: string } diff --git a/admin/src/pages/SettingsPage.tsx b/admin/src/pages/SettingsPage.tsx index d1786957374..6c2f9bf333c 100644 --- a/admin/src/pages/SettingsPage.tsx +++ b/admin/src/pages/SettingsPage.tsx @@ -5,46 +5,46 @@ import {IconButton} from "../components/IconButton.tsx"; import {RotateCw, Save} from "lucide-react"; export const SettingsPage = ()=>{ - const settingsSocket = useStore(state=>state.settingsSocket) - const settings = cleanComments(useStore(state=>state.settings)) + const settingsSocket = useStore(state=>state.settingsSocket) + const settings = cleanComments(useStore(state=>state.settings)) - return
-

-