mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-05-05 12:16:45 +02:00
Merge branch 'develop'
This commit is contained in:
commit
6d92d90a80
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,7 +1,7 @@
|
||||
<!--
|
||||
|
||||
1. If you haven't already, please read https://github.com/ether/etherpad-lite/blob/master/CONTRIBUTING.md#pull-requests .
|
||||
2. Run all the tests, both front-end and back-end. (see https://github.com/ether/etherpad-lite/blob/master/CONTRIBUTING.md#testing)
|
||||
1. If you haven't already, please read https://github.com/ether/etherpad/blob/master/CONTRIBUTING.md#pull-requests .
|
||||
2. Run all the tests, both front-end and back-end. (see https://github.com/ether/etherpad/blob/master/CONTRIBUTING.md#testing)
|
||||
3. Keep business logic and validation on the server-side.
|
||||
4. Update documentation.
|
||||
5. Write `fixes #XXXX` in your comment to auto-close an issue.
|
||||
|
||||
24
.github/workflows/backend-tests.yml
vendored
24
.github/workflows/backend-tests.yml
vendored
@ -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
|
||||
|
||||
6
.github/workflows/build-and-deploy-docs.yml
vendored
6
.github/workflows/build-and-deploy-docs.yml
vendored
@ -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
|
||||
@ -59,10 +59,10 @@ jobs:
|
||||
env:
|
||||
COMMIT_REF: ${{ github.sha }}
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
uses: actions/upload-pages-artifact@v5
|
||||
with:
|
||||
# Upload entire repository
|
||||
path: './doc/.vitepress/dist'
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
uses: actions/deploy-pages@v5
|
||||
|
||||
181
.github/workflows/docker.yml
vendored
181
.github/workflows/docker.yml
vendored
@ -12,12 +12,15 @@ on:
|
||||
- 'v?[0-9]+.[0-9]+.[0-9]+'
|
||||
env:
|
||||
TEST_TAG: etherpad/etherpad:test
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
docker:
|
||||
build-test:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
PNPM_HOME: ~/.pnpm-store
|
||||
steps:
|
||||
@ -26,17 +29,12 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: etherpad
|
||||
|
||||
-
|
||||
name: Set up QEMU
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/setup-qemu-action@v3
|
||||
-
|
||||
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
|
||||
@ -78,13 +76,156 @@ jobs:
|
||||
done
|
||||
(cd src && gnpm run test-container)
|
||||
git clean -dxf .
|
||||
|
||||
build-test-db-drivers:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
MYSQL_DATABASE: etherpad
|
||||
MYSQL_USER: etherpad
|
||||
MYSQL_PASSWORD: password
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: >-
|
||||
--health-cmd="mysqladmin ping -h 127.0.0.1 -uroot -ppassword"
|
||||
--health-interval=5s
|
||||
--health-timeout=5s
|
||||
--health-retries=20
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_DB: etherpad
|
||||
POSTGRES_USER: etherpad
|
||||
POSTGRES_PASSWORD: password
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd="pg_isready -U etherpad -d etherpad"
|
||||
--health-interval=5s
|
||||
--health-timeout=5s
|
||||
--health-retries=20
|
||||
steps:
|
||||
-
|
||||
name: Check out
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: etherpad
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
-
|
||||
name: Build production image
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: ./etherpad
|
||||
target: production
|
||||
load: true
|
||||
tags: ${{ env.TEST_TAG }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
-
|
||||
name: Driver presence test (all 10 ueberdb2 drivers must resolve)
|
||||
run: |
|
||||
docker run --rm -w /opt/etherpad-lite/src "$TEST_TAG" node -e "
|
||||
const mods = [
|
||||
'@elastic/elasticsearch','cassandra-driver','mongodb','mssql',
|
||||
'mysql2','nano','pg','redis','rethinkdb','surrealdb'
|
||||
];
|
||||
let fail = false;
|
||||
for (const m of mods) {
|
||||
try { require(m); console.log('ok', m); }
|
||||
catch (e) { console.error('MISSING', m, e.message); fail = true; }
|
||||
}
|
||||
if (fail) process.exit(1);
|
||||
"
|
||||
-
|
||||
name: MySQL smoke — start Etherpad against mysql service
|
||||
run: |
|
||||
docker run --rm -d \
|
||||
--network host \
|
||||
-e NODE_ENV=production \
|
||||
-e ADMIN_PASSWORD=admin \
|
||||
-e DB_TYPE=mysql \
|
||||
-e DB_HOST=127.0.0.1 \
|
||||
-e DB_PORT=3306 \
|
||||
-e DB_NAME=etherpad \
|
||||
-e DB_USER=etherpad \
|
||||
-e DB_PASS=password \
|
||||
-e DB_CHARSET=utf8mb4 \
|
||||
-e DEFAULT_PAD_TEXT="Test " \
|
||||
--name et-mysql "$TEST_TAG"
|
||||
for i in $(seq 1 60); do
|
||||
if curl -sf http://127.0.0.1:9001/ >/dev/null; then
|
||||
echo "mysql smoke: Etherpad is serving /"
|
||||
docker rm -f et-mysql
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "mysql smoke: timed out waiting for Etherpad"
|
||||
docker logs et-mysql || true
|
||||
docker rm -f et-mysql || true
|
||||
exit 1
|
||||
-
|
||||
name: Postgres smoke — start Etherpad against postgres service
|
||||
run: |
|
||||
docker run --rm -d \
|
||||
--network host \
|
||||
-e NODE_ENV=production \
|
||||
-e ADMIN_PASSWORD=admin \
|
||||
-e DB_TYPE=postgres \
|
||||
-e DB_HOST=127.0.0.1 \
|
||||
-e DB_PORT=5432 \
|
||||
-e DB_NAME=etherpad \
|
||||
-e DB_USER=etherpad \
|
||||
-e DB_PASS=password \
|
||||
-e DEFAULT_PAD_TEXT="Test " \
|
||||
--name et-postgres "$TEST_TAG"
|
||||
for i in $(seq 1 60); do
|
||||
if curl -sf http://127.0.0.1:9001/ >/dev/null; then
|
||||
echo "postgres smoke: Etherpad is serving /"
|
||||
docker rm -f et-postgres
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "postgres smoke: timed out waiting for Etherpad"
|
||||
docker logs et-postgres || true
|
||||
docker rm -f et-postgres || true
|
||||
exit 1
|
||||
|
||||
publish:
|
||||
needs: [build-test, build-test-db-drivers]
|
||||
if: github.event_name == 'push'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
-
|
||||
name: Check out
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: etherpad
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v4
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
-
|
||||
name: Docker meta
|
||||
if: github.event_name == 'push'
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: etherpad/etherpad
|
||||
images: |
|
||||
etherpad/etherpad
|
||||
ghcr.io/ether/etherpad
|
||||
tags: |
|
||||
type=ref,event=branch
|
||||
type=semver,pattern={{version}}
|
||||
@ -92,16 +233,21 @@ jobs:
|
||||
type=semver,pattern={{major}}
|
||||
-
|
||||
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 }}
|
||||
-
|
||||
name: Log in to GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
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
|
||||
@ -109,6 +255,7 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
- name: Update repo description
|
||||
uses: peter-evans/dockerhub-description@v5
|
||||
if: github.ref == 'refs/heads/master'
|
||||
@ -118,8 +265,8 @@ jobs:
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
repository: etherpad/etherpad
|
||||
enable-url-completion: true
|
||||
- name: Check out
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/develop'
|
||||
- name: Check out ether-charts
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: ether-charts
|
||||
|
||||
98
.github/workflows/frontend-admin-tests.yml
vendored
98
.github/workflows/frontend-admin-tests.yml
vendored
@ -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 }}
|
||||
|
||||
119
.github/workflows/frontend-tests.yml
vendored
119
.github/workflows/frontend-tests.yml
vendored
@ -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
|
||||
- name: Upload server log on failure
|
||||
uses: actions/upload-artifact@v7
|
||||
if: failure()
|
||||
with:
|
||||
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 }}-firefox
|
||||
name: playwright-report-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
|
||||
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
|
||||
if: always()
|
||||
with:
|
||||
name: playwright-report-${{ matrix.node }}-webkit
|
||||
path: src/playwright-report/
|
||||
retention-days: 30
|
||||
|
||||
|
||||
|
||||
|
||||
2
.github/workflows/handleRelease.yml
vendored
2
.github/workflows/handleRelease.yml
vendored
@ -54,7 +54,7 @@ jobs:
|
||||
working-directory: bin
|
||||
run: gnpm run generateChangelog ${{ github.ref }} > ${{ github.workspace }}-CHANGELOG.txt
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
uses: softprops/action-gh-release@v3
|
||||
if: ${{startsWith(github.ref, 'refs/tags/v') }}
|
||||
with:
|
||||
body_path: ${{ github.workspace }}-CHANGELOG.txt
|
||||
|
||||
165
.github/workflows/installer-test.yml
vendored
Normal file
165
.github/workflows/installer-test.yml
vendored
Normal file
@ -0,0 +1,165 @@
|
||||
name: "Installer test"
|
||||
|
||||
# Exercises bin/installer.sh and bin/installer.ps1 end-to-end so the
|
||||
# one-line install paths in the README stay working.
|
||||
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- "bin/installer.sh"
|
||||
- "bin/installer.ps1"
|
||||
- ".github/workflows/installer-test.yml"
|
||||
pull_request:
|
||||
paths:
|
||||
- "bin/installer.sh"
|
||||
- "bin/installer.ps1"
|
||||
- ".github/workflows/installer-test.yml"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
shellcheck:
|
||||
name: shellcheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: Run shellcheck on installer.sh
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y shellcheck
|
||||
shellcheck bin/installer.sh
|
||||
|
||||
installer-posix:
|
||||
name: end-to-end install (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Pre-install pnpm (avoid sudo prompt in the installer)
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Run bin/installer.sh against this commit
|
||||
env:
|
||||
ETHERPAD_DIR: ${{ runner.temp }}/etherpad-installer-test
|
||||
ETHERPAD_REPO: ${{ github.server_url }}/${{ github.repository }}.git
|
||||
ETHERPAD_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
NO_COLOR: "1"
|
||||
run: sh ./bin/installer.sh
|
||||
|
||||
- name: Verify clone + dependencies + build artifacts
|
||||
shell: bash
|
||||
env:
|
||||
ETHERPAD_DIR: ${{ runner.temp }}/etherpad-installer-test
|
||||
run: |
|
||||
set -eux
|
||||
test -d "$ETHERPAD_DIR/.git"
|
||||
test -f "$ETHERPAD_DIR/package.json"
|
||||
test -d "$ETHERPAD_DIR/node_modules"
|
||||
test -d "$ETHERPAD_DIR/src/node_modules"
|
||||
# build:etherpad copies the admin SPA into src/templates/admin
|
||||
test -f "$ETHERPAD_DIR/src/templates/admin/index.html"
|
||||
|
||||
- name: Smoke test - start Etherpad and curl /api
|
||||
shell: bash
|
||||
env:
|
||||
ETHERPAD_DIR: ${{ runner.temp }}/etherpad-installer-test
|
||||
run: |
|
||||
set -eu
|
||||
cd "$ETHERPAD_DIR"
|
||||
pnpm run prod >/tmp/etherpad.log 2>&1 &
|
||||
PID=$!
|
||||
# Wait up to 60s for the API to come up.
|
||||
ok=0
|
||||
for i in $(seq 1 60); do
|
||||
if curl -fsS http://localhost:9001/api >/dev/null 2>&1; then
|
||||
echo "Etherpad answered on /api after ${i}s"
|
||||
ok=1
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
if [ "$ok" != "1" ]; then
|
||||
echo "Etherpad did not start within 60s. Last 200 lines of log:" >&2
|
||||
tail -200 /tmp/etherpad.log >&2 || true
|
||||
kill "$PID" 2>/dev/null || true
|
||||
exit 1
|
||||
fi
|
||||
kill "$PID" 2>/dev/null || true
|
||||
wait "$PID" 2>/dev/null || true
|
||||
|
||||
installer-windows:
|
||||
name: end-to-end install (windows-latest)
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Pre-install pnpm
|
||||
run: npm install -g pnpm
|
||||
|
||||
- name: Run bin/installer.ps1 against this commit
|
||||
shell: pwsh
|
||||
env:
|
||||
ETHERPAD_DIR: ${{ runner.temp }}\etherpad-installer-test
|
||||
ETHERPAD_REPO: ${{ github.server_url }}/${{ github.repository }}.git
|
||||
ETHERPAD_BRANCH: ${{ github.head_ref || github.ref_name }}
|
||||
NO_COLOR: "1"
|
||||
run: ./bin/installer.ps1
|
||||
|
||||
- name: Verify clone + dependencies + build artifacts
|
||||
shell: pwsh
|
||||
env:
|
||||
ETHERPAD_DIR: ${{ runner.temp }}\etherpad-installer-test
|
||||
run: |
|
||||
if (-not (Test-Path "$env:ETHERPAD_DIR\.git")) { throw '.git missing' }
|
||||
if (-not (Test-Path "$env:ETHERPAD_DIR\package.json")) { throw 'package.json missing' }
|
||||
if (-not (Test-Path "$env:ETHERPAD_DIR\node_modules")) { throw 'node_modules missing' }
|
||||
if (-not (Test-Path "$env:ETHERPAD_DIR\src\node_modules")) { throw 'src/node_modules missing' }
|
||||
if (-not (Test-Path "$env:ETHERPAD_DIR\src\templates\admin\index.html")) { throw 'admin SPA missing' }
|
||||
|
||||
- name: Smoke test - start Etherpad and curl /api
|
||||
shell: pwsh
|
||||
env:
|
||||
ETHERPAD_DIR: ${{ runner.temp }}\etherpad-installer-test
|
||||
run: |
|
||||
Push-Location $env:ETHERPAD_DIR
|
||||
$logPath = Join-Path $env:RUNNER_TEMP 'etherpad.log'
|
||||
# pnpm on Windows is a .cmd shim, which Start-Process can't run
|
||||
# directly — wrap it in cmd.exe.
|
||||
$proc = Start-Process -FilePath cmd.exe `
|
||||
-ArgumentList '/c','pnpm','run','prod' `
|
||||
-RedirectStandardOutput $logPath `
|
||||
-RedirectStandardError "$logPath.err" `
|
||||
-PassThru -NoNewWindow
|
||||
$ok = $false
|
||||
for ($i = 1; $i -le 90; $i++) {
|
||||
try {
|
||||
Invoke-WebRequest -UseBasicParsing -Uri http://localhost:9001/api -TimeoutSec 2 | Out-Null
|
||||
Write-Host "Etherpad answered on /api after ${i}s"
|
||||
$ok = $true
|
||||
break
|
||||
} catch { Start-Sleep -Seconds 1 }
|
||||
}
|
||||
if (-not $ok) {
|
||||
Write-Host 'Etherpad did not start within 90s. Last 200 lines of log (stdout):'
|
||||
if (Test-Path $logPath) { Get-Content $logPath -Tail 200 }
|
||||
Write-Host 'Last 200 lines of stderr:'
|
||||
if (Test-Path "$logPath.err") { Get-Content "$logPath.err" -Tail 200 }
|
||||
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
|
||||
Pop-Location
|
||||
exit 1
|
||||
}
|
||||
Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
|
||||
Pop-Location
|
||||
10
.github/workflows/load-test.yml
vendored
10
.github/workflows/load-test.yml
vendored
@ -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
|
||||
|
||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@ -75,7 +75,7 @@ jobs:
|
||||
with:
|
||||
ruby-version: 2.7
|
||||
|
||||
- uses: reitzig/actions-asciidoctor@v2.0.2
|
||||
- uses: reitzig/actions-asciidoctor@v2.0.4
|
||||
with:
|
||||
version: 2.0.18
|
||||
- name: Prepare release
|
||||
|
||||
22
.github/workflows/releaseEtherpad.yml
vendored
22
.github/workflows/releaseEtherpad.yml
vendored
@ -1,6 +1,7 @@
|
||||
name: releaseEtherpad.yaml
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # for npm OIDC trusted publishing
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
@ -13,6 +14,15 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
# OIDC trusted publishing needs npm >= 11.5.1, which requires
|
||||
# Node >= 20.17.0. setup-node's `20` resolves to the latest
|
||||
# 20.x, which satisfies that.
|
||||
node-version: 20
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- name: Upgrade npm to >=11.5.1 (required for trusted publishing)
|
||||
run: npm install -g npm@latest
|
||||
- name: Get pnpm store directory
|
||||
shell: bash
|
||||
run: |
|
||||
@ -38,8 +48,12 @@ jobs:
|
||||
- name: Rename etherpad
|
||||
working-directory: ./src
|
||||
run: sed -i 's/ep_etherpad-lite/ep_etherpad/g' package.json
|
||||
- name: Release to npm
|
||||
run: gnpm publish --no-git-checks
|
||||
# Use `npm publish` directly (not `gnpm`/`pnpm` wrappers) because OIDC
|
||||
# trusted publishing requires npm CLI >= 11.5.1 and the wrappers shell
|
||||
# out to npm; calling npm directly avoids any shim ambiguity. The
|
||||
# ep_etherpad package must have a trusted publisher configured on
|
||||
# npmjs.com pointing at this workflow file. See:
|
||||
# https://docs.npmjs.com/trusted-publishers
|
||||
- name: Release to npm via OIDC
|
||||
run: npm publish --provenance --access public
|
||||
working-directory: ./src
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PRIVATE_TOKEN }}
|
||||
|
||||
116
.github/workflows/update-plugins.yml
vendored
Normal file
116
.github/workflows/update-plugins.yml
vendored
Normal file
@ -0,0 +1,116 @@
|
||||
name: Update Plugins
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * *' # Daily at 06:00 UTC
|
||||
workflow_dispatch: # Allow manual trigger
|
||||
|
||||
# The cross-repo work (cloning ether/ep_* repos, pushing updates, merging
|
||||
# Dependabot PRs) is authenticated via secrets.PLUGINS_PAT. The default
|
||||
# GITHUB_TOKEN only needs read access to this repo for actions/checkout.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update-plugins:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out etherpad-lite
|
||||
uses: actions/checkout@v6
|
||||
|
||||
- uses: pnpm/action-setup@v6
|
||||
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"
|
||||
|
||||
- name: Merge clean Dependabot PRs on plugin repos
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.PLUGINS_PAT }}
|
||||
run: |
|
||||
# For every ep_* repo under ether, merge any Dependabot PR whose
|
||||
# mergeStateStatus is CLEAN (no conflicts, branch up to date, all
|
||||
# required checks green). Anything else is left alone for a human.
|
||||
plugins=$(gh repo list ether --limit 200 --json name --jq '.[] | select(.name | startswith("ep_")) | .name')
|
||||
|
||||
merged=""
|
||||
for plugin in $plugins; do
|
||||
repo="ether/${plugin}"
|
||||
prs=$(gh pr list --repo "$repo" \
|
||||
--author "app/dependabot" \
|
||||
--state open \
|
||||
--json number,mergeStateStatus,title \
|
||||
--jq '.[] | select(.mergeStateStatus=="CLEAN") | .number') || continue
|
||||
|
||||
for pr in $prs; do
|
||||
echo "Merging ${repo}#${pr}"
|
||||
if gh pr merge --repo "$repo" --squash --delete-branch "$pr"; then
|
||||
merged="$merged ${repo}#${pr}"
|
||||
else
|
||||
echo "WARN: failed to merge ${repo}#${pr}"
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Merged Dependabot PRs:$merged"
|
||||
@ -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 }}"
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -18,7 +18,6 @@ out/
|
||||
.nyc_output
|
||||
.idea
|
||||
/package-lock.json
|
||||
/src/bin/abiword.exe
|
||||
/src/bin/convertSettings.json
|
||||
/src/bin/etherpad-1.deb
|
||||
/src/bin/node.exe
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
extraction:
|
||||
javascript:
|
||||
index:
|
||||
exclude:
|
||||
- src/static/js/vendors
|
||||
python:
|
||||
index:
|
||||
exclude:
|
||||
- /
|
||||
8
.npmrc
8
.npmrc
@ -1 +1,7 @@
|
||||
auto-install-peers=false
|
||||
strict-dep-builds=false
|
||||
# Use hardlinks when populating node_modules instead of clone/copyfile.
|
||||
# pnpm's default "auto" mode ends up using copy_file_range which fails on
|
||||
# ZFS (https://github.com/pnpm/pnpm/issues/7024) and breaks `docker
|
||||
# compose build` on hosts with a ZFS root (#7342). Hardlinks are fast,
|
||||
# save disk, and work on every filesystem Etherpad supports.
|
||||
package-import-method=hardlink
|
||||
|
||||
5
.pr_agent.toml
Normal file
5
.pr_agent.toml
Normal file
@ -0,0 +1,5 @@
|
||||
[pr_reviewer]
|
||||
run_on_pr_sync = true
|
||||
|
||||
[pr_description]
|
||||
run_on_pr_sync = true
|
||||
145
.travis.yml
145
.travis.yml
@ -1,145 +0,0 @@
|
||||
language: node_js
|
||||
|
||||
node_js:
|
||||
- "lts/*"
|
||||
|
||||
services:
|
||||
- docker
|
||||
|
||||
cache: false
|
||||
|
||||
env:
|
||||
global:
|
||||
- secure: "WMGxFkOeTTlhWB+ChMucRtIqVmMbwzYdNHuHQjKCcj8HBEPdZLfCuK/kf4rG\nVLcLQiIsyllqzNhBGVHG1nyqWr0/LTm8JRqSCDDVIhpyzp9KpCJQQJG2Uwjk\n6/HIJJh/wbxsEdLNV2crYU/EiVO3A4Bq0YTHUlbhUqG3mSCr5Ec="
|
||||
- secure: "gejXUAHYscbR6Bodw35XexpToqWkv2ifeECsbeEmjaLkYzXmUUNWJGknKSu7\nEUsSfQV8w+hxApr1Z+jNqk9aX3K1I4btL3cwk2trnNI8XRAvu1c1Iv60eerI\nkE82Rsd5lwUaMEh+/HoL8ztFCZamVndoNgX7HWp5J/NRZZMmh4g="
|
||||
|
||||
_set_loglevel_warn: &set_loglevel_warn |
|
||||
sed -e 's/"loglevel":[^,]*/"loglevel": "WARN"/' \
|
||||
settings.json.template >settings.json.template.new &&
|
||||
mv settings.json.template.new settings.json.template
|
||||
|
||||
_enable_admin_tests: &enable_admin_tests |
|
||||
sed -e 's/"enableAdminUITests": false/"enableAdminUITests": true,\n"users":{"admin":{"password":"changeme","is_admin":true}}/' \
|
||||
settings.json.template >settings.json.template.new &&
|
||||
mv settings.json.template.new settings.json.template
|
||||
|
||||
_install_libreoffice: &install_libreoffice >-
|
||||
sudo add-apt-repository -y ppa:libreoffice/ppa &&
|
||||
sudo apt-get update &&
|
||||
sudo apt-get -y install libreoffice libreoffice-pdfimport
|
||||
|
||||
# The --legacy-peer-deps flag is required to work around a bug in npm v7:
|
||||
# https://github.com/npm/cli/issues/2199
|
||||
_install_plugins: &install_plugins >-
|
||||
npm install --no-save --legacy-peer-deps
|
||||
ep_align
|
||||
ep_author_hover
|
||||
ep_cursortrace
|
||||
ep_font_size
|
||||
ep_hash_auth
|
||||
ep_headings2
|
||||
ep_markdown
|
||||
ep_readonly_guest
|
||||
ep_spellcheck
|
||||
ep_subscript_and_superscript
|
||||
ep_table_of_contents
|
||||
ep_set_title_on_pad
|
||||
|
||||
jobs:
|
||||
include:
|
||||
# we can only frontend tests from the ether/ organization and not from forks.
|
||||
# To request tests to be run ask a maintainer to fork your repo to ether/
|
||||
- if: fork = false
|
||||
name: "Test the Frontend without Plugins"
|
||||
install:
|
||||
- *set_loglevel_warn
|
||||
- *enable_admin_tests
|
||||
- "src/tests/frontend/travis/sauce_tunnel.sh"
|
||||
- "bin/installDeps.sh"
|
||||
- "export GIT_HASH=$(git rev-parse --verify --short HEAD)"
|
||||
script:
|
||||
- "./src/tests/frontend/travis/runner.sh"
|
||||
- name: "Run the Backend tests without Plugins"
|
||||
install:
|
||||
- *install_libreoffice
|
||||
- *set_loglevel_warn
|
||||
- "bin/installDeps.sh"
|
||||
- "cd src && pnpm install && cd -"
|
||||
script:
|
||||
- "cd src && pnpm test"
|
||||
- name: "Test the Dockerfile"
|
||||
install:
|
||||
- "cd src && pnpm install && cd -"
|
||||
script:
|
||||
- "docker build -t etherpad:test ."
|
||||
- "docker run -d -p 9001:9001 etherpad:test && sleep 3"
|
||||
- "cd src && pnpm run test-container"
|
||||
- name: "Load test Etherpad without Plugins"
|
||||
install:
|
||||
- *set_loglevel_warn
|
||||
- "bin/installDeps.sh"
|
||||
- "cd src && pnpm install && cd -"
|
||||
- "npm install -g etherpad-load-test"
|
||||
script:
|
||||
- "src/tests/frontend/travis/runnerLoadTest.sh"
|
||||
# we can only frontend tests from the ether/ organization and not from forks.
|
||||
# To request tests to be run ask a maintainer to fork your repo to ether/
|
||||
- if: fork = false
|
||||
name: "Test the Frontend Plugins only"
|
||||
install:
|
||||
- *set_loglevel_warn
|
||||
- *enable_admin_tests
|
||||
- "src/tests/frontend/travis/sauce_tunnel.sh"
|
||||
- "bin/installDeps.sh"
|
||||
- "rm src/tests/frontend/specs/*"
|
||||
- *install_plugins
|
||||
- "export GIT_HASH=$(git rev-parse --verify --short HEAD)"
|
||||
script:
|
||||
- "./src/tests/frontend/travis/runner.sh"
|
||||
- name: "Lint test package-lock.json"
|
||||
install:
|
||||
- "npm install lockfile-lint"
|
||||
script:
|
||||
- npx lockfile-lint --path src/package-lock.json --validate-https --allowed-hosts npm
|
||||
- name: "Run the Backend tests with Plugins"
|
||||
install:
|
||||
- *install_libreoffice
|
||||
- *set_loglevel_warn
|
||||
- "bin/installDeps.sh"
|
||||
- *install_plugins
|
||||
- "cd src && pnpm install && cd -"
|
||||
script:
|
||||
- "cd src && pnpm test"
|
||||
- name: "Test the Dockerfile"
|
||||
install:
|
||||
- "cd src && pnpm install && cd -"
|
||||
script:
|
||||
- "docker build -t etherpad:test ."
|
||||
- "docker run -d -p 9001:9001 etherpad:test && sleep 3"
|
||||
- "cd src && pnpm run test-container"
|
||||
- name: "Load test Etherpad with Plugins"
|
||||
install:
|
||||
- *set_loglevel_warn
|
||||
- "bin/installDeps.sh"
|
||||
- *install_plugins
|
||||
- "cd src && npm install && cd -"
|
||||
- "npm install -g etherpad-load-test"
|
||||
script:
|
||||
- "src/tests/frontend/travis/runnerLoadTest.sh"
|
||||
- name: "Test rate limit"
|
||||
install:
|
||||
- "docker network create --subnet=172.23.42.0/16 ep_net"
|
||||
- "docker build -f Dockerfile -t epl-debian-slim ."
|
||||
- "docker build -f src/tests/ratelimit/Dockerfile.nginx -t nginx-latest ."
|
||||
- "docker build -f src/tests/ratelimit/Dockerfile.anotherip -t anotherip ."
|
||||
- "docker run -p 8081:80 --rm --network ep_net --ip 172.23.42.1 -d nginx-latest"
|
||||
- "docker run --name etherpad-docker -p 9000:9001 --rm --network ep_net --ip 172.23.42.2 -e 'TRUST_PROXY=true' epl-debian-slim &"
|
||||
- "docker run --rm --network ep_net --ip 172.23.42.3 --name anotherip -dt anotherip"
|
||||
- "./bin/installDeps.sh"
|
||||
script:
|
||||
- "cd src/tests/ratelimit && bash testlimits.sh"
|
||||
|
||||
notifications:
|
||||
irc:
|
||||
channels:
|
||||
- "irc.freenode.org#etherpad-lite-dev"
|
||||
207
AGENTS.MD
Normal file
207
AGENTS.MD
Normal file
@ -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`. 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 <package>` 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.
|
||||
48
CHANGELOG.md
48
CHANGELOG.md
@ -1,3 +1,51 @@
|
||||
# 2.7.0
|
||||
|
||||
### Breaking changes
|
||||
|
||||
- **Abiword has been replaced with LibreOffice for document import/export.** If you were using Abiword for DOCX/ODT/PDF conversion, update your `settings.json` to point `soffice` at your LibreOffice binary. DOCX export is now supported out of the box.
|
||||
|
||||
### Notable enhancements
|
||||
|
||||
- Added line numbers to the timeslider so you can follow along with specific lines while replaying a pad's history.
|
||||
- Added a playback speed setting to the timeslider — you can now scrub through history faster (or slower) than real time.
|
||||
- Creator-owned pad settings defaults: the user who creates a pad now seeds its default settings, giving pad creators more control over initial configuration.
|
||||
- Cookie names are now configurable via a prefix setting. Useful when running multiple Etherpads on the same domain and you need to keep their session cookies from colliding.
|
||||
- Added a new `aceRegisterLineAttributes` hook so plugins can preserve custom line attributes across Enter / line-split operations. Documentation for the hook is included.
|
||||
- Added a one-line installer script for getting Etherpad running quickly on a fresh machine.
|
||||
- Docker images are now published to GitHub Container Registry (GHCR) in addition to Docker Hub.
|
||||
- npm publishing of core and plugins has been migrated to OIDC trusted publishing for stronger supply-chain security.
|
||||
|
||||
### Notable fixes
|
||||
|
||||
- Database drivers are now bundled with Etherpad again, so fresh installs no longer fail to connect to Postgres, MySQL, and friends out of the box. A regression test has been added to prevent this from breaking again.
|
||||
- Pending changesets are now flushed immediately after a reconnect instead of being silently dropped, and users are warned when a pending edit is not accepted by the server.
|
||||
- Head revision and atext are now captured atomically, preventing the occasional "mismatched apply" errors on busy pads.
|
||||
- Clearing authorship colors can now be undone without forcing a client disconnect.
|
||||
- Added periodic cleanup of expired/stale sessions from the database, and fixed a race condition in the session cleanup timeout.
|
||||
- Error messages returned to clients are now sanitized by default with deduplication, so internal details no longer leak through error responses.
|
||||
- Raised the maximum socket.io message size to 10 MB so large pastes no longer get rejected.
|
||||
- Dev mode entrypoint paths now respect the `x-proxy-path` header, fixing reverse-proxy setups in development.
|
||||
- Numerous list-related fixes: numbered list wrapped lines now indent correctly, ordered list numbering is preserved across bullet interruptions during export, consecutive numbering survives indented sub-bullets, switching from unordered to ordered resets numbering, and line attributes are preserved across drag-and-drop.
|
||||
- Bold (and other) formatting is now retained after copy-paste.
|
||||
- Dead-key / compose-key input no longer eats the preceding space.
|
||||
- `POST` API requests with a JSON body no longer time out.
|
||||
- `appendText` now correctly attributes the new text to the specified author.
|
||||
- `createDiffHTML` no longer fails with `Not a changeset: undefined`.
|
||||
- Added `padId` to the `padUpdate` / `padCreate` hook context.
|
||||
- Fixed `numConnectedUsers` to include the joining user in its count.
|
||||
- Accessibility improvements: keyboard trap fix, better screen reader support, and `aria-live` announcements.
|
||||
- RTL URL parameter `rtl=false` now correctly disables RTL mode.
|
||||
- Language dropdown is now sorted alphabetically by native name.
|
||||
- PageDown now advances the caret by a full page of lines.
|
||||
- ESM/CJS interop issues in the Settings module that had been breaking plugin compatibility have been resolved, with setters added to the CJS compatibility layer and regression tests in place.
|
||||
- Several Docker build fixes: git submodule handling, `hardlink` package-import-method for ZFS, and production-only workspace config.
|
||||
|
||||
### Other
|
||||
|
||||
- Many occurrences of "etherpad-lite" have been renamed to "etherpad" across the codebase and documentation.
|
||||
- Pinned 33 transitive dependencies to patched versions to clear out Dependabot security alerts.
|
||||
- Restricted `GITHUB_TOKEN` permissions in the update-plugins workflow.
|
||||
|
||||
# 2.6.1
|
||||
|
||||
For those wondering where the new updates are and why it was very quite throughout the last 1 1/2 years – I've been working on a new implementation of Etherpad from scratch in Go. It's called Etherpad-Go and you can find it here: `https://github.com/ether/etherpad-go`
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
# 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))
|
||||
(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#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
|
||||
|
||||
@ -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!
|
||||
@ -138,5 +140,5 @@ Etherpad is much more than software. So if you aren't a developer then worry no
|
||||
* Write proposals for grants
|
||||
* Co-Author and Publish CVEs
|
||||
* Work with SFC to maintain legal side of project
|
||||
* Maintain TODO page - https://github.com/ether/etherpad-lite/wiki/TODO#IMPORTANT_TODOS
|
||||
* Maintain TODO page - https://github.com/ether/etherpad/wiki/TODO#IMPORTANT_TODOS
|
||||
|
||||
|
||||
41
Dockerfile
41
Dockerfile
@ -1,12 +1,16 @@
|
||||
# Etherpad Lite Dockerfile
|
||||
# Etherpad Dockerfile
|
||||
#
|
||||
# https://github.com/ether/etherpad-lite
|
||||
# https://github.com/ether/etherpad
|
||||
#
|
||||
# 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
|
||||
|
||||
FROM node:lts-alpine AS adminbuild
|
||||
RUN npm install -g pnpm@latest
|
||||
RUN npm install -g pnpm@${PnpmVersion}
|
||||
WORKDIR /opt/etherpad-lite
|
||||
COPY . .
|
||||
RUN pnpm install
|
||||
@ -14,7 +18,7 @@ RUN pnpm run build:ui
|
||||
|
||||
|
||||
FROM node:lts-alpine AS build
|
||||
LABEL maintainer="Etherpad team, https://github.com/ether/etherpad-lite"
|
||||
LABEL maintainer="Etherpad team, https://github.com/ether/etherpad"
|
||||
|
||||
# Set these arguments when building the image from behind a proxy
|
||||
ARG http_proxy=
|
||||
@ -58,15 +62,7 @@ ARG ETHERPAD_LOCAL_PLUGINS=
|
||||
# ETHERPAD_GITHUB_PLUGINS="ether/ep_plugin"
|
||||
ARG ETHERPAD_GITHUB_PLUGINS=
|
||||
|
||||
# Control whether abiword will be installed, enabling exports to DOC/PDF/ODT formats.
|
||||
# By default, it is not installed.
|
||||
# If given any value, abiword will be installed.
|
||||
#
|
||||
# EXAMPLE:
|
||||
# INSTALL_ABIWORD=true
|
||||
ARG INSTALL_ABIWORD=
|
||||
|
||||
# Control whether libreoffice will be installed, enabling exports to DOC/PDF/ODT formats.
|
||||
# Control whether libreoffice will be installed, enabling exports to DOC/DOCX/PDF/ODT formats.
|
||||
# By default, it is not installed.
|
||||
# If given any value, libreoffice will be installed.
|
||||
#
|
||||
@ -100,13 +96,12 @@ RUN mkdir -p "${EP_DIR}" && chown etherpad:etherpad "${EP_DIR}"
|
||||
# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=863199
|
||||
RUN \
|
||||
mkdir -p /usr/share/man/man1 && \
|
||||
npm install pnpm@latest -g && \
|
||||
npm install pnpm@${PnpmVersion} -g && \
|
||||
apk update && apk upgrade && \
|
||||
apk add --no-cache \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
${INSTALL_ABIWORD:+abiword abiword-plugin-command} \
|
||||
${INSTALL_SOFFICE:+libreoffice openjdk8-jre libreoffice-common} && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
@ -123,8 +118,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
|
||||
|
||||
@ -139,7 +139,7 @@ ARG ETHERPAD_LOCAL_PLUGINS_ENV=
|
||||
ARG ETHERPAD_GITHUB_PLUGINS=
|
||||
|
||||
COPY --chown=etherpad:etherpad ./src/ ./src/
|
||||
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/ templates/admin./src/templates/admin
|
||||
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/templates/admin ./src/templates/admin
|
||||
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/static/oidc ./src/static/oidc
|
||||
|
||||
COPY --chown=etherpad:etherpad ./local_plugin[s] ./local_plugins/
|
||||
@ -162,6 +162,11 @@ ARG ETHERPAD_GITHUB_PLUGINS=
|
||||
ENV NODE_ENV=production
|
||||
ENV ETHERPAD_PRODUCTION=true
|
||||
|
||||
# The full pnpm-workspace.yaml references admin, doc, ui which are not
|
||||
# needed at runtime. Overwrite it with a production-only version so
|
||||
# pnpm install doesn't warn about missing workspace directories.
|
||||
RUN printf 'packages:\n - src\n - bin\n' > pnpm-workspace.yaml
|
||||
|
||||
COPY --chown=etherpad:etherpad ./src ./src
|
||||
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/templates/admin ./src/templates/admin
|
||||
COPY --chown=etherpad:etherpad --from=adminbuild /opt/etherpad-lite/src/static/oidc ./src/static/oidc
|
||||
|
||||
112
README.md
112
README.md
@ -1,37 +1,45 @@
|
||||
# Etherpad: A real-time collaborative editor for the web
|
||||
# Etherpad — the editor for documents that matter
|
||||
|
||||
> Real-time collaborative editing where authorship is the default, your server is the only server, and you decide what AI (if any) ever touches your text.
|
||||
|
||||

|
||||
|
||||
## About
|
||||
|
||||
Etherpad is a real-time collaborative editor [scalable to thousands of
|
||||
simultaneous real time users](http://scale.etherpad.org/). It provides [full
|
||||
data
|
||||
export](https://github.com/ether/etherpad-lite/wiki/Understanding-Etherpad's-Full-Data-Export-capabilities)
|
||||
capabilities, and runs on _your_ server, under _your_ control.
|
||||
**Etherpad is a real-time collaborative editor for documents that matter.**
|
||||
|
||||
Every keystroke is attributed to its author. Every revision is preserved. The timeslider lets you scrub through a document's entire history, character by character. Author colours make collaboration visible at a glance — not buried in a menu.
|
||||
|
||||
Etherpad runs on your server, under your governance. No telemetry. No upsells. AI is a plugin you install, pointed at the model you choose, running on infrastructure you control — not a feature decided for you in a boardroom you weren't in.
|
||||
|
||||
The code is Apache 2.0. The data format is open. It [scales to thousands of simultaneous editors per pad](http://scale.etherpad.org/). Translated into 105 languages. Extended through hundreds of plugins. Used by Wikimedia, governments, public-sector institutions, and self-hosters worldwide since 2009.
|
||||
|
||||
[Full data export](https://github.com/ether/etherpad/wiki/Understanding-Etherpad's-Full-Data-Export-capabilities) is built in. The history is yours.
|
||||
|
||||
## Try it out
|
||||
|
||||
Wikimedia provide a [public Etherpad instance for you to Try Etherpad out.](https://etherpad.wikimedia.org) or [use another public Etherpad instance to see other features](https://github.com/ether/etherpad-lite/wiki/Sites-That-Run-Etherpad#sites-that-run-etherpad)
|
||||
[Try out a public Etherpad instance](https://github.com/ether/etherpad/wiki/Sites-That-Run-Etherpad#sites-that-run-etherpad)
|
||||
|
||||
## Project Status
|
||||
|
||||
We're looking for maintainers and have some funding available. Please contact John McLear if you can help.
|
||||
Etherpad has been doing the same thing — well — since 2009. No pivots, no acquisitions, no enshittification. Maintained by a small volunteer team.
|
||||
|
||||
**We are actively looking for maintainers.** If you have experience with Node.js, real-time systems, or institutional collaboration tooling and you want to work on infrastructure that thousands of organisations quietly depend on, please [open an issue](https://github.com/ether/etherpad/issues) or contact [John McLear](https://github.com/JohnMcLear).
|
||||
|
||||
### Code Quality
|
||||
|
||||
[](https://github.com/ether/etherpad-lite/actions/workflows/codeql-analysis.yml)
|
||||
[](https://github.com/ether/etherpad/actions/workflows/codeql-analysis.yml)
|
||||
|
||||
### Testing
|
||||
|
||||
[](https://github.com/ether/etherpad-lite/actions/workflows/backend-tests.yml)
|
||||
[](https://github.com/ether/etherpad-lite/actions/workflows/load-test.yml)
|
||||
[](https://github.com/ether/etherpad-lite/actions/workflows/rate-limit.yml)
|
||||
[](https://github.com/ether/etherpad-lite/actions/workflows/dockerfile.yml)
|
||||
[](https://github.com/ether/etherpad-lite/actions/workflows/frontend-admin-tests.yml)
|
||||
[](https://github.com/ether/etherpad-lite/actions/workflows/frontend-tests.yml)
|
||||
[](https://github.com/ether/etherpad/actions/workflows/backend-tests.yml)
|
||||
[](https://github.com/ether/etherpad/actions/workflows/load-test.yml)
|
||||
[](https://github.com/ether/etherpad/actions/workflows/rate-limit.yml)
|
||||
[](https://github.com/ether/etherpad/actions/workflows/docker.yml)
|
||||
[](https://github.com/ether/etherpad/actions/workflows/frontend-admin-tests.yml)
|
||||
[](https://github.com/ether/etherpad/actions/workflows/frontend-tests.yml)
|
||||
[](https://saucelabs.com/u/etherpad)
|
||||
[](https://github.com/ether/etherpad-lite/actions/workflows/windows.yml)
|
||||
[](https://github.com/ether/etherpad/actions/workflows/windows.yml)
|
||||
|
||||
### Engagement
|
||||
|
||||
@ -41,15 +49,67 @@ We're looking for maintainers and have some funding available. Please contact J
|
||||

|
||||

|
||||
|
||||
## Who uses Etherpad
|
||||
|
||||
For more than a decade, Etherpad has quietly underpinned the documents that matter to:
|
||||
|
||||
- **Wikimedia Foundation** — collaborative drafting across editor communities.
|
||||
- **Public-sector institutions across the EU** — including organisations that legally cannot use US-cloud SaaS for sovereignty and GDPR reasons.
|
||||
- **Universities and schools worldwide** — including jurisdictions where Google Workspace is no longer permitted in education.
|
||||
- **Civic-tech and democratic-deliberation projects** — citizen assemblies, participatory budgeting, public consultations.
|
||||
- **Newsrooms and investigative journalism teams** — where authorship and editing history matter for legal and editorial integrity.
|
||||
- **Tens of thousands of self-hosted instances** worldwide, run by IT teams who chose Etherpad because it is theirs.
|
||||
|
||||
If your organisation runs Etherpad and would be willing to be listed publicly, please [add it to the wiki](https://github.com/ether/etherpad/wiki/Sites-That-Run-Etherpad).
|
||||
|
||||
## Installation
|
||||
|
||||
### Quick install (one-liner)
|
||||
|
||||
The fastest way to get Etherpad running. Requires `git` and Node.js >= 20.
|
||||
|
||||
**macOS / Linux / WSL:**
|
||||
|
||||
```sh
|
||||
curl -fsSL https://raw.githubusercontent.com/ether/etherpad/master/bin/installer.sh | sh
|
||||
```
|
||||
|
||||
**Windows (PowerShell):**
|
||||
|
||||
```powershell
|
||||
irm https://raw.githubusercontent.com/ether/etherpad/master/bin/installer.ps1 | iex
|
||||
```
|
||||
|
||||
Both installers clone Etherpad into `./etherpad-lite`, install dependencies, and
|
||||
build the frontend. When the installer finishes, run:
|
||||
|
||||
```sh
|
||||
cd etherpad-lite && pnpm run prod
|
||||
```
|
||||
|
||||
Then open <http://localhost:9001>.
|
||||
|
||||
To install and start in one go:
|
||||
|
||||
```sh
|
||||
# macOS / Linux / WSL
|
||||
ETHERPAD_RUN=1 sh -c "$(curl -fsSL https://raw.githubusercontent.com/ether/etherpad/master/bin/installer.sh)"
|
||||
```
|
||||
|
||||
```powershell
|
||||
# Windows
|
||||
$env:ETHERPAD_RUN=1; irm https://raw.githubusercontent.com/ether/etherpad/master/bin/installer.ps1 | iex
|
||||
```
|
||||
|
||||
### Docker-Compose
|
||||
|
||||
The official image is published to both Docker Hub (`etherpad/etherpad`) and GitHub Container Registry (`ghcr.io/ether/etherpad`) with identical tags. Use whichever suits your environment; GHCR avoids Docker Hub's anonymous pull rate limits.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
user: "0:0"
|
||||
image: etherpad/etherpad:latest
|
||||
image: etherpad/etherpad:latest # or: ghcr.io/ether/etherpad:latest
|
||||
tty: true
|
||||
stdin_open: true
|
||||
volumes:
|
||||
@ -100,7 +160,7 @@ volumes:
|
||||
|
||||
### Requirements
|
||||
|
||||
[Node.js](https://nodejs.org/) >= **18.18.2**.
|
||||
[Node.js](https://nodejs.org/) >= 20.
|
||||
|
||||
### Windows, macOS, Linux
|
||||
|
||||
@ -142,7 +202,7 @@ pnpm run plugins i ep_${plugin_name}
|
||||
```
|
||||
|
||||
Also see [the plugin wiki
|
||||
article](https://github.com/ether/etherpad-lite/wiki/Available-Plugins).
|
||||
article](https://github.com/ether/etherpad/wiki/Available-Plugins).
|
||||
|
||||
### Suggested Plugins
|
||||
|
||||
@ -239,7 +299,7 @@ playing!
|
||||
|
||||
## Helpful resources
|
||||
|
||||
The [wiki](https://github.com/ether/etherpad-lite/wiki) is your one-stop
|
||||
The [wiki](https://github.com/ether/etherpad/wiki) is your one-stop
|
||||
resource for Tutorials and How-to's.
|
||||
|
||||
Documentation can be found in `doc/`.
|
||||
@ -257,21 +317,21 @@ dependency or upgrading version.
|
||||
|
||||
If you want to find out how Etherpad's `Easysync` works (the library that makes
|
||||
it really realtime), start with this
|
||||
[PDF](https://github.com/ether/etherpad-lite/raw/master/doc/easysync/easysync-full-description.pdf)
|
||||
[PDF](https://github.com/ether/etherpad/raw/master/doc/easysync/easysync-full-description.pdf)
|
||||
(complex, but worth reading).
|
||||
|
||||
### Contributing
|
||||
|
||||
Read our [**Developer
|
||||
Guidelines**](https://github.com/ether/etherpad-lite/blob/master/CONTRIBUTING.md)
|
||||
Guidelines**](https://github.com/ether/etherpad/blob/master/CONTRIBUTING.md)
|
||||
|
||||
### HTTP API
|
||||
|
||||
Etherpad is designed to be easily embeddable and provides a [HTTP
|
||||
API](https://github.com/ether/etherpad-lite/wiki/HTTP-API) that allows your web
|
||||
API](https://github.com/ether/etherpad/wiki/HTTP-API) that allows your web
|
||||
application to manage pads, users and groups. It is recommended to use the
|
||||
[available client
|
||||
implementations](https://github.com/ether/etherpad-lite/wiki/HTTP-API-client-libraries)
|
||||
implementations](https://github.com/ether/etherpad/wiki/HTTP-API-client-libraries)
|
||||
in order to interact with this API.
|
||||
|
||||
OpenAPI (previously swagger) definitions for the API are exposed under
|
||||
@ -299,12 +359,12 @@ send pull request to each plugin individually.
|
||||
|
||||
## FAQ
|
||||
|
||||
Visit the **[FAQ](https://github.com/ether/etherpad-lite/wiki/FAQ)**.
|
||||
Visit the **[FAQ](https://github.com/ether/etherpad/wiki/FAQ)**.
|
||||
|
||||
## Get in touch
|
||||
|
||||
The official channel for contacting the development team is via the [GitHub
|
||||
issues](https://github.com/ether/etherpad-lite/issues).
|
||||
issues](https://github.com/ether/etherpad/issues).
|
||||
|
||||
For **responsible disclosure of vulnerabilities**, please write a mail to the
|
||||
maintainers (a.mux@inwind.it and contact@etherpad.org).
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "admin",
|
||||
"private": true,
|
||||
"version": "2.6.1",
|
||||
"version": "2.7.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@ -16,29 +16,29 @@
|
||||
"devDependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
"@radix-ui/react-toast": "^1.2.15",
|
||||
"@types/react": "^19.2.9",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.53.1",
|
||||
"@typescript-eslint/parser": "^8.53.1",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.58.2",
|
||||
"@typescript-eslint/parser": "^8.58.2",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"babel-plugin-react-compiler": "19.1.0-rc.3",
|
||||
"eslint": "^9.39.2",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.4.26",
|
||||
"i18next": "^25.8.0",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"lucide-react": "^0.563.0",
|
||||
"react": "^19.2.3",
|
||||
"react-dom": "^19.2.3",
|
||||
"react-hook-form": "^7.71.1",
|
||||
"react-i18next": "^16.5.3",
|
||||
"react-router-dom": "^7.13.0",
|
||||
"eslint": "^10.2.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.0",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"i18next": "^26.0.5",
|
||||
"i18next-browser-languagedetector": "^8.2.1",
|
||||
"lucide-react": "^1.8.0",
|
||||
"react": "^19.2.5",
|
||||
"react-dom": "^19.2.5",
|
||||
"react-hook-form": "^7.72.1",
|
||||
"react-i18next": "^17.0.4",
|
||||
"react-router-dom": "^7.14.1",
|
||||
"socket.io-client": "^4.8.3",
|
||||
"typescript": "^5.9.3",
|
||||
"typescript": "^6.0.3",
|
||||
"vite": "npm:rolldown-vite@7.2.10",
|
||||
"vite-plugin-babel": "^1.4.1",
|
||||
"vite-plugin-static-copy": "^3.1.6",
|
||||
"zustand": "^5.0.10"
|
||||
"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"
|
||||
|
||||
@ -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<IconButtonProps> = ({icon,className,onClick,title, disabled, style})=>{
|
||||
return <button style={style} onClick={onClick} className={"icon-button "+ className} disabled={disabled}>
|
||||
{icon}
|
||||
<span>{title}</span>
|
||||
</button>
|
||||
return <button style={style} onClick={onClick} className={"icon-button "+ className} disabled={disabled}>
|
||||
{icon}
|
||||
<span>{title}</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import {ChangeEventHandler, FC} from "react";
|
||||
import {Search} from 'lucide-react'
|
||||
export type SearchFieldProps = {
|
||||
value: string,
|
||||
onChange: ChangeEventHandler<HTMLInputElement>,
|
||||
placeholder?: string
|
||||
value: string,
|
||||
onChange: ChangeEventHandler<HTMLInputElement>,
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export const SearchField:FC<SearchFieldProps> = ({onChange,value, placeholder})=>{
|
||||
return <span className="search-field">
|
||||
<input value={value} onChange={onChange} placeholder={placeholder}/>
|
||||
<Search/>
|
||||
</span>
|
||||
return <span className="search-field">
|
||||
<input value={value} onChange={onChange} placeholder={placeholder}/>
|
||||
<Search/>
|
||||
</span>
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
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`
|
||||
}
|
||||
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;
|
||||
const localeJSON = await fetch(baseURL)
|
||||
let json;
|
||||
|
||||
try {
|
||||
json = JSON.parse(await localeJSON.text())
|
||||
} catch(e) {
|
||||
callback(new Error("Error loading"), null);
|
||||
}
|
||||
try {
|
||||
json = JSON.parse(await localeJSON.text())
|
||||
} catch(e) {
|
||||
callback(new Error("Error loading"), null);
|
||||
}
|
||||
|
||||
|
||||
callback(null, json);
|
||||
},
|
||||
callback(null, json);
|
||||
},
|
||||
|
||||
save: function () {
|
||||
},
|
||||
save: function () {
|
||||
},
|
||||
|
||||
create: function () {
|
||||
/* save the missing translation */
|
||||
},
|
||||
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
|
||||
|
||||
@ -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<HelpObj>();
|
||||
const settingsSocket = useStore(state=>state.settingsSocket)
|
||||
const [helpData, setHelpData] = useState<HelpObj>();
|
||||
|
||||
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<string, Record<string, string>>) => {
|
||||
return Object.keys(hooks).map((hookName, i) => {
|
||||
return <div key={hookName+i}>
|
||||
<h3>{hookName}</h3>
|
||||
<ul>
|
||||
{Object.keys(hooks[hookName]).map((hook, i) => <li key={hook+i}>{hook}
|
||||
<ul key={hookName+hook+i}>
|
||||
{Object.keys(hooks[hookName][hook]).map((subHook, i) => <li key={i}>{subHook}</li>)}
|
||||
</ul>
|
||||
</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
})
|
||||
const renderHooks = (hooks:Record<string, Record<string, string>>) => {
|
||||
return Object.keys(hooks).map((hookName, i) => {
|
||||
return <div key={hookName+i}>
|
||||
<h3>{hookName}</h3>
|
||||
<ul>
|
||||
{Object.keys(hooks[hookName]).map((hook, i) => <li key={hook+i}>{hook}
|
||||
<ul key={hookName+hook+i}>
|
||||
{Object.keys(hooks[hookName][hook]).map((subHook, i) => <li key={i}>{subHook}</li>)}
|
||||
</ul>
|
||||
</li>)}
|
||||
</ul>
|
||||
</div>
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
if (!helpData) return <div></div>
|
||||
|
||||
return <div>
|
||||
<h1><Trans i18nKey="admin_plugins_info.version"/></h1>
|
||||
<div className="help-block">
|
||||
<div><Trans i18nKey="admin_plugins_info.version_number"/></div>
|
||||
<div>{helpData?.epVersion}</div>
|
||||
<div><Trans i18nKey="admin_plugins_info.version_latest"/></div>
|
||||
<div>{helpData.latestVersion}</div>
|
||||
<div>Git sha</div>
|
||||
<div>{helpData.gitCommit}</div>
|
||||
</div>
|
||||
<h2><Trans i18nKey="admin_plugins.installed"/></h2>
|
||||
<ul>
|
||||
{helpData.installedPlugins.map((plugin, i) => <li key={plugin+i}>{plugin}</li>)}
|
||||
</ul>
|
||||
|
||||
<h2><Trans i18nKey="admin_plugins_info.parts"/></h2>
|
||||
<ul>
|
||||
{helpData.installedParts.map((part, i) => <li key={part+i}>{part}</li>)}
|
||||
</ul>
|
||||
|
||||
<h2><Trans i18nKey="admin_plugins_info.hooks"/></h2>
|
||||
{
|
||||
renderHooks(helpData.installedServerHooks)
|
||||
}
|
||||
|
||||
<h2>
|
||||
<Trans i18nKey="admin_plugins_info.hooks_client"/>
|
||||
{
|
||||
renderHooks(helpData.installedClientHooks)
|
||||
}
|
||||
</h2>
|
||||
|
||||
if (!helpData) return <div></div>
|
||||
|
||||
return <div>
|
||||
<h1><Trans i18nKey="admin_plugins_info.version"/></h1>
|
||||
<div className="help-block">
|
||||
<div><Trans i18nKey="admin_plugins_info.version_number"/></div>
|
||||
<div>{helpData?.epVersion}</div>
|
||||
<div><Trans i18nKey="admin_plugins_info.version_latest"/></div>
|
||||
<div>{helpData.latestVersion}</div>
|
||||
<div>Git sha</div>
|
||||
<div>{helpData.gitCommit}</div>
|
||||
</div>
|
||||
<h2><Trans i18nKey="admin_plugins.installed"/></h2>
|
||||
<ul>
|
||||
{helpData.installedPlugins.map((plugin, i) => <li key={plugin+i}>{plugin}</li>)}
|
||||
</ul>
|
||||
|
||||
<h2><Trans i18nKey="admin_plugins_info.parts"/></h2>
|
||||
<ul>
|
||||
{helpData.installedParts.map((part, i) => <li key={part+i}>{part}</li>)}
|
||||
</ul>
|
||||
|
||||
<h2><Trans i18nKey="admin_plugins_info.hooks"/></h2>
|
||||
{
|
||||
renderHooks(helpData.installedServerHooks)
|
||||
}
|
||||
|
||||
<h2>
|
||||
<Trans i18nKey="admin_plugins_info.hooks_client"/>
|
||||
{
|
||||
renderHooks(helpData.installedClientHooks)
|
||||
}
|
||||
</h2>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -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<boolean>(false)
|
||||
const navigate = useNavigate()
|
||||
const [passwordVisible, setPasswordVisible] = useState<boolean>(false)
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit} = useForm<Inputs>()
|
||||
const {
|
||||
register,
|
||||
handleSubmit} = useForm<Inputs>()
|
||||
|
||||
const login: SubmitHandler<Inputs> = ({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<Inputs> = ({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 <div className="login-background login-page">
|
||||
<div className="login-box login-form">
|
||||
<h1 className="login-title">Etherpad</h1>
|
||||
<form className="login-inner-box input-control" onSubmit={handleSubmit(login)}>
|
||||
<div>Username</div>
|
||||
<input {...register('username', {
|
||||
required: true
|
||||
})} className="login-textinput input-control" type="text" placeholder="Username"/>
|
||||
<div>Password</div>
|
||||
<span className="icon-input">
|
||||
<input {...register('password', {
|
||||
required: true
|
||||
})} className="login-textinput" type={passwordVisible?"text":"password"} placeholder="Password"/>
|
||||
{passwordVisible? <Eye onClick={()=>setPasswordVisible(!passwordVisible)}/> :
|
||||
<EyeOff onClick={()=>setPasswordVisible(!passwordVisible)}/>}
|
||||
</span>
|
||||
<input type="submit" value="Login" className="login-button"/>
|
||||
</form>
|
||||
</div>
|
||||
return <div className="login-background login-page">
|
||||
<div className="login-box login-form">
|
||||
<h1 className="login-title">Etherpad</h1>
|
||||
<form className="login-inner-box input-control" onSubmit={handleSubmit(login)}>
|
||||
<div>Username</div>
|
||||
<input {...register('username', {
|
||||
required: true
|
||||
})} className="login-textinput input-control" type="text" placeholder="Username"/>
|
||||
<div>Password</div>
|
||||
<span className="icon-input">
|
||||
<input {...register('password', {
|
||||
required: true
|
||||
})} className="login-textinput" type={passwordVisible?"text":"password"} placeholder="Password"/>
|
||||
{passwordVisible? <Eye onClick={()=>setPasswordVisible(!passwordVisible)}/> :
|
||||
<EyeOff onClick={()=>setPasswordVisible(!passwordVisible)}/>}
|
||||
</span>
|
||||
<input type="submit" value="Login" className="login-button"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -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<PadSearchQuery>({
|
||||
offset: 0,
|
||||
limit: 12,
|
||||
pattern: '',
|
||||
sortBy: 'padName',
|
||||
ascending: true
|
||||
const settingsSocket = useStore(state=>state.settingsSocket)
|
||||
const [searchParams, setSearchParams] = useState<PadSearchQuery>({
|
||||
offset: 0,
|
||||
limit: 12,
|
||||
pattern: '',
|
||||
sortBy: 'padName',
|
||||
ascending: true
|
||||
})
|
||||
const {t} = useTranslation()
|
||||
const [searchTerm, setSearchTerm] = useState<string>('')
|
||||
const pads = useStore(state=>state.pads)
|
||||
const [currentPage, setCurrentPage] = useState<number>(0)
|
||||
const [deleteDialog, setDeleteDialog] = useState<boolean>(false)
|
||||
const [errorText, setErrorText] = useState<string|null>(null)
|
||||
const [padToDelete, setPadToDelete] = useState<string>('')
|
||||
const [createPadDialogOpen, setCreatePadDialogOpen] = useState<boolean>(false)
|
||||
const {register, handleSubmit} = useForm<PadCreateProps>()
|
||||
const pages = useMemo(()=>{
|
||||
if(!pads){
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.ceil(pads!.total / searchParams.limit)
|
||||
},[pads, searchParams.limit])
|
||||
|
||||
useDebounce(()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
pattern: searchTerm
|
||||
})
|
||||
const {t} = useTranslation()
|
||||
const [searchTerm, setSearchTerm] = useState<string>('')
|
||||
const pads = useStore(state=>state.pads)
|
||||
const [currentPage, setCurrentPage] = useState<number>(0)
|
||||
const [deleteDialog, setDeleteDialog] = useState<boolean>(false)
|
||||
const [errorText, setErrorText] = useState<string|null>(null)
|
||||
const [padToDelete, setPadToDelete] = useState<string>('')
|
||||
const [createPadDialogOpen, setCreatePadDialogOpen] = useState<boolean>(false)
|
||||
const {register, handleSubmit} = useForm<PadCreateProps>()
|
||||
const pages = useMemo(()=>{
|
||||
if(!pads){
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Math.ceil(pads!.total / searchParams.limit)
|
||||
},[pads, searchParams.limit])
|
||||
}, 500, [searchTerm])
|
||||
|
||||
useDebounce(()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
pattern: searchTerm
|
||||
})
|
||||
useEffect(() => {
|
||||
if(!settingsSocket){
|
||||
return
|
||||
}
|
||||
|
||||
}, 500, [searchTerm])
|
||||
settingsSocket.emit('padLoad', searchParams)
|
||||
|
||||
useEffect(() => {
|
||||
if(!settingsSocket){
|
||||
return
|
||||
}
|
||||
}, [settingsSocket, searchParams]);
|
||||
|
||||
settingsSocket.emit('padLoad', searchParams)
|
||||
useEffect(() => {
|
||||
if(!settingsSocket){
|
||||
return
|
||||
}
|
||||
|
||||
}, [settingsSocket, searchParams]);
|
||||
|
||||
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:cleanupPadRevisions', (data)=>{
|
||||
const newPads = useStore.getState().pads?.results ?? []
|
||||
|
||||
if (data.error) {
|
||||
setErrorText(data.error)
|
||||
return
|
||||
}
|
||||
|
||||
newPads.forEach((pad)=>{
|
||||
if (pad.padName === data.padId) {
|
||||
pad.revisionNumber = data.keepRevisions
|
||||
}
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
useStore.getState().setPads({
|
||||
results: newPads,
|
||||
total: useStore.getState().pads!.total
|
||||
})
|
||||
})
|
||||
}, [settingsSocket, pads]);
|
||||
|
||||
settingsSocket.on('results:cleanupPadRevisions', (data)=>{
|
||||
const newPads = useStore.getState().pads?.results ?? []
|
||||
const deletePad = (padID: string)=>{
|
||||
settingsSocket?.emit('deletePad', padID)
|
||||
}
|
||||
|
||||
if (data.error) {
|
||||
setErrorText(data.error)
|
||||
return
|
||||
}
|
||||
const cleanupPad = (padID: string)=>{
|
||||
settingsSocket?.emit('cleanupPadRevisions', padID)
|
||||
}
|
||||
|
||||
newPads.forEach((pad)=>{
|
||||
if (pad.padName === data.padId) {
|
||||
pad.revisionNumber = data.keepRevisions
|
||||
}
|
||||
})
|
||||
|
||||
useStore.getState().setPads({
|
||||
results: newPads,
|
||||
total: useStore.getState().pads!.total
|
||||
})
|
||||
})
|
||||
}, [settingsSocket, pads]);
|
||||
|
||||
const deletePad = (padID: string)=>{
|
||||
settingsSocket?.emit('deletePad', 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 <div>
|
||||
<Dialog.Root open={deleteDialog}><Dialog.Portal>
|
||||
<Dialog.Overlay className="dialog-confirm-overlay" />
|
||||
<Dialog.Content className="dialog-confirm-content">
|
||||
<div className="">
|
||||
<div className=""></div>
|
||||
<div className="">
|
||||
{t("ep_admin_pads:ep_adminpads2_confirm", {
|
||||
padID: padToDelete,
|
||||
})}
|
||||
</div>
|
||||
<div className="settings-button-bar">
|
||||
<button onClick={()=>{
|
||||
setDeleteDialog(false)
|
||||
}}>Cancel</button>
|
||||
<button onClick={()=>{
|
||||
deletePad(padToDelete)
|
||||
setDeleteDialog(false)
|
||||
}}>Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
<Dialog.Root open={errorText !== null}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="dialog-confirm-overlay"/>
|
||||
<Dialog.Content className="dialog-confirm-content">
|
||||
<div>
|
||||
<div>Error occured: {errorText}</div>
|
||||
<div className="settings-button-bar">
|
||||
<button onClick={() => {
|
||||
setErrorText(null)
|
||||
}}>OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
<Dialog.Root open={createPadDialogOpen}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="dialog-confirm-overlay" />
|
||||
<Dialog.Content className="dialog-confirm-content">
|
||||
<Dialog.Title className="dialog-confirm-title"><Trans i18nKey="index.newPad"/></Dialog.Title>
|
||||
<form onSubmit={handleSubmit(onPadCreate)}>
|
||||
<button className="dialog-close-button" onClick={()=>{
|
||||
setCreatePadDialogOpen(false);
|
||||
}}>x</button>
|
||||
<div style={{display: 'grid', gap: '10px', gridTemplateColumns: 'auto auto', marginBottom: '1rem'}}>
|
||||
<label><Trans i18nKey="ep_admin_pads:ep_adminpads2_padname"/></label>
|
||||
<input {...register('padName', {
|
||||
required: true
|
||||
})}/>
|
||||
</div>
|
||||
<input type="submit" value={t('admin_settings.createPad')} className="login-button" />
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
<span className="manage-pads-header">
|
||||
<h1><Trans i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></h1>
|
||||
<span style={{width: '29px', marginBottom: 'auto', marginTop: 'auto', flexGrow: 1}}><IconButton style={{float: 'right'}} icon={<PlusIcon/>} title={<Trans i18nKey="index.newPad"/>} onClick={()=>{
|
||||
setCreatePadDialogOpen(true)
|
||||
}}/></span>
|
||||
</span>
|
||||
<SearchField value={searchTerm} onChange={v=>setSearchTerm(v.target.value)} placeholder={t('ep_admin_pads:ep_adminpads2_search-heading')}/>
|
||||
<table>
|
||||
<thead>
|
||||
<tr className="search-pads">
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'padName')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'padName',
|
||||
ascending: !searchParams.ascending
|
||||
})
|
||||
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_padname"/></th>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'userCount')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'userCount',
|
||||
ascending: !searchParams.ascending
|
||||
})
|
||||
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_pad-user-count"/></th>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'lastEdited')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'lastEdited',
|
||||
ascending: !searchParams.ascending
|
||||
})
|
||||
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_last-edited"/></th>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'revisionNumber')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'revisionNumber',
|
||||
ascending: !searchParams.ascending
|
||||
})
|
||||
}}>Revision number</th>
|
||||
<th><Trans i18nKey="ep_admin_pads:ep_adminpads2_action"/></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="search-pads-body">
|
||||
{
|
||||
pads?.results?.map((pad)=>{
|
||||
return <tr key={pad.padName}>
|
||||
<td style={{textAlign: 'center'}}>{pad.padName}</td>
|
||||
<td style={{textAlign: 'center'}}>{pad.userCount}</td>
|
||||
<td style={{textAlign: 'center'}}>{new Date(pad.lastEdited).toLocaleString()}</td>
|
||||
<td style={{textAlign: 'center'}}>{pad.revisionNumber}</td>
|
||||
<td>
|
||||
<div className="settings-button-bar">
|
||||
<IconButton icon={<Trash2/>} title={<Trans i18nKey="ep_admin_pads:ep_adminpads2_delete.value"/>} onClick={()=>{
|
||||
setPadToDelete(pad.padName)
|
||||
setDeleteDialog(true)
|
||||
}}/>
|
||||
<IconButton icon={<FileStack/>} title={<Trans i18nKey="ep_admin_pads:ep_adminpads2_cleanup"/>} onClick={()=>{
|
||||
cleanupPad(pad.padName)
|
||||
}}/>
|
||||
<IconButton icon={<Eye/>} title={<Trans i18nKey="index.createOpenPad"/>} onClick={()=>window.open(`../../p/${pad.padName}`, '_blank')}/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="settings-button-bar pad-pagination">
|
||||
<button disabled={currentPage == 0} onClick={()=>{
|
||||
setCurrentPage(currentPage-1)
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
offset: (Number(currentPage)-1)*searchParams.limit})
|
||||
}}><ChevronLeft/><span>Previous Page</span></button>
|
||||
<span>{currentPage+1} out of {pages}</span>
|
||||
<button disabled={pages == 0 || pages == currentPage+1} onClick={()=>{
|
||||
const newCurrentPage = currentPage+1
|
||||
setCurrentPage(newCurrentPage)
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
offset: (Number(newCurrentPage))*searchParams.limit
|
||||
})
|
||||
}}><span>Next Page</span><ChevronRight/></button>
|
||||
return <div>
|
||||
<Dialog.Root open={deleteDialog}><Dialog.Portal>
|
||||
<Dialog.Overlay className="dialog-confirm-overlay" />
|
||||
<Dialog.Content className="dialog-confirm-content">
|
||||
<div className="">
|
||||
<div className=""></div>
|
||||
<div className="">
|
||||
{t("ep_admin_pads:ep_adminpads2_confirm", {
|
||||
padID: padToDelete,
|
||||
})}
|
||||
</div>
|
||||
<div className="settings-button-bar">
|
||||
<button onClick={()=>{
|
||||
setDeleteDialog(false)
|
||||
}}>Cancel</button>
|
||||
<button onClick={()=>{
|
||||
deletePad(padToDelete)
|
||||
setDeleteDialog(false)
|
||||
}}>Ok</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
<Dialog.Root open={errorText !== null}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="dialog-confirm-overlay"/>
|
||||
<Dialog.Content className="dialog-confirm-content">
|
||||
<div>
|
||||
<div>Error occured: {errorText}</div>
|
||||
<div className="settings-button-bar">
|
||||
<button onClick={() => {
|
||||
setErrorText(null)
|
||||
}}>OK</button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
<Dialog.Root open={createPadDialogOpen}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="dialog-confirm-overlay" />
|
||||
<Dialog.Content className="dialog-confirm-content">
|
||||
<Dialog.Title className="dialog-confirm-title"><Trans i18nKey="index.newPad"/></Dialog.Title>
|
||||
<form onSubmit={handleSubmit(onPadCreate)}>
|
||||
<button className="dialog-close-button" onClick={()=>{
|
||||
setCreatePadDialogOpen(false);
|
||||
}}>x</button>
|
||||
<div style={{display: 'grid', gap: '10px', gridTemplateColumns: 'auto auto', marginBottom: '1rem'}}>
|
||||
<label><Trans i18nKey="ep_admin_pads:ep_adminpads2_padname"/></label>
|
||||
<input {...register('padName', {
|
||||
required: true
|
||||
})}/>
|
||||
</div>
|
||||
<input type="submit" value={t('admin_settings.createPad')} className="login-button" />
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
<span className="manage-pads-header">
|
||||
<h1><Trans i18nKey="ep_admin_pads:ep_adminpads2_manage-pads"/></h1>
|
||||
<span style={{width: '29px', marginBottom: 'auto', marginTop: 'auto', flexGrow: 1}}><IconButton style={{float: 'right'}} icon={<PlusIcon/>} title={<Trans i18nKey="index.newPad"/>} onClick={()=>{
|
||||
setCreatePadDialogOpen(true)
|
||||
}}/></span>
|
||||
</span>
|
||||
<SearchField value={searchTerm} onChange={v=>setSearchTerm(v.target.value)} placeholder={t('ep_admin_pads:ep_adminpads2_search-heading')}/>
|
||||
<table>
|
||||
<thead>
|
||||
<tr className="search-pads">
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'padName')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'padName',
|
||||
ascending: !searchParams.ascending
|
||||
})
|
||||
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_padname"/></th>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'userCount')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'userCount',
|
||||
ascending: !searchParams.ascending
|
||||
})
|
||||
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_pad-user-count"/></th>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'lastEdited')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'lastEdited',
|
||||
ascending: !searchParams.ascending
|
||||
})
|
||||
}}><Trans i18nKey="ep_admin_pads:ep_adminpads2_last-edited"/></th>
|
||||
<th className={determineSorting(searchParams.sortBy, searchParams.ascending, 'revisionNumber')} onClick={()=>{
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
sortBy: 'revisionNumber',
|
||||
ascending: !searchParams.ascending
|
||||
})
|
||||
}}>Revision number</th>
|
||||
<th><Trans i18nKey="ep_admin_pads:ep_adminpads2_action"/></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="search-pads-body">
|
||||
{
|
||||
pads?.results?.map((pad)=>{
|
||||
return <tr key={pad.padName}>
|
||||
<td style={{textAlign: 'center'}}>{pad.padName}</td>
|
||||
<td style={{textAlign: 'center'}}>{pad.userCount}</td>
|
||||
<td style={{textAlign: 'center'}}>{new Date(pad.lastEdited).toLocaleString()}</td>
|
||||
<td style={{textAlign: 'center'}}>{pad.revisionNumber}</td>
|
||||
<td>
|
||||
<div className="settings-button-bar">
|
||||
<IconButton icon={<Trash2/>} title={<Trans i18nKey="ep_admin_pads:ep_adminpads2_delete.value"/>} onClick={()=>{
|
||||
setPadToDelete(pad.padName)
|
||||
setDeleteDialog(true)
|
||||
}}/>
|
||||
<IconButton icon={<FileStack/>} title={<Trans i18nKey="ep_admin_pads:ep_adminpads2_cleanup"/>} onClick={()=>{
|
||||
cleanupPad(pad.padName)
|
||||
}}/>
|
||||
<IconButton icon={<Eye/>} title={<Trans i18nKey="index.createOpenPad"/>} onClick={()=>window.open(`../../p/${pad.padName}`, '_blank')}/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
})
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="settings-button-bar pad-pagination">
|
||||
<button disabled={currentPage == 0} onClick={()=>{
|
||||
setCurrentPage(currentPage-1)
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
offset: (Number(currentPage)-1)*searchParams.limit})
|
||||
}}><ChevronLeft/><span>Previous Page</span></button>
|
||||
<span>{currentPage+1} out of {pages}</span>
|
||||
<button disabled={pages == 0 || pages == currentPage+1} onClick={()=>{
|
||||
const newCurrentPage = currentPage+1
|
||||
setCurrentPage(newCurrentPage)
|
||||
setSearchParams({
|
||||
...searchParams,
|
||||
offset: (Number(newCurrentPage))*searchParams.limit
|
||||
})
|
||||
}}><span>Next Page</span><ChevronRight/></button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -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<string, Record<string, string>>,
|
||||
installedParts: string[],
|
||||
installedPlugins: string[],
|
||||
installedServerHooks: Record<string, never>,
|
||||
latestVersion: string
|
||||
epVersion: string
|
||||
gitCommit: string
|
||||
installedClientHooks: Record<string, Record<string, string>>,
|
||||
installedParts: string[],
|
||||
installedPlugins: string[],
|
||||
installedServerHooks: Record<string, never>,
|
||||
latestVersion: string
|
||||
}
|
||||
|
||||
@ -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 <div className="settings-page">
|
||||
<h1><Trans i18nKey="admin_settings.current"/></h1>
|
||||
<textarea value={settings} className="settings" onChange={v => {
|
||||
useStore.getState().setSettings(v.target.value)
|
||||
}}/>
|
||||
<div className="settings-button-bar">
|
||||
<IconButton className="settingsButton" icon={<Save/>}
|
||||
title={<Trans i18nKey="admin_settings.current_save.value"/>} onClick={() => {
|
||||
if (isJSONClean(settings!)) {
|
||||
// JSON is clean so emit it to the server
|
||||
settingsSocket!.emit('saveSettings', settings!);
|
||||
useStore.getState().setToastState({
|
||||
open: true,
|
||||
title: "Successfully saved settings",
|
||||
success: true
|
||||
})
|
||||
} else {
|
||||
useStore.getState().setToastState({
|
||||
open: true,
|
||||
title: "Error saving settings",
|
||||
success: false
|
||||
})
|
||||
}
|
||||
}}/>
|
||||
<IconButton className="settingsButton" icon={<RotateCw/>}
|
||||
title={<Trans i18nKey="admin_settings.current_restart.value"/>} onClick={() => {
|
||||
settingsSocket!.emit('restartServer');
|
||||
}}/>
|
||||
</div>
|
||||
<div className="separator"/>
|
||||
<div className="settings-button-bar">
|
||||
<a rel="noopener noreferrer" target="_blank"
|
||||
href="https://github.com/ether/etherpad-lite/wiki/Example-Production-Settings.JSON"><Trans
|
||||
i18nKey="admin_settings.current_example-prod"/></a>
|
||||
<a rel="noopener noreferrer" target="_blank"
|
||||
href="https://github.com/ether/etherpad-lite/wiki/Example-Development-Settings.JSON"><Trans
|
||||
i18nKey="admin_settings.current_example-devel"/></a>
|
||||
</div>
|
||||
return <div className="settings-page">
|
||||
<h1><Trans i18nKey="admin_settings.current"/></h1>
|
||||
<textarea value={settings} className="settings" onChange={v => {
|
||||
useStore.getState().setSettings(v.target.value)
|
||||
}}/>
|
||||
<div className="settings-button-bar">
|
||||
<IconButton className="settingsButton" icon={<Save/>}
|
||||
title={<Trans i18nKey="admin_settings.current_save.value"/>} onClick={() => {
|
||||
if (isJSONClean(settings!)) {
|
||||
// JSON is clean so emit it to the server
|
||||
settingsSocket!.emit('saveSettings', settings!);
|
||||
useStore.getState().setToastState({
|
||||
open: true,
|
||||
title: "Successfully saved settings",
|
||||
success: true
|
||||
})
|
||||
} else {
|
||||
useStore.getState().setToastState({
|
||||
open: true,
|
||||
title: "Error saving settings",
|
||||
success: false
|
||||
})
|
||||
}
|
||||
}}/>
|
||||
<IconButton className="settingsButton" icon={<RotateCw/>}
|
||||
title={<Trans i18nKey="admin_settings.current_restart.value"/>} onClick={() => {
|
||||
settingsSocket!.emit('restartServer');
|
||||
}}/>
|
||||
</div>
|
||||
<div className="separator"/>
|
||||
<div className="settings-button-bar">
|
||||
<a rel="noopener noreferrer" target="_blank"
|
||||
href="https://github.com/ether/etherpad-lite/wiki/Example-Production-Settings.JSON"><Trans
|
||||
i18nKey="admin_settings.current_example-prod"/></a>
|
||||
<a rel="noopener noreferrer" target="_blank"
|
||||
href="https://github.com/ether/etherpad-lite/wiki/Example-Development-Settings.JSON"><Trans
|
||||
i18nKey="admin_settings.current_example-devel"/></a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -4,49 +4,49 @@ import {PadSearchResult} from "../utils/PadSearch.ts";
|
||||
import {InstalledPlugin} from "../pages/Plugin.ts";
|
||||
|
||||
type ToastState = {
|
||||
description?:string,
|
||||
title: string,
|
||||
open: boolean,
|
||||
success: boolean
|
||||
description?:string,
|
||||
title: string,
|
||||
open: boolean,
|
||||
success: boolean
|
||||
}
|
||||
|
||||
|
||||
type StoreState = {
|
||||
settings: string|undefined,
|
||||
setSettings: (settings: string) => void,
|
||||
settingsSocket: Socket|undefined,
|
||||
setSettingsSocket: (socket: Socket) => void,
|
||||
showLoading: boolean,
|
||||
setShowLoading: (show: boolean) => void,
|
||||
setPluginsSocket: (socket: Socket) => void
|
||||
pluginsSocket: Socket|undefined,
|
||||
toastState: ToastState,
|
||||
setToastState: (val: ToastState)=>void,
|
||||
pads: PadSearchResult|undefined,
|
||||
setPads: (pads: PadSearchResult)=>void,
|
||||
installedPlugins: InstalledPlugin[],
|
||||
setInstalledPlugins: (plugins: InstalledPlugin[])=>void
|
||||
settings: string|undefined,
|
||||
setSettings: (settings: string) => void,
|
||||
settingsSocket: Socket|undefined,
|
||||
setSettingsSocket: (socket: Socket) => void,
|
||||
showLoading: boolean,
|
||||
setShowLoading: (show: boolean) => void,
|
||||
setPluginsSocket: (socket: Socket) => void
|
||||
pluginsSocket: Socket|undefined,
|
||||
toastState: ToastState,
|
||||
setToastState: (val: ToastState)=>void,
|
||||
pads: PadSearchResult|undefined,
|
||||
setPads: (pads: PadSearchResult)=>void,
|
||||
installedPlugins: InstalledPlugin[],
|
||||
setInstalledPlugins: (plugins: InstalledPlugin[])=>void
|
||||
}
|
||||
|
||||
|
||||
export const useStore = create<StoreState>()((set) => ({
|
||||
settings: undefined,
|
||||
setSettings: (settings: string) => set({settings}),
|
||||
settingsSocket: undefined,
|
||||
setSettingsSocket: (socket: Socket) => set({settingsSocket: socket}),
|
||||
showLoading: false,
|
||||
setShowLoading: (show: boolean) => set({showLoading: show}),
|
||||
pluginsSocket: undefined,
|
||||
setPluginsSocket: (socket: Socket) => set({pluginsSocket: socket}),
|
||||
setToastState: (val )=>set({toastState: val}),
|
||||
toastState: {
|
||||
open: false,
|
||||
title: '',
|
||||
description:'',
|
||||
success: false
|
||||
},
|
||||
pads: undefined,
|
||||
setPads: (pads)=>set({pads}),
|
||||
installedPlugins: [],
|
||||
setInstalledPlugins: (plugins)=>set({installedPlugins: plugins})
|
||||
settings: undefined,
|
||||
setSettings: (settings: string) => set({settings}),
|
||||
settingsSocket: undefined,
|
||||
setSettingsSocket: (socket: Socket) => set({settingsSocket: socket}),
|
||||
showLoading: false,
|
||||
setShowLoading: (show: boolean) => set({showLoading: show}),
|
||||
pluginsSocket: undefined,
|
||||
setPluginsSocket: (socket: Socket) => set({pluginsSocket: socket}),
|
||||
setToastState: (val )=>set({toastState: val}),
|
||||
toastState: {
|
||||
open: false,
|
||||
title: '',
|
||||
description:'',
|
||||
success: false
|
||||
},
|
||||
pads: undefined,
|
||||
setPads: (pads)=>set({pads}),
|
||||
installedPlugins: [],
|
||||
setInstalledPlugins: (plugins)=>set({installedPlugins: plugins})
|
||||
}));
|
||||
|
||||
@ -3,27 +3,27 @@ import {useCallback, useEffect, useRef} from "react";
|
||||
type Args = any[]
|
||||
|
||||
export const useAnimationFrame = <Fn extends (...args: Args)=>void>(
|
||||
callback: Fn,
|
||||
wait = 0
|
||||
callback: Fn,
|
||||
wait = 0
|
||||
): ((...args: Parameters<Fn>)=>void)=>{
|
||||
const rafId = useRef(0)
|
||||
const render = useCallback(
|
||||
(...args: Parameters<Fn>)=>{
|
||||
cancelAnimationFrame(rafId.current)
|
||||
const timeStart = performance.now()
|
||||
const rafId = useRef(0)
|
||||
const render = useCallback(
|
||||
(...args: Parameters<Fn>)=>{
|
||||
cancelAnimationFrame(rafId.current)
|
||||
const timeStart = performance.now()
|
||||
|
||||
const renderFrame = (timeNow: number)=>{
|
||||
if(timeNow-timeStart<wait){
|
||||
rafId.current = requestAnimationFrame(renderFrame)
|
||||
return
|
||||
}
|
||||
callback(...args)
|
||||
}
|
||||
rafId.current = requestAnimationFrame(renderFrame)
|
||||
}, [callback, wait]
|
||||
)
|
||||
const renderFrame = (timeNow: number)=>{
|
||||
if(timeNow-timeStart<wait){
|
||||
rafId.current = requestAnimationFrame(renderFrame)
|
||||
return
|
||||
}
|
||||
callback(...args)
|
||||
}
|
||||
rafId.current = requestAnimationFrame(renderFrame)
|
||||
}, [callback, wait]
|
||||
)
|
||||
|
||||
|
||||
useEffect(()=>cancelAnimationFrame(rafId.current),[])
|
||||
return render
|
||||
useEffect(()=>cancelAnimationFrame(rafId.current),[])
|
||||
return render
|
||||
}
|
||||
@ -3,18 +3,18 @@ import * as Dialog from '@radix-ui/react-dialog';
|
||||
import brand from './brand.svg'
|
||||
|
||||
export const LoadingScreen = ()=>{
|
||||
const showLoading = useStore(state => state.showLoading)
|
||||
const showLoading = useStore(state => state.showLoading)
|
||||
|
||||
return <Dialog.Root open={showLoading}><Dialog.Portal>
|
||||
<Dialog.Overlay className="loading-screen fixed inset-0 bg-black bg-opacity-50 z-50 dialog-overlay" />
|
||||
<Dialog.Content className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 dialog-content">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="animate-spin w-16 h-16 border-t-2 border-b-2 border-[--fg-color] rounded-full"></div>
|
||||
<div className="mt-4 text-[--fg-color]">
|
||||
<img src={brand}/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
return <Dialog.Root open={showLoading}><Dialog.Portal>
|
||||
<Dialog.Overlay className="loading-screen fixed inset-0 bg-black bg-opacity-50 z-50 dialog-overlay" />
|
||||
<Dialog.Content className="fixed top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 z-50 dialog-content">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="animate-spin w-16 h-16 border-t-2 border-b-2 border-[--fg-color] rounded-full"></div>
|
||||
<div className="mt-4 text-[--fg-color]">
|
||||
<img src={brand}/>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
}
|
||||
|
||||
@ -1,20 +1,20 @@
|
||||
export type PadSearchQuery = {
|
||||
pattern: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
ascending: boolean;
|
||||
sortBy: string;
|
||||
pattern: string;
|
||||
offset: number;
|
||||
limit: number;
|
||||
ascending: boolean;
|
||||
sortBy: string;
|
||||
}
|
||||
|
||||
|
||||
export type PadSearchResult = {
|
||||
total: number;
|
||||
results?: PadType[]
|
||||
total: number;
|
||||
results?: PadType[]
|
||||
}
|
||||
|
||||
export type PadType = {
|
||||
padName: string;
|
||||
lastEdited: number;
|
||||
userCount: number;
|
||||
revisionNumber: number;
|
||||
padName: string;
|
||||
lastEdited: number;
|
||||
userCount: number;
|
||||
revisionNumber: number;
|
||||
}
|
||||
|
||||
@ -3,24 +3,24 @@ import {useStore} from "../store/store.ts";
|
||||
import {useMemo} from "react";
|
||||
|
||||
export const ToastDialog = ()=>{
|
||||
const toastState = useStore(state => state.toastState)
|
||||
const resultingClass = useMemo(()=> {
|
||||
return toastState.success?'ToastRootSuccess':'ToastRootFailure'
|
||||
}, [toastState.success])
|
||||
const toastState = useStore(state => state.toastState)
|
||||
const resultingClass = useMemo(()=> {
|
||||
return toastState.success?'ToastRootSuccess':'ToastRootFailure'
|
||||
}, [toastState.success])
|
||||
|
||||
console.log()
|
||||
return <>
|
||||
<Toast.Root className={"ToastRoot "+resultingClass} open={toastState && toastState.open} onOpenChange={()=>{
|
||||
useStore.getState().setToastState({
|
||||
...toastState!,
|
||||
open: !toastState?.open
|
||||
})
|
||||
}}>
|
||||
<Toast.Title className="ToastTitle">{toastState.title}</Toast.Title>
|
||||
<Toast.Description asChild>
|
||||
{toastState.description}
|
||||
</Toast.Description>
|
||||
</Toast.Root>
|
||||
<Toast.Viewport className="ToastViewport"/>
|
||||
</>
|
||||
console.log()
|
||||
return <>
|
||||
<Toast.Root className={"ToastRoot "+resultingClass} open={toastState && toastState.open} onOpenChange={()=>{
|
||||
useStore.getState().setToastState({
|
||||
...toastState!,
|
||||
open: !toastState?.open
|
||||
})
|
||||
}}>
|
||||
<Toast.Title className="ToastTitle">{toastState.title}</Toast.Title>
|
||||
<Toast.Description asChild>
|
||||
{toastState.description}
|
||||
</Toast.Description>
|
||||
</Toast.Root>
|
||||
<Toast.Viewport className="ToastViewport"/>
|
||||
</>
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
export const determineSorting = (sortBy: string, ascending: boolean, currentSymbol: string) => {
|
||||
if (sortBy === currentSymbol) {
|
||||
return ascending ? 'sort up' : 'sort down';
|
||||
}
|
||||
return 'sort none';
|
||||
if (sortBy === currentSymbol) {
|
||||
return ascending ? 'sort up' : 'sort down';
|
||||
}
|
||||
return 'sort none';
|
||||
}
|
||||
|
||||
@ -4,19 +4,19 @@ import {useAnimationFrame} from "./AnimationFrameHook";
|
||||
const defaultDeps: DependencyList = []
|
||||
|
||||
export const useDebounce = (
|
||||
fn:EffectCallback,
|
||||
wait = 0,
|
||||
deps = defaultDeps
|
||||
fn:EffectCallback,
|
||||
wait = 0,
|
||||
deps = defaultDeps
|
||||
):void => {
|
||||
const isFirstRender = useRef(true)
|
||||
const render = useAnimationFrame(fn, wait)
|
||||
const isFirstRender = useRef(true)
|
||||
const render = useAnimationFrame(fn, wait)
|
||||
|
||||
useMemo(()=>{
|
||||
if(isFirstRender.current){
|
||||
isFirstRender.current = false
|
||||
return
|
||||
}
|
||||
useMemo(()=>{
|
||||
if(isFirstRender.current){
|
||||
isFirstRender.current = false
|
||||
return
|
||||
}
|
||||
|
||||
render()
|
||||
}, deps)
|
||||
render()
|
||||
}, deps)
|
||||
}
|
||||
@ -1,70 +1,70 @@
|
||||
export const cleanComments = (json: string|undefined)=>{
|
||||
if (json !== undefined){
|
||||
json = json.replace(/\/\*.*?\*\//g, ""); // remove single line comments
|
||||
json = json.replace(/ *\/\*.*(.|\n)*?\*\//g, ""); // remove multi line comments
|
||||
json = json.replace(/[ \t]+$/gm, ""); // trim trailing spaces
|
||||
json = json.replace(/^(\n)/gm, ""); // remove empty lines
|
||||
}
|
||||
return json;
|
||||
if (json !== undefined){
|
||||
json = json.replace(/\/\*.*?\*\//g, ""); // remove single line comments
|
||||
json = json.replace(/ *\/\*.*(.|\n)*?\*\//g, ""); // remove multi line comments
|
||||
json = json.replace(/[ \t]+$/gm, ""); // trim trailing spaces
|
||||
json = json.replace(/^(\n)/gm, ""); // remove empty lines
|
||||
}
|
||||
return json;
|
||||
}
|
||||
|
||||
export const minify = (json: string)=>{
|
||||
let tokenizer = /"|(\/\*)|(\*\/)|(\/\/)|\n|\r/g,
|
||||
in_string = false,
|
||||
in_multiline_comment = false,
|
||||
in_singleline_comment = false,
|
||||
tmp, tmp2, new_str = [], ns = 0, from = 0, lc, rc
|
||||
;
|
||||
let tokenizer = /"|(\/\*)|(\*\/)|(\/\/)|\n|\r/g,
|
||||
in_string = false,
|
||||
in_multiline_comment = false,
|
||||
in_singleline_comment = false,
|
||||
tmp, tmp2, new_str = [], ns = 0, from = 0, lc, rc
|
||||
;
|
||||
|
||||
tokenizer.lastIndex = 0;
|
||||
tokenizer.lastIndex = 0;
|
||||
|
||||
while (tmp = tokenizer.exec(json)) {
|
||||
lc = RegExp.leftContext;
|
||||
rc = RegExp.rightContext;
|
||||
if (!in_multiline_comment && !in_singleline_comment) {
|
||||
tmp2 = lc.substring(from);
|
||||
if (!in_string) {
|
||||
tmp2 = tmp2.replace(/(\n|\r|\s)*/g,"");
|
||||
}
|
||||
new_str[ns++] = tmp2;
|
||||
}
|
||||
from = tokenizer.lastIndex;
|
||||
|
||||
if (tmp[0] == "\"" && !in_multiline_comment && !in_singleline_comment) {
|
||||
tmp2 = lc.match(/(\\)*$/);
|
||||
if (!in_string || !tmp2 || (tmp2[0].length % 2) == 0) { // start of string with ", or unescaped " character found to end string
|
||||
in_string = !in_string;
|
||||
}
|
||||
from--; // include " character in next catch
|
||||
rc = json.substring(from);
|
||||
}
|
||||
else if (tmp[0] == "/*" && !in_string && !in_multiline_comment && !in_singleline_comment) {
|
||||
in_multiline_comment = true;
|
||||
}
|
||||
else if (tmp[0] == "*/" && !in_string && in_multiline_comment && !in_singleline_comment) {
|
||||
in_multiline_comment = false;
|
||||
}
|
||||
else if (tmp[0] == "//" && !in_string && !in_multiline_comment && !in_singleline_comment) {
|
||||
in_singleline_comment = true;
|
||||
}
|
||||
else if ((tmp[0] == "\n" || tmp[0] == "\r") && !in_string && !in_multiline_comment && in_singleline_comment) {
|
||||
in_singleline_comment = false;
|
||||
}
|
||||
else if (!in_multiline_comment && !in_singleline_comment && !(/\n|\r|\s/.test(tmp[0]))) {
|
||||
new_str[ns++] = tmp[0];
|
||||
}
|
||||
while (tmp = tokenizer.exec(json)) {
|
||||
lc = RegExp.leftContext;
|
||||
rc = RegExp.rightContext;
|
||||
if (!in_multiline_comment && !in_singleline_comment) {
|
||||
tmp2 = lc.substring(from);
|
||||
if (!in_string) {
|
||||
tmp2 = tmp2.replace(/(\n|\r|\s)*/g,"");
|
||||
}
|
||||
new_str[ns++] = tmp2;
|
||||
}
|
||||
new_str[ns++] = rc;
|
||||
return new_str.join("");
|
||||
from = tokenizer.lastIndex;
|
||||
|
||||
if (tmp[0] == "\"" && !in_multiline_comment && !in_singleline_comment) {
|
||||
tmp2 = lc.match(/(\\)*$/);
|
||||
if (!in_string || !tmp2 || (tmp2[0].length % 2) == 0) { // start of string with ", or unescaped " character found to end string
|
||||
in_string = !in_string;
|
||||
}
|
||||
from--; // include " character in next catch
|
||||
rc = json.substring(from);
|
||||
}
|
||||
else if (tmp[0] == "/*" && !in_string && !in_multiline_comment && !in_singleline_comment) {
|
||||
in_multiline_comment = true;
|
||||
}
|
||||
else if (tmp[0] == "*/" && !in_string && in_multiline_comment && !in_singleline_comment) {
|
||||
in_multiline_comment = false;
|
||||
}
|
||||
else if (tmp[0] == "//" && !in_string && !in_multiline_comment && !in_singleline_comment) {
|
||||
in_singleline_comment = true;
|
||||
}
|
||||
else if ((tmp[0] == "\n" || tmp[0] == "\r") && !in_string && !in_multiline_comment && in_singleline_comment) {
|
||||
in_singleline_comment = false;
|
||||
}
|
||||
else if (!in_multiline_comment && !in_singleline_comment && !(/\n|\r|\s/.test(tmp[0]))) {
|
||||
new_str[ns++] = tmp[0];
|
||||
}
|
||||
}
|
||||
new_str[ns++] = rc;
|
||||
return new_str.join("");
|
||||
}
|
||||
|
||||
export const isJSONClean = (data: string) => {
|
||||
let cleanSettings = minify(data);
|
||||
// this is a bit naive. In theory some key/value might contain the sequences ',]' or ',}'
|
||||
cleanSettings = cleanSettings.replace(',]', ']').replace(',}', '}');
|
||||
try {
|
||||
return typeof JSON.parse(cleanSettings) === 'object';
|
||||
} catch (e) {
|
||||
return false; // the JSON failed to be parsed
|
||||
}
|
||||
let cleanSettings = minify(data);
|
||||
// this is a bit naive. In theory some key/value might contain the sequences ',]' or ',}'
|
||||
cleanSettings = cleanSettings.replace(',]', ']').replace(',}', '}');
|
||||
try {
|
||||
return typeof JSON.parse(cleanSettings) === 'object';
|
||||
} catch (e) {
|
||||
return false; // the JSON failed to be parsed
|
||||
}
|
||||
};
|
||||
|
||||
132
best_practices.md
Normal file
132
best_practices.md
Normal file
@ -0,0 +1,132 @@
|
||||
# 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#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
|
||||
|
||||
* PRs MUST include a non-empty description explaining what the change does and why
|
||||
* PRs without a description should be flagged as incomplete
|
||||
* 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
|
||||
* PRs should be issued against the **develop** branch: we never pull directly into **master**
|
||||
* PRs **should not have conflicts** with develop. If there are, please resolve them rebasing and force-pushing
|
||||
* when preparing your PR, please make sure that you have included the relevant **changes to the documentation** (preferably with usage examples)
|
||||
* contain meaningful and detailed **commit messages** in the form:
|
||||
```
|
||||
submodule: description
|
||||
|
||||
longer description of the change you have made, eventually mentioning the
|
||||
number of the issue that is being fixed, in the form: Fixes #someIssueNumber
|
||||
```
|
||||
* if the PR is a **bug fix**:
|
||||
* The commit that fixes the bug should **include a regression test** that
|
||||
would fail if the bug fix was reverted. Adding the regression test in the
|
||||
same commit as the bug fix makes it easier for a reviewer to verify that the
|
||||
test is appropriate for the bug fix.
|
||||
* If there is a bug report, **the pull request description should include the
|
||||
text "`Fixes #xxx`"** so that the bug report is auto-closed when the PR is
|
||||
merged. It is less useful to say the same thing in a commit message because
|
||||
GitHub will spam the bug report every time the commit is rebased, and
|
||||
because a bug number alone becomes meaningless in forks. (A full URL would
|
||||
be better, but ideally each commit is readable on its own without the need
|
||||
to examine an external reference to understand motivation or context.)
|
||||
* think about stability: code has to be backwards compatible as much as possible. Always **assume your code will be run with an older version of the DB/config file**
|
||||
* if you want to remove a feature, **deprecate it instead**:
|
||||
* write an issue with your deprecation plan
|
||||
* output a `WARN` in the log informing that the feature is going to be removed
|
||||
* remove the feature in the next version
|
||||
* if you want to add a new feature, put it under a **feature flag**:
|
||||
* once the new feature has reached a minimal level of stability, do a PR for it, so it can be integrated early
|
||||
* expose a mechanism for enabling/disabling the feature
|
||||
* the new feature should be **disabled** by default. With the feature disabled, the code path should be exactly the same as before your contribution. This is a __necessary condition__ for early integration
|
||||
* think of the PR not as something that __you wrote__, but as something that __someone else is going to read__. The commit series in the PR should tell a novice developer the story of your thoughts when developing it
|
||||
|
||||
## How to write a bug report
|
||||
|
||||
* Please be polite, we all are humans and problems can occur.
|
||||
* Please add as much information as possible, for example
|
||||
* client os(s) and version(s)
|
||||
* browser(s) and version(s), is the problem reproducible on different clients
|
||||
* special environments like firewalls or antivirus
|
||||
* host os and version
|
||||
* npm and nodejs version
|
||||
* Logfiles if available
|
||||
* steps to reproduce
|
||||
* what you expected to happen
|
||||
* what actually happened
|
||||
* Please format logfiles and code examples with markdown see github Markdown help below the issue textarea for more information.
|
||||
|
||||
If you send logfiles, please set the loglevel switch DEBUG in your settings.json file:
|
||||
|
||||
```
|
||||
/* The log level we are using, can be: DEBUG, INFO, WARN, ERROR */
|
||||
"loglevel": "DEBUG",
|
||||
```
|
||||
|
||||
The logfile location is defined in startup script or the log is directly shown in the commandline after you have started etherpad.
|
||||
|
||||
## General goals of Etherpad
|
||||
To make sure everybody is going in the same direction:
|
||||
* easy to install for admins and easy to use for people
|
||||
* easy to integrate into other apps, but also usable as standalone
|
||||
* lightweight and scalable
|
||||
* extensible, as much functionality should be extendable with plugins so changes don't have to be done in core.
|
||||
Also, keep it maintainable. We don't wanna end up as the monster Etherpad was!
|
||||
|
||||
## How to work with git?
|
||||
* Don't work in your master branch.
|
||||
* Make a new branch for every feature you're working on. (This ensures that you can work you can do lots of small, independent pull requests instead of one big one with complete different features)
|
||||
* Don't use the online edit function of github (this only creates ugly and not working commits!)
|
||||
* Try to make clean commits that are easy readable (including descriptive commit messages!)
|
||||
* Test before you push. Sounds easy, it isn't!
|
||||
* Don't check in stuff that gets generated during build or runtime
|
||||
* Make small pull requests that are easy to review but make sure they do add value by themselves / individually
|
||||
|
||||
## 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: 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!
|
||||
* If you do make changes, document them! (see below)
|
||||
* Use protocol independent urls "//"
|
||||
|
||||
## Branching model / git workflow
|
||||
see git flow http://nvie.com/posts/a-successful-git-branching-model/
|
||||
|
||||
### `master` branch
|
||||
* the stable
|
||||
* This is the branch everyone should use for production stuff
|
||||
|
||||
### `develop`branch
|
||||
* everything that is READY to go into master at some point in time
|
||||
* This stuff is tested and ready to go out
|
||||
|
||||
### release branches
|
||||
* stuff that should go into master very soon
|
||||
* only bugfixes go into these (see http://nvie.com/posts/a-successful-git-branching-model/ for why)
|
||||
* we should not be blocking new features to develop, just because we feel that we should be releasing it to master soon. This is the situation that release branches solve/handle.
|
||||
|
||||
### hotfix branches
|
||||
* fixes for bugs in master
|
||||
|
||||
### feature branches (in your own repos)
|
||||
* these are the branches where you develop your features in
|
||||
* If it's ready to go out, it will be merged into develop
|
||||
|
||||
Over the time we pull features from feature branches into the develop branch. Every month we pull from develop into master. Bugs in master get fixed in hotfix branches. These branches will get merged into master AND develop. There should never be commits in master that aren't in develop
|
||||
|
||||
## Documentation
|
||||
The docs are in the `doc/` folder in the git repository, so people can easily find the suitable docs for the current git revision.
|
||||
|
||||
Documentation should be kept up-to-date. This means, whenever you add a new API method, add a new hook or change the database model, pack the relevant changes to the docs in the same pull request.
|
||||
|
||||
You can build the docs e.g. produce html, using `make docs`. At some point in the future we will provide an online documentation. The current documentation in the github wiki should always reflect the state of `master` (!), since there are no docs in master, yet.
|
||||
|
||||
## Testing
|
||||
Front-end tests are found in the `tests/frontend/` folder in the repository. Run them by pointing your browser to `<yourdomainhere>/tests/frontend`.
|
||||
|
||||
Back-end tests can be run from the `src` directory, via `npm test`.
|
||||
You can use `npm test -- --inspect-brk` and navigate to `edge://inspect` or `chrome://inspect` to debug the tests.
|
||||
|
||||
@ -4,14 +4,14 @@ import {installedPluginsPath} from "ep_etherpad-lite/static/js/pluginfw/installe
|
||||
const pluginsModule = require('ep_etherpad-lite/static/js/pluginfw/plugins');
|
||||
|
||||
export const persistInstalledPlugins = async () => {
|
||||
const plugins:PackageData[] = []
|
||||
const installedPlugins = {plugins: plugins};
|
||||
for (const pkg of Object.values(await pluginsModule.getPackages()) as PackageData[]) {
|
||||
installedPlugins.plugins.push({
|
||||
name: pkg.name,
|
||||
version: pkg.version,
|
||||
});
|
||||
}
|
||||
installedPlugins.plugins = [...new Set(installedPlugins.plugins)];
|
||||
writeFileSync(installedPluginsPath, JSON.stringify(installedPlugins));
|
||||
const plugins:PackageData[] = []
|
||||
const installedPlugins = {plugins: plugins};
|
||||
for (const pkg of Object.values(await pluginsModule.getPackages()) as PackageData[]) {
|
||||
installedPlugins.plugins.push({
|
||||
name: pkg.name,
|
||||
version: pkg.version,
|
||||
});
|
||||
}
|
||||
installedPlugins.plugins = [...new Set(installedPlugins.plugins)];
|
||||
writeFileSync(installedPluginsPath, JSON.stringify(installedPlugins));
|
||||
};
|
||||
|
||||
@ -31,7 +31,7 @@ while true; do
|
||||
esac
|
||||
done
|
||||
|
||||
ETHER_REPO="https://github.com/ether/etherpad-lite.git"
|
||||
ETHER_REPO="https://github.com/ether/etherpad.git"
|
||||
ETHER_WEB_REPO="https://github.com/ether/ether.github.com.git"
|
||||
TMP_DIR="/tmp/"
|
||||
|
||||
@ -186,7 +186,7 @@ function publish_release {
|
||||
|
||||
function todo_notification {
|
||||
echo "Release procedure was successful, but you have to do some steps manually:"
|
||||
echo "- Update the wiki at https://github.com/ether/etherpad-lite/wiki"
|
||||
echo "- Update the wiki at https://github.com/ether/etherpad/wiki"
|
||||
echo "- Create a pull request on github to merge the master branch back to develop"
|
||||
echo "- Announce the new release on the mailing list, blog.etherpad.org and Twitter"
|
||||
}
|
||||
|
||||
@ -1,49 +0,0 @@
|
||||
@echo off
|
||||
|
||||
:: Change directory to etherpad-lite root
|
||||
cd /D "%~dp0\.."
|
||||
|
||||
:: Is node installed?
|
||||
cmd /C node -e "" || ( echo "Please install node.js ( https://nodejs.org )" && exit /B 1 )
|
||||
|
||||
echo _
|
||||
echo Ensure that all dependencies are up to date... If this is the first time you have run Etherpad please be patient.
|
||||
|
||||
|
||||
:: Install admin ui only if available
|
||||
IF EXIST admin (
|
||||
cd /D .\admin
|
||||
dir
|
||||
cmd /C pnpm i || exit /B 1
|
||||
cmd /C pnpm run build || exit /B 1
|
||||
cd /D ..
|
||||
)
|
||||
|
||||
:: Install ui only if available
|
||||
IF EXIST ui (
|
||||
cd /D .\ui
|
||||
dir
|
||||
cmd /C pnpm i || exit /B 1
|
||||
cmd /C pnpm run build || exit /B 1
|
||||
cd /D ..
|
||||
)
|
||||
|
||||
|
||||
cmd /C pnpm i || exit /B 1
|
||||
|
||||
cd /D "%~dp0\.."
|
||||
|
||||
echo _
|
||||
echo Clearing cache...
|
||||
del /S var\minified*
|
||||
|
||||
echo _
|
||||
echo Setting up settings.json...
|
||||
IF NOT EXIST settings.json (
|
||||
echo Can't find settings.json.
|
||||
echo Copying settings.json.template...
|
||||
cmd /C copy settings.json.template settings.json || exit /B 1
|
||||
)
|
||||
|
||||
echo _
|
||||
echo Installed Etherpad! To run Etherpad type start.bat
|
||||
155
bin/installer.ps1
Normal file
155
bin/installer.ps1
Normal file
@ -0,0 +1,155 @@
|
||||
# Etherpad one-line installer for Windows (PowerShell).
|
||||
#
|
||||
# Usage:
|
||||
# irm https://raw.githubusercontent.com/ether/etherpad-lite/master/bin/installer.ps1 | iex
|
||||
#
|
||||
# Optional environment variables:
|
||||
# $env:ETHERPAD_DIR Directory to install into (default: .\etherpad-lite)
|
||||
# $env:ETHERPAD_BRANCH Branch / tag to clone (default: master)
|
||||
# $env:ETHERPAD_REPO Repo URL (default: https://github.com/ether/etherpad.git)
|
||||
# $env:ETHERPAD_RUN If "1", start Etherpad after install
|
||||
# $env:NO_COLOR If set, disables coloured output
|
||||
|
||||
#Requires -Version 5.1
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
# ---------- pretty output ----------
|
||||
$useColor = -not $env:NO_COLOR
|
||||
function Write-Step([string]$msg) {
|
||||
if ($useColor) { Write-Host "==> $msg" -ForegroundColor Green }
|
||||
else { Write-Host "==> $msg" }
|
||||
}
|
||||
function Write-Warn([string]$msg) {
|
||||
if ($useColor) { Write-Host "==> $msg" -ForegroundColor Yellow }
|
||||
else { Write-Host "==> $msg" }
|
||||
}
|
||||
function Write-Fatal([string]$msg) {
|
||||
if ($useColor) { Write-Host "==> $msg" -ForegroundColor Red }
|
||||
else { Write-Host "==> $msg" }
|
||||
exit 1
|
||||
}
|
||||
|
||||
function Test-Cmd([string]$name) {
|
||||
return [bool](Get-Command $name -ErrorAction SilentlyContinue)
|
||||
}
|
||||
|
||||
# ---------- defaults ----------
|
||||
$EtherpadDir = if ($env:ETHERPAD_DIR) { $env:ETHERPAD_DIR } else { 'etherpad-lite' }
|
||||
$EtherpadBranch = if ($env:ETHERPAD_BRANCH) { $env:ETHERPAD_BRANCH } else { 'master' }
|
||||
$EtherpadRepo = if ($env:ETHERPAD_REPO) { $env:ETHERPAD_REPO } else { 'https://github.com/ether/etherpad.git' }
|
||||
$RequiredNodeMajor = 20
|
||||
|
||||
Write-Step 'Etherpad installer'
|
||||
|
||||
# ---------- prerequisite checks ----------
|
||||
if (-not (Test-Cmd git)) {
|
||||
Write-Fatal 'git is required but not installed. See https://git-scm.com/download/win'
|
||||
}
|
||||
if (-not (Test-Cmd node)) {
|
||||
Write-Fatal "Node.js is required (>= $RequiredNodeMajor). Install it from https://nodejs.org"
|
||||
}
|
||||
|
||||
$nodeMajor = [int](node -p 'process.versions.node.split(".")[0]')
|
||||
if ($nodeMajor -lt $RequiredNodeMajor) {
|
||||
$nodeVer = (node --version)
|
||||
Write-Fatal "Node.js >= $RequiredNodeMajor required. You have $nodeVer."
|
||||
}
|
||||
|
||||
if (-not (Test-Cmd pnpm)) {
|
||||
Write-Step 'Installing pnpm globally'
|
||||
if (-not (Test-Cmd npm)) {
|
||||
Write-Fatal "npm not found. Install Node.js >= $RequiredNodeMajor."
|
||||
}
|
||||
npm install -g pnpm
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Fatal 'Failed to install pnpm. Install it manually: https://pnpm.io/installation'
|
||||
}
|
||||
if (-not (Test-Cmd pnpm)) {
|
||||
Write-Fatal 'pnpm install reported success but pnpm is still not on PATH. Open a new shell and re-run.'
|
||||
}
|
||||
}
|
||||
|
||||
# ---------- clone ----------
|
||||
if (Test-Path $EtherpadDir) {
|
||||
if (Test-Path (Join-Path $EtherpadDir '.git')) {
|
||||
Write-Warn "$EtherpadDir already exists; updating to $EtherpadBranch."
|
||||
Push-Location $EtherpadDir
|
||||
try {
|
||||
# Verify the existing checkout points at the expected remote.
|
||||
$existingRemote = (git remote get-url origin 2>$null)
|
||||
if ($existingRemote -and $existingRemote -ne $EtherpadRepo) {
|
||||
Write-Fatal "$EtherpadDir is checked out from '$existingRemote', expected '$EtherpadRepo'. Refusing to overwrite."
|
||||
}
|
||||
|
||||
# Refuse to clobber meaningful local changes. pnpm-lock.yaml is
|
||||
# excluded because `pnpm i` rewrites it during installation,
|
||||
# which would otherwise make every re-run of the installer fail.
|
||||
$statusLines = (git status --porcelain) -split "`n" |
|
||||
Where-Object { $_ -and ($_ -notmatch '\bpnpm-lock\.yaml$') }
|
||||
if ($statusLines) {
|
||||
$statusLines | ForEach-Object { Write-Host $_ }
|
||||
Write-Fatal "$EtherpadDir has uncommitted changes. Commit/stash them or remove the directory."
|
||||
}
|
||||
|
||||
git fetch --tags --prune origin
|
||||
if ($LASTEXITCODE -ne 0) { Write-Fatal "git fetch failed in $EtherpadDir" }
|
||||
|
||||
# Discard any pnpm-lock.yaml changes from a prior pnpm install
|
||||
# so the subsequent checkout doesn't refuse to overwrite.
|
||||
git checkout -- pnpm-lock.yaml 2>$null
|
||||
|
||||
# Switch to the requested branch / tag and fast-forward to it.
|
||||
git show-ref --verify --quiet "refs/remotes/origin/$EtherpadBranch"
|
||||
$isBranch = ($LASTEXITCODE -eq 0)
|
||||
git show-ref --verify --quiet "refs/tags/$EtherpadBranch"
|
||||
$isTag = ($LASTEXITCODE -eq 0)
|
||||
|
||||
if ($isBranch) {
|
||||
git checkout -B $EtherpadBranch "origin/$EtherpadBranch"
|
||||
if ($LASTEXITCODE -ne 0) { Write-Fatal "git checkout $EtherpadBranch failed" }
|
||||
} elseif ($isTag) {
|
||||
git checkout --detach "refs/tags/$EtherpadBranch"
|
||||
if ($LASTEXITCODE -ne 0) { Write-Fatal "git checkout tag $EtherpadBranch failed" }
|
||||
} else {
|
||||
Write-Fatal "Branch or tag '$EtherpadBranch' not found on origin."
|
||||
}
|
||||
|
||||
$installedRev = (git rev-parse --short HEAD)
|
||||
Write-Step "Updated $EtherpadDir to $EtherpadBranch @ $installedRev"
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
} else {
|
||||
Write-Fatal "$EtherpadDir exists and is not a git checkout. Aborting."
|
||||
}
|
||||
} else {
|
||||
Write-Step "Cloning Etherpad ($EtherpadBranch) into $EtherpadDir"
|
||||
git clone --depth 1 --branch $EtherpadBranch $EtherpadRepo $EtherpadDir
|
||||
if ($LASTEXITCODE -ne 0) { Write-Fatal 'git clone failed.' }
|
||||
}
|
||||
|
||||
Push-Location $EtherpadDir
|
||||
|
||||
# ---------- install + build ----------
|
||||
Write-Step 'Installing dependencies (pnpm i)'
|
||||
pnpm i
|
||||
if ($LASTEXITCODE -ne 0) { Pop-Location; Write-Fatal 'pnpm i failed.' }
|
||||
|
||||
Write-Step 'Building Etherpad (pnpm run build:etherpad)'
|
||||
pnpm run build:etherpad
|
||||
if ($LASTEXITCODE -ne 0) { Pop-Location; Write-Fatal 'pnpm run build:etherpad failed.' }
|
||||
|
||||
# ---------- done ----------
|
||||
Write-Host ''
|
||||
if ($useColor) { Write-Host "🎉 Etherpad is installed in $EtherpadDir" -ForegroundColor Green }
|
||||
else { Write-Host "Etherpad is installed in $EtherpadDir" }
|
||||
Write-Host 'To start Etherpad:'
|
||||
Write-Host " cd $EtherpadDir; pnpm run prod"
|
||||
Write-Host 'Then open http://localhost:9001 in your browser.'
|
||||
Write-Host ''
|
||||
|
||||
if ($env:ETHERPAD_RUN -eq '1') {
|
||||
Write-Step 'Starting Etherpad on http://localhost:9001'
|
||||
pnpm run prod
|
||||
}
|
||||
133
bin/installer.sh
Executable file
133
bin/installer.sh
Executable file
@ -0,0 +1,133 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Etherpad one-line installer.
|
||||
#
|
||||
# Usage:
|
||||
# curl -fsSL https://raw.githubusercontent.com/ether/etherpad-lite/master/bin/installer.sh | sh
|
||||
#
|
||||
# Optional environment variables:
|
||||
# ETHERPAD_DIR Directory to install into (default: ./etherpad-lite)
|
||||
# ETHERPAD_BRANCH Branch / tag to clone (default: master)
|
||||
# ETHERPAD_RUN If set to 1, start Etherpad after install
|
||||
# NO_COLOR If set, disables coloured output
|
||||
|
||||
set -eu
|
||||
|
||||
# ---------- pretty output ----------
|
||||
if [ -z "${NO_COLOR:-}" ] && [ -t 1 ]; then
|
||||
bold=$(printf '\033[1m')
|
||||
green=$(printf '\033[32m')
|
||||
red=$(printf '\033[31m')
|
||||
yellow=$(printf '\033[33m')
|
||||
reset=$(printf '\033[0m')
|
||||
else
|
||||
bold=''; green=''; red=''; yellow=''; reset=''
|
||||
fi
|
||||
|
||||
step() { printf '%s==>%s %s%s%s\n' "$green" "$reset" "$bold" "$*" "$reset"; }
|
||||
warn() { printf '%s==>%s %s\n' "$yellow" "$reset" "$*" >&2; }
|
||||
fatal() { printf '%s==>%s %s\n' "$red" "$reset" "$*" >&2; exit 1; }
|
||||
|
||||
is_cmd() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
# ---------- defaults ----------
|
||||
ETHERPAD_DIR="${ETHERPAD_DIR:-etherpad-lite}"
|
||||
ETHERPAD_BRANCH="${ETHERPAD_BRANCH:-master}"
|
||||
ETHERPAD_REPO="${ETHERPAD_REPO:-https://github.com/ether/etherpad.git}"
|
||||
REQUIRED_NODE_MAJOR=20
|
||||
|
||||
step "Etherpad installer"
|
||||
|
||||
# ---------- prerequisite checks ----------
|
||||
is_cmd git || fatal "git is required but not installed. See https://git-scm.com/downloads"
|
||||
|
||||
if ! is_cmd node; then
|
||||
fatal "Node.js is required (>= ${REQUIRED_NODE_MAJOR}). Install it from https://nodejs.org"
|
||||
fi
|
||||
|
||||
NODE_MAJOR=$(node -p "process.versions.node.split('.')[0]")
|
||||
if [ "$NODE_MAJOR" -lt "$REQUIRED_NODE_MAJOR" ]; then
|
||||
fatal "Node.js >= ${REQUIRED_NODE_MAJOR} required. You have $(node --version)."
|
||||
fi
|
||||
|
||||
if ! is_cmd pnpm; then
|
||||
step "Installing pnpm globally"
|
||||
is_cmd npm || fatal "npm not found. Install Node.js >= ${REQUIRED_NODE_MAJOR}."
|
||||
if ! npm install -g pnpm 2>/dev/null; then
|
||||
warn "Global npm install requires elevated permissions; retrying with sudo."
|
||||
is_cmd sudo || fatal "sudo not available. Install pnpm manually: https://pnpm.io/installation"
|
||||
sudo npm install -g pnpm || \
|
||||
fatal "Failed to install pnpm. Install it manually: https://pnpm.io/installation"
|
||||
fi
|
||||
is_cmd pnpm || \
|
||||
fatal "pnpm install reported success but pnpm is still not on PATH. Open a new shell and re-run."
|
||||
fi
|
||||
|
||||
# ---------- clone ----------
|
||||
if [ -d "$ETHERPAD_DIR" ]; then
|
||||
if [ -d "$ETHERPAD_DIR/.git" ]; then
|
||||
warn "$ETHERPAD_DIR already exists; updating to $ETHERPAD_BRANCH."
|
||||
cd "$ETHERPAD_DIR" || fatal "Cannot cd into $ETHERPAD_DIR"
|
||||
|
||||
# Verify the existing checkout points at the expected remote.
|
||||
EXISTING_REMOTE=$(git remote get-url origin 2>/dev/null || echo "")
|
||||
if [ -n "$EXISTING_REMOTE" ] && [ "$EXISTING_REMOTE" != "$ETHERPAD_REPO" ]; then
|
||||
fatal "$ETHERPAD_DIR is checked out from '$EXISTING_REMOTE', expected '$ETHERPAD_REPO'. Refusing to overwrite."
|
||||
fi
|
||||
|
||||
# Refuse to clobber meaningful local changes. pnpm-lock.yaml is excluded
|
||||
# because `pnpm i` rewrites it during installation, which would otherwise
|
||||
# make every re-run of the installer fail.
|
||||
DIRTY=$(git status --porcelain 2>/dev/null | awk '$2 != "pnpm-lock.yaml" {print}')
|
||||
if [ -n "$DIRTY" ]; then
|
||||
printf '%s\n' "$DIRTY" >&2
|
||||
fatal "$ETHERPAD_DIR has uncommitted changes. Commit/stash them or remove the directory."
|
||||
fi
|
||||
|
||||
git fetch --tags --prune origin || fatal "git fetch failed in $ETHERPAD_DIR"
|
||||
|
||||
# Discard any pnpm-lock.yaml changes from a prior pnpm install so the
|
||||
# subsequent checkout doesn't refuse to overwrite local changes.
|
||||
git checkout -- pnpm-lock.yaml 2>/dev/null || true
|
||||
|
||||
# Switch to the requested branch / tag and fast-forward to it.
|
||||
if git show-ref --verify --quiet "refs/remotes/origin/$ETHERPAD_BRANCH"; then
|
||||
git checkout -B "$ETHERPAD_BRANCH" "origin/$ETHERPAD_BRANCH" || \
|
||||
fatal "git checkout $ETHERPAD_BRANCH failed"
|
||||
elif git show-ref --verify --quiet "refs/tags/$ETHERPAD_BRANCH"; then
|
||||
git checkout --detach "refs/tags/$ETHERPAD_BRANCH" || \
|
||||
fatal "git checkout tag $ETHERPAD_BRANCH failed"
|
||||
else
|
||||
fatal "Branch or tag '$ETHERPAD_BRANCH' not found on origin."
|
||||
fi
|
||||
|
||||
INSTALLED_REV=$(git rev-parse --short HEAD)
|
||||
step "Updated $ETHERPAD_DIR to $ETHERPAD_BRANCH @ $INSTALLED_REV"
|
||||
cd - >/dev/null || exit 1
|
||||
else
|
||||
fatal "$ETHERPAD_DIR exists and is not a git checkout. Aborting."
|
||||
fi
|
||||
else
|
||||
step "Cloning Etherpad ($ETHERPAD_BRANCH) into $ETHERPAD_DIR"
|
||||
git clone --depth 1 --branch "$ETHERPAD_BRANCH" "$ETHERPAD_REPO" "$ETHERPAD_DIR"
|
||||
fi
|
||||
|
||||
cd "$ETHERPAD_DIR"
|
||||
|
||||
# ---------- install + build ----------
|
||||
step "Installing dependencies (pnpm i)"
|
||||
pnpm i
|
||||
|
||||
step "Building Etherpad (pnpm run build:etherpad)"
|
||||
pnpm run build:etherpad
|
||||
|
||||
# ---------- done ----------
|
||||
printf '\n%s🎉 Etherpad is installed in %s%s\n' "$green" "$ETHERPAD_DIR" "$reset"
|
||||
printf 'To start Etherpad:\n'
|
||||
printf ' cd %s && pnpm run prod\n' "$ETHERPAD_DIR"
|
||||
printf 'Then open http://localhost:9001 in your browser.\n\n'
|
||||
|
||||
if [ "${ETHERPAD_RUN:-0}" = "1" ]; then
|
||||
step "Starting Etherpad on http://localhost:9001"
|
||||
exec pnpm run prod
|
||||
fi
|
||||
@ -8,46 +8,46 @@ const VERSION=pjson.version
|
||||
console.log(`Building docs for version ${VERSION}`)
|
||||
|
||||
const createDirIfNotExists = (dir: fs.PathLike) => {
|
||||
if (!fs.existsSync(dir)){
|
||||
fs.mkdirSync(dir)
|
||||
}
|
||||
if (!fs.existsSync(dir)){
|
||||
fs.mkdirSync(dir)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function copyFolderSync(from: fs.PathLike, to: fs.PathLike) {
|
||||
if(fs.existsSync(to)){
|
||||
const stat = fs.lstatSync(to)
|
||||
if (stat.isDirectory()){
|
||||
fs.rmSync(to, { recursive: true })
|
||||
}
|
||||
else{
|
||||
fs.rmSync(to)
|
||||
}
|
||||
if(fs.existsSync(to)){
|
||||
const stat = fs.lstatSync(to)
|
||||
if (stat.isDirectory()){
|
||||
fs.rmSync(to, { recursive: true })
|
||||
}
|
||||
fs.mkdirSync(to);
|
||||
fs.readdirSync(from).forEach(element => {
|
||||
if (fs.lstatSync(path.join(<string>from, element)).isFile()) {
|
||||
if (typeof from === "string") {
|
||||
if (typeof to === "string") {
|
||||
fs.copyFileSync(path.join(from, element), path.join(to, element))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (typeof from === "string") {
|
||||
if (typeof to === "string") {
|
||||
copyFolderSync(path.join(from, element), path.join(to, element))
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
else{
|
||||
fs.rmSync(to)
|
||||
}
|
||||
}
|
||||
fs.mkdirSync(to);
|
||||
fs.readdirSync(from).forEach(element => {
|
||||
if (fs.lstatSync(path.join(<string>from, element)).isFile()) {
|
||||
if (typeof from === "string") {
|
||||
if (typeof to === "string") {
|
||||
fs.copyFileSync(path.join(from, element), path.join(to, element))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (typeof from === "string") {
|
||||
if (typeof to === "string") {
|
||||
copyFolderSync(path.join(from, element), path.join(to, element))
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
exec('asciidoctor -v', (err,stdout)=>{
|
||||
if (err){
|
||||
console.log('Please install asciidoctor')
|
||||
console.log('https://asciidoctor.org/docs/install-toolchain/')
|
||||
process.exit(1)
|
||||
}
|
||||
if (err){
|
||||
console.log('Please install asciidoctor')
|
||||
console.log('https://asciidoctor.org/docs/install-toolchain/')
|
||||
process.exit(1)
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
{
|
||||
"name": "bin",
|
||||
"version": "2.6.1",
|
||||
"version": "2.7.0",
|
||||
"description": "",
|
||||
"main": "checkAllPads.js",
|
||||
"directories": {
|
||||
"doc": "doc"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.13.3",
|
||||
"axios": "^1.15.1",
|
||||
"ep_etherpad-lite": "workspace:../src",
|
||||
"log4js": "^6.9.1",
|
||||
"semver": "^7.7.3",
|
||||
"semver": "^7.7.4",
|
||||
"tsx": "^4.21.0",
|
||||
"ueberdb2": "^5.0.23"
|
||||
"ueberdb2": "^5.0.48"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.0.10",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/semver": "^7.7.1",
|
||||
"typescript": "^5.9.3"
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"scripts": {
|
||||
"makeDocs": "node --import tsx make_docs.ts",
|
||||
|
||||
@ -374,6 +374,56 @@ log4js.configure({
|
||||
'Translation files help with Etherpad accessibility.');
|
||||
}
|
||||
|
||||
// Check template files for absolute `/static/plugins/...` asset paths.
|
||||
// Those break any Etherpad instance hosted behind a reverse proxy at a
|
||||
// sub-path (e.g. https://example.com/etherpad/pad) because the browser
|
||||
// resolves them against the domain root instead of the proxy prefix.
|
||||
// See #5203, and ep_embedmedia#4 for the original fix this check is modelled on.
|
||||
//
|
||||
// Autofix only rewrites paths in `templates/` (rendered under `/p/<pad>/...`
|
||||
// where `../static/plugins/...` is correct). Files under `static/` are
|
||||
// served from `/static/plugins/<plugin>/static/...` and need a different
|
||||
// relative prefix that depends on the file's depth, so we only warn.
|
||||
const STATIC_ABS = /(?<![./:\w])\/static\/plugins\//g;
|
||||
const scanDir = async (dir: 'templates' | 'static') => {
|
||||
const abs = `${pluginPath}/${dir}`;
|
||||
if (!files.includes(dir)) return;
|
||||
const scanFiles: string[] = [];
|
||||
const walk = async (d: string) => {
|
||||
for (const entry of await fsp.readdir(d, {withFileTypes: true})) {
|
||||
const full = `${d}/${entry.name}`;
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === 'node_modules' || entry.name === '.git') continue;
|
||||
await walk(full);
|
||||
} else if (/\.(ejs|html)$/.test(entry.name)) {
|
||||
scanFiles.push(full);
|
||||
}
|
||||
}
|
||||
};
|
||||
await walk(abs);
|
||||
for (const fp of scanFiles) {
|
||||
const src = await fsp.readFile(fp, 'utf8');
|
||||
if (!STATIC_ABS.test(src)) continue;
|
||||
STATIC_ABS.lastIndex = 0;
|
||||
const rel = path.relative(pluginPath, fp);
|
||||
if (dir === 'templates') {
|
||||
logger.warn(`${rel} contains absolute '/static/plugins/...' asset paths; ` +
|
||||
'these break reverse-proxied Etherpad deployments. Use ' +
|
||||
"'../static/plugins/...' instead.");
|
||||
if (autoFix) {
|
||||
logger.info(`Autofixing absolute /static/plugins/ paths in ${rel}`);
|
||||
await fsp.writeFile(fp, src.replace(STATIC_ABS, '../static/plugins/'));
|
||||
}
|
||||
} else {
|
||||
logger.warn(`${rel} contains absolute '/static/plugins/...' asset paths; ` +
|
||||
'these break reverse-proxied Etherpad deployments. Use a path ' +
|
||||
"relative to this file's location under 'static/' (no leading '/').");
|
||||
}
|
||||
}
|
||||
};
|
||||
await scanDir('templates');
|
||||
await scanDir('static');
|
||||
|
||||
|
||||
if (files.includes('.ep_initialized')) {
|
||||
logger.warn(
|
||||
@ -397,10 +447,16 @@ log4js.configure({
|
||||
if (files.includes('static')) {
|
||||
const staticFiles = await fsp.readdir(`${pluginPath}/static`);
|
||||
if (!staticFiles.includes('tests')) {
|
||||
logger.warn('Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin');
|
||||
logger.warn('Test files not found, please create tests. https://github.com/ether/etherpad/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin');
|
||||
}
|
||||
} else {
|
||||
logger.warn('Test files not found, please create tests. https://github.com/ether/etherpad-lite/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin');
|
||||
logger.warn('Test files not found, please create tests. https://github.com/ether/etherpad/wiki/Creating-a-plugin#writing-and-running-front-end-tests-for-your-plugin');
|
||||
}
|
||||
|
||||
// Update all dependencies to their latest compatible versions.
|
||||
if (autoFix) {
|
||||
logger.info('Updating dependencies...');
|
||||
execSync('pnpm update', {cwd: `${pluginPath}/`, stdio: 'inherit'});
|
||||
}
|
||||
|
||||
// Install dependencies so we can run ESLint. This should also create or update package-lock.json
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# 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))
|
||||
(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#get-in-touch))
|
||||
|
||||
## Pull requests
|
||||
|
||||
@ -131,5 +131,5 @@ Etherpad is much more than software. So if you aren't a developer then worry no
|
||||
* Write proposals for grants
|
||||
* Co-Author and Publish CVEs
|
||||
* Work with SFC to maintain legal side of project
|
||||
* Maintain TODO page - https://github.com/ether/etherpad-lite/wiki/TODO#IMPORTANT_TODOS
|
||||
* Maintain TODO page - https://github.com/ether/etherpad/wiki/TODO#IMPORTANT_TODOS
|
||||
|
||||
|
||||
@ -16,13 +16,13 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Install libreoffice
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1.4.2
|
||||
uses: awalsh128/cache-apt-pkgs-action@v1.6.0
|
||||
with:
|
||||
packages: libreoffice libreoffice-pdfimport
|
||||
version: 1.0
|
||||
-
|
||||
name: Install etherpad core
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ether/etherpad-lite
|
||||
path: etherpad-lite
|
||||
@ -44,20 +44,9 @@ jobs:
|
||||
${{ runner.os }}-pnpm-store-
|
||||
-
|
||||
name: Checkout plugin repository
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: plugin
|
||||
-
|
||||
name: Determine plugin name
|
||||
id: plugin_name
|
||||
working-directory: ./plugin
|
||||
run: |
|
||||
npx -c 'printf %s\\n "::set-output name=plugin_name::${npm_package_name}"'
|
||||
-
|
||||
name: Link plugin directory
|
||||
working-directory: ./plugin
|
||||
run: |
|
||||
pnpm link --global
|
||||
- name: Remove tests
|
||||
working-directory: ./etherpad-lite
|
||||
run: rm -rf ./src/tests/backend/specs
|
||||
@ -65,28 +54,18 @@ jobs:
|
||||
name: Install Etherpad core dependencies
|
||||
working-directory: ./etherpad-lite
|
||||
run: bin/installDeps.sh
|
||||
- name: Link plugin to etherpad-lite
|
||||
- name: Install plugin
|
||||
working-directory: ./etherpad-lite
|
||||
run: |
|
||||
pnpm link --global $PLUGIN_NAME
|
||||
pnpm run plugins i --path ../../plugin
|
||||
env:
|
||||
PLUGIN_NAME: ${{ steps.plugin_name.outputs.plugin_name }}
|
||||
- name: Link ep_etherpad-lite
|
||||
working-directory: ./etherpad-lite/src
|
||||
run: |
|
||||
pnpm link --global
|
||||
- name: Link etherpad to plugin
|
||||
working-directory: ./plugin
|
||||
run: |
|
||||
pnpm link --global ep_etherpad-lite
|
||||
-
|
||||
name: Run the backend tests
|
||||
working-directory: ./etherpad-lite
|
||||
working-directory: ./etherpad-lite/src
|
||||
run: |
|
||||
res=$(find .. -path "./node_modules/ep_*/static/tests/backend/specs/**" | wc -l)
|
||||
shopt -s globstar
|
||||
res=$(find ./plugin_packages -path "*/static/tests/backend/specs/*" 2>/dev/null | wc -l)
|
||||
if [ $res -eq 0 ]; then
|
||||
echo "No backend tests found"
|
||||
else
|
||||
pnpm run test
|
||||
npx cross-env NODE_ENV=production mocha --import=tsx --timeout 120000 --recursive node_modules/ep_*/static/tests/backend/specs/**
|
||||
fi
|
||||
|
||||
@ -12,7 +12,7 @@ jobs:
|
||||
steps:
|
||||
-
|
||||
name: Check out Etherpad core
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: ether/etherpad-lite
|
||||
- uses: pnpm/action-setup@v3
|
||||
@ -33,7 +33,7 @@ jobs:
|
||||
${{ runner.os }}-pnpm-store-
|
||||
-
|
||||
name: Check out the plugin
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
path: ./node_modules/__tmp
|
||||
-
|
||||
@ -78,7 +78,7 @@ jobs:
|
||||
- name: Run the frontend tests
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm run prod &
|
||||
pnpm run dev &
|
||||
connected=false
|
||||
can_connect() {
|
||||
curl -sSfo /dev/null http://localhost:9001/ || return 1
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
# This workflow will run tests using node and then publish a package to the npm registry when a release is created
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/publishing-nodejs-packages
|
||||
#
|
||||
# Publishing uses npm Trusted Publishing (OIDC) — no NPM_TOKEN secret is
|
||||
# required. Each package must have a trusted publisher configured on npmjs.com
|
||||
# pointing at this workflow file. See:
|
||||
# https://docs.npmjs.com/trusted-publishers
|
||||
|
||||
name: Node.js Package
|
||||
|
||||
@ -9,16 +14,24 @@ on:
|
||||
jobs:
|
||||
publish-npm:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # for the atomic version-bump push (branch + tag)
|
||||
id-token: write # for npm OIDC trusted publishing
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
# OIDC trusted publishing needs npm >= 11.5.1, which requires
|
||||
# Node >= 20.17.0. setup-node's `20` resolves to the latest
|
||||
# 20.x, which satisfies that.
|
||||
node-version: 20
|
||||
registry-url: https://registry.npmjs.org/
|
||||
- name: Upgrade npm to >=11.5.1 (required for trusted publishing)
|
||||
run: npm install -g npm@latest
|
||||
- name: Check out Etherpad core
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
repository: ether/etherpad-lite
|
||||
- uses: pnpm/action-setup@v3
|
||||
- uses: pnpm/action-setup@v5
|
||||
name: Install pnpm
|
||||
with:
|
||||
version: 10
|
||||
@ -27,7 +40,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
|
||||
- uses: actions/cache@v4
|
||||
- uses: actions/cache@v5
|
||||
name: Setup pnpm cache
|
||||
with:
|
||||
path: ${{ env.STORE_PATH }}
|
||||
@ -35,7 +48,7 @@ jobs:
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-store-
|
||||
-
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
@ -47,8 +60,22 @@ jobs:
|
||||
git config user.name 'github-actions[bot]'
|
||||
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
|
||||
pnpm i
|
||||
# `pnpm version patch` bumps package.json, makes a commit, and creates
|
||||
# a `v<new-version>` tag. Capture the new tag name from package.json
|
||||
# rather than parsing pnpm's output, which has historically varied.
|
||||
pnpm version patch
|
||||
git push --follow-tags
|
||||
NEW_TAG="v$(node -p "require('./package.json').version")"
|
||||
# CRITICAL: use --atomic so the branch update and the tag update
|
||||
# succeed (or fail) as a single transaction on the server. The old
|
||||
# `git push --follow-tags` was non-atomic per ref: if a concurrent
|
||||
# publish run won the race, the branch fast-forward would be rejected
|
||||
# but the tag push would still land — leaving a dangling tag with no
|
||||
# matching commit on the branch. Subsequent runs would then forever
|
||||
# try to bump to the same already-existing tag and fail with
|
||||
# `tag 'vN+1' already exists`. With --atomic, a rejected branch push
|
||||
# rejects the tag push too, and the next workflow tick can retry
|
||||
# cleanly against the up-to-date refs.
|
||||
git push --atomic origin "${GITHUB_REF_NAME}" "${NEW_TAG}"
|
||||
# This is required if the package has a prepare script that uses something
|
||||
# in dependencies or devDependencies.
|
||||
-
|
||||
@ -63,12 +90,10 @@ jobs:
|
||||
# already-used version number. By running `npm publish` after `git push`,
|
||||
# back-to-back merges will cause the first merge's workflow to fail but
|
||||
# the second's will succeed.
|
||||
-
|
||||
run: pnpm publish
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
#-
|
||||
# name: Add package to etherpad organization
|
||||
# run: pnpm access grant read-write etherpad:developers
|
||||
# env:
|
||||
# NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
|
||||
#
|
||||
# Use `npm publish` directly (not `pnpm publish`) because OIDC trusted
|
||||
# publishing requires npm CLI >= 11.5.1 and `pnpm publish` shells out to
|
||||
# whichever `npm` is on PATH; calling `npm` directly avoids any shim
|
||||
# ambiguity.
|
||||
- name: Publish to npm via OIDC
|
||||
run: npm publish --provenance --access public
|
||||
|
||||
@ -1,6 +1,11 @@
|
||||
name: Node.js Package
|
||||
on: [push]
|
||||
|
||||
# id-token: write must be granted here so the reusable npmpublish workflow
|
||||
# can request an OIDC token for npm trusted publishing.
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
backend:
|
||||
@ -14,5 +19,8 @@ jobs:
|
||||
needs:
|
||||
- backend
|
||||
- frontend
|
||||
permissions:
|
||||
contents: write # for the version bump push
|
||||
id-token: write # for npm OIDC trusted publishing
|
||||
uses: ./.github/workflows/npmpublish.yml
|
||||
secrets: inherit
|
||||
|
||||
169
bin/setup-trusted-publishers.sh
Executable file
169
bin/setup-trusted-publishers.sh
Executable file
@ -0,0 +1,169 @@
|
||||
#!/bin/sh
|
||||
#
|
||||
# Configure npm Trusted Publishers (OIDC) for ep_etherpad and every
|
||||
# ether/ep_* plugin in bulk.
|
||||
#
|
||||
# Prerequisites:
|
||||
# - npm CLI >= 11.5.1 (the version that ships `npm trust github`)
|
||||
# - Logged into npmjs.com as a maintainer of the packages: `npm login`
|
||||
# - `gh` CLI logged in (only needed for plugin discovery; pass --packages
|
||||
# to skip discovery and use a static list)
|
||||
#
|
||||
# Usage:
|
||||
# bin/setup-trusted-publishers.sh # all ether/ep_* plugins + ep_etherpad
|
||||
# bin/setup-trusted-publishers.sh --dry-run # print what would happen
|
||||
# bin/setup-trusted-publishers.sh --packages ep_align,ep_webrtc
|
||||
# bin/setup-trusted-publishers.sh --skip-existing # don't fail if already configured
|
||||
# bin/setup-trusted-publishers.sh --otp 123456 # supply 2FA OTP up front
|
||||
#
|
||||
# Note: `npm trust github` requires 2FA. If your account has 2FA enabled
|
||||
# (it should), pass --otp once and the same code will be reused for every
|
||||
# package call inside the same minute. The TOTP code typically expires
|
||||
# every 30s, so you may need to run the script in chunks via --packages.
|
||||
#
|
||||
# Each package gets a GitHub Actions trusted publisher pointing at the
|
||||
# canonical workflow file used by that package family:
|
||||
# - plugins: .github/workflows/test-and-release.yml
|
||||
# - ep_etherpad: .github/workflows/releaseEtherpad.yml
|
||||
#
|
||||
# Existing configurations cannot be overwritten — only one trust relationship
|
||||
# per package is allowed today. Use `--skip-existing` to ignore those failures.
|
||||
|
||||
set -eu
|
||||
|
||||
# `npm trust github --file` wants ONLY the workflow filename (basename),
|
||||
# not the full .github/workflows/<name> path.
|
||||
PLUGIN_WORKFLOW="test-and-release.yml"
|
||||
CORE_WORKFLOW="releaseEtherpad.yml"
|
||||
CORE_PACKAGE="ep_etherpad"
|
||||
CORE_REPO="etherpad-lite"
|
||||
ORG="ether"
|
||||
|
||||
DRY_RUN=0
|
||||
SKIP_EXISTING=0
|
||||
PACKAGES=""
|
||||
OTP=""
|
||||
|
||||
usage() {
|
||||
sed -n '2,/^$/p' "$0" | sed 's/^# \?//'
|
||||
exit "${1:-0}"
|
||||
}
|
||||
|
||||
# ---------- arg parsing ----------
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--dry-run) DRY_RUN=1; shift ;;
|
||||
--skip-existing) SKIP_EXISTING=1; shift ;;
|
||||
--packages) PACKAGES="$2"; shift 2 ;;
|
||||
--otp) OTP="$2"; shift 2 ;;
|
||||
-h|--help) usage 0 ;;
|
||||
*) printf 'Unknown flag: %s\n' "$1" >&2; usage 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ---------- prereq checks ----------
|
||||
is_cmd() { command -v "$1" >/dev/null 2>&1; }
|
||||
|
||||
is_cmd npm || { echo "npm CLI not found." >&2; exit 1; }
|
||||
|
||||
NPM_MAJOR=$(npm --version | cut -d. -f1)
|
||||
NPM_MINOR=$(npm --version | cut -d. -f2)
|
||||
NPM_PATCH=$(npm --version | cut -d. -f3)
|
||||
if [ "$NPM_MAJOR" -lt 11 ] || \
|
||||
{ [ "$NPM_MAJOR" -eq 11 ] && [ "$NPM_MINOR" -lt 5 ]; } || \
|
||||
{ [ "$NPM_MAJOR" -eq 11 ] && [ "$NPM_MINOR" -eq 5 ] && [ "$NPM_PATCH" -lt 1 ]; }; then
|
||||
echo "npm >= 11.5.1 required (you have $(npm --version)). Run: npm install -g npm@latest" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify auth (whoami fails if not logged in). Skipped in --dry-run.
|
||||
if [ "$DRY_RUN" != "1" ]; then
|
||||
if ! npm whoami >/dev/null 2>&1; then
|
||||
echo "Not logged into npm. Run 'npm login' first." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# ---------- discover packages ----------
|
||||
if [ -z "$PACKAGES" ]; then
|
||||
is_cmd gh || {
|
||||
echo "gh CLI not found. Either install it or pass --packages ep_a,ep_b,..." >&2
|
||||
exit 1
|
||||
}
|
||||
echo "Discovering ether/ep_* repos..."
|
||||
PACKAGES=$(gh repo list "$ORG" --limit 300 --json name,isArchived \
|
||||
--jq '.[] | select(.name | startswith("ep_")) | select(.isArchived | not) | .name' \
|
||||
| tr '\n' ',' | sed 's/,$//')
|
||||
PACKAGES="${CORE_PACKAGE},${PACKAGES}"
|
||||
fi
|
||||
|
||||
# ---------- per-package setup ----------
|
||||
configure_one() {
|
||||
PKG="$1"
|
||||
if [ "$PKG" = "$CORE_PACKAGE" ]; then
|
||||
REPO="$CORE_REPO"
|
||||
WORKFLOW="$CORE_WORKFLOW"
|
||||
else
|
||||
REPO="$PKG"
|
||||
WORKFLOW="$PLUGIN_WORKFLOW"
|
||||
fi
|
||||
|
||||
printf '%-40s -> %s/%s @ %s\n' "$PKG" "$ORG" "$REPO" "$WORKFLOW"
|
||||
|
||||
if [ "$DRY_RUN" = "1" ]; then
|
||||
printf ' (dry-run) would run: npm trust github %s --repository %s/%s --file %s --yes\n' \
|
||||
"$PKG" "$ORG" "$REPO" "$WORKFLOW"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Disable -e around the npm call so a non-zero exit can never short-circuit
|
||||
# the STATUS / --skip-existing handling below. In practice the wrapping
|
||||
# `if configure_one` already suppresses errexit inside this function (POSIX
|
||||
# errexit-in-conditional behaviour), but relying on that is fragile — anyone
|
||||
# later refactoring the call site out of an `if` would silently reintroduce
|
||||
# the bug. The explicit shim makes the intent obvious and survives such
|
||||
# refactors.
|
||||
set +e
|
||||
if [ -n "$OTP" ]; then
|
||||
OUTPUT=$(npm trust github "$PKG" --repository "$ORG/$REPO" --file "$WORKFLOW" --otp "$OTP" --yes 2>&1)
|
||||
else
|
||||
OUTPUT=$(npm trust github "$PKG" --repository "$ORG/$REPO" --file "$WORKFLOW" --yes 2>&1)
|
||||
fi
|
||||
STATUS=$?
|
||||
set -e
|
||||
if [ "$STATUS" -eq 0 ]; then
|
||||
printf ' ok\n'
|
||||
else
|
||||
# The npm registry returns 409 Conflict when a trust relationship
|
||||
# already exists (you can only have one per package today). Treat
|
||||
# that as success when --skip-existing is set, alongside the older
|
||||
# "already exists/configured" string match.
|
||||
if [ "$SKIP_EXISTING" = "1" ] && \
|
||||
echo "$OUTPUT" | grep -qiE "409 Conflict|already (exists|configured)"; then
|
||||
printf ' already configured (skipped)\n'
|
||||
return 0
|
||||
fi
|
||||
printf ' FAILED:\n%s\n' "$OUTPUT" | sed 's/^/ /'
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
FAILED=""
|
||||
TOTAL=0
|
||||
OK=0
|
||||
IFS=','
|
||||
for PKG in $PACKAGES; do
|
||||
TOTAL=$((TOTAL + 1))
|
||||
if configure_one "$PKG"; then
|
||||
OK=$((OK + 1))
|
||||
else
|
||||
FAILED="$FAILED $PKG"
|
||||
fi
|
||||
done
|
||||
unset IFS
|
||||
|
||||
printf '\n%d/%d packages configured\n' "$OK" "$TOTAL"
|
||||
if [ -n "$FAILED" ]; then
|
||||
printf 'Failed:%s\n' "$FAILED"
|
||||
exit 1
|
||||
fi
|
||||
@ -2,10 +2,12 @@
|
||||
set -e
|
||||
mydir=$(cd "${0%/*}" && pwd -P) || exit 1
|
||||
cd "${mydir}"/..
|
||||
OUTDATED=$(npm outdated --depth=0 | awk '{print $1}' | grep '^ep_') || {
|
||||
outdated_raw=$(pnpm --filter ep_etherpad-lite outdated --depth=0 2>&1) || true
|
||||
OUTDATED=$(printf '%s\n' "$outdated_raw" | awk '{print $1}' | grep '^ep_' | grep -v '^ep_etherpad-lite$') || true
|
||||
if [ -z "$OUTDATED" ]; then
|
||||
echo "All plugins are up-to-date"
|
||||
exit 0
|
||||
}
|
||||
fi
|
||||
set -- ${OUTDATED}
|
||||
echo "Updating plugins: $*"
|
||||
exec pnpm install "$@"
|
||||
exec pnpm --filter ep_etherpad-lite update "$@"
|
||||
|
||||
@ -73,7 +73,7 @@ export default defineConfig({
|
||||
},
|
||||
|
||||
socialLinks: [
|
||||
{ icon: 'github', link: 'https://github.com/ether/etherpad-lite' }
|
||||
{ icon: 'github', link: 'https://github.com/ether/etherpad' }
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
== Changeset Library
|
||||
|
||||
The https://github.com/ether/etherpad-lite/blob/develop/src/static/js/Changeset.ts[changeset
|
||||
The https://github.com/ether/etherpad/blob/develop/src/static/js/Changeset.ts[changeset
|
||||
library]
|
||||
provides tools to create, read, and apply changesets.
|
||||
|
||||
@ -31,7 +31,7 @@ const AttributePool = require('ep_etherpad-lite/static/js/AttributePool');
|
||||
----
|
||||
|
||||
Changesets do not include any attribute key–value pairs. Instead, they use
|
||||
numeric identifiers that reference attributes kept in an https://github.com/ether/etherpad-lite/blob/develop/src/static/js/AttributePool.ts[attribute pool].
|
||||
numeric identifiers that reference attributes kept in an https://github.com/ether/etherpad/blob/develop/src/static/js/AttributePool.ts[attribute pool].
|
||||
This attribute interning reduces the transmission overhead of attributes that
|
||||
are used many times.
|
||||
|
||||
@ -42,5 +42,5 @@ historical attribute used in the pad.
|
||||
|
||||
Detailed information about the changesets & Easysync protocol:
|
||||
|
||||
* https://github.com/ether/etherpad-lite/blob/develop/doc/easysync/easysync-notes.pdf[Easysync Protocol]
|
||||
* https://github.com/ether/etherpad-lite/blob/develop/doc/easysync/easysync-full-description.pdf[Etherpad and EasySync Technical Manual]
|
||||
* https://github.com/ether/etherpad/blob/develop/doc/easysync/easysync-notes.pdf[Easysync Protocol]
|
||||
* https://github.com/ether/etherpad/blob/develop/doc/easysync/easysync-full-description.pdf[Etherpad and EasySync Technical Manual]
|
||||
|
||||
@ -29,7 +29,7 @@ const AttributePool = require('ep_etherpad-lite/static/js/AttributePool');
|
||||
|
||||
Changesets do not include any attribute key–value pairs. Instead, they use
|
||||
numeric identifiers that reference attributes kept in an [attribute
|
||||
pool](https://github.com/ether/etherpad-lite/blob/develop/src/static/js/AttributePool.ts).
|
||||
pool](https://github.com/ether/etherpad/blob/develop/src/static/js/AttributePool.ts).
|
||||
This attribute interning reduces the transmission overhead of attributes that
|
||||
are used many times.
|
||||
|
||||
@ -40,5 +40,5 @@ historical attribute used in the pad.
|
||||
|
||||
Detailed information about the changesets & Easysync protocol:
|
||||
|
||||
* [Easysync Protocol](https://github.com/ether/etherpad-lite/blob/develop/doc/easysync/easysync-notes.pdf)
|
||||
* [Etherpad and EasySync Technical Manual](https://github.com/ether/etherpad-lite/blob/develop/doc/easysync/easysync-full-description.pdf)
|
||||
* [Easysync Protocol](https://github.com/ether/etherpad/blob/develop/doc/easysync/easysync-notes.pdf)
|
||||
* [Etherpad and EasySync Technical Manual](https://github.com/ether/etherpad/blob/develop/doc/easysync/easysync-full-description.pdf)
|
||||
|
||||
@ -211,6 +211,34 @@ The return value of this hook will add elements into the "lineMarkerAttribute"
|
||||
category, making the aceDomLineProcessLineAttributes hook (documented below)
|
||||
call for those elements.
|
||||
|
||||
=== aceRegisterLineAttributes
|
||||
|
||||
Called from: `src/static/js/ace2_inner.ts`
|
||||
|
||||
Things in context: None
|
||||
|
||||
Tells Etherpad which line attributes should be preserved when a user presses
|
||||
Enter to split a line. Without this hook, custom line attributes (such as
|
||||
headings or alignment) are lost when a line is split.
|
||||
|
||||
The return value should be an array of attribute names:
|
||||
|
||||
[source,javascript]
|
||||
----
|
||||
exports.aceRegisterLineAttributes = function(){
|
||||
return [ 'heading' ];
|
||||
}
|
||||
----
|
||||
|
||||
When Enter is pressed on a line that has a registered attribute:
|
||||
|
||||
* **Middle or end of line:** the attribute is copied to the new line below.
|
||||
* **Start of line (column 0):** the attribute moves down with the text content,
|
||||
and the now-empty line above is cleared.
|
||||
|
||||
This is backwards compatible — on Etherpad versions that do not have this hook,
|
||||
the registration is silently ignored.
|
||||
|
||||
=== aceInitialized
|
||||
|
||||
Called from: `src/static/js/ace2_inner.js`
|
||||
|
||||
@ -207,6 +207,33 @@ The return value of this hook will add elements into the "lineMarkerAttribute"
|
||||
category, making the aceDomLineProcessLineAttributes hook (documented below)
|
||||
call for those elements.
|
||||
|
||||
## aceRegisterLineAttributes
|
||||
|
||||
Called from: `src/static/js/ace2_inner.ts`
|
||||
|
||||
Things in context: None
|
||||
|
||||
Tells Etherpad which line attributes should be preserved when a user presses
|
||||
Enter to split a line. Without this hook, custom line attributes (such as
|
||||
headings or alignment) are lost when a line is split.
|
||||
|
||||
The return value should be an array of attribute names:
|
||||
|
||||
```
|
||||
exports.aceRegisterLineAttributes = function(){
|
||||
return [ 'heading' ];
|
||||
}
|
||||
```
|
||||
|
||||
When Enter is pressed on a line that has a registered attribute:
|
||||
|
||||
* **Middle or end of line:** the attribute is copied to the new line below.
|
||||
* **Start of line (column 0):** the attribute moves down with the text content,
|
||||
and the now-empty line above is cleared.
|
||||
|
||||
This is backwards compatible — on Etherpad versions that do not have this hook,
|
||||
the registration is silently ignored.
|
||||
|
||||
## aceInitialized
|
||||
|
||||
Called from: `src/static/js/ace2_inner.js`
|
||||
|
||||
@ -65,7 +65,7 @@ Portal submits content into new blog post
|
||||
=== Usage
|
||||
|
||||
==== API version
|
||||
The latest version is `1.2.15`
|
||||
The latest version is `1.3.0`
|
||||
|
||||
The current version can be queried via /api.
|
||||
|
||||
|
||||
@ -98,7 +98,7 @@ Portal submits content into new blog post
|
||||
## Usage
|
||||
|
||||
### API version
|
||||
The latest version is `1.2.15`
|
||||
The latest version is `1.3.0`
|
||||
|
||||
The current version can be queried via /api.
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ Cookies used by Etherpad.
|
||||
| Name | Sample value | Domain | Path | Expires/max-age | Http-only | Secure | Usage description |
|
||||
|-------------------|----------------------------------|-------------|------|-----------------|-----------|--------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| express_sid | s%3A7yCNjRmTW8ylGQ53I2IhOwYF9... | example.org | / | Session | true | true | Session ID of the [Express web framework](https://expressjs.com). When Etherpad is behind a reverse proxy, and an administrator wants to use session stickiness, he may use this cookie. If you are behind a reverse proxy, please remember to set `trustProxy: true` in `settings.json`. Set in [webaccess.js#L131](https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/node/hooks/express/webaccess.js#L131). |
|
||||
| language | en | example.org | / | Session | false | true | The language of the UI (e.g.: `en-GB`, `it`). Set in [pad_editor.js#L111](https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/static/js/pad_editor.js#L111). |
|
||||
| language | en | example.org | / | Session | false | true | The language of the UI (e.g.: `en-GB`, `it`). Set by the pad client when the user changes **My View → Language** (currently in `src/static/js/pad.ts`, via `setMyViewLanguage()`). |
|
||||
| prefs / prefsHttp | %7B%22epThemesExtTheme%22... | example.org | /p | year 3000 | false | true | Client-side preferences (e.g.: font family, chat always visible, show authorship colors, ...). Set in [pad_cookie.js#L49](https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/static/js/pad_cookie.js#L49). `prefs` is used if Etherpad is accessed over HTTPS, `prefsHttp` if accessed over HTTP. For more info see https://github.com/ether/etherpad-lite/issues/3179. |
|
||||
| token | t.tFzkihhhBf4xKEpCK3PU | example.org | / | 60 days | false | true | A random token representing the author, of the form `t.randomstring_of_lenght_20`. The random string is generated by the client, at ([pad.js#L55-L66](https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/static/js/pad.js#L55-L66)). This cookie is always set by the client (at [pad.js#L153-L158](https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/static/js/pad.js#L153-L158)) without any solicitation from the server. It is used for all the pads accessed via the web UI (not used for the HTTP API). On the server side, its value is accessed at [SecurityManager.js#L33](https://github.com/ether/etherpad-lite/blob/01497aa399690e44393e91c19917d11d025df71b/src/node/db/SecurityManager.js#L33). |
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ If you are ok downloading a https://hub.docker.com/r/etherpad/etherpad[prebuilt
|
||||
docker pull etherpad/etherpad
|
||||
|
||||
# gets a specific version
|
||||
docker pull etherpad/etherpad:1.8.0
|
||||
docker pull etherpad/etherpad:2.6.1
|
||||
----
|
||||
|
||||
=== Build a personalized container
|
||||
@ -62,24 +62,11 @@ The variable value has to be a space separated, double quoted list of plugin nam
|
||||
|
||||
Some plugins will need personalized settings. Just refer to the previous section, and include them in your custom `settings.json.docker`.
|
||||
|
||||
==== Rebuilding including export functionality for DOC/PDF/ODT
|
||||
==== Rebuilding including export functionality for DOC/DOCX/PDF/ODT
|
||||
|
||||
If you want to be able to export your pads to DOC/PDF/ODT files, you can install
|
||||
either Abiword or Libreoffice via setting a build variable.
|
||||
|
||||
===== Via Abiword
|
||||
|
||||
For installing Abiword, set the `INSTALL_ABIWORD` build variable to any value.
|
||||
|
||||
Also, you will need to configure the path to the abiword executable
|
||||
via setting the `abiword` property in `<BASEDIR>/settings.json.docker` to
|
||||
`/usr/bin/abiword` or via setting the environment variable `ABIWORD` to
|
||||
`/usr/bin/abiword`.
|
||||
|
||||
===== Via Libreoffice
|
||||
|
||||
For installing Libreoffice instead, set the `INSTALL_SOFFICE` build variable
|
||||
to any value.
|
||||
If you want to be able to export your pads to DOC/DOCX/PDF/ODT files, you can
|
||||
install Libreoffice via setting the `INSTALL_SOFFICE` build variable to any
|
||||
value.
|
||||
|
||||
Also, you will need to configure the path to the libreoffice executable
|
||||
via setting the `soffice` property in `<BASEDIR>/settings.json.docker` to
|
||||
@ -464,12 +451,8 @@ For the editor container, you can also make it full width by adding `full-width-
|
||||
| How long may clients use served javascript code (in seconds)? Not setting this may cause problems during deployment. Set to 0 to disable caching.
|
||||
| `21600` (6 hours)
|
||||
|
||||
| `ABIWORD`
|
||||
| Absolute path to the Abiword executable. Abiword is needed to get advanced import/export features of pads. Setting it to null disables Abiword and will only allow plain text and HTML import/exports.
|
||||
| `null`
|
||||
|
||||
| `SOFFICE`
|
||||
| This is the absolute path to the soffice executable. LibreOffice can be used in lieu of Abiword to export pads. Setting it to null disables LibreOffice exporting.
|
||||
| Absolute path to the soffice (LibreOffice) executable. Needed for advanced import/export of pads (docx, pdf, odt). Setting it to null disables LibreOffice and will only allow plain text and HTML import/exports.
|
||||
| `null`
|
||||
|
||||
| `ALLOW_UNKNOWN_FILE_ENDS`
|
||||
|
||||
@ -1,15 +1,21 @@
|
||||
# Docker
|
||||
|
||||
The official Docker image is available on https://hub.docker.com/r/etherpad/etherpad.
|
||||
The official Docker image is published to two registries with identical tags:
|
||||
|
||||
## Downloading from Docker Hub
|
||||
If you are ok downloading a [prebuilt image from Docker Hub](https://hub.docker.com/r/etherpad/etherpad), these are the commands:
|
||||
- Docker Hub (canonical): https://hub.docker.com/r/etherpad/etherpad
|
||||
- GitHub Container Registry (mirror): https://github.com/ether/etherpad/pkgs/container/etherpad
|
||||
|
||||
The GHCR mirror is useful if you are hitting Docker Hub anonymous pull rate limits (for example on Kubernetes clusters).
|
||||
|
||||
## Downloading a prebuilt image
|
||||
```bash
|
||||
# gets the latest published version
|
||||
# from Docker Hub
|
||||
docker pull etherpad/etherpad
|
||||
docker pull etherpad/etherpad:2.6.1
|
||||
|
||||
# gets a specific version
|
||||
docker pull etherpad/etherpad:1.8.0
|
||||
# from GHCR (same image, same tags)
|
||||
docker pull ghcr.io/ether/etherpad
|
||||
docker pull ghcr.io/ether/etherpad:2.6.1
|
||||
```
|
||||
|
||||
## Build a personalized container
|
||||
@ -29,24 +35,11 @@ The variable value has to be a space separated, double quoted list of plugin nam
|
||||
|
||||
Some plugins will need personalized settings. Just refer to the previous section, and include them in your custom `settings.json.docker`.
|
||||
|
||||
### Rebuilding including export functionality for DOC/PDF/ODT
|
||||
### Rebuilding including export functionality for DOC/DOCX/PDF/ODT
|
||||
|
||||
If you want to be able to export your pads to DOC/PDF/ODT files, you can install
|
||||
either Abiword or Libreoffice via setting a build variable.
|
||||
|
||||
#### Via Abiword
|
||||
|
||||
For installing Abiword, set the `INSTALL_ABIWORD` build variable to any value.
|
||||
|
||||
Also, you will need to configure the path to the abiword executable
|
||||
via setting the `abiword` property in `<BASEDIR>/settings.json.docker` to
|
||||
`/usr/bin/abiword` or via setting the environment variable `ABIWORD` to
|
||||
`/usr/bin/abiword`.
|
||||
|
||||
#### Via Libreoffice
|
||||
|
||||
For installing Libreoffice instead, set the `INSTALL_SOFFICE` build variable
|
||||
to any value.
|
||||
If you want to be able to export your pads to DOC/DOCX/PDF/ODT files, you can
|
||||
install Libreoffice via setting the `INSTALL_SOFFICE` build variable to any
|
||||
value.
|
||||
|
||||
Also, you will need to configure the path to the libreoffice executable
|
||||
via setting the `soffice` property in `<BASEDIR>/settings.json.docker` to
|
||||
@ -202,8 +195,7 @@ For the editor container, you can also make it full width by adding `full-width-
|
||||
| `EDIT_ONLY` | Users may edit pads but not create new ones. Pad creation is only via the API. This applies both to group pads and regular pads. | `false` |
|
||||
| `MINIFY` | If true, all css & js will be minified before sending to the client. This will improve the loading performance massively, but makes it difficult to debug the javascript/css | `true` |
|
||||
| `MAX_AGE` | How long may clients use served javascript code (in seconds)? Not setting this may cause problems during deployment. Set to 0 to disable caching. | `21600` (6 hours) |
|
||||
| `ABIWORD` | Absolute path to the Abiword executable. Abiword is needed to get advanced import/export features of pads. Setting it to null disables Abiword and will only allow plain text and HTML import/exports. | `null` |
|
||||
| `SOFFICE` | This is the absolute path to the soffice executable. LibreOffice can be used in lieu of Abiword to export pads. Setting it to null disables LibreOffice exporting. | `null` |
|
||||
| `SOFFICE` | Absolute path to the soffice (LibreOffice) executable. Needed for advanced import/export of pads (docx, pdf, odt). Setting it to null disables LibreOffice and will only allow plain text and HTML import/exports. | `null` |
|
||||
| `ALLOW_UNKNOWN_FILE_ENDS` | Allow import of file types other than the supported ones: txt, doc, docx, rtf, odt, html & htm | `true` |
|
||||
| `REQUIRE_AUTHENTICATION` | This setting is used if you require authentication of all users. Note: "/admin" always requires authentication. | `false` |
|
||||
| `REQUIRE_AUTHORIZATION` | Require authorization by a module, or a user with is_admin set, see below. | `false` |
|
||||
|
||||
137
doc/npm-trusted-publishing.md
Normal file
137
doc/npm-trusted-publishing.md
Normal file
@ -0,0 +1,137 @@
|
||||
# npm Trusted Publishing (OIDC)
|
||||
|
||||
Etherpad and every `ether/ep_*` plugin publish to npm using
|
||||
[npm Trusted Publishing][npm-tp] over OpenID Connect. This eliminates the need
|
||||
to store, rotate, or accidentally leak long-lived `NPM_TOKEN` secrets — each
|
||||
publish is authenticated against the GitHub Actions runner with a short-lived
|
||||
OIDC token instead.
|
||||
|
||||
[npm-tp]: https://docs.npmjs.com/trusted-publishers
|
||||
|
||||
## How it works
|
||||
|
||||
1. The publish workflow declares `permissions: id-token: write`.
|
||||
2. GitHub Actions issues a signed OIDC token to the runner.
|
||||
3. The npm CLI (>= 11.5.1) trades that OIDC token for a short-lived publish
|
||||
credential against npmjs.com.
|
||||
4. npmjs.com checks the OIDC claims (org, repo, workflow file, branch /
|
||||
environment) against the package's configured *trusted publisher* and, if
|
||||
they match, accepts the publish. Provenance attestations are recorded
|
||||
automatically.
|
||||
|
||||
No `NPM_TOKEN` secret is needed in any plugin or in core.
|
||||
|
||||
## One-time setup per package
|
||||
|
||||
Trusted publishing has to be enabled **once per package**. Use the bundled
|
||||
script to do every package in one go via the `npm trust` CLI (npm >= 11.5.1):
|
||||
|
||||
```sh
|
||||
# 1. Make sure npm CLI is recent enough
|
||||
npm install -g npm@latest
|
||||
|
||||
# 2. Log in to npmjs.com as a maintainer
|
||||
npm login
|
||||
|
||||
# 3. Bulk-configure every ether/ep_* plugin + ep_etherpad
|
||||
bin/setup-trusted-publishers.sh
|
||||
|
||||
# Or preview without changing anything
|
||||
bin/setup-trusted-publishers.sh --dry-run
|
||||
|
||||
# Or target a specific subset
|
||||
bin/setup-trusted-publishers.sh --packages ep_align,ep_webrtc
|
||||
|
||||
# Or ignore packages that are already configured (the registry only allows
|
||||
# one trust relationship per package today)
|
||||
bin/setup-trusted-publishers.sh --skip-existing
|
||||
|
||||
# Supply a 2FA OTP up front (required if your npm account has 2FA enabled —
|
||||
# it should). The same OTP is reused for every package call inside the same
|
||||
# minute, so for large batches you may need to chunk via --packages.
|
||||
bin/setup-trusted-publishers.sh --otp 123456
|
||||
```
|
||||
|
||||
> **2FA / OTP note.** `npm trust github` requires an OTP whenever the
|
||||
> account has 2FA enabled. Without `--otp`, npm will prompt interactively
|
||||
> per package, which is unworkable in bulk. Pass `--otp <code>` once and the
|
||||
> script will forward it to every `npm trust github` call. TOTP codes
|
||||
> typically expire every 30 seconds, so for >30s runs split the work with
|
||||
> `--packages ep_a,ep_b,...` and re-run with a fresh code.
|
||||
|
||||
The script discovers all non-archived `ether/ep_*` repos via `gh repo list`
|
||||
and runs `npm trust github <pkg> --repository <org>/<repo> --file <workflow>
|
||||
--yes` for each one. `ep_etherpad` is mapped to the `etherpad-lite` repo and
|
||||
the `releaseEtherpad.yml` workflow; everything else is mapped to its
|
||||
same-named repo and `test-and-release.yml`.
|
||||
|
||||
If you'd rather click through the npmjs.com UI for a single package: open
|
||||
`https://www.npmjs.com/package/<name>/access` → **Trusted Publisher** →
|
||||
**Add trusted publisher** → Publisher: GitHub Actions, Organization: `ether`,
|
||||
Repository: as above, Workflow filename: as above, Environment: blank.
|
||||
|
||||
Once added, the next push to `main`/`master` will publish via OIDC with no
|
||||
token at all.
|
||||
|
||||
## Migrating an existing package
|
||||
|
||||
If a package previously had an `NPM_TOKEN` secret in CI:
|
||||
|
||||
1. Add the trusted publisher on npmjs.com (steps above).
|
||||
2. Bump the workflow to the OIDC version — done in
|
||||
`bin/plugins/lib/npmpublish.yml` (which is propagated to every plugin by
|
||||
the `update-plugins` workflow).
|
||||
3. Remove the now-unused `NPM_TOKEN` secret from the GitHub repo settings.
|
||||
|
||||
## Requirements
|
||||
|
||||
- **Node.js**: >= 20.17.0 on the runner. npm 11 requires
|
||||
`^20.17.0 || >=22.9.0`. The npm docs nominally recommend Node 22.14+, but
|
||||
Node 20.17+ works fine — the project's `engines.node` already requires
|
||||
`>=20.0.0`, and `setup-node@v6 with version: 20` resolves to the latest 20.x.
|
||||
- **npm CLI**: >= 11.5.1. The publish workflow runs `npm install -g npm@latest`
|
||||
before publishing so the bundled npm version doesn't matter.
|
||||
- **Runner**: must be a GitHub-hosted (cloud) runner. Self-hosted runners are
|
||||
not yet supported by npm trusted publishing.
|
||||
- **`package.json`**: must declare a `repository` field pointing at the
|
||||
GitHub repo so npm can verify the OIDC claim. Example:
|
||||
|
||||
```json
|
||||
{
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ether/ep_align.git"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Why call `npm publish` directly?
|
||||
|
||||
The publish workflows run `npm publish --provenance --access public` rather
|
||||
than `pnpm publish` or `gnpm publish`. Both wrappers shell out to whichever
|
||||
`npm` is on `PATH`, but they obscure version requirements: trusted publishing
|
||||
requires npm >= 11.5.1, and going through the wrapper makes it easy to end up
|
||||
with the wrong CLI version. Invoking `npm` directly removes that ambiguity.
|
||||
|
||||
`pnpm` is still used for everything else (install, build, version bump) — only
|
||||
the final publish step calls `npm` directly.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**`npm error 404 Not Found - PUT https://registry.npmjs.org/<pkg>`**
|
||||
|
||||
The trusted publisher hasn't been configured on npmjs.com for that package, or
|
||||
the repository / workflow filename in the trusted publisher config doesn't
|
||||
match the running workflow. Double-check the workflow filename — it must be the
|
||||
*basename* of the workflow YAML, not the job name.
|
||||
|
||||
**`npm error code E_OIDC_NO_TOKEN`**
|
||||
|
||||
The workflow is missing `permissions: id-token: write`. Add it to the job
|
||||
(or to the top-level `permissions:` block).
|
||||
|
||||
**`npm error need: 11.5.1`**
|
||||
|
||||
The runner is using an older bundled npm. The workflow runs
|
||||
`npm install -g npm@latest` to fix this — make sure that step ran before the
|
||||
publish step.
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"devDependencies": {
|
||||
"vitepress": "^2.0.0-alpha.15"
|
||||
"vitepress": "^2.0.0-alpha.17"
|
||||
},
|
||||
"scripts": {
|
||||
"docs:dev": "vitepress dev",
|
||||
|
||||
@ -38,7 +38,7 @@ ep_<plugin>/
|
||||
├ locales/
|
||||
│ ├ en.json ◄─ English (US) strings
|
||||
│ └ qqq.json ◄─ optional hints for translators
|
||||
├ .travis.yml ◄─ Travis CI config
|
||||
├ .github/workflows/ ◄─ CI workflows (backend / frontend tests, npm publish)
|
||||
├ LICENSE
|
||||
├ README.md
|
||||
├ ep.json ◄─ Etherpad plugin definition
|
||||
|
||||
863
docs/superpowers/plans/2026-04-20-issue-7570-ueberdb2-drivers.md
Normal file
863
docs/superpowers/plans/2026-04-20-issue-7570-ueberdb2-drivers.md
Normal file
@ -0,0 +1,863 @@
|
||||
# Fix #7570 (ueberdb2 driver bundling) — Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Restore Etherpad Docker production startup for every supported DB backend (broken by `ueberdb2@5.0.45` moving drivers to optional peer deps), and add CI that would have caught it.
|
||||
|
||||
**Architecture:** Two coupled PRs. Upstream PR to `ether/ueberDB` moves the 10 DB drivers back from `peerDependencies` (optional) to `dependencies` and publishes `5.0.46`. Downstream PR to `ether/etherpad-lite` bumps `ueberdb2`, declares the same 10 drivers as direct deps as a defensive safety net, and adds a `build-test-db-drivers` CI job with a require-each-driver presence check plus MySQL + Postgres smoke tests that gate publish.
|
||||
|
||||
**Tech Stack:** Node.js / pnpm / TypeScript, ueberdb2 KV abstraction, Docker Buildx, GitHub Actions service containers, vitest + testcontainers (upstream tests).
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-04-20-issue-7570-ueberdb2-drivers-design.md`
|
||||
|
||||
**Conventions:**
|
||||
- All pushes land on `johnmclear/` forks — never `ether/*` directly.
|
||||
- The branch `fix/issue-7570-ueberdb2-drivers` already exists in `/home/jose/etherpad/etherpad-lite` with the design spec committed.
|
||||
- Working dirs:
|
||||
- Downstream: `/home/jose/etherpad/etherpad-lite`
|
||||
- Upstream ueberDB will be cloned to: `/home/jose/etherpad/ueberDB`
|
||||
|
||||
---
|
||||
|
||||
## Phase A — Upstream (`ether/ueberDB`)
|
||||
|
||||
### Task A1: Create johnmclear/ueberDB fork and clone it
|
||||
|
||||
**Files:** none (git/gh only)
|
||||
|
||||
- [ ] **Step 1: Create the fork on GitHub**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
gh repo fork ether/ueberDB --clone=false --default-branch-only
|
||||
```
|
||||
Expected: `✓ Created fork johnmclear/ueberDB`.
|
||||
|
||||
If it already exists the command prints `johnmclear/ueberDB already exists` — that is fine, continue.
|
||||
|
||||
- [ ] **Step 2: Clone the fork locally**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
git clone https://github.com/johnmclear/ueberDB.git /home/jose/etherpad/ueberDB
|
||||
cd /home/jose/etherpad/ueberDB
|
||||
git remote add upstream https://github.com/ether/ueberDB.git
|
||||
git fetch upstream
|
||||
```
|
||||
Expected: clone succeeds, `git remote -v` shows both `origin` (johnmclear) and `upstream` (ether).
|
||||
|
||||
- [ ] **Step 3: Identify default branch and sync**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
git -C /home/jose/etherpad/ueberDB remote show upstream | grep 'HEAD branch'
|
||||
```
|
||||
Expected: prints either `HEAD branch: develop` or `HEAD branch: main`. Record the name — subsequent steps refer to it as `<default>`.
|
||||
|
||||
Run:
|
||||
```bash
|
||||
git -C /home/jose/etherpad/ueberDB checkout <default>
|
||||
git -C /home/jose/etherpad/ueberDB pull upstream <default>
|
||||
git -C /home/jose/etherpad/ueberDB push origin <default>
|
||||
```
|
||||
Expected: fork's default branch now matches upstream.
|
||||
|
||||
- [ ] **Step 4: Create feature branch**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
git -C /home/jose/etherpad/ueberDB checkout -b fix/bundle-driver-deps
|
||||
```
|
||||
Expected: switched to a new branch.
|
||||
|
||||
---
|
||||
|
||||
### Task A2: Confirm the baseline (existing tests green)
|
||||
|
||||
**Files:** none — read-only validation
|
||||
|
||||
- [ ] **Step 1: Install deps**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/jose/etherpad/ueberDB && pnpm install
|
||||
```
|
||||
Expected: install succeeds, no unusual errors.
|
||||
|
||||
- [ ] **Step 2: Run type check and lint**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
pnpm run ts-check && pnpm run lint
|
||||
```
|
||||
Expected: both pass with exit 0.
|
||||
|
||||
- [ ] **Step 3: Skip the full driver test suite**
|
||||
|
||||
Do **not** run `pnpm test` here — it uses testcontainers and spins up every database, which is slow and requires Docker. CI will run it. If Docker is available and the engineer wants to sanity-run it:
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
Expected: all driver suites pass.
|
||||
|
||||
---
|
||||
|
||||
### Task A3: Move drivers from optional peer deps to dependencies
|
||||
|
||||
**Files:**
|
||||
- Modify: `/home/jose/etherpad/ueberDB/package.json`
|
||||
|
||||
- [ ] **Step 1: Read current package.json**
|
||||
|
||||
Open `/home/jose/etherpad/ueberDB/package.json`. Locate the three relevant blocks: `dependencies`, `peerDependencies`, `peerDependenciesMeta`. Current state (as of 5.0.45):
|
||||
|
||||
```json
|
||||
"dependencies": {
|
||||
"async": "^3.2.6",
|
||||
"dirty-ts": "^1.1.8",
|
||||
"rusty-store-kv": "^1.3.1",
|
||||
"simple-git": "^3.36.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@elastic/elasticsearch": "^9.3.4",
|
||||
"cassandra-driver": "^4.8.0",
|
||||
"mongodb": "^7.1.1",
|
||||
"mssql": "^12.2.1",
|
||||
"mysql2": "^3.22.0",
|
||||
"nano": "^11.0.5",
|
||||
"pg": "^8.20.0",
|
||||
"redis": "^5.12.1",
|
||||
"rethinkdb": "^2.4.2",
|
||||
"surrealdb": "^2.0.3"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@elastic/elasticsearch": {"optional": true},
|
||||
"cassandra-driver": {"optional": true},
|
||||
"mongodb": {"optional": true},
|
||||
"mssql": {"optional": true},
|
||||
"mysql2": {"optional": true},
|
||||
"nano": {"optional": true},
|
||||
"pg": {"optional": true},
|
||||
"redis": {"optional": true},
|
||||
"rethinkdb": {"optional": true},
|
||||
"surrealdb": {"optional": true}
|
||||
},
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Rewrite the three blocks**
|
||||
|
||||
Replace the three blocks with:
|
||||
|
||||
```json
|
||||
"dependencies": {
|
||||
"@elastic/elasticsearch": "^9.3.4",
|
||||
"async": "^3.2.6",
|
||||
"cassandra-driver": "^4.8.0",
|
||||
"dirty-ts": "^1.1.8",
|
||||
"mongodb": "^7.1.1",
|
||||
"mssql": "^12.2.1",
|
||||
"mysql2": "^3.22.0",
|
||||
"nano": "^11.0.5",
|
||||
"pg": "^8.20.0",
|
||||
"redis": "^5.12.1",
|
||||
"rethinkdb": "^2.4.2",
|
||||
"rusty-store-kv": "^1.3.1",
|
||||
"simple-git": "^3.36.0",
|
||||
"surrealdb": "^2.0.3"
|
||||
},
|
||||
```
|
||||
|
||||
Delete the `peerDependencies` and `peerDependenciesMeta` blocks entirely.
|
||||
|
||||
Keys must be alphabetically sorted in the merged `dependencies` block (matches existing convention).
|
||||
|
||||
- [ ] **Step 3: Bump version**
|
||||
|
||||
In the same file, change `"version": "5.0.45"` to `"version": "5.0.46"`.
|
||||
|
||||
- [ ] **Step 4: Regenerate lockfile**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/jose/etherpad/ueberDB && pnpm install
|
||||
```
|
||||
Expected: `pnpm-lock.yaml` updates. No errors. No warnings about missing peer deps (since there are now none).
|
||||
|
||||
- [ ] **Step 5: Re-run ts-check and lint**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
pnpm run ts-check && pnpm run lint
|
||||
```
|
||||
Expected: exit 0.
|
||||
|
||||
- [ ] **Step 6: Verify drivers now resolve in a fresh node_modules**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /tmp && rm -rf uberdb-smoke && mkdir uberdb-smoke && cd uberdb-smoke
|
||||
npm init -y >/dev/null
|
||||
npm install file:/home/jose/etherpad/ueberDB 2>&1 | tail -3
|
||||
node -e "
|
||||
const mods = [
|
||||
'@elastic/elasticsearch','cassandra-driver','mongodb','mssql',
|
||||
'mysql2','nano','pg','redis','rethinkdb','surrealdb'
|
||||
];
|
||||
for (const m of mods) {
|
||||
try { require(m); console.log('ok', m); }
|
||||
catch (e) { console.error('MISSING', m, e.message); process.exit(1); }
|
||||
}
|
||||
"
|
||||
```
|
||||
Expected: prints `ok` ten times, exits 0.
|
||||
|
||||
This is the direct repro test for the issue.
|
||||
|
||||
---
|
||||
|
||||
### Task A4: Commit and push upstream fix
|
||||
|
||||
**Files:**
|
||||
- Stage: `/home/jose/etherpad/ueberDB/package.json`, `/home/jose/etherpad/ueberDB/pnpm-lock.yaml`
|
||||
|
||||
- [ ] **Step 1: Review staged diff**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/jose/etherpad/ueberDB
|
||||
git diff package.json
|
||||
git status
|
||||
```
|
||||
Expected: only `package.json` and `pnpm-lock.yaml` modified. Confirm the diff matches Task A3.
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
git add package.json pnpm-lock.yaml
|
||||
git commit -m "$(cat <<'EOF'
|
||||
fix: bundle DB drivers as dependencies (restore pre-5.0.45 behavior)
|
||||
|
||||
Moves the ten DB drivers back from optional peerDependencies to
|
||||
dependencies so consumers (notably Etherpad Docker production images)
|
||||
get them installed automatically.
|
||||
|
||||
Fixes Etherpad issue ether/etherpad-lite#7570:
|
||||
"Cannot find module 'mysql2'" at startup when pnpm production install
|
||||
skips optional peer deps.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
Expected: one commit created on `fix/bundle-driver-deps`.
|
||||
|
||||
- [ ] **Step 3: Push to fork**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
git push -u origin fix/bundle-driver-deps
|
||||
```
|
||||
Expected: branch pushed to `johnmclear/ueberDB`.
|
||||
|
||||
---
|
||||
|
||||
### Task A5: Open upstream PR
|
||||
|
||||
**Files:** none
|
||||
|
||||
- [ ] **Step 1: Create PR**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
gh pr create \
|
||||
--repo ether/ueberDB \
|
||||
--base <default> \
|
||||
--head johnmclear:fix/bundle-driver-deps \
|
||||
--title "fix: bundle DB drivers as dependencies (fix Etherpad #7570)" \
|
||||
--body "$(cat <<'EOF'
|
||||
## Summary
|
||||
- Moves all ten DB drivers (`@elastic/elasticsearch`, `cassandra-driver`, `mongodb`, `mssql`, `mysql2`, `nano`, `pg`, `redis`, `rethinkdb`, `surrealdb`) from `peerDependencies` + `peerDependenciesMeta.optional` back to `dependencies`.
|
||||
- Deletes `peerDependenciesMeta`.
|
||||
- Bumps version to 5.0.46.
|
||||
|
||||
## Why
|
||||
`ueberdb2@5.0.45` declared the drivers as optional peer deps. Production installs (e.g., `pnpm install --prod`) skip optional peer deps, so consumers hit `Error: Cannot find module 'mysql2'` at first `require` of a driver.
|
||||
|
||||
Reported against Etherpad Docker: https://github.com/ether/etherpad-lite/issues/7570
|
||||
|
||||
## Test plan
|
||||
- [ ] CI green (vitest + testcontainers exercises every driver)
|
||||
- [ ] Local smoke: `npm install ueberdb2@next` into a fresh project, then `require()` each of the ten driver modules — all resolve
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
Replace `<default>` with whatever Task A1 step 3 recorded (`develop` or `main`).
|
||||
|
||||
Expected: PR URL printed.
|
||||
|
||||
- [ ] **Step 2: Record PR URL**
|
||||
|
||||
Note the PR URL — downstream task (closing #7571) references it.
|
||||
|
||||
---
|
||||
|
||||
### Task A6: Await merge + publish to npm
|
||||
|
||||
**Files:** none — gating task
|
||||
|
||||
- [ ] **Step 1: Merge upstream PR**
|
||||
|
||||
Once review passes on the PR opened in Task A5, merge it. Then:
|
||||
|
||||
```bash
|
||||
cd /home/jose/etherpad/ueberDB
|
||||
git checkout <default>
|
||||
git pull upstream <default>
|
||||
```
|
||||
Expected: local default branch is at the merge commit.
|
||||
|
||||
- [ ] **Step 2: Publish to npm**
|
||||
|
||||
Run the project's existing release workflow. Typically:
|
||||
```bash
|
||||
cd /home/jose/etherpad/ueberDB
|
||||
npm publish
|
||||
```
|
||||
(Or trigger the CI publish workflow if one exists — check `.github/workflows/` first.)
|
||||
|
||||
Expected: `ueberdb2@5.0.46` is live on npm. Verify:
|
||||
```bash
|
||||
npm view ueberdb2 version
|
||||
```
|
||||
Expected output: `5.0.46`.
|
||||
|
||||
**Do not start Phase B until this step completes** — downstream `pnpm install` must be able to resolve `ueberdb2@^5.0.46` from the public registry.
|
||||
|
||||
---
|
||||
|
||||
## Phase B — Downstream (`ether/etherpad-lite`)
|
||||
|
||||
### Task B1: Close Copilot's PR #7571
|
||||
|
||||
**Files:** none — GitHub issue action
|
||||
|
||||
- [ ] **Step 1: Comment and close**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
gh pr close 7571 \
|
||||
--repo ether/etherpad-lite \
|
||||
--comment "Superseded by the upstream ueberdb2 fix (<Task A5 PR URL>) plus a clean downstream replacement PR incoming on branch \`fix/issue-7570-ueberdb2-drivers\`. See design spec: docs/superpowers/specs/2026-04-20-issue-7570-ueberdb2-drivers-design.md."
|
||||
```
|
||||
Expected: PR #7571 closed with comment.
|
||||
|
||||
---
|
||||
|
||||
### Task B2: Reproduce the bug locally (baseline)
|
||||
|
||||
**Files:** none — validation
|
||||
|
||||
- [ ] **Step 1: Confirm we are on the feature branch**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/jose/etherpad/etherpad-lite
|
||||
git status
|
||||
```
|
||||
Expected: `On branch fix/issue-7570-ueberdb2-drivers`.
|
||||
|
||||
- [ ] **Step 2: Build current Docker production image**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/jose/etherpad/etherpad-lite
|
||||
docker build --target production -t etherpad:pre-fix .
|
||||
```
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 3: Demonstrate the missing module**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
docker run --rm etherpad:pre-fix node -e "try { require('mysql2'); console.log('HAS mysql2'); } catch(e) { console.log('MISSING mysql2:', e.message); }"
|
||||
```
|
||||
Expected output: `MISSING mysql2: Cannot find module 'mysql2'`.
|
||||
|
||||
This confirms the bug reproduces in the current prod image. Keep this evidence — the same command run in Task B5 Step 3 against the fixed image should print `HAS mysql2`.
|
||||
|
||||
---
|
||||
|
||||
### Task B3: Bump ueberdb2 and add drivers to `src/package.json`
|
||||
|
||||
**Files:**
|
||||
- Modify: `/home/jose/etherpad/etherpad-lite/src/package.json`
|
||||
|
||||
- [ ] **Step 1: Read current dependencies block**
|
||||
|
||||
Open `/home/jose/etherpad/etherpad-lite/src/package.json`. Locate the `dependencies` object.
|
||||
|
||||
- [ ] **Step 2: Add ten driver entries, keep alphabetical order, bump ueberdb2**
|
||||
|
||||
Within `dependencies`, add these keys (inserted in alphabetical position; do not touch any other key):
|
||||
|
||||
```json
|
||||
"@elastic/elasticsearch": "^9.3.4",
|
||||
"cassandra-driver": "^4.8.0",
|
||||
"mongodb": "^7.1.1",
|
||||
"mssql": "^12.2.1",
|
||||
"mysql2": "^3.22.0",
|
||||
"nano": "^11.0.5",
|
||||
"pg": "^8.20.0",
|
||||
"redis": "^5.12.1",
|
||||
"rethinkdb": "^2.4.2",
|
||||
"surrealdb": "^2.0.3",
|
||||
```
|
||||
|
||||
And change the existing `ueberdb2` entry from its current value to:
|
||||
```json
|
||||
"ueberdb2": "^5.0.46",
|
||||
```
|
||||
|
||||
**Do not touch** any other dependency, devDependency, script, or metadata field. Specifically the following must remain unchanged vs `origin/develop`:
|
||||
- `typescript`, `oidc-provider`, `eslint-config-etherpad`, `@types/express-session`, `resolve` versions
|
||||
- `repository.url`
|
||||
- `test-admin` npm script and its project list
|
||||
- Every other field.
|
||||
|
||||
- [ ] **Step 3: Verify diff is minimal**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/jose/etherpad/etherpad-lite
|
||||
git diff src/package.json
|
||||
```
|
||||
Expected: exactly 11 lines added (10 new drivers + no other lines) and exactly 1 line modified (the `ueberdb2` version bump). If anything else shows up, revert it.
|
||||
|
||||
---
|
||||
|
||||
### Task B4: Regenerate `pnpm-lock.yaml`
|
||||
|
||||
**Files:**
|
||||
- Modify: `/home/jose/etherpad/etherpad-lite/pnpm-lock.yaml`
|
||||
|
||||
- [ ] **Step 1: Install**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/jose/etherpad/etherpad-lite/src
|
||||
pnpm install
|
||||
```
|
||||
Expected: install succeeds, resolves `ueberdb2@5.0.46` plus the ten drivers. No missing-peer-dep warnings.
|
||||
|
||||
- [ ] **Step 2: Sanity-check lockfile scope**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/jose/etherpad/etherpad-lite
|
||||
git diff --stat pnpm-lock.yaml
|
||||
```
|
||||
Expected: only `pnpm-lock.yaml` modified. No unrelated `package.json` files touched.
|
||||
|
||||
---
|
||||
|
||||
### Task B5: Verify fix locally
|
||||
|
||||
**Files:** none — validation
|
||||
|
||||
- [ ] **Step 1: Rebuild Docker production image with the fix**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/jose/etherpad/etherpad-lite
|
||||
docker build --target production -t etherpad:post-fix .
|
||||
```
|
||||
Expected: build succeeds.
|
||||
|
||||
- [ ] **Step 2: Presence test — all ten drivers resolve**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
docker run --rm etherpad:post-fix node -e "
|
||||
const mods = [
|
||||
'@elastic/elasticsearch','cassandra-driver','mongodb','mssql',
|
||||
'mysql2','nano','pg','redis','rethinkdb','surrealdb'
|
||||
];
|
||||
for (const m of mods) {
|
||||
try { require(m); console.log('ok', m); }
|
||||
catch (e) { console.error('MISSING', m, e.message); process.exit(1); }
|
||||
}
|
||||
"
|
||||
```
|
||||
Expected output: ten lines starting with `ok `, exit code 0.
|
||||
|
||||
- [ ] **Step 3: MySQL end-to-end repro (exact issue reporter scenario)**
|
||||
|
||||
Create `/tmp/et7570-compose.yml`:
|
||||
```yaml
|
||||
services:
|
||||
app:
|
||||
image: etherpad:post-fix
|
||||
depends_on:
|
||||
- mariadb
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
ADMIN_PASSWORD: admin
|
||||
DB_CHARSET: utf8mb4
|
||||
DB_HOST: mariadb
|
||||
DB_NAME: etherpad
|
||||
DB_PASS: password
|
||||
DB_PORT: 3306
|
||||
DB_TYPE: mysql
|
||||
DB_USER: user
|
||||
DEFAULT_PAD_TEXT: "Test "
|
||||
TRUST_PROXY: "true"
|
||||
ports:
|
||||
- "9001:9001"
|
||||
mariadb:
|
||||
image: mariadb:11.4
|
||||
environment:
|
||||
MYSQL_DATABASE: etherpad
|
||||
MYSQL_USER: user
|
||||
MYSQL_PASSWORD: password
|
||||
MYSQL_ROOT_PASSWORD: root
|
||||
MARIADB_AUTO_UPGRADE: 1
|
||||
```
|
||||
|
||||
Run:
|
||||
```bash
|
||||
docker compose -f /tmp/et7570-compose.yml up -d
|
||||
sleep 30
|
||||
curl -sf http://localhost:9001/ >/dev/null && echo "OK: Etherpad serves / over MySQL"
|
||||
docker compose -f /tmp/et7570-compose.yml logs app | grep -c "Cannot find module 'mysql2'" || true
|
||||
docker compose -f /tmp/et7570-compose.yml down -v
|
||||
```
|
||||
Expected: `OK: Etherpad serves / over MySQL`, and the grep count is `0`.
|
||||
|
||||
If `curl` fails, inspect `docker compose logs app` — startup may just be slow on the first run; wait 30 more seconds and retry `curl`. If the missing-module error still appears, Task B3/B4 were not applied cleanly.
|
||||
|
||||
---
|
||||
|
||||
### Task B6: Add the `build-test-db-drivers` CI job
|
||||
|
||||
**Files:**
|
||||
- Modify: `/home/jose/etherpad/etherpad-lite/.github/workflows/docker.yml`
|
||||
|
||||
- [ ] **Step 1: Read the current workflow end-to-end**
|
||||
|
||||
Open `/home/jose/etherpad/etherpad-lite/.github/workflows/docker.yml` and identify:
|
||||
- The `jobs:` key and the existing job(s) under it (likely `docker` and/or `publish` after PR #7569).
|
||||
- The value of `env.TEST_TAG` (expected: `etherpad/etherpad:test`).
|
||||
- Any `needs:` on a `publish` job.
|
||||
|
||||
Do not restructure existing jobs. Only append the new job and extend `needs:`.
|
||||
|
||||
- [ ] **Step 2: Append the new job**
|
||||
|
||||
Append the following job under `jobs:` (same indentation as existing jobs), placed after the existing `build-test` / `docker` job:
|
||||
|
||||
```yaml
|
||||
build-test-db-drivers:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
services:
|
||||
mysql:
|
||||
image: mysql:8
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: password
|
||||
MYSQL_DATABASE: etherpad
|
||||
MYSQL_USER: etherpad
|
||||
MYSQL_PASSWORD: password
|
||||
ports:
|
||||
- 3306:3306
|
||||
options: >-
|
||||
--health-cmd="mysqladmin ping -h 127.0.0.1 -uroot -ppassword"
|
||||
--health-interval=5s
|
||||
--health-timeout=5s
|
||||
--health-retries=20
|
||||
postgres:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_DB: etherpad
|
||||
POSTGRES_USER: etherpad
|
||||
POSTGRES_PASSWORD: password
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd="pg_isready -U etherpad -d etherpad"
|
||||
--health-interval=5s
|
||||
--health-timeout=5s
|
||||
--health-retries=20
|
||||
steps:
|
||||
- name: Check out
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
path: etherpad
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
- name: Build production image
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: ./etherpad
|
||||
target: production
|
||||
load: true
|
||||
tags: ${{ env.TEST_TAG }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
- name: Driver presence test (all 10 drivers must resolve)
|
||||
run: |
|
||||
docker run --rm "$TEST_TAG" node -e "
|
||||
const mods = [
|
||||
'@elastic/elasticsearch','cassandra-driver','mongodb','mssql',
|
||||
'mysql2','nano','pg','redis','rethinkdb','surrealdb'
|
||||
];
|
||||
let fail = false;
|
||||
for (const m of mods) {
|
||||
try { require(m); console.log('ok', m); }
|
||||
catch (e) { console.error('MISSING', m, e.message); fail = true; }
|
||||
}
|
||||
if (fail) process.exit(1);
|
||||
"
|
||||
- name: MySQL smoke — start Etherpad against mysql service
|
||||
run: |
|
||||
docker run --rm -d \
|
||||
--network host \
|
||||
-e NODE_ENV=production \
|
||||
-e ADMIN_PASSWORD=admin \
|
||||
-e DB_TYPE=mysql \
|
||||
-e DB_HOST=127.0.0.1 \
|
||||
-e DB_PORT=3306 \
|
||||
-e DB_NAME=etherpad \
|
||||
-e DB_USER=etherpad \
|
||||
-e DB_PASS=password \
|
||||
-e DB_CHARSET=utf8mb4 \
|
||||
-e DEFAULT_PAD_TEXT="Test " \
|
||||
--name et-mysql "$TEST_TAG"
|
||||
for i in $(seq 1 60); do
|
||||
if curl -sf http://127.0.0.1:9001/ >/dev/null; then
|
||||
echo "mysql smoke: Etherpad is serving /"
|
||||
docker rm -f et-mysql
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "mysql smoke: timed out waiting for Etherpad"
|
||||
docker logs et-mysql || true
|
||||
docker rm -f et-mysql || true
|
||||
exit 1
|
||||
- name: Postgres smoke — start Etherpad against postgres service
|
||||
run: |
|
||||
docker run --rm -d \
|
||||
--network host \
|
||||
-e NODE_ENV=production \
|
||||
-e ADMIN_PASSWORD=admin \
|
||||
-e DB_TYPE=postgres \
|
||||
-e DB_HOST=127.0.0.1 \
|
||||
-e DB_PORT=5432 \
|
||||
-e DB_NAME=etherpad \
|
||||
-e DB_USER=etherpad \
|
||||
-e DB_PASS=password \
|
||||
-e DEFAULT_PAD_TEXT="Test " \
|
||||
--name et-postgres "$TEST_TAG"
|
||||
for i in $(seq 1 60); do
|
||||
if curl -sf http://127.0.0.1:9001/ >/dev/null; then
|
||||
echo "postgres smoke: Etherpad is serving /"
|
||||
docker rm -f et-postgres
|
||||
exit 0
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
echo "postgres smoke: timed out waiting for Etherpad"
|
||||
docker logs et-postgres || true
|
||||
docker rm -f et-postgres || true
|
||||
exit 1
|
||||
```
|
||||
|
||||
Notes:
|
||||
- `$TEST_TAG` comes from workflow-level `env.TEST_TAG` — no need to redeclare.
|
||||
- Port 9001 is Etherpad's default bind port; `--network host` lets us hit the service containers directly.
|
||||
- Stage order (presence → mysql → postgres) means the fastest, clearest failure mode runs first.
|
||||
|
||||
- [ ] **Step 3: Extend `publish` job's `needs:` to gate on this new job**
|
||||
|
||||
Locate the `publish` job (added in PR #7569). Its `needs:` currently looks like either:
|
||||
```yaml
|
||||
needs: build-test
|
||||
```
|
||||
or
|
||||
```yaml
|
||||
needs: [build-test]
|
||||
```
|
||||
|
||||
Change to:
|
||||
```yaml
|
||||
needs: [build-test, build-test-db-drivers]
|
||||
```
|
||||
|
||||
If no `publish` job exists yet (shouldn't happen post-#7569), the existing job that pushes images (probably containing `docker/login-action`) is the one to gate. Apply the same change to its `needs:`.
|
||||
|
||||
- [ ] **Step 4: Lint the YAML**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/jose/etherpad/etherpad-lite
|
||||
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/docker.yml'))"
|
||||
```
|
||||
Expected: exit 0, no output.
|
||||
|
||||
- [ ] **Step 5: Confirm no unrelated changes to `docker.yml`**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
git diff .github/workflows/docker.yml | head -50
|
||||
git diff --stat .github/workflows/docker.yml
|
||||
```
|
||||
Expected: only additions (the new job + the `needs:` change). No other lines modified.
|
||||
|
||||
---
|
||||
|
||||
### Task B7: Commit and push downstream fix
|
||||
|
||||
**Files:**
|
||||
- Stage: `src/package.json`, `pnpm-lock.yaml`, `.github/workflows/docker.yml`
|
||||
|
||||
- [ ] **Step 1: Verify final diff scope**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/jose/etherpad/etherpad-lite
|
||||
git status
|
||||
git diff --stat
|
||||
```
|
||||
Expected (modified files only):
|
||||
```
|
||||
.github/workflows/docker.yml | 90+ additions
|
||||
pnpm-lock.yaml | large change
|
||||
src/package.json | 11 additions, 1 change
|
||||
```
|
||||
The spec commit from earlier (branch `fix/issue-7570-ueberdb2-drivers`) is already present from previous work — `git log --oneline` should show it as the tip.
|
||||
|
||||
- [ ] **Step 2: Commit**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/jose/etherpad/etherpad-lite
|
||||
git add src/package.json pnpm-lock.yaml .github/workflows/docker.yml
|
||||
git commit -m "$(cat <<'EOF'
|
||||
fix(#7570): bundle DB drivers, add regression CI
|
||||
|
||||
- Bump ueberdb2 to ^5.0.46 (upstream now re-bundles drivers).
|
||||
- Declare all 10 ueberdb2 DB drivers as direct src dependencies as a
|
||||
defensive safety net against a future upstream drift.
|
||||
- Add build-test-db-drivers CI job that blocks the publish job:
|
||||
* all-10-drivers presence check in the built prod image
|
||||
* end-to-end MySQL smoke (reproduces #7570)
|
||||
* end-to-end Postgres smoke
|
||||
Any stage failure blocks Docker Hub / GHCR publish.
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
Expected: one commit on `fix/issue-7570-ueberdb2-drivers`.
|
||||
|
||||
- [ ] **Step 3: Add johnmclear fork remote if needed**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
cd /home/jose/etherpad/etherpad-lite
|
||||
git remote -v | grep -q '^fork' || git remote add fork https://github.com/johnmclear/etherpad-lite.git
|
||||
```
|
||||
Expected: `fork` remote exists.
|
||||
|
||||
- [ ] **Step 4: Push branch to fork**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
git push -u fork fix/issue-7570-ueberdb2-drivers
|
||||
```
|
||||
Expected: branch pushed to `johnmclear/etherpad-lite`.
|
||||
|
||||
---
|
||||
|
||||
### Task B8: Open downstream PR
|
||||
|
||||
**Files:** none
|
||||
|
||||
- [ ] **Step 1: Create PR**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
gh pr create \
|
||||
--repo ether/etherpad-lite \
|
||||
--base develop \
|
||||
--head johnmclear:fix/issue-7570-ueberdb2-drivers \
|
||||
--title "fix(#7570): bundle DB drivers, add regression CI" \
|
||||
--body "$(cat <<'EOF'
|
||||
## Summary
|
||||
- Bumps `ueberdb2` to `^5.0.46` — upstream PR `<Task A5 PR URL>` restored drivers as real dependencies.
|
||||
- Declares all 10 ueberdb2 DB drivers as direct `src/package.json` dependencies as a defensive safety net against any future upstream drift.
|
||||
- Adds a new `build-test-db-drivers` CI job that blocks `publish`:
|
||||
- presence test for all 10 drivers in the built production image
|
||||
- MySQL service-container smoke (reproduces #7570)
|
||||
- Postgres service-container smoke
|
||||
- Supersedes #7571 (which mixed scope).
|
||||
|
||||
## Test plan
|
||||
- [ ] `build-test` passes on this PR (existing coverage)
|
||||
- [ ] `build-test-db-drivers` passes — specifically the MySQL stage is the live reproduction of #7570
|
||||
- [ ] Local: `docker compose up` with reporter's MySQL config reaches healthy and serves `/`
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
Expected: PR URL printed. Record it.
|
||||
|
||||
---
|
||||
|
||||
### Task B9: Verify downstream CI is green
|
||||
|
||||
**Files:** none — validation + iteration
|
||||
|
||||
- [ ] **Step 1: Watch CI**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
gh pr checks <PR number from B8> --repo ether/etherpad-lite --watch
|
||||
```
|
||||
Expected: all checks green. The two critical ones:
|
||||
- `build-test` (pre-existing)
|
||||
- `build-test-db-drivers` (new, with its four stages)
|
||||
|
||||
- [ ] **Step 2: If a stage fails, read the logs and fix**
|
||||
|
||||
Run:
|
||||
```bash
|
||||
gh run view --log-failed --repo ether/etherpad-lite
|
||||
```
|
||||
Common failure modes and fixes:
|
||||
- **Presence test fails for a specific module** → `src/package.json` missing that driver. Re-check Task B3.
|
||||
- **MySQL smoke times out** → increase retry loop from 60 to 90 iterations, or increase `--health-retries` on the service, and push again. Do not skip the stage.
|
||||
- **Postgres smoke times out** → same pattern.
|
||||
- **YAML parse error** → re-run Task B6 Step 4 locally, fix, force-push.
|
||||
|
||||
Iterate until green. Per project rule, update PR title/description after every push and post `/review` as a comment to trigger Qodo re-review.
|
||||
|
||||
- [ ] **Step 3: Hand off for human review**
|
||||
|
||||
Once CI is green, notify the user the PR is ready for review. No further automated action.
|
||||
|
||||
---
|
||||
|
||||
## Self-review
|
||||
|
||||
**Spec coverage:**
|
||||
- Upstream ueberdb2 driver move → Tasks A1–A6
|
||||
- Downstream `ueberdb2` bump + driver safety-net list → B3, B4
|
||||
- Close #7571 → B1
|
||||
- CI job (presence + MySQL + Postgres, gating publish) → B6
|
||||
- No unrelated bumps → explicit guard in B3 Step 2 + B7 Step 1 diff check
|
||||
- Local verification matches spec's "Local verification before pushing" → B5 Steps 1–3
|
||||
- Rollout order matches spec → Phase A gates Phase B via A6
|
||||
- Commit targets use johnmclear fork → A1 for upstream, B7 Step 3 for downstream
|
||||
|
||||
**Placeholders:** One deliberate placeholder remains (`<default>` for ueberDB's default branch name in A1/A4/A5, and `<Task A5 PR URL>` referenced from B1/B8). These are values the engineer fills in once observed from `gh`/`git`; they are not "TBD implementation details."
|
||||
|
||||
**Consistency:** `build-test-db-drivers` job name used identically in B6, B7 commit message, and B8 PR body. Driver list of ten appears identically in A3, B3, B5, and B6. Version `5.0.46` used consistently across A3, A5, A6, B3.
|
||||
@ -0,0 +1,222 @@
|
||||
# Design: Fix #7570 — `Cannot find module 'mysql2'` in Docker production image
|
||||
|
||||
Upstream: <https://github.com/ether/etherpad-lite/issues/7570>
|
||||
|
||||
## Problem
|
||||
|
||||
`ueberdb2@5.0.45` moved its ten database driver dependencies from normal
|
||||
`dependencies` to `peerDependencies` with `peerDependenciesMeta.optional = true`.
|
||||
Production `pnpm install` in the Etherpad Docker image does not install
|
||||
optional peer dependencies, so the drivers are absent. At startup,
|
||||
`ueberdb2`'s driver loader does `require('mysql2')` (or the configured
|
||||
driver) and crashes:
|
||||
|
||||
```
|
||||
Error: Cannot find module 'mysql2'
|
||||
Require stack:
|
||||
- /opt/etherpad-lite/node_modules/.pnpm/ueberdb2@5.0.45/node_modules/ueberdb2/dist/mysql_db-*.js
|
||||
- /opt/etherpad-lite/node_modules/.pnpm/ueberdb2@5.0.45/node_modules/ueberdb2/dist/index.js
|
||||
- /opt/etherpad-lite/src/node/db/DB.ts
|
||||
```
|
||||
|
||||
The bug affects every non-default DB backend (mysql, postgres, mongodb,
|
||||
mssql, redis, couchdb, cassandra, elasticsearch, rethinkdb, surrealdb).
|
||||
The reporter hit it with MySQL because MySQL is the most common prod
|
||||
backend; the class of failure is identical for the other nine.
|
||||
|
||||
The earlier Copilot PR #7571 attempted a fix but (a) initially only listed
|
||||
`mysql2`, (b) now lists all ten but only regression-tests MySQL in CI,
|
||||
and (c) accumulated unrelated scope (bumps to `typescript`,
|
||||
`oidc-provider`, `eslint-config-etherpad`, `@types/express-session`,
|
||||
`resolve`; firefox removed from `test-admin`; a repo URL change; a
|
||||
docker.yml restructure that collides with the already-merged GHCR PR
|
||||
#7569). This spec replaces it.
|
||||
|
||||
## Goals
|
||||
|
||||
1. Restore Docker prod startup for every supported DB backend.
|
||||
2. Prevent the same class of regression from silently returning.
|
||||
3. Keep the downstream diff minimal and reviewable.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Refactoring ueberdb2 to a plugin-per-driver architecture.
|
||||
- Changing which backends Etherpad officially supports.
|
||||
- Addressing other unrelated dependency bumps.
|
||||
|
||||
## Architecture
|
||||
|
||||
Two independent changes that stack:
|
||||
|
||||
**Upstream fix (`ether/ueberDB`)** — the real "reintroduce bundling."
|
||||
Move the ten DB drivers from `peerDependencies` + `peerDependenciesMeta`
|
||||
back to `dependencies`. Publish as a new patch version. Restores
|
||||
pre-5.0.45 behavior where `npm install ueberdb2` pulls the drivers.
|
||||
|
||||
**Downstream fix (`ether/etherpad-lite`)** — bump `ueberdb2` to the new
|
||||
version, additionally declare the ten drivers as direct `dependencies`
|
||||
in `src/package.json` as a defensive safety net, and add a CI regression
|
||||
test that would have caught #7570.
|
||||
|
||||
The downstream driver listing is redundant on a good day and
|
||||
load-bearing on a bad day: if a future ueberdb2 release drifts the
|
||||
peer-vs-dep classification again, Etherpad stays buildable and the CI
|
||||
job reports the drift loudly rather than only at Docker startup in
|
||||
production.
|
||||
|
||||
## Upstream change (`ether/ueberDB`)
|
||||
|
||||
**File:** `package.json`
|
||||
|
||||
1. Move these ten entries from `peerDependencies` → `dependencies`,
|
||||
preserving version ranges exactly as currently declared in 5.0.45:
|
||||
- `@elastic/elasticsearch` `^9.3.4`
|
||||
- `cassandra-driver` `^4.8.0`
|
||||
- `mongodb` `^7.1.1`
|
||||
- `mssql` `^12.2.1`
|
||||
- `mysql2` `^3.22.0`
|
||||
- `nano` `^11.0.5`
|
||||
- `pg` `^8.20.0`
|
||||
- `redis` `^5.12.1`
|
||||
- `rethinkdb` `^2.4.2`
|
||||
- `surrealdb` `^2.0.3`
|
||||
2. Delete the `peerDependenciesMeta` block entirely.
|
||||
3. Version bump: `5.0.45` → `5.0.46`.
|
||||
|
||||
**Tradeoff accepted:** every ueberdb2 consumer now installs ~250 MB of
|
||||
driver code regardless of which backend they use. This is the pre-5.0.45
|
||||
behavior, is what Etherpad needs, and avoids a larger
|
||||
plugin-per-driver refactor.
|
||||
|
||||
**Verification:** ueberdb2's existing vitest + testcontainers suite
|
||||
exercises every driver end-to-end. If the migration is correct, CI stays
|
||||
green. Nothing else needs adding upstream.
|
||||
|
||||
**Delivery:** PR from `johnmclear/ueberDB` fork into `ether/ueberDB`
|
||||
default branch. After merge, publish `ueberdb2@5.0.46` to npm via the
|
||||
existing release workflow.
|
||||
|
||||
## Downstream change (`ether/etherpad-lite`)
|
||||
|
||||
### Decision: replace PR #7571 rather than rebase
|
||||
|
||||
Copilot's #7571 has correct direction but mixed scope. Cleanest path is
|
||||
to close it with a pointer to the replacement and open a fresh branch
|
||||
off the current `ether/etherpad-lite:develop` (post #7569 merge). No
|
||||
shared commits.
|
||||
|
||||
### `src/package.json`
|
||||
|
||||
- Bump `ueberdb2` → `^5.0.46`.
|
||||
- Add the ten drivers to `dependencies`, with the **same version ranges
|
||||
ueberdb2 itself declares**, so the two lists can't drift on the
|
||||
range:
|
||||
- `@elastic/elasticsearch` `^9.3.4`
|
||||
- `cassandra-driver` `^4.8.0`
|
||||
- `mongodb` `^7.1.1`
|
||||
- `mssql` `^12.2.1`
|
||||
- `mysql2` `^3.22.0`
|
||||
- `nano` `^11.0.5`
|
||||
- `pg` `^8.20.0`
|
||||
- `redis` `^5.12.1`
|
||||
- `rethinkdb` `^2.4.2`
|
||||
- `surrealdb` `^2.0.3`
|
||||
- No other edits. Specifically do **not** bump
|
||||
`typescript`, `oidc-provider`, `eslint-config-etherpad`,
|
||||
`@types/express-session`, or `resolve`, and do **not** change the
|
||||
`repository.url` field or the `test-admin` project list.
|
||||
|
||||
### `pnpm-lock.yaml`
|
||||
|
||||
Regenerated via `pnpm install` on the clean branch. Not cherry-picked
|
||||
from #7571.
|
||||
|
||||
### `.github/workflows/docker.yml`
|
||||
|
||||
Add one new job, `build-test-db-drivers`, alongside the existing
|
||||
`build-test`. **No restructuring of existing jobs.** The `publish` job's
|
||||
`needs:` is extended from `[build-test]` to `[build-test, build-test-db-drivers]`
|
||||
so driver regressions block publication.
|
||||
|
||||
The new job runs four sequential stages in a single job (so any failure
|
||||
blocks `publish` via `needs:`):
|
||||
|
||||
1. **Build production image** — reuse the existing buildx + GHA cache
|
||||
pattern from `build-test`.
|
||||
|
||||
2. **Driver presence test (all ten, fast).** Run one container:
|
||||
```
|
||||
docker run --rm "$TEST_TAG" node -e "
|
||||
const mods = [
|
||||
'@elastic/elasticsearch','cassandra-driver','mongodb','mssql',
|
||||
'mysql2','nano','pg','redis','rethinkdb','surrealdb'
|
||||
];
|
||||
for (const m of mods) {
|
||||
try { require(m); console.log('ok', m); }
|
||||
catch (e) { console.error('MISSING', m, e.message); process.exit(1); }
|
||||
}
|
||||
"
|
||||
```
|
||||
This is the precise regression test for the #7570 class — catches
|
||||
"driver missing from production image" for every backend in seconds.
|
||||
|
||||
3. **MySQL smoke test.** `mysql:8` service container, launch Etherpad
|
||||
with `DB_TYPE=mysql` against the service, poll container health for
|
||||
up to ~2 minutes, fail on unhealthy. Reproduces the issue reporter's
|
||||
scenario.
|
||||
|
||||
4. **Postgres smoke test.** `postgres:16` service container, same
|
||||
pattern with `DB_TYPE=postgres`. Covers the other common prod
|
||||
backend.
|
||||
|
||||
Stages 2–4 are the actual regression signal; stage 1 just prepares the
|
||||
image they run against.
|
||||
|
||||
The other seven backends (mongodb, mssql, redis, couchdb/nano,
|
||||
cassandra, elasticsearch, rethinkdb, surrealdb) are covered by the
|
||||
presence test only. Full service-container smokes for all ten would
|
||||
10× CI time and several of them (SurrealDB, RethinkDB, Cassandra) are
|
||||
awkward to stand up reliably on GitHub-hosted runners. If a specific
|
||||
backend regresses in practice, we upgrade its coverage then.
|
||||
|
||||
## Rollout order
|
||||
|
||||
1. Open fork PR against `ether/ueberDB` with the ten-driver move + version bump.
|
||||
2. Merge and publish `ueberdb2@5.0.46` to npm.
|
||||
3. Close `ether/etherpad-lite#7571` with a comment linking here.
|
||||
4. Open fork PR against `ether/etherpad-lite:develop` with the
|
||||
`src/package.json` + `pnpm-lock.yaml` + `docker.yml` changes.
|
||||
5. Confirm CI goes green on the new PR — specifically the MySQL stage
|
||||
of `build-test-db-drivers` is the live reproduction of #7570.
|
||||
|
||||
Step 4 depends on step 2 (the new `pnpm-lock.yaml` must be able to
|
||||
resolve `ueberdb2@^5.0.46` from the public registry).
|
||||
|
||||
## Local verification before pushing
|
||||
|
||||
- `pnpm install` resolves cleanly with no warnings about missing peer deps.
|
||||
- `docker build --target production -t etherpad:test .` succeeds.
|
||||
- `docker run --rm etherpad:test node -e "<presence script>"` prints `ok` for all ten.
|
||||
- `docker compose up` with the issue reporter's exact compose file
|
||||
(mariadb:11.4, `DB_TYPE=mysql`) reaches a healthy state and serves
|
||||
`/`.
|
||||
|
||||
## Testing
|
||||
|
||||
The new CI job is itself the regression test. No separate unit tests
|
||||
added — the failure mode is a packaging concern, not a code-path
|
||||
concern, and unit tests cannot observe it.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- Reverting or auditing the unrelated bumps included in #7571. If any
|
||||
of those bumps is wanted independently, it gets its own PR.
|
||||
- Reworking the Docker image to slim down the ~250 MB driver payload
|
||||
for users who only need SQLite. If this matters, future work could
|
||||
introduce a build arg that prunes unneeded drivers post-install.
|
||||
|
||||
## Commit targets
|
||||
|
||||
Per project rules, both PRs originate from `johnmclear/` forks, never
|
||||
direct commits to `ether/*`. ueberDB fork to be created if it does not
|
||||
already exist.
|
||||
32
package.json
32
package.json
@ -46,9 +46,35 @@
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/ether/etherpad-lite.git"
|
||||
"url": "https://github.com/ether/etherpad.git"
|
||||
},
|
||||
"engineStrict": true,
|
||||
"version": "2.6.1",
|
||||
"license": "Apache-2.0"
|
||||
"version": "2.7.0",
|
||||
"license": "Apache-2.0",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild"
|
||||
],
|
||||
"ignoredBuiltDependencies": [
|
||||
"@scarf/scarf"
|
||||
],
|
||||
"overrides": {
|
||||
"basic-ftp": ">=5.3.0",
|
||||
"brace-expansion@>=2.0.0 <2.0.3": ">=2.0.3",
|
||||
"diff@>=6.0.0 <8.0.3": ">=8.0.3",
|
||||
"flatted": ">=3.4.2",
|
||||
"follow-redirects": ">=1.16.0",
|
||||
"glob@>=10.2.0 <10.5.0": ">=10.5.0",
|
||||
"js-yaml@>=4.0.0 <4.1.1": ">=4.1.1",
|
||||
"lodash": ">=4.18.0",
|
||||
"minimatch@>=9.0.0 <9.0.7": ">=9.0.7",
|
||||
"path-to-regexp@>=8.0.0 <8.4.0": ">=8.4.0",
|
||||
"picomatch@>=4.0.0 <4.0.4": ">=4.0.4",
|
||||
"qs@>=6.7.0 <6.14.2": ">=6.14.2",
|
||||
"serialize-javascript": ">=7.0.5",
|
||||
"socket.io-parser@>=4.0.0 <4.2.6": ">=4.2.6",
|
||||
"tar@<7.5.11": ">=7.5.11",
|
||||
"vite@>=7.0.0 <7.3.2": ">=7.3.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
6302
pnpm-lock.yaml
generated
6302
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -5,6 +5,13 @@ packages:
|
||||
- doc
|
||||
- ui
|
||||
onlyBuiltDependencies:
|
||||
- '@scarf/scarf'
|
||||
- '@swc/core'
|
||||
- esbuild
|
||||
# Explicitly ignore build scripts we don't want to run. Listing them here
|
||||
# stops pnpm from failing with ERR_PNPM_IGNORED_BUILDS when they're
|
||||
# encountered as transitive deps (e.g. scarf pulled in via swagger-ui-dist).
|
||||
ignoredBuiltDependencies:
|
||||
- '@scarf/scarf'
|
||||
# Belt-and-suspenders: even if a fresh transitive dep slips through with a
|
||||
# postinstall script, downgrade to a warning so CI doesn't break for
|
||||
# downstream plugin repos that pull etherpad-lite as their core install.
|
||||
strictDepBuilds: false
|
||||
|
||||
@ -91,7 +91,7 @@
|
||||
* "password": "${PASSW:}" // if PASSW is not defined would result in password === ''
|
||||
*
|
||||
* If you want to use an empty value (null) as default value for a variable,
|
||||
* simply do not set it, without putting any colons: "${ABIWORD}".
|
||||
* simply do not set it, without putting any colons: "${SOFFICE}".
|
||||
*
|
||||
* 3) if you want to use newlines in the default value of a string parameter,
|
||||
* use "\n" as usual.
|
||||
@ -205,6 +205,12 @@
|
||||
**/
|
||||
"enableDarkMode": "${ENABLE_DARK_MODE:true}",
|
||||
|
||||
/**
|
||||
* Enable creator-owned Pad-wide Settings and new-pad default seeding from My View.
|
||||
* Disabled by default to preserve the legacy single-settings behavior.
|
||||
**/
|
||||
"enablePadWideSettings": "${ENABLE_PAD_WIDE_SETTINGS:false}",
|
||||
|
||||
/*
|
||||
* Node native SSL support
|
||||
*
|
||||
@ -348,25 +354,23 @@
|
||||
*/
|
||||
"maxAge": "${MAX_AGE:21600}", // 60 * 60 * 6 = 6 hours
|
||||
|
||||
/*
|
||||
* Absolute path to the Abiword executable.
|
||||
*
|
||||
* Abiword is needed to get advanced import/export features of pads. Setting
|
||||
* it to null disables Abiword and will only allow plain text and HTML
|
||||
* import/exports.
|
||||
*/
|
||||
"abiword": "${ABIWORD:null}",
|
||||
|
||||
/*
|
||||
* This is the absolute path to the soffice executable.
|
||||
*
|
||||
* LibreOffice can be used in lieu of Abiword to export pads.
|
||||
* Setting it to null disables LibreOffice exporting.
|
||||
* LibreOffice is used for advanced import/export of pads (docx, pdf, odt).
|
||||
* Setting it to null disables LibreOffice and will only allow plain text
|
||||
* and HTML import/exports.
|
||||
*/
|
||||
"soffice": "${SOFFICE:null}",
|
||||
|
||||
/*
|
||||
* Allow import of file types other than the supported ones:
|
||||
* When true (the default), the "Microsoft Word" export button downloads a .docx file via
|
||||
* LibreOffice (requires "soffice" to be set). Set to false to revert to legacy .doc output
|
||||
* (which also requires "soffice").
|
||||
*/
|
||||
"docxExport": "${DOCX_EXPORT:true}",
|
||||
|
||||
/*
|
||||
* txt, doc, docx, rtf, odt, html & htm
|
||||
*/
|
||||
"allowUnknownFileEnds": "${ALLOW_UNKNOWN_FILE_ENDS:true}",
|
||||
@ -574,13 +578,17 @@
|
||||
|
||||
"socketIo": {
|
||||
/*
|
||||
* Maximum permitted client message size (in bytes). All messages from
|
||||
* clients that are larger than this will be rejected. Large values make it
|
||||
* possible to paste large amounts of text, and plugins may require a larger
|
||||
* value to work properly, but increasing the value increases susceptibility
|
||||
* to denial of service attacks (malicious clients can exhaust memory).
|
||||
* Maximum permitted client message size (in bytes). This controls the
|
||||
* maximum single-message size for socket.io and directly affects large
|
||||
* paste operations. All messages from clients that are larger than this
|
||||
* will be rejected. Large values make it possible to paste large amounts
|
||||
* of text, and plugins may require a larger value to work properly, but
|
||||
* increasing the value increases susceptibility to denial of service
|
||||
* attacks (malicious clients can exhaust memory).
|
||||
*
|
||||
* 1MB accommodates large pastes while still preventing abuse.
|
||||
*/
|
||||
"maxHttpBufferSize": "${SOCKETIO_MAX_HTTP_BUFFER_SIZE:50000}"
|
||||
"maxHttpBufferSize": "${SOCKETIO_MAX_HTTP_BUFFER_SIZE:1000000}"
|
||||
},
|
||||
|
||||
/*
|
||||
|
||||
@ -82,7 +82,7 @@
|
||||
* "password": "${PASSW:}" // if PASSW is not defined would result in password === ''
|
||||
*
|
||||
* If you want to use an empty value (null) as default value for a variable,
|
||||
* simply do not set it, without putting any colons: "${ABIWORD}".
|
||||
* simply do not set it, without putting any colons: "${SOFFICE}".
|
||||
*
|
||||
* 3) if you want to use newlines in the default value of a string parameter,
|
||||
* use "\n" as usual.
|
||||
@ -225,7 +225,7 @@
|
||||
/*
|
||||
* An Example of MySQL Configuration (commented out).
|
||||
*
|
||||
* See: https://github.com/ether/etherpad-lite/wiki/How-to-use-Etherpad-Lite-with-MySQL
|
||||
* See: https://github.com/ether/etherpad/wiki/How-to-use-Etherpad-Lite-with-MySQL
|
||||
*/
|
||||
|
||||
/*
|
||||
@ -335,25 +335,23 @@
|
||||
*/
|
||||
"maxAge": 21600, // 60 * 60 * 6 = 6 hours
|
||||
|
||||
/*
|
||||
* Absolute path to the Abiword executable.
|
||||
*
|
||||
* Abiword is needed to get advanced import/export features of pads. Setting
|
||||
* it to null disables Abiword and will only allow plain text and HTML
|
||||
* import/exports.
|
||||
*/
|
||||
"abiword": null,
|
||||
|
||||
/*
|
||||
* This is the absolute path to the soffice executable.
|
||||
*
|
||||
* LibreOffice can be used in lieu of Abiword to export pads.
|
||||
* Setting it to null disables LibreOffice exporting.
|
||||
* LibreOffice is used for advanced import/export of pads (docx, pdf, odt).
|
||||
* Setting it to null disables LibreOffice and will only allow plain text
|
||||
* and HTML import/exports.
|
||||
*/
|
||||
"soffice": null,
|
||||
|
||||
/*
|
||||
* Allow import of file types other than the supported ones:
|
||||
* When true (the default), the "Microsoft Word" export button downloads a .docx file via
|
||||
* LibreOffice (requires "soffice" to be set). Set to false to revert to legacy .doc output
|
||||
* (which also requires "soffice").
|
||||
*/
|
||||
"docxExport": true,
|
||||
|
||||
/*
|
||||
* txt, doc, docx, rtf, odt, html & htm
|
||||
*/
|
||||
"allowUnknownFileEnds": true,
|
||||
@ -385,6 +383,13 @@
|
||||
* Settings controlling the session cookie issued by Etherpad.
|
||||
*/
|
||||
"cookie": {
|
||||
/*
|
||||
* Prefix for all cookie names set by Etherpad. Set this to "ep_" or similar
|
||||
* if Etherpad's cookie names (token, sessionID, etc.) conflict with those
|
||||
* of another application on the same domain. Default: "" (no prefix).
|
||||
*/
|
||||
// "prefix": "ep_",
|
||||
|
||||
/*
|
||||
* How often (in milliseconds) the key used to sign the express_sid cookie
|
||||
* should be rotated. Long rotation intervals reduce signature verification
|
||||
@ -456,7 +461,13 @@
|
||||
* Automatic session refreshes can be disabled (not recommended) by setting
|
||||
* this to null.
|
||||
*/
|
||||
"sessionRefreshInterval": 86400000 // = 1d * 24h/d * 60m/h * 60s/m * 1000ms/s
|
||||
"sessionRefreshInterval": 86400000, // = 1d * 24h/d * 60m/h * 60s/m * 1000ms/s
|
||||
|
||||
/*
|
||||
* Whether to periodically clean up expired and stale sessions from the
|
||||
* database. Set to false to disable. Default: true.
|
||||
*/
|
||||
"sessionCleanup": true
|
||||
},
|
||||
|
||||
/*
|
||||
@ -560,13 +571,17 @@
|
||||
|
||||
"socketIo": {
|
||||
/*
|
||||
* Maximum permitted client message size (in bytes). All messages from
|
||||
* clients that are larger than this will be rejected. Large values make it
|
||||
* possible to paste large amounts of text, and plugins may require a larger
|
||||
* value to work properly, but increasing the value increases susceptibility
|
||||
* to denial of service attacks (malicious clients can exhaust memory).
|
||||
* Maximum permitted client message size (in bytes). This controls the
|
||||
* maximum single-message size for socket.io and directly affects large
|
||||
* paste operations. All messages from clients that are larger than this
|
||||
* will be rejected. Large values make it possible to paste large amounts
|
||||
* of text, and plugins may require a larger value to work properly, but
|
||||
* increasing the value increases susceptibility to denial of service
|
||||
* attacks (malicious clients can exhaust memory).
|
||||
*
|
||||
* 1MB accommodates large pastes while still preventing abuse.
|
||||
*/
|
||||
"maxHttpBufferSize": 50000
|
||||
"maxHttpBufferSize": 1000000
|
||||
},
|
||||
|
||||
/*
|
||||
@ -628,6 +643,12 @@
|
||||
**/
|
||||
"enableDarkMode": "${ENABLE_DARK_MODE:true}",
|
||||
|
||||
/**
|
||||
* Enable creator-owned Pad-wide Settings and new-pad default seeding from My View.
|
||||
* Disabled by default to preserve the legacy single-settings behavior.
|
||||
**/
|
||||
"enablePadWideSettings": "${ENABLE_PAD_WIDE_SETTINGS:false}",
|
||||
|
||||
/*
|
||||
* From Etherpad 1.8.5 onwards, when Etherpad is in production mode commits from individual users are rate limited
|
||||
*
|
||||
|
||||
@ -1 +0,0 @@
|
||||
auto-install-peers=false
|
||||
@ -117,7 +117,6 @@
|
||||
"pad.importExport.exportword": "مايكروسوفت وورد",
|
||||
"pad.importExport.exportpdf": "صيغة المستندات المحمولة",
|
||||
"pad.importExport.exportopen": "ODF (نسق المستند المفتوح)",
|
||||
"pad.importExport.abiword.innerHTML": "لا يمكنك الاستيراد إلا من نص عادي أو من تنسيقات HTML. للحصول على المزيد من ميزات الاستيراد المتقدمة، يرجى <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">تثبيت AbiWord أو LibreOffice</a>.",
|
||||
"pad.modals.connected": "متصل.",
|
||||
"pad.modals.reconnecting": "إعادة الاتصال ببادك..",
|
||||
"pad.modals.forcereconnect": "فرض إعادة الاتصال",
|
||||
|
||||
@ -52,7 +52,6 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "Sólo se pue importar dende los formatos de testu planu o HTML. Pa carauterístiques d'importación más avanzaes <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instala Abiword o LibreOffice</a>.",
|
||||
"pad.modals.connected": "Coneutáu.",
|
||||
"pad.modals.reconnecting": "Reconeutando col to bloc...",
|
||||
"pad.modals.forcereconnect": "Forzar la reconexón",
|
||||
|
||||
@ -66,7 +66,6 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (açıq sənəd formatı)",
|
||||
"pad.importExport.abiword.innerHTML": "Siz yalnız adi mətndən və ya HTML-dən idxal edə bilərsiniz. İdxalın daha mürəkkəb funksiyaları üçün, zəhmət olmasa, <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">AbiWord və ya LibreOffice quraşdırın</a>.",
|
||||
"pad.modals.connected": "Bağlandı.",
|
||||
"pad.modals.reconnecting": "Sizin lövhə yenidən qoşulur…",
|
||||
"pad.modals.forcereconnect": "Məcbur təkrarən bağlan",
|
||||
|
||||
@ -62,7 +62,6 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (قالب سند باز)",
|
||||
"pad.importExport.abiword.innerHTML": "شما تنها میتوانید از قالب متن ساده یا اچتیامال درونریزی کنید. برای بیشتر شدن ویژگیهای درونریزی پیشرفته <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">AbiWord</a> را نصب کنید.",
|
||||
"pad.modals.connected": "متصل شد.",
|
||||
"pad.modals.reconnecting": "در حال اتصال دوباره به دفترچه یادداشت شما..",
|
||||
"pad.modals.forcereconnect": "واداشتن به اتصال دوباره",
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
"Jim-by",
|
||||
"Red Winged Duck",
|
||||
"Renessaince",
|
||||
"Ucukor",
|
||||
"Wizardist"
|
||||
]
|
||||
},
|
||||
@ -69,7 +70,7 @@
|
||||
"pad.toolbar.showusers.title": "Паказаць карыстальнікаў у гэтым дакумэнце",
|
||||
"pad.colorpicker.save": "Захаваць",
|
||||
"pad.colorpicker.cancel": "Скасаваць",
|
||||
"pad.loading": "Загрузка...",
|
||||
"pad.loading": "Загрузка…",
|
||||
"pad.noCookie": "Кукі ня знойдзеныя. Калі ласка, дазвольце кукі ў вашым браўзэры! Паміж наведваньнямі вашая сэсія і налады ня будуць захаваныя. Гэта можа адбывацца таму, што ў некаторых броўзэрах Etherpad заключаны ўнутры iFrame. Праверце, калі ласка, што Etherpad знаходзіцца ў тым жа паддамэне/дамэне, што і бацькоўскі iFrame",
|
||||
"pad.permissionDenied": "Вы ня маеце дазволу на доступ да гэтага дакумэнта",
|
||||
"pad.settings.padSettings": "Налады дакумэнта",
|
||||
@ -87,7 +88,7 @@
|
||||
"pad.settings.about": "Пра",
|
||||
"pad.settings.poweredBy": "Працуе на",
|
||||
"pad.importExport.import_export": "Імпарт/Экспарт",
|
||||
"pad.importExport.import": "Загрузіжайце любыя тэкставыя файлы або дакумэнты",
|
||||
"pad.importExport.import": "Даслаць будзь-які тэкставы файл ці дакумэнт",
|
||||
"pad.importExport.importSuccessful": "Пасьпяхова!",
|
||||
"pad.importExport.export": "Экспартаваць бягучы дакумэнт як:",
|
||||
"pad.importExport.exportetherpad": "Etherpad",
|
||||
@ -96,7 +97,6 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "Вы можаце імпартаваць толькі з звычайнага тэксту або HTML. Дзеля больш пашыраных магчымасьцяў імпарту, калі ласка, <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">усталюйце AbiWord альбо LibreOffice</a>.",
|
||||
"pad.modals.connected": "Падлучыліся.",
|
||||
"pad.modals.reconnecting": "Перападлучэньне да вашага дакумэнта…",
|
||||
"pad.modals.forcereconnect": "Прымусовае перападлучэньне",
|
||||
@ -115,7 +115,7 @@
|
||||
"pad.modals.slowcommit.explanation": "Сэрвэр не адказвае.",
|
||||
"pad.modals.slowcommit.cause": "Гэта можа быць выклікана праблемамі зь сеткавым падлучэньнем.",
|
||||
"pad.modals.badChangeset.explanation": "Сэрвэр сынхранізацыі вызначыў зробленае вамі рэдагаваньне як недапушчальнае.",
|
||||
"pad.modals.badChangeset.cause": "Гэта можа адбывацца празь няслушную канфігурацыю сэрвэра або празь іншыя нечаканыя дзеяньні. Калі ласка, скантактуйцеся з адміністратарам, калі вы лічыце, што гэта памылка. Паспрабуйце перападлучыцца, каб працягнуць рэдагаваньне.",
|
||||
"pad.modals.badChangeset.cause": "Гэта можа адбывацца празь няслушную канфіґурацыю сэрвэра або празь іншыя нечаканыя дзеяньні. Калі ласка, скантактуйцеся з адміністратарам, калі вы думаеце, што гэта памылка. Паспрабуйце перападлучыцца, каб працягнуць рэдагаваньне.",
|
||||
"pad.modals.corruptPad.explanation": "Дакумэнт, да якога вы спрабуеце атрымаць доступ, пашкоджаны.",
|
||||
"pad.modals.corruptPad.cause": "Гэта можа быць выклікана няправільнай канфігурацыяй сэрвэру або іншымі нечаканымі дзеяньнямі. Калі ласка, скантактуйцеся з адміністратарам службы.",
|
||||
"pad.modals.deleted": "Выдалены.",
|
||||
@ -125,7 +125,7 @@
|
||||
"pad.modals.rejected.explanation": "Сэрвэр адхіліў паведамленьне, адасланае вашым броўзэрам.",
|
||||
"pad.modals.disconnected": "Вы былі адключаныя.",
|
||||
"pad.modals.disconnected.explanation": "Злучэньне з сэрвэрам было страчанае",
|
||||
"pad.modals.disconnected.cause": "Магчыма, сэрвэр недаступны. Калі ласка, паведаміце адміністратару службы, калі праблема будзе паўтарацца.",
|
||||
"pad.modals.disconnected.cause": "Магчыма, сэрвэр недаступны. Калі ласка, абвясьціце адміністратара службы, калі праблема будзе паўтарацца.",
|
||||
"pad.share": "Падзяліцца дакумэнтам",
|
||||
"pad.share.readonly": "Толькі для чытаньня",
|
||||
"pad.share.link": "Спасылка",
|
||||
|
||||
@ -38,7 +38,6 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (پاچین سندئ قالب)",
|
||||
"pad.importExport.abiword.innerHTML": "شما تا توانیت که شه ساده گین متنی ئین قالب یا اچتیامال بی تئ کنیت . په گیشتیرین کارا ئییان پیشرفته ئین بی تئ کورتینا <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">AbiWord</a> نصب کنیت.",
|
||||
"pad.modals.connected": "وصل بوت.",
|
||||
"pad.modals.userdup": "نوکین دروازه گئ پاچ کورتین",
|
||||
"pad.modals.unauth": "مجاز نه اینت",
|
||||
|
||||
@ -52,7 +52,6 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "Ne c'hallit enporzhiañ nemet furmadoù testennoù plaen pe HTML. Evit arc'hwelioù enporzhiañ emdroetoc'h, staliit <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">Abiword pe LibreOffice</a>.",
|
||||
"pad.modals.connected": "Kevreet.",
|
||||
"pad.modals.reconnecting": "Adkevreañ war-zu ho pad...",
|
||||
"pad.modals.forcereconnect": "Adkevreañ dre heg",
|
||||
|
||||
@ -93,7 +93,6 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "Només podeu importar de text sense format o HTML. Per a opcions d'importació més avançades <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instal·leu l'Abiword</a>.",
|
||||
"pad.modals.connected": "Connectat.",
|
||||
"pad.modals.reconnecting": "S'està tornant a connectar al vostre pad…",
|
||||
"pad.modals.forcereconnect": "Força tornar a connectar",
|
||||
|
||||
@ -117,7 +117,6 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "Importovat lze pouze z formátů prostého textu nebo HTML. Pokročilejší funkce pro import naleznete v <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instalaci AbiWord nebo LibreOffice</a>.",
|
||||
"pad.modals.connected": "Připojeno.",
|
||||
"pad.modals.reconnecting": "Opětovné připojení k Padu...",
|
||||
"pad.modals.forcereconnect": "Vynutit znovupřipojení",
|
||||
|
||||
@ -29,7 +29,7 @@
|
||||
"admin_plugins_info.hooks": "Installerede hooks",
|
||||
"admin_settings": "Indstillinger",
|
||||
"index.newPad": "Ny Pad",
|
||||
"index.createOpenPad": "eller opret/åbn en Pad med navnet:",
|
||||
"index.createOpenPad": "Åbn pad efter navn",
|
||||
"pad.toolbar.bold.title": "Fed (Ctrl-B)",
|
||||
"pad.toolbar.italic.title": "Kursiv (Ctrl-I)",
|
||||
"pad.toolbar.underline.title": "Understregning (Ctrl-U)",
|
||||
@ -50,7 +50,7 @@
|
||||
"pad.colorpicker.save": "Gem",
|
||||
"pad.colorpicker.cancel": "Afbryd",
|
||||
"pad.loading": "Indlæser ...",
|
||||
"pad.noCookie": "Cookie kunne ikke findes. Tillad venligst cookier i din browser!",
|
||||
"pad.noCookie": "Cookie kunne ikke findes. Tillad venligst cookies i din browser! Din session og dine indstillinger gemmes ikke mellem besøg. Dette kan skyldes, at Etherpad er inkluderet i en iFrame i nogle browsere. Sørg for, at Etherpad er på samme underdomæne/domæne som den overordnede iFrame.",
|
||||
"pad.permissionDenied": "Du har ikke tilladelse til at få adgang til denne pad.",
|
||||
"pad.settings.padSettings": "Pad indstillinger",
|
||||
"pad.settings.myView": "Min visning",
|
||||
@ -74,7 +74,6 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "Du kan kun importere fra almindelig tekst eller HTML-formater. For mere avancerede importfunktioner, <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">installer AbiWord</a>.",
|
||||
"pad.modals.connected": "Forbundet.",
|
||||
"pad.modals.reconnecting": "Genopretter forbindelsen til din pad...",
|
||||
"pad.modals.forcereconnect": "Gennemtving genoprettelse af forbindelsen",
|
||||
|
||||
@ -122,7 +122,6 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "Du kannst nur aus reinen Text- oder HTML-Formaten importieren. Für umfangreichere Importfunktionen <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">muss AbiWord oder LibreOffice auf dem Server installiert werden</a>.",
|
||||
"pad.modals.connected": "Verbunden.",
|
||||
"pad.modals.reconnecting": "Dein Pad wird neu verbunden...",
|
||||
"pad.modals.forcereconnect": "Erneutes Verbinden erzwingen",
|
||||
|
||||
@ -93,7 +93,6 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "Şıma şenê tenya metınanê zelalan ya zi formatanê HTML-i biyarê. Seba vêşi xısusiyetanê arezekerdışi ra gırey <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">AbiWordi ya zi LibreOfficeyi bar kerên</a>.",
|
||||
"pad.modals.connected": "Gıre diya.",
|
||||
"pad.modals.reconnecting": "Pada şıma rê fına irtibat kewê no",
|
||||
"pad.modals.forcereconnect": "Mecbur anciya gırê de",
|
||||
|
||||
@ -82,13 +82,19 @@
|
||||
"pad.loading": "Zacytujo se...",
|
||||
"pad.noCookie": "Cookie njejo se namakał. Pšosym dowólśo cookieje w swójom wobglědowaku! Wašo pósejźenje a waše nastajenja se mjazy dwěma woglědoma njeskładuju. To móžo se stas, gaž Etherpad jo w někotarych wobglědowakach w iFrame wopśimjony. Pšosym zawěsććo, až Etherpad jo na samskej póddomenje/domenje ako nadrědowany iFrame",
|
||||
"pad.permissionDenied": "Njamaš pśistupne pšawo za toś ten zapisnik.",
|
||||
"pad.settings.padSettings": "Nastajenja zapisnika",
|
||||
"pad.settings.title": "Nastajenja",
|
||||
"pad.settings.padSettings": "Nastajenja cełego zapisnika",
|
||||
"pad.settings.userSettings": "Wužywaŕske nastajenja",
|
||||
"pad.settings.myView": "Mój naglěd",
|
||||
"pad.settings.disablechat": "Chat znjemóžniś",
|
||||
"pad.settings.darkMode": "Śamny modus",
|
||||
"pad.settings.stickychat": "Chat pśecej na wobrazowce pokazaś",
|
||||
"pad.settings.chatandusers": "Chat a wužywarje pokazaś",
|
||||
"pad.settings.colorcheck": "Awtorowe barwy",
|
||||
"pad.settings.linenocheck": "Smužkowe numery",
|
||||
"pad.settings.rtlcheck": "Wopśimjeśe wótpšawa nalěwo cytaś?",
|
||||
"pad.settings.enforceSettings": "Nastajenja za drugich wužywarjow wunuzkaś",
|
||||
"pad.settings.enforcedNotice": "Toś te nastajenja su se zastajili za tebje wót awtora toś togo zapisnika. Pšašaj se awtora zapisnika, lěc musyš je změniś.",
|
||||
"pad.settings.fontType": "Pismowa družyna:",
|
||||
"pad.settings.fontType.normal": "Normalny",
|
||||
"pad.settings.language": "Rěc:",
|
||||
@ -106,7 +112,7 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "Móžoš jano z fprmatow lutnego teksta abo z HTML-formata importěrowaś. Za wěcej rozšyrjone importěrowańske funkcije <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instalěruj pšosym Abiword abo LibreOffice</a>.",
|
||||
"pad.importExport.noConverter.innerHTML": "Móžoš jano z formatow lutnego teksta abo z HTML-formata importěrowaś. Za wěcej rozšyrjone importowe funkcije <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instalěruj pšosym LibreOffice</a>.",
|
||||
"pad.modals.connected": "Zwězany.",
|
||||
"pad.modals.reconnecting": "Zwězujo se znowego z twójim zapisnikom...",
|
||||
"pad.modals.forcereconnect": "Znowego zwězaś",
|
||||
@ -137,6 +143,8 @@
|
||||
"pad.modals.disconnected": "Zwisk jo pśetergnjony.",
|
||||
"pad.modals.disconnected.explanation": "Zwisk ze serwerom jo se zgubił",
|
||||
"pad.modals.disconnected.cause": "Serwer njestoj k dispoziciji. Pšosym informěruj słužbowego administratora, jolic to se dalej stawa.",
|
||||
"pad.gritter.unacceptedCommit.title": "Njeskłaźona změna",
|
||||
"pad.gritter.unacceptedCommit.text": "Waša nejnowša změna hyšći njejo skłaźona. Zwěžćo znowego a wopytaj hyšći raz.",
|
||||
"pad.share": "Toś ten zapisnik źěliś",
|
||||
"pad.share.readonly": "Jano cytajobny",
|
||||
"pad.share.link": "Wótkaz",
|
||||
@ -155,6 +163,12 @@
|
||||
"timeslider.exportCurrent": "Aktualnu wersiju eksportěrowaś ako:",
|
||||
"timeslider.version": "Wersija {{version}}",
|
||||
"timeslider.saved": "Składowany {{day}}. {{month}} {{year}}",
|
||||
"timeslider.settings.playbackSpeed": "Malsnosć wótgrawanja:",
|
||||
"timeslider.settings.playbackSpeed.original": "Originalna malsnosć",
|
||||
"timeslider.settings.playbackSpeed.realtime": "Napšawdny cas",
|
||||
"timeslider.settings.playbackSpeed.200ms": "200 ms",
|
||||
"timeslider.settings.playbackSpeed.500ms": "500 ms",
|
||||
"timeslider.settings.playbackSpeed.1000ms": "1000 ms",
|
||||
"timeslider.playPause": "Wopśimjeśe zapisnika wótgraś/pawzěrowaś",
|
||||
"timeslider.backRevision": "Wó jadnu wersiju w toś tom dokumenśe slědk hyś",
|
||||
"timeslider.forwardRevision": "Wó jadnu wersiju w toś tom dokumenśe doprědka hyś",
|
||||
|
||||
@ -50,7 +50,6 @@
|
||||
"pad.importExport.exportword": "माइक्रोसफ्ट वर्ड",
|
||||
"pad.importExport.exportpdf": "पिडिएफ",
|
||||
"pad.importExport.exportopen": "ओडिएफ (खुल्ला कागजात ढाँचा)",
|
||||
"pad.importExport.abiword.innerHTML": "तम सादा पाठ या HTML ढाँचा बठेइ मात्तरी आयात अरीसकन्छऽ। विस्तारित आयात विशेषता खिलाई कृपया <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">abiword स्थापना अरऽ</a>।",
|
||||
"pad.modals.connected": "जोडीयाको।",
|
||||
"pad.modals.reconnecting": "तमरा प्याडमि दोबरा जडान अद्दाछ़..",
|
||||
"pad.modals.forcereconnect": "बलात् पुन:जडान",
|
||||
|
||||
@ -103,7 +103,6 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "Μπορείτε να εισάγετε απλό κείμενο ή HTML. Για προηγμένες δυνατότητες εισαγωγής παρακαλούμε <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">εγκαταστήστε το AbiWord ή το LibreOffice</a>.",
|
||||
"pad.modals.connected": "Συνδεμένοι.",
|
||||
"pad.modals.reconnecting": "Επανασύνδεση στο pad σας…",
|
||||
"pad.modals.forcereconnect": "Επιβολή επανασύνδεσης",
|
||||
|
||||
@ -53,7 +53,6 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "You only can import from plain text or HTML formats. For more advanced import features please <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">install AbiWord or LibreOffice</a>.",
|
||||
"pad.modals.connected": "Connected.",
|
||||
"pad.modals.reconnecting": "Reconnecting to your pad…",
|
||||
"pad.modals.forcereconnect": "Force reconnect",
|
||||
|
||||
@ -83,13 +83,19 @@
|
||||
"pad.noCookie": "Cookie could not be found. Please allow cookies in your browser! Your session and settings will not be saved between visits. This may be due to Etherpad being included in an iFrame in some Browsers. Please ensure Etherpad is on the same subdomain/domain as the parent iFrame",
|
||||
"pad.permissionDenied": "You do not have permission to access this pad",
|
||||
|
||||
"pad.settings.padSettings": "Pad Settings",
|
||||
"pad.settings.title": "Settings",
|
||||
"pad.settings.padSettings": "Pad-wide Settings",
|
||||
"pad.settings.userSettings": "User Settings",
|
||||
"pad.settings.myView": "My View",
|
||||
"pad.settings.disablechat": "Disable Chat",
|
||||
"pad.settings.darkMode": "Dark mode",
|
||||
"pad.settings.stickychat": "Chat always on screen",
|
||||
"pad.settings.chatandusers": "Show Chat and Users",
|
||||
"pad.settings.colorcheck": "Authorship colors",
|
||||
"pad.settings.linenocheck": "Line numbers",
|
||||
"pad.settings.rtlcheck": "Read content from right to left?",
|
||||
"pad.settings.enforceSettings": "Enforce settings for other users",
|
||||
"pad.settings.enforcedNotice": "These settings are locked for you by this pad's creator. Ask the pad creator if you need them changed.",
|
||||
"pad.settings.fontType": "Font type:",
|
||||
"pad.settings.fontType.normal": "Normal",
|
||||
"pad.settings.language": "Language:",
|
||||
@ -108,7 +114,7 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "You only can import from plain text or HTML formats. For more advanced import features please <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">install AbiWord or LibreOffice</a>.",
|
||||
"pad.importExport.noConverter.innerHTML": "You can only import from plain text or HTML formats. For more advanced import features, please <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">install LibreOffice</a>.",
|
||||
|
||||
"pad.modals.connected": "Connected.",
|
||||
"pad.modals.reconnecting": "Reconnecting to your pad…",
|
||||
@ -151,6 +157,8 @@
|
||||
"pad.modals.disconnected": "You have been disconnected.",
|
||||
"pad.modals.disconnected.explanation": "The connection to the server was lost",
|
||||
"pad.modals.disconnected.cause": "The server may be unavailable. Please notify the service administrator if this continues to happen.",
|
||||
"pad.gritter.unacceptedCommit.title": "Unsaved edit",
|
||||
"pad.gritter.unacceptedCommit.text": "Your recent edit is still not saved. Reconnect and try again.",
|
||||
|
||||
"pad.share": "Share this pad",
|
||||
"pad.share.readonly": "Read only",
|
||||
@ -171,6 +179,12 @@
|
||||
"timeslider.exportCurrent": "Export current version as:",
|
||||
"timeslider.version": "Version {{version}}",
|
||||
"timeslider.saved": "Saved {{month}} {{day}}, {{year}}",
|
||||
"timeslider.settings.playbackSpeed": "Playback speed:",
|
||||
"timeslider.settings.playbackSpeed.original": "Original speed",
|
||||
"timeslider.settings.playbackSpeed.realtime": "Realtime",
|
||||
"timeslider.settings.playbackSpeed.200ms": "200 ms",
|
||||
"timeslider.settings.playbackSpeed.500ms": "500 ms",
|
||||
"timeslider.settings.playbackSpeed.1000ms": "1000 ms",
|
||||
|
||||
"timeslider.playPause": "Playback / Pause Pad Contents",
|
||||
"timeslider.backRevision":"Go back a revision in this Pad",
|
||||
|
||||
@ -52,7 +52,6 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Formato “OpenDocument”)",
|
||||
"pad.importExport.abiword.innerHTML": "Nur kapablas enporti de plata teksto aŭ HTML. Por pli speciala importkapablo, bonvolu <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instalu la programon, Abiword</a>.",
|
||||
"pad.modals.connected": "Konektita.",
|
||||
"pad.modals.reconnecting": "Rekonektanta al via redaktilo..",
|
||||
"pad.modals.forcereconnect": "Perforte rekonekti",
|
||||
|
||||
@ -104,7 +104,6 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "Solo se puede importar desde texto plano o formatos HTML. Para obtener funciones de importación más avanzadas, <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">instale AbiWord o LibreOffice</a>.",
|
||||
"pad.modals.connected": "Conectado.",
|
||||
"pad.modals.reconnecting": "Reconectando a tu pad...",
|
||||
"pad.modals.forcereconnect": "Forzar reconexión",
|
||||
|
||||
@ -46,7 +46,6 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "Paraku on ainult lihttekstis voi HTML-vormingus dokumentide importimine võimaldatud. Rohkem võimaluste jaoks peab <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-in-Ubuntu-or-OpenSuse-or-SLES-with-AbiWord\">paigaldama abiword</a>.",
|
||||
"pad.modals.connected": "Ühendatud.",
|
||||
"pad.modals.reconnecting": "Proovitakse luua ühendus klade juurde...",
|
||||
"pad.modals.forcereconnect": "Sunni ühenduse taasloomist",
|
||||
|
||||
@ -46,6 +46,14 @@
|
||||
"admin_settings.current_save.value": "Gorde Ezarpenak",
|
||||
"admin_settings.page-title": "Ezarpenak - Etherpad",
|
||||
"index.newPad": "Pad berria",
|
||||
"index.settings": "Ezarpenak",
|
||||
"index.receiveSessionDescription": "Hemen beste nabigatzaile batetik edo gailu batetik Etherpad saioa jaso ahal izango duzu. Kontuan izan, ordea, honek zure egungo saioa ezabatuko duela.",
|
||||
"index.copyLink": "2. Kopiatu esteka",
|
||||
"index.copyLinkDescription": "Egin klik beheko botoian esteka arbelean kopiatzeko.",
|
||||
"index.copyLinkButton": "Kopiatu esteka arbelean",
|
||||
"index.transferToSystem": "3. Kopiatu saioa sistema berrira",
|
||||
"index.transferToSystemDescription": "Ireki kopiatutako esteka helburuko nabigatzailean edo gailuan zure saioa transferitzeko.",
|
||||
"index.transferSessionDescription": "Transferitu zure uneko saioa nabigatzailera edo gailura beheko botoian klik eginez. Honek zure saioa transferituko duen orrialde baterako esteka bat kopiatuko du helburuko nabigatzailean edo gailuan irekitzean.",
|
||||
"index.createOpenPad": "Ireki Pad bat honako izenarekin:",
|
||||
"index.openPad": "ireki existitzen den eta hurrengo izena duen Pad-a:",
|
||||
"index.recentPadsEmpty": "Ez da aurkitu duela gutxiko pad-ik",
|
||||
@ -101,7 +109,6 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (Open Document Format)",
|
||||
"pad.importExport.abiword.innerHTML": "Testu laua edo HTML formatudun testuak bakarrik inporta ditzakezu. Aurreratuagoak diren inportazio aukerak izateko <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">AbiWord edo LibreOffice instala ezazu</a>.",
|
||||
"pad.modals.connected": "Konektatuta.",
|
||||
"pad.modals.reconnecting": "Zure pad-era birkonektatzen...",
|
||||
"pad.modals.forcereconnect": "Behartu berkonexioa",
|
||||
|
||||
@ -98,7 +98,6 @@
|
||||
"pad.importExport.exportword": "Microsoft Word",
|
||||
"pad.importExport.exportpdf": "PDF",
|
||||
"pad.importExport.exportopen": "ODF (قالب سند باز)",
|
||||
"pad.importExport.abiword.innerHTML": "شما تنها میتوانید از قالب متن ساده یا اچتیامال درونریزی کنید. برای بیشتر شدن ویژگیهای درونریزی پیشرفته <a href=\"https://github.com/ether/etherpad-lite/wiki/How-to-enable-importing-and-exporting-different-file-formats-with-AbiWord\">AbiWord یا LibreOffice را نصب کنید</a>.",
|
||||
"pad.modals.connected": "متصل شد.",
|
||||
"pad.modals.reconnecting": "در حال اتصال دوباره به پد شما...",
|
||||
"pad.modals.forcereconnect": "واداشتن به اتصال دوباره",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user