Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ab3cbbbb8 | |||
| d9d048a866 | |||
| cbc8cff62d |
@@ -19,9 +19,26 @@ jobs:
|
|||||||
build-and-push-backend:
|
build-and-push-backend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
-
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||||
|
owner: ${{ github.repository_owner }}
|
||||||
|
repositories: "impress,secrets"
|
||||||
-
|
-
|
||||||
name: Checkout repository
|
name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
-
|
||||||
|
name: Load sops secrets
|
||||||
|
uses: rouja/actions-sops@main
|
||||||
|
with:
|
||||||
|
secret-file: secrets/numerique-gouv/impress/secrets.enc.env
|
||||||
|
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||||
-
|
-
|
||||||
name: Docker meta
|
name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
@@ -31,7 +48,7 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Login to DockerHub
|
name: Login to DockerHub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
|
run: echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USER" --password-stdin
|
||||||
-
|
-
|
||||||
name: Run trivy scan
|
name: Run trivy scan
|
||||||
uses: numerique-gouv/action-trivy-cache@main
|
uses: numerique-gouv/action-trivy-cache@main
|
||||||
@@ -53,9 +70,26 @@ jobs:
|
|||||||
build-and-push-frontend:
|
build-and-push-frontend:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
-
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||||
|
owner: ${{ github.repository_owner }}
|
||||||
|
repositories: "impress,secrets"
|
||||||
-
|
-
|
||||||
name: Checkout repository
|
name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
-
|
||||||
|
name: Load sops secrets
|
||||||
|
uses: rouja/actions-sops@main
|
||||||
|
with:
|
||||||
|
secret-file: secrets/numerique-gouv/impress/secrets.enc.env
|
||||||
|
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||||
-
|
-
|
||||||
name: Docker meta
|
name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
@@ -65,7 +99,7 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Login to DockerHub
|
name: Login to DockerHub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
|
run: echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USER" --password-stdin
|
||||||
-
|
-
|
||||||
name: Run trivy scan
|
name: Run trivy scan
|
||||||
uses: numerique-gouv/action-trivy-cache@main
|
uses: numerique-gouv/action-trivy-cache@main
|
||||||
@@ -88,9 +122,26 @@ jobs:
|
|||||||
build-and-push-y-provider:
|
build-and-push-y-provider:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
|
-
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||||
|
owner: ${{ github.repository_owner }}
|
||||||
|
repositories: "impress,secrets"
|
||||||
-
|
-
|
||||||
name: Checkout repository
|
name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
-
|
||||||
|
name: Load sops secrets
|
||||||
|
uses: rouja/actions-sops@main
|
||||||
|
with:
|
||||||
|
secret-file: secrets/numerique-gouv/impress/secrets.enc.env
|
||||||
|
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||||
-
|
-
|
||||||
name: Docker meta
|
name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
@@ -100,7 +151,7 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Login to DockerHub
|
name: Login to DockerHub
|
||||||
if: github.event_name != 'pull_request'
|
if: github.event_name != 'pull_request'
|
||||||
run: echo "${{ secrets.DOCKER_HUB_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_HUB_USER }}" --password-stdin
|
run: echo "$DOCKER_HUB_PASSWORD" | docker login -u "$DOCKER_HUB_USER" --password-stdin
|
||||||
-
|
-
|
||||||
name: Run trivy scan
|
name: Run trivy scan
|
||||||
uses: numerique-gouv/action-trivy-cache@main
|
uses: numerique-gouv/action-trivy-cache@main
|
||||||
@@ -128,12 +179,29 @@ jobs:
|
|||||||
if: |
|
if: |
|
||||||
github.event_name != 'pull_request'
|
github.event_name != 'pull_request'
|
||||||
steps:
|
steps:
|
||||||
|
-
|
||||||
|
uses: actions/create-github-app-token@v1
|
||||||
|
id: app-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.APP_ID }}
|
||||||
|
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||||
|
owner: ${{ github.repository_owner }}
|
||||||
|
repositories: "impress,secrets"
|
||||||
-
|
-
|
||||||
name: Checkout repository
|
name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
token: ${{ steps.app-token.outputs.token }}
|
||||||
|
-
|
||||||
|
name: Load sops secrets
|
||||||
|
uses: rouja/actions-sops@main
|
||||||
|
with:
|
||||||
|
secret-file: secrets/numerique-gouv/impress/secrets.enc.env
|
||||||
|
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||||
-
|
-
|
||||||
name: Call argocd github webhook
|
name: Call argocd github webhook
|
||||||
run: |
|
run: |
|
||||||
data='{"ref": "'$GITHUB_REF'","repository": {"html_url":"'$GITHUB_SERVER_URL'/'$GITHUB_REPOSITORY'"}}'
|
data='{"ref": "'$GITHUB_REF'","repository": {"html_url":"'$GITHUB_SERVER_URL'/'$GITHUB_REPOSITORY'"}}'
|
||||||
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac ''${{ secrets.ARGOCD_PREPROD_WEBHOOK_SECRET}}'' | awk '{print "X-Hub-Signature: sha1="$2}')
|
sig=$(echo -n ${data} | openssl dgst -sha1 -hmac ''${ARGOCD_WEBHOOK_SECRET}'' | awk '{print "X-Hub-Signature: sha1="$2}')
|
||||||
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" ${{ vars.ARGOCD_PREPROD_WEBHOOK_URL }}
|
curl -X POST -H 'X-GitHub-Event:push' -H "Content-Type: application/json" -H "${sig}" --data "${data}" $ARGOCD_WEBHOOK_URL
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ name: Helmfile lint
|
|||||||
run-name: Helmfile lint
|
run-name: Helmfile lint
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- 'main'
|
- 'main'
|
||||||
@@ -13,18 +12,11 @@ jobs:
|
|||||||
container:
|
container:
|
||||||
image: ghcr.io/helmfile/helmfile:latest
|
image: ghcr.io/helmfile/helmfile:latest
|
||||||
steps:
|
steps:
|
||||||
-
|
-
|
||||||
name: Checkout repository
|
uses: numerique-gouv/action-helmfile-lint@main
|
||||||
uses: actions/checkout@v4
|
with:
|
||||||
-
|
app-id: ${{ secrets.APP_ID }}
|
||||||
name: Helmfile lint
|
age-key: ${{ secrets.SOPS_PRIVATE }}
|
||||||
shell: bash
|
private-key: ${{ secrets.PRIVATE_KEY }}
|
||||||
run: |
|
helmfile-src: "src/helm"
|
||||||
set -e
|
repositories: "impress,secrets"
|
||||||
HELMFILE=src/helm/helmfile.yaml
|
|
||||||
environments=$(awk '/environments:/ {flag=1; next} flag && NF {print} !NF {flag=0}' "$HELMFILE" | grep -E '^[[:space:]]{2}[a-zA-Z]+' | sed 's/^[[:space:]]*//;s/:.*//')
|
|
||||||
for env in $environments; do
|
|
||||||
echo "################### $env lint ###################"
|
|
||||||
helmfile -e $env -f $HELMFILE lint || exit 1
|
|
||||||
echo -e "\n"
|
|
||||||
done
|
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ run-name: Release Chart
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
paths:
|
|
||||||
- src/helm/impress/**
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
@@ -27,8 +25,12 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
|
||||||
- name: Publish Helm charts
|
- name: Run chart-releaser
|
||||||
uses: numerique-gouv/helm-gh-pages@add-overwrite-option
|
uses: helm/chart-releaser-action@v1.6.0
|
||||||
with:
|
with:
|
||||||
charts_dir: ./src/helm
|
charts_dir: ./src/helm
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
skip_existing: True
|
||||||
|
mark_as_latest: False
|
||||||
|
env:
|
||||||
|
CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
|
CR_GIT_REPO: numerique-gouv/helm-repo
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "secrets"]
|
||||||
|
path = secrets
|
||||||
|
url = ../secrets
|
||||||
|
|||||||
+3
-24
@@ -11,23 +11,7 @@ and this project adheres to
|
|||||||
|
|
||||||
## Added
|
## Added
|
||||||
|
|
||||||
- 🔧(backend) add option to configure list of essential OIDC claims #525 & #531
|
🔧(helm) add option to disable default tls setting by @dominikkaminski #519
|
||||||
- 🔧(helm) add option to disable default tls setting by @dominikkaminski #519
|
|
||||||
- 💄(frontend) Add left panel #420
|
|
||||||
- 💄(frontend) add filtering to left panel #475
|
|
||||||
- ✨(frontend) new share modal ui #489
|
|
||||||
- ✨(frontend) add favorite feature #515
|
|
||||||
|
|
||||||
## Changed
|
|
||||||
|
|
||||||
- 🏗️(yjs-server) organize yjs server #528
|
|
||||||
- ♻️(frontend) better separation collaboration process #528
|
|
||||||
- 💄(frontend) updating the header and leftpanel for responsive #421
|
|
||||||
- 💄(frontend) update DocsGrid component #431
|
|
||||||
- 💄(frontend) update DocsGridOptions component #432
|
|
||||||
- 💄(frontend) update DocHeader ui #446
|
|
||||||
- 💄(frontend) update doc versioning ui #463
|
|
||||||
- 💄(frontend) update doc summary ui #473
|
|
||||||
|
|
||||||
|
|
||||||
## [1.10.0] - 2024-12-17
|
## [1.10.0] - 2024-12-17
|
||||||
@@ -47,11 +31,6 @@ and this project adheres to
|
|||||||
- ⚡️(e2e) reduce flakiness on e2e tests #511
|
- ⚡️(e2e) reduce flakiness on e2e tests #511
|
||||||
|
|
||||||
|
|
||||||
## Fixed
|
|
||||||
- 🐛(frontend) update doc editor height #481
|
|
||||||
- 💄(frontend) add doc search #485
|
|
||||||
|
|
||||||
|
|
||||||
## [1.9.0] - 2024-12-11
|
## [1.9.0] - 2024-12-11
|
||||||
|
|
||||||
## Added
|
## Added
|
||||||
@@ -197,7 +176,7 @@ and this project adheres to
|
|||||||
|
|
||||||
- 🛂(frontend) match email if no existing user matches the sub
|
- 🛂(frontend) match email if no existing user matches the sub
|
||||||
- 🐛(backend) gitlab oicd userinfo endpoint #232
|
- 🐛(backend) gitlab oicd userinfo endpoint #232
|
||||||
- 🛂(frontend) redirect to the OIDC when private doc and unauthenticated #292
|
- 🛂(frontend) redirect to the OIDC when private doc and unauthentified #292
|
||||||
- ♻️(backend) getting list of document versions available for a user #258
|
- ♻️(backend) getting list of document versions available for a user #258
|
||||||
- 🔧(backend) fix configuration to avoid different ssl warning #297
|
- 🔧(backend) fix configuration to avoid different ssl warning #297
|
||||||
- 🐛(frontend) fix editor break line not working #302
|
- 🐛(frontend) fix editor break line not working #302
|
||||||
@@ -326,7 +305,7 @@ and this project adheres to
|
|||||||
- ⚡️(e2e) unique login between tests (#80)
|
- ⚡️(e2e) unique login between tests (#80)
|
||||||
- ⚡️(CI) improve e2e job (#86)
|
- ⚡️(CI) improve e2e job (#86)
|
||||||
- ♻️(frontend) improve the error and message info ui (#93)
|
- ♻️(frontend) improve the error and message info ui (#93)
|
||||||
- ✏️(frontend) change all occurrences of pad to doc (#99)
|
- ✏️(frontend) change all occurences of pad to doc (#99)
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
curl https://raw.githubusercontent.com/numerique-gouv/tools/refs/heads/main/kind/create_cluster.sh | bash -s -- impress
|
curl https://raw.githubusercontent.com/numerique-gouv/tools/refs/heads/main/kind/create_cluster.sh | bash -s -- impress
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Installation on a k8s cluster
|
# Installation on a k8s cluster
|
||||||
|
|
||||||
This document is a step-by-step guide that describes how to install Docs on a k8s cluster without AI features. It's a teaching document to learn how it's work. It needs to be adapt for production environment.
|
This document is a step-by-step guide that describes how to install Docs on a k8s cluster without AI features.
|
||||||
|
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
@@ -194,7 +194,7 @@ redis-master-0 1/1 Running 0 10m
|
|||||||
Now you are ready to deploy Impress without AI. AI requiered more dependancies (openai API). To deploy impress you need to provide all previous informations to the helm chart.
|
Now you are ready to deploy Impress without AI. AI requiered more dependancies (openai API). To deploy impress you need to provide all previous informations to the helm chart.
|
||||||
|
|
||||||
```
|
```
|
||||||
$ helm repo add impress https://suitenumerique.github.io/docs/
|
$ helm repo add impress https://numerique-gouv.github.io/impress/
|
||||||
$ helm repo update
|
$ helm repo update
|
||||||
$ helm install impress impress/docs -f examples/impress.values.yaml
|
$ helm install impress impress/docs -f examples/impress.values.yaml
|
||||||
$ kubectl get po
|
$ kubectl get po
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ Whenever we are cooking a new release (e.g. `4.18.1`) we should follow a standar
|
|||||||
2. Bump the release number for backend project, frontend projects, and Helm files:
|
2. Bump the release number for backend project, frontend projects, and Helm files:
|
||||||
|
|
||||||
- for backend, update the version number by hand in `pyproject.toml`,
|
- for backend, update the version number by hand in `pyproject.toml`,
|
||||||
- for each project (`src/frontend`, `src/frontend/apps/*`, `src/frontend/packages/*`, `src/mail`), run `yarn version --new-version --no-git-tag-version 4.18.1` in their directory. This will update their `package.json` for you,
|
- for each projects (`src/frontend`, `src/frontend/apps/*`, `src/frontend/packages/*`, `src/mail`), run `yarn version --new-version --no-git-tag-version 4.18.1` in their directory. This will update their `package.json` for you,
|
||||||
- for Helm, update Docker image tag in files located at `src/helm/env.d` for both `preprod` and `production` environments:
|
- for Helm, update Docker image tag in files located at `src/helm/env.d` for both `preprod` and `production` environments:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
Submodule
+1
Submodule secrets added at 38594182e8
@@ -140,7 +140,6 @@ class UserViewSet(
|
|||||||
permission_classes = [permissions.IsSelf]
|
permission_classes = [permissions.IsSelf]
|
||||||
queryset = models.User.objects.all()
|
queryset = models.User.objects.all()
|
||||||
serializer_class = serializers.UserSerializer
|
serializer_class = serializers.UserSerializer
|
||||||
ordering = ["-created_at"]
|
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
"""
|
"""
|
||||||
@@ -630,7 +629,7 @@ class DocumentViewSet(
|
|||||||
nginx.ingress.kubernetes.io/auth-url annotation to understand how the Nginx ingress
|
nginx.ingress.kubernetes.io/auth-url annotation to understand how the Nginx ingress
|
||||||
is configured to do this.
|
is configured to do this.
|
||||||
|
|
||||||
Based on the original url and the logged-in user, we must decide if we authorize Nginx
|
Based on the original url and the logged in user, we must decide if we authorize Nginx
|
||||||
to let this request go through (by returning a 200 code) or if we block it (by returning
|
to let this request go through (by returning a 200 code) or if we block it (by returning
|
||||||
a 403 error). Note that we return 403 errors without any further details for security
|
a 403 error). Note that we return 403 errors without any further details for security
|
||||||
reasons.
|
reasons.
|
||||||
@@ -835,7 +834,7 @@ class DocumentAccessViewSet(
|
|||||||
serializer_class = serializers.DocumentAccessSerializer
|
serializer_class = serializers.DocumentAccessSerializer
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
"""Add new access to the document and email the new added user."""
|
"""Add a new access to the document and send an email to the new added user."""
|
||||||
access = serializer.save()
|
access = serializer.save()
|
||||||
language = self.request.headers.get("Content-Language", "en-us")
|
language = self.request.headers.get("Content-Language", "en-us")
|
||||||
|
|
||||||
@@ -847,7 +846,7 @@ class DocumentAccessViewSet(
|
|||||||
)
|
)
|
||||||
|
|
||||||
def perform_update(self, serializer):
|
def perform_update(self, serializer):
|
||||||
"""Update access to the document and notify the collaboration server."""
|
"""Update an access to the document and notify the collaboration server."""
|
||||||
access = serializer.save()
|
access = serializer.save()
|
||||||
|
|
||||||
access_user_id = None
|
access_user_id = None
|
||||||
@@ -860,7 +859,7 @@ class DocumentAccessViewSet(
|
|||||||
)
|
)
|
||||||
|
|
||||||
def perform_destroy(self, instance):
|
def perform_destroy(self, instance):
|
||||||
"""Delete access to the document and notify the collaboration server."""
|
"""Delete an access to the document and notify the collaboration server."""
|
||||||
instance.delete()
|
instance.delete()
|
||||||
|
|
||||||
# Notify collaboration server about the access removed
|
# Notify collaboration server about the access removed
|
||||||
@@ -1099,7 +1098,7 @@ class InvitationViewset(
|
|||||||
return queryset
|
return queryset
|
||||||
|
|
||||||
def perform_create(self, serializer):
|
def perform_create(self, serializer):
|
||||||
"""Save invitation to a document then email the invited user."""
|
"""Save invitation to a document then send an email to the invited user."""
|
||||||
invitation = serializer.save()
|
invitation = serializer.save()
|
||||||
|
|
||||||
language = self.request.headers.get("Content-Language", "en-us")
|
language = self.request.headers.get("Content-Language", "en-us")
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
"""Authentication Backends for the Impress core app."""
|
"""Authentication Backends for the Impress core app."""
|
||||||
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@@ -13,8 +11,6 @@ from mozilla_django_oidc.auth import (
|
|||||||
|
|
||||||
from core.models import User
|
from core.models import User
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
||||||
"""Custom OpenID Connect (OIDC) Authentication Backend.
|
"""Custom OpenID Connect (OIDC) Authentication Backend.
|
||||||
@@ -63,29 +59,10 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
|||||||
|
|
||||||
return userinfo
|
return userinfo
|
||||||
|
|
||||||
def verify_claims(self, claims):
|
|
||||||
"""
|
|
||||||
Verify the presence of essential claims and the "sub" (which is mandatory as defined
|
|
||||||
by the OIDC specification) to decide if authentication should be allowed.
|
|
||||||
"""
|
|
||||||
essential_claims = settings.USER_OIDC_ESSENTIAL_CLAIMS
|
|
||||||
missing_claims = [claim for claim in essential_claims if claim not in claims]
|
|
||||||
|
|
||||||
if missing_claims:
|
|
||||||
logger.error("Missing essential claims: %s", missing_claims)
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
def get_or_create_user(self, access_token, id_token, payload):
|
def get_or_create_user(self, access_token, id_token, payload):
|
||||||
"""Return a User based on userinfo. Create a new user if no match is found."""
|
"""Return a User based on userinfo. Create a new user if no match is found."""
|
||||||
|
|
||||||
user_info = self.get_userinfo(access_token, id_token, payload)
|
user_info = self.get_userinfo(access_token, id_token, payload)
|
||||||
|
|
||||||
if not self.verify_claims(user_info):
|
|
||||||
raise SuspiciousOperation("Claims verification failed.")
|
|
||||||
|
|
||||||
sub = user_info["sub"]
|
|
||||||
email = user_info.get("email")
|
email = user_info.get("email")
|
||||||
|
|
||||||
# Get user's full name from OIDC fields defined in settings
|
# Get user's full name from OIDC fields defined in settings
|
||||||
@@ -98,6 +75,12 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
|||||||
"short_name": short_name,
|
"short_name": short_name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sub = user_info.get("sub")
|
||||||
|
if not sub:
|
||||||
|
raise SuspiciousOperation(
|
||||||
|
_("User info contained no recognizable user identification")
|
||||||
|
)
|
||||||
|
|
||||||
user = self.get_existing_user(sub, email)
|
user = self.get_existing_user(sub, email)
|
||||||
|
|
||||||
if user:
|
if user:
|
||||||
@@ -118,13 +101,15 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
|||||||
return full_name or None
|
return full_name or None
|
||||||
|
|
||||||
def get_existing_user(self, sub, email):
|
def get_existing_user(self, sub, email):
|
||||||
"""Fetch an existing user by sub (or email as a fallback respecting fallback setting."""
|
"""Fetch existing user by sub or email."""
|
||||||
try:
|
try:
|
||||||
return User.objects.get(sub=sub)
|
return User.objects.get(sub=sub)
|
||||||
except User.DoesNotExist:
|
except User.DoesNotExist:
|
||||||
if email and settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION:
|
if email and settings.OIDC_FALLBACK_TO_EMAIL_FOR_IDENTIFICATION:
|
||||||
return User.objects.filter(email=email).first()
|
try:
|
||||||
|
return User.objects.get(email=email)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
pass
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_user_if_needed(self, user, claims):
|
def update_user_if_needed(self, user, claims):
|
||||||
@@ -134,4 +119,4 @@ class OIDCAuthenticationBackend(MozillaOIDCAuthenticationBackend):
|
|||||||
)
|
)
|
||||||
if has_changed:
|
if has_changed:
|
||||||
updated_claims = {key: value for key, value in claims.items() if value}
|
updated_claims = {key: value for key, value in claims.items() if value}
|
||||||
self.UserModel.objects.filter(id=user.id).update(**updated_claims)
|
self.UserModel.objects.filter(sub=user.sub).update(**updated_claims)
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from mozilla_django_oidc.urls import urlpatterns as mozilla_oidc_urls
|
from mozilla_django_oidc.urls import urlpatterns as mozzila_oidc_urls
|
||||||
|
|
||||||
from .views import OIDCLogoutCallbackView, OIDCLogoutView
|
from .views import OIDCLogoutCallbackView, OIDCLogoutView
|
||||||
|
|
||||||
@@ -14,5 +14,5 @@ urlpatterns = [
|
|||||||
OIDCLogoutCallbackView.as_view(),
|
OIDCLogoutCallbackView.as_view(),
|
||||||
name="oidc_logout_callback",
|
name="oidc_logout_callback",
|
||||||
),
|
),
|
||||||
*mozilla_oidc_urls,
|
*mozzila_oidc_urls,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ class UserFactory(factory.django.DjangoModelFactory):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = models.User
|
model = models.User
|
||||||
skip_postgeneration_save = True
|
|
||||||
|
|
||||||
sub = factory.Sequence(lambda n: f"user{n!s}")
|
sub = factory.Sequence(lambda n: f"user{n!s}")
|
||||||
email = factory.Faker("email")
|
email = factory.Faker("email")
|
||||||
@@ -37,8 +36,6 @@ class UserFactory(factory.django.DjangoModelFactory):
|
|||||||
if create and (extracted is True):
|
if create and (extracted is True):
|
||||||
UserDocumentAccessFactory(user=self, role="owner")
|
UserDocumentAccessFactory(user=self, role="owner")
|
||||||
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
@factory.post_generation
|
@factory.post_generation
|
||||||
def with_owned_template(self, create, extracted, **kwargs):
|
def with_owned_template(self, create, extracted, **kwargs):
|
||||||
"""
|
"""
|
||||||
@@ -48,8 +45,6 @@ class UserFactory(factory.django.DjangoModelFactory):
|
|||||||
if create and (extracted is True):
|
if create and (extracted is True):
|
||||||
UserTemplateAccessFactory(user=self, role="owner")
|
UserTemplateAccessFactory(user=self, role="owner")
|
||||||
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
|
|
||||||
class DocumentFactory(factory.django.DjangoModelFactory):
|
class DocumentFactory(factory.django.DjangoModelFactory):
|
||||||
"""A factory to create documents"""
|
"""A factory to create documents"""
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Generated by Django 5.0.3 on 2024-05-28 20:29
|
# Generated by Django 5.0.3 on 2024-05-28 20:29
|
||||||
|
|
||||||
|
import django.contrib.auth.models
|
||||||
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import timezone_field.fields
|
import timezone_field.fields
|
||||||
import uuid
|
import uuid
|
||||||
@@ -143,7 +145,7 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
migrations.AddConstraint(
|
migrations.AddConstraint(
|
||||||
model_name='documentaccess',
|
model_name='documentaccess',
|
||||||
constraint=models.CheckConstraint(condition=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_document_access_either_user_or_team', violation_error_message='Either user or team must be set, not both.'),
|
constraint=models.CheckConstraint(check=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_document_access_either_user_or_team', violation_error_message='Either user or team must be set, not both.'),
|
||||||
),
|
),
|
||||||
migrations.AddConstraint(
|
migrations.AddConstraint(
|
||||||
model_name='invitation',
|
model_name='invitation',
|
||||||
@@ -159,6 +161,6 @@ class Migration(migrations.Migration):
|
|||||||
),
|
),
|
||||||
migrations.AddConstraint(
|
migrations.AddConstraint(
|
||||||
model_name='templateaccess',
|
model_name='templateaccess',
|
||||||
constraint=models.CheckConstraint(condition=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_template_access_either_user_or_team', violation_error_message='Either user or team must be set, not both.'),
|
constraint=models.CheckConstraint(check=models.Q(models.Q(('team', ''), ('user__isnull', False)), models.Q(('team__gt', ''), ('user__isnull', True)), _connector='OR'), name='check_template_access_either_user_or_team', violation_error_message='Either user or team must be set, not both.'),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
# Generated by Django 5.1.4 on 2025-01-13 22:57
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('core', '0012_make_document_creator_and_invitation_issuer_optional'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterModelOptions(
|
|
||||||
name='user',
|
|
||||||
options={'ordering': ('-created_at',), 'verbose_name': 'user', 'verbose_name_plural': 'users'},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -155,7 +155,7 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
|||||||
email = models.EmailField(_("identity email address"), blank=True, null=True)
|
email = models.EmailField(_("identity email address"), blank=True, null=True)
|
||||||
|
|
||||||
# Unlike the "email" field which stores the email coming from the OIDC token, this field
|
# Unlike the "email" field which stores the email coming from the OIDC token, this field
|
||||||
# stores the email used by staff users to log in to the admin site
|
# stores the email used by staff users to login to the admin site
|
||||||
admin_email = models.EmailField(
|
admin_email = models.EmailField(
|
||||||
_("admin email address"), unique=True, blank=True, null=True
|
_("admin email address"), unique=True, blank=True, null=True
|
||||||
)
|
)
|
||||||
@@ -199,7 +199,6 @@ class User(AbstractBaseUser, BaseModel, auth_models.PermissionsMixin):
|
|||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
db_table = "impress_user"
|
db_table = "impress_user"
|
||||||
ordering = ("-created_at",)
|
|
||||||
verbose_name = _("user")
|
verbose_name = _("user")
|
||||||
verbose_name_plural = _("users")
|
verbose_name_plural = _("users")
|
||||||
|
|
||||||
@@ -696,7 +695,7 @@ class DocumentAccess(BaseAccess):
|
|||||||
violation_error_message=_("This team is already in this document."),
|
violation_error_message=_("This team is already in this document."),
|
||||||
),
|
),
|
||||||
models.CheckConstraint(
|
models.CheckConstraint(
|
||||||
condition=models.Q(user__isnull=False, team="")
|
check=models.Q(user__isnull=False, team="")
|
||||||
| models.Q(user__isnull=True, team__gt=""),
|
| models.Q(user__isnull=True, team__gt=""),
|
||||||
name="check_document_access_either_user_or_team",
|
name="check_document_access_either_user_or_team",
|
||||||
violation_error_message=_("Either user or team must be set, not both."),
|
violation_error_message=_("Either user or team must be set, not both."),
|
||||||
@@ -761,7 +760,7 @@ class Template(BaseModel):
|
|||||||
"""
|
"""
|
||||||
document_html = weasyprint.HTML(
|
document_html = weasyprint.HTML(
|
||||||
string=DjangoTemplate(self.code).render(
|
string=DjangoTemplate(self.code).render(
|
||||||
Context({"body": html.format_html("{}", body_html), **metadata})
|
Context({"body": html.format_html(body_html), **metadata})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
css = weasyprint.CSS(
|
css = weasyprint.CSS(
|
||||||
@@ -780,7 +779,7 @@ class Template(BaseModel):
|
|||||||
Generate and return a docx document wrapped around the current template
|
Generate and return a docx document wrapped around the current template
|
||||||
"""
|
"""
|
||||||
template_string = DjangoTemplate(self.code).render(
|
template_string = DjangoTemplate(self.code).render(
|
||||||
Context({"body": html.format_html("{}", body_html), **metadata})
|
Context({"body": html.format_html(body_html), **metadata})
|
||||||
)
|
)
|
||||||
|
|
||||||
html_string = f"""
|
html_string = f"""
|
||||||
@@ -798,6 +797,7 @@ class Template(BaseModel):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
reference_docx = "core/static/reference.docx"
|
reference_docx = "core/static/reference.docx"
|
||||||
|
output = BytesIO()
|
||||||
|
|
||||||
# Convert the HTML to a temporary docx file
|
# Convert the HTML to a temporary docx file
|
||||||
with tempfile.NamedTemporaryFile(suffix=".docx", prefix="docx_") as tmp_file:
|
with tempfile.NamedTemporaryFile(suffix=".docx", prefix="docx_") as tmp_file:
|
||||||
@@ -884,7 +884,7 @@ class TemplateAccess(BaseAccess):
|
|||||||
violation_error_message=_("This team is already in this template."),
|
violation_error_message=_("This team is already in this template."),
|
||||||
),
|
),
|
||||||
models.CheckConstraint(
|
models.CheckConstraint(
|
||||||
condition=models.Q(user__isnull=False, team="")
|
check=models.Q(user__isnull=False, team="")
|
||||||
| models.Q(user__isnull=True, team__gt=""),
|
| models.Q(user__isnull=True, team__gt=""),
|
||||||
name="check_template_access_either_user_or_team",
|
name="check_template_access_either_user_or_team",
|
||||||
violation_error_message=_("Either user or team must be set, not both."),
|
violation_error_message=_("Either user or team must be set, not both."),
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class CollaborationService:
|
|||||||
def reset_connections(self, room, user_id=None):
|
def reset_connections(self, room, user_id=None):
|
||||||
"""
|
"""
|
||||||
Reset connections of a room in the collaboration server.
|
Reset connections of a room in the collaboration server.
|
||||||
Resetting a connection means that the user will be disconnected and will
|
Reseting a connection means that the user will be disconnected and will
|
||||||
have to reconnect to the collaboration server, with updated rights.
|
have to reconnect to the collaboration server, with updated rights.
|
||||||
"""
|
"""
|
||||||
endpoint = "reset-connections"
|
endpoint = "reset-connections"
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
"""Unit tests for the Authentication Backends."""
|
"""Unit tests for the Authentication Backends."""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from logging import Logger
|
|
||||||
from unittest import mock
|
|
||||||
|
|
||||||
from django.core.exceptions import SuspiciousOperation
|
from django.core.exceptions import SuspiciousOperation
|
||||||
from django.test.utils import override_settings
|
from django.test.utils import override_settings
|
||||||
@@ -130,12 +128,11 @@ def test_authentication_getter_existing_user_with_email(
|
|||||||
("Jack", "Duy", "jack.duy@example.com"),
|
("Jack", "Duy", "jack.duy@example.com"),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_authentication_getter_existing_user_change_fields_sub(
|
def test_authentication_getter_existing_user_change_fields(
|
||||||
first_name, last_name, email, django_assert_num_queries, monkeypatch
|
first_name, last_name, email, django_assert_num_queries, monkeypatch
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
It should update the email or name fields on the user when they change
|
It should update the email or name fields on the user when they change.
|
||||||
and the user was identified by its "sub".
|
|
||||||
"""
|
"""
|
||||||
klass = OIDCAuthenticationBackend()
|
klass = OIDCAuthenticationBackend()
|
||||||
user = UserFactory(
|
user = UserFactory(
|
||||||
@@ -165,48 +162,6 @@ def test_authentication_getter_existing_user_change_fields_sub(
|
|||||||
assert user.short_name == first_name
|
assert user.short_name == first_name
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"first_name, last_name, email",
|
|
||||||
[
|
|
||||||
("Jack", "Doe", "john.doe@example.com"),
|
|
||||||
("John", "Duy", "john.doe@example.com"),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
def test_authentication_getter_existing_user_change_fields_email(
|
|
||||||
first_name, last_name, email, django_assert_num_queries, monkeypatch
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
It should update the name fields on the user when they change
|
|
||||||
and the user was identified by its "email" as fallback.
|
|
||||||
"""
|
|
||||||
klass = OIDCAuthenticationBackend()
|
|
||||||
user = UserFactory(
|
|
||||||
full_name="John Doe", short_name="John", email="john.doe@example.com"
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_userinfo_mocked(*args):
|
|
||||||
return {
|
|
||||||
"sub": "123",
|
|
||||||
"email": user.email,
|
|
||||||
"first_name": first_name,
|
|
||||||
"last_name": last_name,
|
|
||||||
}
|
|
||||||
|
|
||||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
|
||||||
|
|
||||||
# One and only one additional update query when a field has changed
|
|
||||||
with django_assert_num_queries(3):
|
|
||||||
authenticated_user = klass.get_or_create_user(
|
|
||||||
access_token="test-token", id_token=None, payload=None
|
|
||||||
)
|
|
||||||
|
|
||||||
assert user == authenticated_user
|
|
||||||
user.refresh_from_db()
|
|
||||||
assert user.email == email
|
|
||||||
assert user.full_name == f"{first_name:s} {last_name:s}"
|
|
||||||
assert user.short_name == first_name
|
|
||||||
|
|
||||||
|
|
||||||
def test_authentication_getter_new_user_no_email(monkeypatch):
|
def test_authentication_getter_new_user_no_email(monkeypatch):
|
||||||
"""
|
"""
|
||||||
If no user matches the user's info sub, a user should be created.
|
If no user matches the user's info sub, a user should be created.
|
||||||
@@ -258,6 +213,29 @@ def test_authentication_getter_new_user_with_email(monkeypatch):
|
|||||||
assert models.User.objects.count() == 1
|
assert models.User.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_authentication_getter_invalid_token(django_assert_num_queries, monkeypatch):
|
||||||
|
"""The user's info doesn't contain a sub."""
|
||||||
|
klass = OIDCAuthenticationBackend()
|
||||||
|
|
||||||
|
def get_userinfo_mocked(*args):
|
||||||
|
return {
|
||||||
|
"test": "123",
|
||||||
|
}
|
||||||
|
|
||||||
|
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
||||||
|
|
||||||
|
with (
|
||||||
|
django_assert_num_queries(0),
|
||||||
|
pytest.raises(
|
||||||
|
SuspiciousOperation,
|
||||||
|
match="User info contained no recognizable user identification",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
|
||||||
|
|
||||||
|
assert models.User.objects.exists() is False
|
||||||
|
|
||||||
|
|
||||||
@override_settings(OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo")
|
@override_settings(OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo")
|
||||||
@responses.activate
|
@responses.activate
|
||||||
def test_authentication_get_userinfo_json_response():
|
def test_authentication_get_userinfo_json_response():
|
||||||
@@ -363,7 +341,7 @@ def test_authentication_getter_existing_disabled_user_via_email(
|
|||||||
django_assert_num_queries, monkeypatch
|
django_assert_num_queries, monkeypatch
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
If an existing user does not match the sub but matches the email and is disabled,
|
If an existing user does not matches the sub but matches the email and is disabled,
|
||||||
an error should be raised and a user should not be created.
|
an error should be raised and a user should not be created.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -387,102 +365,3 @@ def test_authentication_getter_existing_disabled_user_via_email(
|
|||||||
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
|
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
|
||||||
|
|
||||||
assert models.User.objects.count() == 1
|
assert models.User.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
# Essential claims
|
|
||||||
|
|
||||||
|
|
||||||
def test_authentication_verify_claims_default(django_assert_num_queries, monkeypatch):
|
|
||||||
"""The sub claim should be mandatory by default."""
|
|
||||||
klass = OIDCAuthenticationBackend()
|
|
||||||
|
|
||||||
def get_userinfo_mocked(*args):
|
|
||||||
return {
|
|
||||||
"test": "123",
|
|
||||||
}
|
|
||||||
|
|
||||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
|
||||||
|
|
||||||
with (
|
|
||||||
django_assert_num_queries(0),
|
|
||||||
pytest.raises(
|
|
||||||
KeyError,
|
|
||||||
match="sub",
|
|
||||||
),
|
|
||||||
):
|
|
||||||
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
|
|
||||||
|
|
||||||
assert models.User.objects.exists() is False
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
|
||||||
"essential_claims, missing_claims",
|
|
||||||
[
|
|
||||||
(["email", "sub"], ["email"]),
|
|
||||||
(["Email", "sub"], ["Email"]), # Case sensitivity
|
|
||||||
],
|
|
||||||
)
|
|
||||||
@override_settings(OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo")
|
|
||||||
@mock.patch.object(Logger, "error")
|
|
||||||
def test_authentication_verify_claims_essential_missing(
|
|
||||||
mock_logger,
|
|
||||||
essential_claims,
|
|
||||||
missing_claims,
|
|
||||||
django_assert_num_queries,
|
|
||||||
monkeypatch,
|
|
||||||
):
|
|
||||||
"""Ensure SuspiciousOperation is raised if essential claims are missing."""
|
|
||||||
|
|
||||||
klass = OIDCAuthenticationBackend()
|
|
||||||
|
|
||||||
def get_userinfo_mocked(*args):
|
|
||||||
return {
|
|
||||||
"sub": "123",
|
|
||||||
"last_name": "Doe",
|
|
||||||
}
|
|
||||||
|
|
||||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
|
||||||
|
|
||||||
with (
|
|
||||||
django_assert_num_queries(0),
|
|
||||||
pytest.raises(
|
|
||||||
SuspiciousOperation,
|
|
||||||
match="Claims verification failed",
|
|
||||||
),
|
|
||||||
override_settings(USER_OIDC_ESSENTIAL_CLAIMS=essential_claims),
|
|
||||||
):
|
|
||||||
klass.get_or_create_user(access_token="test-token", id_token=None, payload=None)
|
|
||||||
|
|
||||||
assert models.User.objects.exists() is False
|
|
||||||
mock_logger.assert_called_once_with("Missing essential claims: %s", missing_claims)
|
|
||||||
|
|
||||||
|
|
||||||
@override_settings(
|
|
||||||
OIDC_OP_USER_ENDPOINT="http://oidc.endpoint.test/userinfo",
|
|
||||||
USER_OIDC_ESSENTIAL_CLAIMS=["email", "last_name"],
|
|
||||||
)
|
|
||||||
def test_authentication_verify_claims_success(django_assert_num_queries, monkeypatch):
|
|
||||||
"""Ensure user is authenticated when all essential claims are present."""
|
|
||||||
|
|
||||||
klass = OIDCAuthenticationBackend()
|
|
||||||
|
|
||||||
def get_userinfo_mocked(*args):
|
|
||||||
return {
|
|
||||||
"email": "john.doe@example.com",
|
|
||||||
"last_name": "Doe",
|
|
||||||
"sub": "123",
|
|
||||||
}
|
|
||||||
|
|
||||||
monkeypatch.setattr(OIDCAuthenticationBackend, "get_userinfo", get_userinfo_mocked)
|
|
||||||
|
|
||||||
with django_assert_num_queries(6):
|
|
||||||
user = klass.get_or_create_user(
|
|
||||||
access_token="test-token", id_token=None, payload=None
|
|
||||||
)
|
|
||||||
|
|
||||||
assert models.User.objects.filter(id=user.id).exists()
|
|
||||||
|
|
||||||
assert user.sub == "123"
|
|
||||||
assert user.full_name == "Doe"
|
|
||||||
assert user.short_name is None
|
|
||||||
assert user.email == "john.doe@example.com"
|
|
||||||
|
|||||||
@@ -698,7 +698,7 @@ def test_api_document_accesses_delete_administrators_except_owners(
|
|||||||
mock_reset_connections, # pylint: disable=redefined-outer-name
|
mock_reset_connections, # pylint: disable=redefined-outer-name
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Users who are administrators in a document should be allowed to delete access
|
Users who are administrators in a document should be allowed to delete an access
|
||||||
from the document provided it is not ownership.
|
from the document provided it is not ownership.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
|
|||||||
@@ -285,7 +285,7 @@ def test_api_document_versions_retrieve_authenticated_related(via, mock_user_tea
|
|||||||
assert response.status_code == 404
|
assert response.status_code == 404
|
||||||
|
|
||||||
# Create a new version should not make it available to the user because
|
# Create a new version should not make it available to the user because
|
||||||
# only the current version is available to the user, but it is excluded
|
# only the current version is available to the user but it is excluded
|
||||||
# from the list
|
# from the list
|
||||||
document.content = "new content 1"
|
document.content = "new content 1"
|
||||||
document.save()
|
document.save()
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ def test_api_documents_ai_transform_authenticated_forbidden(reach, role):
|
|||||||
@patch("openai.resources.chat.completions.Completions.create")
|
@patch("openai.resources.chat.completions.Completions.create")
|
||||||
def test_api_documents_ai_transform_authenticated_success(mock_create, reach, role):
|
def test_api_documents_ai_transform_authenticated_success(mock_create, reach, role):
|
||||||
"""
|
"""
|
||||||
Authenticated who are not related to a document should be able to request AI transform
|
Autenticated who are not related to a document should be able to request AI transform
|
||||||
if the link reach and role permit it.
|
if the link reach and role permit it.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ def test_api_documents_ai_translate_authenticated_forbidden(reach, role):
|
|||||||
@patch("openai.resources.chat.completions.Completions.create")
|
@patch("openai.resources.chat.completions.Completions.create")
|
||||||
def test_api_documents_ai_translate_authenticated_success(mock_create, reach, role):
|
def test_api_documents_ai_translate_authenticated_success(mock_create, reach, role):
|
||||||
"""
|
"""
|
||||||
Authenticated who are not related to a document should be able to request AI translate
|
Autenticated who are not related to a document should be able to request AI translate
|
||||||
if the link reach and role permit it.
|
if the link reach and role permit it.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ def test_api_documents_attachment_upload_authenticated_forbidden(reach, role):
|
|||||||
)
|
)
|
||||||
def test_api_documents_attachment_upload_authenticated_success(reach, role):
|
def test_api_documents_attachment_upload_authenticated_success(reach, role):
|
||||||
"""
|
"""
|
||||||
Authenticated who are not related to a document should be able to upload a file
|
Autenticated who are not related to a document should be able to upload a file
|
||||||
if the link reach and role permit it.
|
if the link reach and role permit it.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
@@ -225,7 +225,7 @@ def test_api_documents_attachment_upload_invalid(client):
|
|||||||
|
|
||||||
|
|
||||||
def test_api_documents_attachment_upload_size_limit_exceeded(settings):
|
def test_api_documents_attachment_upload_size_limit_exceeded(settings):
|
||||||
"""The uploaded file should not exceed the maximum size in settings."""
|
"""The uploaded file should not exceeed the maximum size in settings."""
|
||||||
settings.DOCUMENT_IMAGE_MAX_SIZE = 1048576 # 1 MB for test
|
settings.DOCUMENT_IMAGE_MAX_SIZE = 1048576 # 1 MB for test
|
||||||
|
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ def test_api_documents_media_auth_authenticated_restricted():
|
|||||||
@pytest.mark.parametrize("via", VIA)
|
@pytest.mark.parametrize("via", VIA)
|
||||||
def test_api_documents_media_auth_related(via, mock_user_teams):
|
def test_api_documents_media_auth_related(via, mock_user_teams):
|
||||||
"""
|
"""
|
||||||
Users who have specific access to a document, whatever the role, should be able to
|
Users who have a specific access to a document, whatever the role, should be able to
|
||||||
retrieve related attachments.
|
retrieve related attachments.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
|
|||||||
@@ -647,7 +647,7 @@ def test_api_template_accesses_delete_administrators_except_owners(
|
|||||||
via, mock_user_teams
|
via, mock_user_teams
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Users who are administrators in a template should be allowed to delete access
|
Users who are administrators in a template should be allowed to delete an access
|
||||||
from the template provided it is not ownership.
|
from the template provided it is not ownership.
|
||||||
"""
|
"""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ def test_models_documents_file_key():
|
|||||||
def test_models_documents_get_abilities_forbidden(is_authenticated, reach, role):
|
def test_models_documents_get_abilities_forbidden(is_authenticated, reach, role):
|
||||||
"""
|
"""
|
||||||
Check abilities returned for a document giving insufficient roles to link holders
|
Check abilities returned for a document giving insufficient roles to link holders
|
||||||
i.e. anonymous users or authenticated users who have no specific role on the document.
|
i.e anonymous users or authenticated users who have no specific role on the document.
|
||||||
"""
|
"""
|
||||||
document = factories.DocumentFactory(link_reach=reach, link_role=role)
|
document = factories.DocumentFactory(link_reach=reach, link_role=role)
|
||||||
user = factories.UserFactory() if is_authenticated else AnonymousUser()
|
user = factories.UserFactory() if is_authenticated else AnonymousUser()
|
||||||
@@ -121,7 +121,7 @@ def test_models_documents_get_abilities_forbidden(is_authenticated, reach, role)
|
|||||||
def test_models_documents_get_abilities_reader(is_authenticated, reach):
|
def test_models_documents_get_abilities_reader(is_authenticated, reach):
|
||||||
"""
|
"""
|
||||||
Check abilities returned for a document giving reader role to link holders
|
Check abilities returned for a document giving reader role to link holders
|
||||||
i.e. anonymous users or authenticated users who have no specific role on the document.
|
i.e anonymous users or authenticated users who have no specific role on the document.
|
||||||
"""
|
"""
|
||||||
document = factories.DocumentFactory(link_reach=reach, link_role="reader")
|
document = factories.DocumentFactory(link_reach=reach, link_role="reader")
|
||||||
user = factories.UserFactory() if is_authenticated else AnonymousUser()
|
user = factories.UserFactory() if is_authenticated else AnonymousUser()
|
||||||
@@ -158,7 +158,7 @@ def test_models_documents_get_abilities_reader(is_authenticated, reach):
|
|||||||
def test_models_documents_get_abilities_editor(is_authenticated, reach):
|
def test_models_documents_get_abilities_editor(is_authenticated, reach):
|
||||||
"""
|
"""
|
||||||
Check abilities returned for a document giving editor role to link holders
|
Check abilities returned for a document giving editor role to link holders
|
||||||
i.e. anonymous users or authenticated users who have no specific role on the document.
|
i.e anonymous users or authenticated users who have no specific role on the document.
|
||||||
"""
|
"""
|
||||||
document = factories.DocumentFactory(link_reach=reach, link_role="editor")
|
document = factories.DocumentFactory(link_reach=reach, link_role="editor")
|
||||||
user = factories.UserFactory() if is_authenticated else AnonymousUser()
|
user = factories.UserFactory() if is_authenticated else AnonymousUser()
|
||||||
@@ -449,7 +449,7 @@ def test_models_documents__email_invitation__success():
|
|||||||
|
|
||||||
def test_models_documents__email_invitation__success_fr():
|
def test_models_documents__email_invitation__success_fr():
|
||||||
"""
|
"""
|
||||||
The email invitation is sent successfully in French.
|
The email invitation is sent successfully in french.
|
||||||
"""
|
"""
|
||||||
document = factories.DocumentFactory()
|
document = factories.DocumentFactory()
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ def test_models_users_id_unique():
|
|||||||
|
|
||||||
|
|
||||||
def test_models_users_send_mail_main_existing():
|
def test_models_users_send_mail_main_existing():
|
||||||
"""The 'email_user' method should send mail to the user's email address."""
|
"""The "email_user' method should send mail to the user's email address."""
|
||||||
user = factories.UserFactory()
|
user = factories.UserFactory()
|
||||||
|
|
||||||
with mock.patch("django.core.mail.send_mail") as mock_send:
|
with mock.patch("django.core.mail.send_mail") as mock_send:
|
||||||
@@ -37,7 +37,7 @@ def test_models_users_send_mail_main_existing():
|
|||||||
|
|
||||||
|
|
||||||
def test_models_users_send_mail_main_missing():
|
def test_models_users_send_mail_main_missing():
|
||||||
"""The 'email_user' method should fail if the user has no email address."""
|
"""The "email_user' method should fail if the user has no email address."""
|
||||||
user = factories.UserFactory(email=None)
|
user = factories.UserFactory(email=None)
|
||||||
|
|
||||||
with pytest.raises(ValueError) as excinfo:
|
with pytest.raises(ValueError) as excinfo:
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Test AI API endpoints in the impress core app.
|
Test ai API endpoints in the impress core app.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ class Command(BaseCommand):
|
|||||||
"""Define required arguments "email" and "password"."""
|
"""Define required arguments "email" and "password"."""
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--email",
|
"--email",
|
||||||
help="Email for the user.",
|
help=("Email for the user."),
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--password",
|
"--password",
|
||||||
|
|||||||
@@ -474,9 +474,6 @@ class Base(Configuration):
|
|||||||
environ_prefix=None,
|
environ_prefix=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
USER_OIDC_ESSENTIAL_CLAIMS = values.ListValue(
|
|
||||||
default=[], environ_name="USER_OIDC_ESSENTIAL_CLAIMS", environ_prefix=None
|
|
||||||
)
|
|
||||||
USER_OIDC_FIELDS_TO_FULLNAME = values.ListValue(
|
USER_OIDC_FIELDS_TO_FULLNAME = values.ListValue(
|
||||||
default=["first_name", "last_name"],
|
default=["first_name", "last_name"],
|
||||||
environ_name="USER_OIDC_FIELDS_TO_FULLNAME",
|
environ_name="USER_OIDC_FIELDS_TO_FULLNAME",
|
||||||
@@ -622,9 +619,8 @@ class Base(Configuration):
|
|||||||
release=get_release(),
|
release=get_release(),
|
||||||
integrations=[DjangoIntegration()],
|
integrations=[DjangoIntegration()],
|
||||||
)
|
)
|
||||||
# Add the application name to the Sentry scope
|
with sentry_sdk.configure_scope() as scope:
|
||||||
scope = sentry_sdk.get_global_scope()
|
scope.set_extra("application", "backend")
|
||||||
scope.set_tag("application", "backend")
|
|
||||||
|
|
||||||
|
|
||||||
class Build(Base):
|
class Build(Base):
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ license = { file = "LICENSE" }
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"boto3==1.35.90",
|
"boto3==1.35.81",
|
||||||
"Brotli==1.1.0",
|
"Brotli==1.1.0",
|
||||||
"celery[redis]==5.4.0",
|
"celery[redis]==5.4.0",
|
||||||
"django-configurations==2.5.1",
|
"django-configurations==2.5.1",
|
||||||
@@ -47,7 +47,7 @@ dependencies = [
|
|||||||
"jsonschema==4.23.0",
|
"jsonschema==4.23.0",
|
||||||
"markdown==3.7",
|
"markdown==3.7",
|
||||||
"nested-multipart-parser==1.5.0",
|
"nested-multipart-parser==1.5.0",
|
||||||
"openai==1.58.1",
|
"openai==1.57.4",
|
||||||
"psycopg[binary]==3.2.3",
|
"psycopg[binary]==3.2.3",
|
||||||
"PyJWT==2.10.1",
|
"PyJWT==2.10.1",
|
||||||
"pypandoc==1.14",
|
"pypandoc==1.14",
|
||||||
@@ -73,17 +73,17 @@ dev = [
|
|||||||
"drf-spectacular-sidecar==2024.12.1",
|
"drf-spectacular-sidecar==2024.12.1",
|
||||||
"freezegun==1.5.1",
|
"freezegun==1.5.1",
|
||||||
"ipdb==0.13.13",
|
"ipdb==0.13.13",
|
||||||
"ipython==8.31.0",
|
"ipython==8.30.0",
|
||||||
"pyfakefs==5.7.3",
|
"pyfakefs==5.7.3",
|
||||||
"pylint-django==2.6.1",
|
"pylint-django==2.6.1",
|
||||||
"pylint==3.3.3",
|
"pylint==3.3.2",
|
||||||
"pytest-cov==6.0.0",
|
"pytest-cov==6.0.0",
|
||||||
"pytest-django==4.9.0",
|
"pytest-django==4.9.0",
|
||||||
"pytest==8.3.4",
|
"pytest==8.3.4",
|
||||||
"pytest-icdiff==0.9",
|
"pytest-icdiff==0.9",
|
||||||
"pytest-xdist==3.6.1",
|
"pytest-xdist==3.6.1",
|
||||||
"responses==0.25.3",
|
"responses==0.25.3",
|
||||||
"ruff==0.8.4",
|
"ruff==0.8.3",
|
||||||
"types-requests==2.32.0.20241016",
|
"types-requests==2.32.0.20241016",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -36,25 +36,18 @@ export const createDoc = async (
|
|||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
name: 'New doc',
|
name: 'Create a new document',
|
||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
const input = page.getByRole('textbox', { name: 'doc title input' });
|
await page.getByRole('heading', { name: 'Untitled document' }).click();
|
||||||
await input.click();
|
await page.keyboard.type(randomDocs[i]);
|
||||||
await input.fill(randomDocs[i]);
|
await page.getByText('Created at ').click();
|
||||||
await input.blur();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return randomDocs;
|
return randomDocs;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verifyDocName = async (page: Page, docName: string) => {
|
|
||||||
const input = page.getByRole('textbox', { name: 'doc title input' });
|
|
||||||
await expect(input).toBeVisible();
|
|
||||||
await expect(input).toHaveText(docName);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const addNewMember = async (
|
export const addNewMember = async (
|
||||||
page: Page,
|
page: Page,
|
||||||
index: number,
|
index: number,
|
||||||
@@ -67,9 +60,7 @@ export const addNewMember = async (
|
|||||||
response.status() === 200,
|
response.status() === 200,
|
||||||
);
|
);
|
||||||
|
|
||||||
const inputSearch = page.getByRole('combobox', {
|
const inputSearch = page.getByLabel(/Find a member to add to the document/);
|
||||||
name: 'Quick search input',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Select a new user
|
// Select a new user
|
||||||
await inputSearch.fill(fillText);
|
await inputSearch.fill(fillText);
|
||||||
@@ -84,9 +75,13 @@ export const addNewMember = async (
|
|||||||
await page.getByRole('option', { name: users[index].email }).click();
|
await page.getByRole('option', { name: users[index].email }).click();
|
||||||
|
|
||||||
// Choose a role
|
// Choose a role
|
||||||
await page.getByLabel('doc-role-dropdown').click();
|
await page.getByRole('combobox', { name: /Choose a role/ }).click();
|
||||||
await page.getByRole('button', { name: role }).click();
|
await page.getByRole('option', { name: role }).click();
|
||||||
await page.getByRole('button', { name: 'Invite' }).click();
|
await page.getByRole('button', { name: 'Validate' }).click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByText(`User ${users[index].email} added to the document.`),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
return users[index].email;
|
return users[index].email;
|
||||||
};
|
};
|
||||||
@@ -102,22 +97,24 @@ export const goToGridDoc = async (
|
|||||||
const header = page.locator('header').first();
|
const header = page.locator('header').first();
|
||||||
await header.locator('h2').getByText('Docs').click();
|
await header.locator('h2').getByText('Docs').click();
|
||||||
|
|
||||||
const docsGrid = page.getByTestId('docs-grid');
|
const datagrid = page.getByLabel('Datagrid of the documents page 1');
|
||||||
await expect(docsGrid).toBeVisible();
|
const datagridTable = datagrid.getByRole('table');
|
||||||
await expect(docsGrid.getByTestId('grid-loader')).toBeHidden();
|
|
||||||
|
|
||||||
const rows = docsGrid.getByRole('row');
|
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = datagridTable.getByRole('row');
|
||||||
const row = title
|
const row = title
|
||||||
? rows.filter({
|
? rows.filter({
|
||||||
hasText: title,
|
hasText: title,
|
||||||
})
|
})
|
||||||
: rows.nth(nthRow);
|
: rows.nth(nthRow);
|
||||||
|
|
||||||
await expect(row).toBeVisible();
|
const docTitleCell = row.getByRole('cell').nth(1);
|
||||||
|
|
||||||
|
const docTitle = await docTitleCell.textContent();
|
||||||
|
|
||||||
const docTitleContent = row.locator('[aria-describedby="doc-title"]').first();
|
|
||||||
const docTitle = await docTitleContent.textContent();
|
|
||||||
expect(docTitle).toBeDefined();
|
expect(docTitle).toBeDefined();
|
||||||
|
|
||||||
await row.getByRole('link').first().click();
|
await row.getByRole('link').first().click();
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import path from 'path';
|
|||||||
|
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { createDoc, verifyDocName } from './common';
|
import { createDoc } from './common';
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
CRISP_WEBSITE_ID: null,
|
CRISP_WEBSITE_ID: null,
|
||||||
@@ -129,8 +129,7 @@ test.describe('Config', () => {
|
|||||||
browserName,
|
browserName,
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
|
await expect(page.locator('h2').getByText(randomDoc[0])).toBeVisible();
|
||||||
await verifyDocName(page, randomDoc[0]);
|
|
||||||
|
|
||||||
const webSocket = await webSocketPromise;
|
const webSocket = await webSocketPromise;
|
||||||
expect(webSocket.url()).toContain('ws://localhost:8083/collaboration/ws/');
|
expect(webSocket.url()).toContain('ws://localhost:8083/collaboration/ws/');
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import {
|
import { createDoc, goToGridDoc, keyCloakSignIn, randomName } from './common';
|
||||||
createDoc,
|
|
||||||
goToGridDoc,
|
|
||||||
keyCloakSignIn,
|
|
||||||
randomName,
|
|
||||||
verifyDocName,
|
|
||||||
} from './common';
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@@ -24,12 +18,15 @@ test.describe('Doc Create', () => {
|
|||||||
const header = page.locator('header').first();
|
const header = page.locator('header').first();
|
||||||
await header.locator('h2').getByText('Docs').click();
|
await header.locator('h2').getByText('Docs').click();
|
||||||
|
|
||||||
await expect(page.getByTestId('grid-loader')).toBeVisible();
|
const datagrid = page.getByLabel('Datagrid of the documents page 1');
|
||||||
|
const datagridTable = datagrid.getByRole('table');
|
||||||
|
|
||||||
const docsGrid = page.getByTestId('docs-grid');
|
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||||
await expect(docsGrid).toBeVisible();
|
timeout: 10000,
|
||||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
});
|
||||||
await expect(docsGrid.getByText(docTitle)).toBeVisible();
|
await expect(datagridTable.getByText(docTitle)).toBeVisible({
|
||||||
|
timeout: 5000,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,7 +64,7 @@ test.describe('Doc Create: Not loggued', () => {
|
|||||||
|
|
||||||
await goToGridDoc(page, { title });
|
await goToGridDoc(page, { title });
|
||||||
|
|
||||||
await verifyDocName(page, title);
|
await expect(page.getByRole('heading', { name: title })).toBeVisible();
|
||||||
|
|
||||||
const editor = page.locator('.ProseMirror');
|
const editor = page.locator('.ProseMirror');
|
||||||
await expect(editor.getByText('This is a normal text')).toBeVisible();
|
await expect(editor.getByText('This is a normal text')).toBeVisible();
|
||||||
|
|||||||
@@ -2,12 +2,7 @@ import path from 'path';
|
|||||||
|
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import {
|
import { createDoc, goToGridDoc, mockedDocument } from './common';
|
||||||
createDoc,
|
|
||||||
goToGridDoc,
|
|
||||||
mockedDocument,
|
|
||||||
verifyDocName,
|
|
||||||
} from './common';
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@@ -103,7 +98,7 @@ test.describe('Doc Editor', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const randomDoc = await createDoc(page, 'doc-editor', browserName, 1);
|
const randomDoc = await createDoc(page, 'doc-editor', browserName, 1);
|
||||||
await verifyDocName(page, randomDoc[0]);
|
await expect(page.locator('h2').getByText(randomDoc[0])).toBeVisible();
|
||||||
|
|
||||||
let webSocket = await webSocketPromise;
|
let webSocket = await webSocketPromise;
|
||||||
expect(webSocket.url()).toContain(
|
expect(webSocket.url()).toContain(
|
||||||
@@ -121,15 +116,17 @@ test.describe('Doc Editor', () => {
|
|||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
const selectVisibility = page.getByLabel('Visibility', { exact: true });
|
const selectVisibility = page.getByRole('combobox', {
|
||||||
|
name: 'Visibility',
|
||||||
|
});
|
||||||
|
|
||||||
// When the visibility is changed, the ws should close the connection (backend signal)
|
// When the visibility is changed, the ws should closed the connection (backend signal)
|
||||||
const wsClosePromise = webSocket.waitForEvent('close');
|
const wsClosePromise = webSocket.waitForEvent('close');
|
||||||
|
|
||||||
await selectVisibility.click();
|
await selectVisibility.click();
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('option', {
|
||||||
name: 'Connected',
|
name: 'Authenticated',
|
||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
@@ -156,7 +153,7 @@ test.describe('Doc Editor', () => {
|
|||||||
}) => {
|
}) => {
|
||||||
const randomDoc = await createDoc(page, 'doc-markdown', browserName, 1);
|
const randomDoc = await createDoc(page, 'doc-markdown', browserName, 1);
|
||||||
|
|
||||||
await verifyDocName(page, randomDoc[0]);
|
await expect(page.locator('h2').getByText(randomDoc[0])).toBeVisible();
|
||||||
|
|
||||||
const editor = page.locator('.ProseMirror');
|
const editor = page.locator('.ProseMirror');
|
||||||
await editor.click();
|
await editor.click();
|
||||||
@@ -181,7 +178,7 @@ test.describe('Doc Editor', () => {
|
|||||||
}) => {
|
}) => {
|
||||||
// Check the first doc
|
// Check the first doc
|
||||||
const [firstDoc] = await createDoc(page, 'doc-switch-1', browserName, 1);
|
const [firstDoc] = await createDoc(page, 'doc-switch-1', browserName, 1);
|
||||||
await verifyDocName(page, firstDoc);
|
await expect(page.locator('h2').getByText(firstDoc)).toBeVisible();
|
||||||
|
|
||||||
const editor = page.locator('.ProseMirror');
|
const editor = page.locator('.ProseMirror');
|
||||||
await editor.click();
|
await editor.click();
|
||||||
@@ -190,8 +187,7 @@ test.describe('Doc Editor', () => {
|
|||||||
|
|
||||||
// Check the second doc
|
// Check the second doc
|
||||||
const [secondDoc] = await createDoc(page, 'doc-switch-2', browserName, 1);
|
const [secondDoc] = await createDoc(page, 'doc-switch-2', browserName, 1);
|
||||||
await verifyDocName(page, secondDoc);
|
await expect(page.locator('h2').getByText(secondDoc)).toBeVisible();
|
||||||
|
|
||||||
await expect(editor.getByText('Hello World Doc 1')).toBeHidden();
|
await expect(editor.getByText('Hello World Doc 1')).toBeHidden();
|
||||||
await editor.click();
|
await editor.click();
|
||||||
await editor.fill('Hello World Doc 2');
|
await editor.fill('Hello World Doc 2');
|
||||||
@@ -201,18 +197,9 @@ test.describe('Doc Editor', () => {
|
|||||||
await goToGridDoc(page, {
|
await goToGridDoc(page, {
|
||||||
title: firstDoc,
|
title: firstDoc,
|
||||||
});
|
});
|
||||||
await verifyDocName(page, firstDoc);
|
await expect(page.locator('h2').getByText(firstDoc)).toBeVisible();
|
||||||
await expect(editor.getByText('Hello World Doc 2')).toBeHidden();
|
await expect(editor.getByText('Hello World Doc 2')).toBeHidden();
|
||||||
await expect(editor.getByText('Hello World Doc 1')).toBeVisible();
|
await expect(editor.getByText('Hello World Doc 1')).toBeVisible();
|
||||||
|
|
||||||
await page
|
|
||||||
.getByRole('button', {
|
|
||||||
name: 'New doc',
|
|
||||||
})
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(editor.getByText('Hello World Doc 1')).toBeHidden();
|
|
||||||
await expect(editor.getByText('Hello World Doc 2')).toBeHidden();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it saves the doc when we change pages', async ({
|
test('it saves the doc when we change pages', async ({
|
||||||
@@ -221,7 +208,7 @@ test.describe('Doc Editor', () => {
|
|||||||
}) => {
|
}) => {
|
||||||
// Check the first doc
|
// Check the first doc
|
||||||
const [doc] = await createDoc(page, 'doc-saves-change', browserName, 1);
|
const [doc] = await createDoc(page, 'doc-saves-change', browserName, 1);
|
||||||
await verifyDocName(page, doc);
|
await expect(page.locator('h2').getByText(doc)).toBeVisible();
|
||||||
|
|
||||||
const editor = page.locator('.ProseMirror');
|
const editor = page.locator('.ProseMirror');
|
||||||
await editor.click();
|
await editor.click();
|
||||||
@@ -232,7 +219,7 @@ test.describe('Doc Editor', () => {
|
|||||||
nthRow: 2,
|
nthRow: 2,
|
||||||
});
|
});
|
||||||
|
|
||||||
await verifyDocName(page, secondDoc);
|
await expect(page.locator('h2').getByText(secondDoc)).toBeVisible();
|
||||||
|
|
||||||
await goToGridDoc(page, {
|
await goToGridDoc(page, {
|
||||||
title: doc,
|
title: doc,
|
||||||
@@ -246,9 +233,8 @@ test.describe('Doc Editor', () => {
|
|||||||
test.skip(browserName === 'webkit', 'This test is very flaky with webkit');
|
test.skip(browserName === 'webkit', 'This test is very flaky with webkit');
|
||||||
|
|
||||||
// Check the first doc
|
// Check the first doc
|
||||||
const doc = await goToGridDoc(page);
|
const [doc] = await createDoc(page, 'doc-quit-1', browserName, 1);
|
||||||
|
await expect(page.locator('h2').getByText(doc)).toBeVisible();
|
||||||
await verifyDocName(page, doc);
|
|
||||||
|
|
||||||
const editor = page.locator('.ProseMirror');
|
const editor = page.locator('.ProseMirror');
|
||||||
await editor.click();
|
await editor.click();
|
||||||
@@ -281,10 +267,9 @@ test.describe('Doc Editor', () => {
|
|||||||
|
|
||||||
await goToGridDoc(page);
|
await goToGridDoc(page);
|
||||||
|
|
||||||
const card = page.getByLabel('It is the card information');
|
await expect(
|
||||||
await expect(card).toBeVisible();
|
page.getByText('Read only, you cannot edit this document.'),
|
||||||
|
).toBeVisible();
|
||||||
await expect(card.getByText('Reader')).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it adds an image to the doc editor', async ({ page, browserName }) => {
|
test('it adds an image to the doc editor', async ({ page, browserName }) => {
|
||||||
|
|||||||
@@ -3,44 +3,13 @@ import cs from 'convert-stream';
|
|||||||
import jsdom from 'jsdom';
|
import jsdom from 'jsdom';
|
||||||
import pdf from 'pdf-parse';
|
import pdf from 'pdf-parse';
|
||||||
|
|
||||||
import { createDoc, verifyDocName } from './common';
|
import { createDoc } from './common';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Doc Export', () => {
|
test.describe('Doc Export', () => {
|
||||||
test('it check if all elements are visible', async ({
|
|
||||||
page,
|
|
||||||
browserName,
|
|
||||||
}) => {
|
|
||||||
await createDoc(page, 'doc-editor', browserName, 1);
|
|
||||||
await page
|
|
||||||
.getByRole('button', {
|
|
||||||
name: 'download',
|
|
||||||
})
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page
|
|
||||||
.locator('div')
|
|
||||||
.filter({ hasText: /^Download$/ })
|
|
||||||
.first(),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
page.getByText(
|
|
||||||
'Upload your docs to a Microsoft Word, Open Office or PDF document',
|
|
||||||
),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
page.getByRole('combobox', { name: 'Template' }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(page.getByRole('combobox', { name: 'Format' })).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
page.getByRole('button', { name: 'Close the modal' }),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(page.getByRole('button', { name: 'Download' })).toBeVisible();
|
|
||||||
});
|
|
||||||
test('it converts the doc to pdf with a template integrated', async ({
|
test('it converts the doc to pdf with a template integrated', async ({
|
||||||
page,
|
page,
|
||||||
browserName,
|
browserName,
|
||||||
@@ -51,14 +20,15 @@ test.describe('Doc Export', () => {
|
|||||||
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
return download.suggestedFilename().includes(`${randomDoc}.pdf`);
|
||||||
});
|
});
|
||||||
|
|
||||||
await verifyDocName(page, randomDoc);
|
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||||
|
|
||||||
await page.locator('.ProseMirror.bn-editor').click();
|
await page.locator('.ProseMirror.bn-editor').click();
|
||||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||||
|
|
||||||
|
await page.getByLabel('Open the document options').click();
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
name: 'download',
|
name: 'Export',
|
||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
@@ -87,19 +57,19 @@ test.describe('Doc Export', () => {
|
|||||||
return download.suggestedFilename().includes(`${randomDoc}.docx`);
|
return download.suggestedFilename().includes(`${randomDoc}.docx`);
|
||||||
});
|
});
|
||||||
|
|
||||||
await verifyDocName(page, randomDoc);
|
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||||
|
|
||||||
await page.locator('.ProseMirror.bn-editor').click();
|
await page.locator('.ProseMirror.bn-editor').click();
|
||||||
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
|
||||||
|
|
||||||
|
await page.getByLabel('Open the document options').click();
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
name: 'download',
|
name: 'Export',
|
||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await page.getByRole('combobox', { name: 'Format' }).click();
|
await page.getByText('Docx').click();
|
||||||
await page.getByRole('option', { name: 'Word / Open Office' }).click();
|
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
@@ -127,7 +97,7 @@ test.describe('Doc Export', () => {
|
|||||||
await route.continue();
|
await route.continue();
|
||||||
});
|
});
|
||||||
|
|
||||||
await verifyDocName(page, randomDoc);
|
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||||
|
|
||||||
await page.locator('.bn-block-outer').last().fill('Hello World');
|
await page.locator('.bn-block-outer').last().fill('Hello World');
|
||||||
await page.locator('.bn-block-outer').last().click();
|
await page.locator('.bn-block-outer').last().click();
|
||||||
@@ -220,9 +190,10 @@ test.describe('Doc Export', () => {
|
|||||||
.click();
|
.click();
|
||||||
|
|
||||||
// Download
|
// Download
|
||||||
|
await page.getByLabel('Open the document options').click();
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
name: 'download',
|
name: 'Export',
|
||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
|
||||||
|
|
||||||
import { createDoc, verifyDocName } from './common';
|
|
||||||
|
|
||||||
type SmallDoc = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
test.describe('Document favorite', () => {
|
|
||||||
test('it check the favorite workflow', async ({ page, browserName }) => {
|
|
||||||
const id = Math.random().toString(7);
|
|
||||||
await page.goto('/');
|
|
||||||
|
|
||||||
// Create document
|
|
||||||
const createdDoc = await createDoc(page, `Doc ${id}`, browserName, 1);
|
|
||||||
await verifyDocName(page, createdDoc[0]);
|
|
||||||
|
|
||||||
// Reload page
|
|
||||||
await page.reload();
|
|
||||||
await page.goto('/');
|
|
||||||
|
|
||||||
// Get all documents
|
|
||||||
let docs: SmallDoc[] = [];
|
|
||||||
const response = await page.waitForResponse(
|
|
||||||
(response) =>
|
|
||||||
response.url().endsWith('documents/?page=1') &&
|
|
||||||
response.status() === 200,
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
docs = result.results as SmallDoc[];
|
|
||||||
const docsGrid = page.getByTestId('docs-grid');
|
|
||||||
await docsGrid.getByRole('heading', { name: 'All docs' }).click();
|
|
||||||
await expect(docsGrid.getByText(`Doc ${id}`)).toBeVisible();
|
|
||||||
const doc = docs.find((doc) => doc.title === createdDoc[0]) as SmallDoc;
|
|
||||||
|
|
||||||
// Check document
|
|
||||||
expect(doc).not.toBeUndefined();
|
|
||||||
expect(doc?.title).toBe(createdDoc[0]);
|
|
||||||
|
|
||||||
// Open document actions
|
|
||||||
const button = docsGrid.getByTestId(`docs-grid-actions-button-${doc.id}`);
|
|
||||||
await expect(button).toBeVisible();
|
|
||||||
await button.click();
|
|
||||||
|
|
||||||
// Pin document
|
|
||||||
const pinButton = page.getByTestId(`docs-grid-actions-pin-${docs[0].id}`);
|
|
||||||
await expect(pinButton).toBeVisible();
|
|
||||||
await pinButton.click();
|
|
||||||
|
|
||||||
// Check response
|
|
||||||
const responsePin = await page.waitForResponse(
|
|
||||||
(response) =>
|
|
||||||
response.url().includes(`documents/${doc.id}/favorite/`) &&
|
|
||||||
response.status() === 201,
|
|
||||||
);
|
|
||||||
expect(responsePin.ok()).toBeTruthy();
|
|
||||||
|
|
||||||
// Check left panel favorites
|
|
||||||
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
|
|
||||||
await expect(leftPanelFavorites).toBeVisible();
|
|
||||||
await expect(leftPanelFavorites.getByText(`Doc ${id}`)).toBeVisible();
|
|
||||||
|
|
||||||
//
|
|
||||||
await button.click();
|
|
||||||
const unpinButton = page.getByTestId(
|
|
||||||
`docs-grid-actions-unpin-${docs[0].id}`,
|
|
||||||
);
|
|
||||||
await expect(unpinButton).toBeVisible();
|
|
||||||
await unpinButton.click();
|
|
||||||
|
|
||||||
// Check left panel favorites
|
|
||||||
await expect(leftPanelFavorites.getByText(`Doc ${id}`)).toBeHidden();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,14 +1,264 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
type SmallDoc = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Documents Grid', () => {
|
||||||
|
test('checks all the elements are visible', async ({ page }) => {
|
||||||
|
await expect(page.locator('h2').getByText('Documents')).toBeVisible();
|
||||||
|
|
||||||
|
const datagrid = page
|
||||||
|
.getByLabel('Datagrid of the documents page 1')
|
||||||
|
.getByRole('table');
|
||||||
|
|
||||||
|
const thead = datagrid.locator('thead');
|
||||||
|
await expect(thead.getByText(/Document name/i)).toBeVisible();
|
||||||
|
await expect(thead.getByText(/Created at/i)).toBeVisible();
|
||||||
|
await expect(thead.getByText(/Updated at/i)).toBeVisible();
|
||||||
|
await expect(thead.getByText(/Your role/i)).toBeVisible();
|
||||||
|
await expect(thead.getByText(/Members/i)).toBeVisible();
|
||||||
|
|
||||||
|
const row1 = datagrid.getByRole('row').nth(1).getByRole('cell');
|
||||||
|
const docName = await row1.nth(1).textContent();
|
||||||
|
expect(docName).toBeDefined();
|
||||||
|
|
||||||
|
const docCreatedAt = await row1.nth(2).textContent();
|
||||||
|
expect(docCreatedAt).toBeDefined();
|
||||||
|
|
||||||
|
const docUpdatedAt = await row1.nth(3).textContent();
|
||||||
|
expect(docUpdatedAt).toBeDefined();
|
||||||
|
|
||||||
|
const docRole = await row1.nth(4).textContent();
|
||||||
|
expect(
|
||||||
|
docRole &&
|
||||||
|
['Administrator', 'Owner', 'Reader', 'Editor'].includes(docRole),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
const docUserNumber = await row1.nth(5).textContent();
|
||||||
|
expect(docUserNumber).toBeDefined();
|
||||||
|
|
||||||
|
// Open the document
|
||||||
|
await row1.nth(1).click();
|
||||||
|
|
||||||
|
await expect(page.locator('h2').getByText(docName!)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
{
|
||||||
|
nameColumn: 'Document name',
|
||||||
|
ordering: 'title',
|
||||||
|
cellNumber: 1,
|
||||||
|
orderDefault: '',
|
||||||
|
orderDesc: '&ordering=-title',
|
||||||
|
orderAsc: '&ordering=title',
|
||||||
|
defaultColumn: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nameColumn: 'Created at',
|
||||||
|
ordering: 'created_at',
|
||||||
|
cellNumber: 2,
|
||||||
|
orderDefault: '',
|
||||||
|
orderDesc: '&ordering=-created_at',
|
||||||
|
orderAsc: '&ordering=created_at',
|
||||||
|
defaultColumn: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
nameColumn: 'Updated at',
|
||||||
|
ordering: 'updated_at',
|
||||||
|
cellNumber: 3,
|
||||||
|
orderDefault: '&ordering=-updated_at',
|
||||||
|
orderDesc: '&ordering=updated_at',
|
||||||
|
orderAsc: '',
|
||||||
|
defaultColumn: true,
|
||||||
|
},
|
||||||
|
].forEach(
|
||||||
|
({
|
||||||
|
nameColumn,
|
||||||
|
ordering,
|
||||||
|
cellNumber,
|
||||||
|
orderDefault,
|
||||||
|
orderDesc,
|
||||||
|
orderAsc,
|
||||||
|
defaultColumn,
|
||||||
|
}) => {
|
||||||
|
test(`checks datagrid ordering ${ordering}`, async ({ page }) => {
|
||||||
|
const responsePromise = page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes(`/documents/?page=1${orderDefault}`) &&
|
||||||
|
response.status() === 200,
|
||||||
|
);
|
||||||
|
|
||||||
|
const responsePromiseOrderingDesc = page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes(`/documents/?page=1${orderDesc}`) &&
|
||||||
|
response.status() === 200,
|
||||||
|
);
|
||||||
|
|
||||||
|
const responsePromiseOrderingAsc = page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes(`/documents/?page=1${orderAsc}`) &&
|
||||||
|
response.status() === 200,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Checks the initial state
|
||||||
|
const datagrid = page.getByLabel('Datagrid of the documents page 1');
|
||||||
|
const datagridTable = datagrid.getByRole('table');
|
||||||
|
const thead = datagridTable.locator('thead');
|
||||||
|
|
||||||
|
const response = await responsePromise;
|
||||||
|
expect(response.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
const docNameRow1 = datagridTable
|
||||||
|
.getByRole('row')
|
||||||
|
.nth(1)
|
||||||
|
.getByRole('cell')
|
||||||
|
.nth(cellNumber);
|
||||||
|
const docNameRow2 = datagridTable
|
||||||
|
.getByRole('row')
|
||||||
|
.nth(2)
|
||||||
|
.getByRole('cell')
|
||||||
|
.nth(cellNumber);
|
||||||
|
|
||||||
|
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
await expect(docNameRow1).toHaveText(/.*/);
|
||||||
|
await expect(docNameRow2).toHaveText(/.*/);
|
||||||
|
const initialDocNameRow1 = await docNameRow1.textContent();
|
||||||
|
const initialDocNameRow2 = await docNameRow2.textContent();
|
||||||
|
|
||||||
|
expect(initialDocNameRow1).toBeDefined();
|
||||||
|
expect(initialDocNameRow2).toBeDefined();
|
||||||
|
|
||||||
|
// Ordering ASC
|
||||||
|
await thead.getByText(nameColumn).click();
|
||||||
|
|
||||||
|
const responseOrderingAsc = await responsePromiseOrderingAsc;
|
||||||
|
expect(responseOrderingAsc.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(docNameRow1).toHaveText(/.*/);
|
||||||
|
await expect(docNameRow2).toHaveText(/.*/);
|
||||||
|
const textDocNameRow1Asc = await docNameRow1.textContent();
|
||||||
|
const textDocNameRow2Asc = await docNameRow2.textContent();
|
||||||
|
|
||||||
|
const compare = (comp1: string, comp2: string) => {
|
||||||
|
const comparisonResult = comp1.localeCompare(comp2, 'en', {
|
||||||
|
caseFirst: 'false',
|
||||||
|
ignorePunctuation: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line playwright/no-conditional-in-test
|
||||||
|
return defaultColumn ? comparisonResult >= 0 : comparisonResult <= 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(
|
||||||
|
textDocNameRow1Asc &&
|
||||||
|
textDocNameRow2Asc &&
|
||||||
|
compare(textDocNameRow1Asc, textDocNameRow2Asc),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
// Ordering Desc
|
||||||
|
await thead.getByText(nameColumn).click();
|
||||||
|
|
||||||
|
const responseOrderingDesc = await responsePromiseOrderingDesc;
|
||||||
|
expect(responseOrderingDesc.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(docNameRow1).toHaveText(/.*/);
|
||||||
|
await expect(docNameRow2).toHaveText(/.*/);
|
||||||
|
const textDocNameRow1Desc = await docNameRow1.textContent();
|
||||||
|
const textDocNameRow2Desc = await docNameRow2.textContent();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
textDocNameRow1Desc &&
|
||||||
|
textDocNameRow2Desc &&
|
||||||
|
compare(textDocNameRow2Desc, textDocNameRow1Desc),
|
||||||
|
).toBeTruthy();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
test('checks the pagination', async ({ page }) => {
|
||||||
|
const responsePromisePage1 = page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes(`/documents/?page=1`) &&
|
||||||
|
response.status() === 200,
|
||||||
|
);
|
||||||
|
|
||||||
|
const responsePromisePage2 = page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes(`/documents/?page=2`) &&
|
||||||
|
response.status() === 200,
|
||||||
|
);
|
||||||
|
|
||||||
|
const datagridPage1 = page
|
||||||
|
.getByLabel('Datagrid of the documents page 1')
|
||||||
|
.getByRole('table');
|
||||||
|
|
||||||
|
const responsePage1 = await responsePromisePage1;
|
||||||
|
expect(responsePage1.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
datagridPage1.getByRole('row').nth(1).getByRole('cell').nth(1),
|
||||||
|
).toHaveText(/.*/);
|
||||||
|
|
||||||
|
await page.getByLabel('Go to page 2').click();
|
||||||
|
|
||||||
|
const datagridPage2 = page
|
||||||
|
.getByLabel('Datagrid of the documents page 2')
|
||||||
|
.getByRole('table');
|
||||||
|
|
||||||
|
const responsePage2 = await responsePromisePage2;
|
||||||
|
expect(responsePage2.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
datagridPage2.getByRole('row').nth(1).getByRole('cell').nth(1),
|
||||||
|
).toHaveText(/.*/);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it deletes the document', async ({ page }) => {
|
||||||
|
const datagrid = page
|
||||||
|
.getByLabel('Datagrid of the documents page 1')
|
||||||
|
.getByRole('table');
|
||||||
|
|
||||||
|
const docRow = datagrid.getByRole('row').nth(1).getByRole('cell');
|
||||||
|
|
||||||
|
const docName = await docRow.nth(1).textContent();
|
||||||
|
|
||||||
|
await docRow
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Delete the document',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.locator('h2').getByText(`Deleting the document "${docName}"`),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Confirm deletion',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByText('The document has been deleted.'),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await expect(datagrid.getByText(docName!)).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
test.describe('Documents Grid mobile', () => {
|
test.describe('Documents Grid mobile', () => {
|
||||||
test.use({ viewport: { width: 500, height: 1200 } });
|
test.use({ viewport: { width: 500, height: 1200 } });
|
||||||
|
|
||||||
@@ -76,256 +326,19 @@ test.describe('Documents Grid mobile', () => {
|
|||||||
|
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
|
|
||||||
const docsGrid = page.getByTestId('docs-grid');
|
const datagrid = page.getByLabel('Datagrid of the documents page 1');
|
||||||
await expect(docsGrid).toBeVisible();
|
const tableDatagrid = datagrid.getByRole('table');
|
||||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
|
||||||
|
|
||||||
const rows = docsGrid.getByRole('row');
|
await expect(datagrid.getByLabel('Loading data')).toBeHidden({
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = tableDatagrid.getByRole('row');
|
||||||
const row = rows.filter({
|
const row = rows.filter({
|
||||||
hasText: 'My mocked document',
|
hasText: 'My mocked document',
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(row.getByRole('cell').nth(0)).toHaveText('My mocked document');
|
||||||
row.locator('[aria-describedby="doc-title"]').nth(0),
|
await expect(row.getByRole('cell').nth(1)).toHaveText('Public');
|
||||||
).toHaveText('My mocked document');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Document grid item options', () => {
|
|
||||||
test('it deletes the document', async ({ page }) => {
|
|
||||||
let docs: SmallDoc[] = [];
|
|
||||||
const response = await page.waitForResponse(
|
|
||||||
(response) =>
|
|
||||||
response.url().endsWith('documents/?page=1') &&
|
|
||||||
response.status() === 200,
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
docs = result.results as SmallDoc[];
|
|
||||||
|
|
||||||
const button = page.getByTestId(`docs-grid-actions-button-${docs[0].id}`);
|
|
||||||
await expect(button).toBeVisible();
|
|
||||||
await button.click();
|
|
||||||
|
|
||||||
const removeButton = page.getByTestId(
|
|
||||||
`docs-grid-actions-remove-${docs[0].id}`,
|
|
||||||
);
|
|
||||||
await expect(removeButton).toBeVisible();
|
|
||||||
await removeButton.click();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.getByRole('heading', { name: 'Delete a doc' }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await page
|
|
||||||
.getByRole('button', {
|
|
||||||
name: 'Confirm deletion',
|
|
||||||
})
|
|
||||||
.click();
|
|
||||||
|
|
||||||
const refetchResponse = await page.waitForResponse(
|
|
||||||
(response) =>
|
|
||||||
response.url().endsWith('documents/?page=1') &&
|
|
||||||
response.status() === 200,
|
|
||||||
);
|
|
||||||
|
|
||||||
const resultRefetch = await refetchResponse.json();
|
|
||||||
expect(resultRefetch.count).toBe(result.count - 1);
|
|
||||||
await expect(page.getByTestId('main-layout-loader')).toBeHidden();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.getByText('The document has been deleted.'),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(button).toBeHidden();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("it checks if the delete option is disabled if we don't have the destroy capability", async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await page.route('*/**/api/v1.0/documents/?page=1', async (route) => {
|
|
||||||
await route.fulfill({
|
|
||||||
json: {
|
|
||||||
results: [
|
|
||||||
{
|
|
||||||
id: 'mocked-document-id',
|
|
||||||
content: '',
|
|
||||||
title: 'Mocked document',
|
|
||||||
accesses: [],
|
|
||||||
abilities: {
|
|
||||||
destroy: false, // Means not owner
|
|
||||||
link_configuration: false,
|
|
||||||
versions_destroy: false,
|
|
||||||
versions_list: true,
|
|
||||||
versions_retrieve: true,
|
|
||||||
accesses_manage: false, // Means not admin
|
|
||||||
update: false,
|
|
||||||
partial_update: false, // Means not editor
|
|
||||||
retrieve: true,
|
|
||||||
},
|
|
||||||
link_reach: 'restricted',
|
|
||||||
created_at: '2021-09-01T09:00:00Z',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
await page.goto('/');
|
|
||||||
|
|
||||||
const button = page.getByTestId(
|
|
||||||
`docs-grid-actions-button-mocked-document-id`,
|
|
||||||
);
|
|
||||||
await expect(button).toBeVisible();
|
|
||||||
await button.click();
|
|
||||||
const removeButton = page.getByTestId(
|
|
||||||
`docs-grid-actions-remove-mocked-document-id`,
|
|
||||||
);
|
|
||||||
await expect(removeButton).toBeVisible();
|
|
||||||
await removeButton.isDisabled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Documents filters', () => {
|
|
||||||
test('it checks the prebuild left panel filters', async ({ page }) => {
|
|
||||||
// All Docs
|
|
||||||
await expect(page.getByTestId('grid-loader')).toBeVisible();
|
|
||||||
const response = await page.waitForResponse(
|
|
||||||
(response) =>
|
|
||||||
response.url().endsWith('documents/?page=1') &&
|
|
||||||
response.status() === 200,
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
const allCount = result.count as number;
|
|
||||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
|
||||||
|
|
||||||
const allDocs = page.getByLabel('All docs');
|
|
||||||
const myDocs = page.getByLabel('My docs');
|
|
||||||
const sharedWithMe = page.getByLabel('Shared with me');
|
|
||||||
|
|
||||||
// Initial state
|
|
||||||
await expect(allDocs).toBeVisible();
|
|
||||||
await expect(allDocs).toHaveCSS('background-color', 'rgb(238, 238, 238)');
|
|
||||||
await expect(allDocs).toHaveAttribute('aria-selected', 'true');
|
|
||||||
|
|
||||||
await expect(myDocs).toBeVisible();
|
|
||||||
await expect(myDocs).toHaveCSS('background-color', 'rgba(0, 0, 0, 0)');
|
|
||||||
await expect(myDocs).toHaveAttribute('aria-selected', 'false');
|
|
||||||
|
|
||||||
await expect(sharedWithMe).toBeVisible();
|
|
||||||
await expect(sharedWithMe).toHaveCSS(
|
|
||||||
'background-color',
|
|
||||||
'rgba(0, 0, 0, 0)',
|
|
||||||
);
|
|
||||||
await expect(sharedWithMe).toHaveAttribute('aria-selected', 'false');
|
|
||||||
|
|
||||||
await allDocs.click();
|
|
||||||
|
|
||||||
let url = new URL(page.url());
|
|
||||||
let target = url.searchParams.get('target');
|
|
||||||
expect(target).toBe('all_docs');
|
|
||||||
|
|
||||||
// My docs
|
|
||||||
await myDocs.click();
|
|
||||||
url = new URL(page.url());
|
|
||||||
target = url.searchParams.get('target');
|
|
||||||
expect(target).toBe('my_docs');
|
|
||||||
await expect(page.getByTestId('grid-loader')).toBeVisible();
|
|
||||||
const responseMyDocs = await page.waitForResponse(
|
|
||||||
(response) =>
|
|
||||||
response.url().endsWith('documents/?page=1&is_creator_me=true') &&
|
|
||||||
response.status() === 200,
|
|
||||||
);
|
|
||||||
const resultMyDocs = await responseMyDocs.json();
|
|
||||||
const countMyDocs = resultMyDocs.count as number;
|
|
||||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
|
||||||
expect(countMyDocs).toBeLessThanOrEqual(allCount);
|
|
||||||
|
|
||||||
// Shared with me
|
|
||||||
await sharedWithMe.click();
|
|
||||||
url = new URL(page.url());
|
|
||||||
target = url.searchParams.get('target');
|
|
||||||
expect(target).toBe('shared_with_me');
|
|
||||||
await expect(page.getByTestId('grid-loader')).toBeVisible();
|
|
||||||
const responseSharedWithMe = await page.waitForResponse(
|
|
||||||
(response) =>
|
|
||||||
response.url().includes('documents/?page=1&is_creator_me=false') &&
|
|
||||||
response.status() === 200,
|
|
||||||
);
|
|
||||||
const resultSharedWithMe = await responseSharedWithMe.json();
|
|
||||||
const countSharedWithMe = resultSharedWithMe.count as number;
|
|
||||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
|
||||||
expect(countSharedWithMe).toBeLessThanOrEqual(allCount);
|
|
||||||
expect(countSharedWithMe + countMyDocs).toEqual(allCount);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Documents Grid', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.goto('/');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('checks all the elements are visible', async ({ page }) => {
|
|
||||||
let docs: SmallDoc[] = [];
|
|
||||||
await expect(page.getByTestId('grid-loader')).toBeVisible();
|
|
||||||
|
|
||||||
const response = await page.waitForResponse(
|
|
||||||
(response) =>
|
|
||||||
response.url().endsWith('documents/?page=1') &&
|
|
||||||
response.status() === 200,
|
|
||||||
);
|
|
||||||
const result = await response.json();
|
|
||||||
docs = result.results as SmallDoc[];
|
|
||||||
|
|
||||||
await expect(page.getByTestId('grid-loader')).toBeHidden();
|
|
||||||
await expect(page.locator('h4').getByText('All docs')).toBeVisible();
|
|
||||||
|
|
||||||
const thead = page.getByTestId('docs-grid-header');
|
|
||||||
await expect(thead.getByText(/Name/i)).toBeVisible();
|
|
||||||
await expect(thead.getByText(/Updated at/i)).toBeVisible();
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
docs.map(async (doc) => {
|
|
||||||
await expect(
|
|
||||||
page.getByTestId(`docs-grid-name-${doc.id}`),
|
|
||||||
).toBeVisible();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('checks the infinite scroll', async ({ page }) => {
|
|
||||||
let docs: SmallDoc[] = [];
|
|
||||||
const responsePromisePage1 = page.waitForResponse(
|
|
||||||
(response) =>
|
|
||||||
response.url().endsWith(`/documents/?page=1`) &&
|
|
||||||
response.status() === 200,
|
|
||||||
);
|
|
||||||
|
|
||||||
const responsePromisePage2 = page.waitForResponse(
|
|
||||||
(response) =>
|
|
||||||
response.url().endsWith(`/documents/?page=2`) &&
|
|
||||||
response.status() === 200,
|
|
||||||
);
|
|
||||||
|
|
||||||
const responsePage1 = await responsePromisePage1;
|
|
||||||
expect(responsePage1.ok()).toBeTruthy();
|
|
||||||
let result = await responsePage1.json();
|
|
||||||
docs = result.results as SmallDoc[];
|
|
||||||
await Promise.all(
|
|
||||||
docs.map(async (doc) => {
|
|
||||||
await expect(
|
|
||||||
page.getByTestId(`docs-grid-name-${doc.id}`),
|
|
||||||
).toBeVisible();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await page.getByTestId('infinite-scroll-trigger').scrollIntoViewIfNeeded();
|
|
||||||
const responsePage2 = await responsePromisePage2;
|
|
||||||
result = await responsePage2.json();
|
|
||||||
docs = result.results as SmallDoc[];
|
|
||||||
await Promise.all(
|
|
||||||
docs.map(async (doc) => {
|
|
||||||
await expect(
|
|
||||||
page.getByTestId(`docs-grid-name-${doc.id}`),
|
|
||||||
).toBeVisible();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
mockedAccesses,
|
mockedAccesses,
|
||||||
mockedDocument,
|
mockedDocument,
|
||||||
mockedInvitations,
|
mockedInvitations,
|
||||||
verifyDocName,
|
|
||||||
} from './common';
|
} from './common';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
@@ -60,31 +59,84 @@ test.describe('Doc Header', () => {
|
|||||||
const card = page.getByLabel(
|
const card = page.getByLabel(
|
||||||
'It is the card information about the document.',
|
'It is the card information about the document.',
|
||||||
);
|
);
|
||||||
|
await expect(card.locator('a').getByText('home')).toBeVisible();
|
||||||
const docTitle = card.getByRole('textbox', { name: 'doc title input' });
|
await expect(card.locator('h2').getByText('Mocked document')).toBeVisible();
|
||||||
await expect(docTitle).toBeVisible();
|
await expect(card.getByText('Public')).toBeVisible();
|
||||||
|
|
||||||
await expect(card.getByText('Public document')).toBeVisible();
|
|
||||||
|
|
||||||
await expect(card.getByText('Owner ·')).toBeVisible();
|
|
||||||
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
|
||||||
await expect(page.getByRole('button', { name: 'download' })).toBeVisible();
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Open the document options' }),
|
card.getByText('Created at 09/01/2021, 11:00 AM'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
await expect(card.getByText('Your role: Owner')).toBeVisible();
|
||||||
|
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it updates the title doc', async ({ page, browserName }) => {
|
test('it updates the title doc', async ({ page, browserName }) => {
|
||||||
await createDoc(page, 'doc-update', browserName, 1);
|
const [randomDoc] = await createDoc(page, 'doc-update', browserName, 1);
|
||||||
const docTitle = page.getByRole('textbox', { name: 'doc title input' });
|
|
||||||
await expect(docTitle).toBeVisible();
|
await page.getByRole('heading', { name: randomDoc }).fill(' ');
|
||||||
await docTitle.fill('Hello World');
|
await page.getByText('Created at').click();
|
||||||
await docTitle.blur();
|
|
||||||
await verifyDocName(page, 'Hello World');
|
await expect(
|
||||||
|
page.getByRole('heading', { name: 'Untitled document' }),
|
||||||
|
).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it updates the title doc from editor heading', async ({ page }) => {
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Create a new document',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
|
const docHeader = page.getByLabel(
|
||||||
|
'It is the card information about the document.',
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
docHeader.getByRole('heading', { name: 'Untitled document', level: 2 }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
const editor = page.locator('.ProseMirror');
|
||||||
|
|
||||||
|
await editor.locator('h1').click();
|
||||||
|
await page.keyboard.type('Hello World', { delay: 100 });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
docHeader.getByRole('heading', { name: 'Hello World', level: 2 }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByText('Document title updated successfully'),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await docHeader
|
||||||
|
.getByRole('heading', { name: 'Hello World', level: 2 })
|
||||||
|
.fill('Top World');
|
||||||
|
|
||||||
|
await editor.locator('h1').fill('Super World');
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
docHeader.getByRole('heading', { name: 'Top World', level: 2 }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await editor.locator('h1').fill('');
|
||||||
|
|
||||||
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
|
||||||
|
await docHeader
|
||||||
|
.getByRole('heading', { name: 'Top World', level: 2 })
|
||||||
|
.fill(' ');
|
||||||
|
|
||||||
|
await page.getByText('Created at').click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
docHeader.getByRole('heading', { name: 'Untitled document', level: 2 }),
|
||||||
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it deletes the doc', async ({ page, browserName }) => {
|
test('it deletes the doc', async ({ page, browserName }) => {
|
||||||
const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1);
|
const [randomDoc] = await createDoc(page, 'doc-delete', browserName, 1);
|
||||||
|
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||||
|
|
||||||
await page.getByLabel('Open the document options').click();
|
await page.getByLabel('Open the document options').click();
|
||||||
await page
|
await page
|
||||||
@@ -94,13 +146,7 @@ test.describe('Doc Header', () => {
|
|||||||
.click();
|
.click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('heading', { name: 'Delete a doc' }),
|
page.locator('h2').getByText(`Deleting the document "${randomDoc}"`),
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.getByText(
|
|
||||||
`Are you sure you want to delete the document "${randomDoc}"?`,
|
|
||||||
),
|
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
@@ -113,7 +159,9 @@ test.describe('Doc Header', () => {
|
|||||||
page.getByText('The document has been deleted.'),
|
page.getByText('The document has been deleted.'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'New do' })).toBeVisible();
|
await expect(
|
||||||
|
page.getByRole('button', { name: 'Create a new document' }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
const row = page
|
const row = page
|
||||||
.getByLabel('Datagrid of the documents page 1')
|
.getByLabel('Datagrid of the documents page 1')
|
||||||
@@ -147,13 +195,16 @@ test.describe('Doc Header', () => {
|
|||||||
|
|
||||||
await goToGridDoc(page);
|
await goToGridDoc(page);
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'download' })).toBeVisible();
|
await expect(
|
||||||
|
page.locator('h2').getByText('Mocked document'),
|
||||||
|
).toHaveAttribute('contenteditable');
|
||||||
|
|
||||||
await page.getByLabel('Open the document options').click();
|
await page.getByLabel('Open the document options').click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('button', { name: 'Export' })).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Delete document' }),
|
page.getByRole('button', { name: 'Delete document' }),
|
||||||
).toBeDisabled();
|
).toBeHidden();
|
||||||
|
|
||||||
// Click somewhere else to close the options
|
// Click somewhere else to close the options
|
||||||
await page.click('body', { position: { x: 0, y: 0 } });
|
await page.click('body', { position: { x: 0, y: 0 } });
|
||||||
@@ -161,40 +212,34 @@ test.describe('Doc Header', () => {
|
|||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
const shareModal = page.getByLabel('Share modal');
|
const shareModal = page.getByLabel('Share modal');
|
||||||
await expect(shareModal).toBeVisible();
|
|
||||||
await expect(page.getByText('Share the document')).toBeVisible();
|
|
||||||
|
|
||||||
await expect(page.getByPlaceholder('Type a name or email')).toBeVisible();
|
await expect(
|
||||||
|
shareModal.getByRole('combobox', {
|
||||||
|
name: 'Visibility',
|
||||||
|
}),
|
||||||
|
).not.toHaveAttribute('disabled');
|
||||||
|
await expect(shareModal.getByText('Search by email')).toBeVisible();
|
||||||
|
|
||||||
const invitationCard = shareModal.getByLabel('List invitation card');
|
const invitationCard = shareModal.getByLabel('List invitation card');
|
||||||
await expect(invitationCard).toBeVisible();
|
|
||||||
await expect(
|
await expect(
|
||||||
invitationCard.getByText('test@invitation.test').first(),
|
invitationCard.getByText('test@invitation.test'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(invitationCard.getByLabel('doc-role-dropdown')).toBeVisible();
|
|
||||||
|
|
||||||
await invitationCard.getByRole('button', { name: 'more_horiz' }).click();
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', {
|
invitationCard.getByRole('combobox', { name: 'Role' }),
|
||||||
|
).toBeEnabled();
|
||||||
|
await expect(
|
||||||
|
invitationCard.getByRole('button', {
|
||||||
name: 'delete',
|
name: 'delete',
|
||||||
}),
|
}),
|
||||||
).toBeEnabled();
|
).toBeEnabled();
|
||||||
await invitationCard.click();
|
|
||||||
|
|
||||||
const memberCard = shareModal.getByLabel('List members card');
|
const memberCard = shareModal.getByLabel('List members card');
|
||||||
await expect(memberCard).toBeVisible();
|
await expect(memberCard.getByText('test@accesses.test')).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
memberCard.getByText('test@accesses.test').first(),
|
memberCard.getByRole('combobox', { name: 'Role' }),
|
||||||
).toBeVisible();
|
).toBeEnabled();
|
||||||
await expect(memberCard.getByLabel('doc-role-dropdown')).toBeVisible();
|
|
||||||
await expect(
|
await expect(
|
||||||
memberCard.getByRole('button', { name: 'more_horiz' }),
|
memberCard.getByRole('button', {
|
||||||
).toBeVisible();
|
|
||||||
await memberCard.getByRole('button', { name: 'more_horiz' }).click();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.getByRole('button', {
|
|
||||||
name: 'delete',
|
name: 'delete',
|
||||||
}),
|
}),
|
||||||
).toBeEnabled();
|
).toBeEnabled();
|
||||||
@@ -228,12 +273,16 @@ test.describe('Doc Header', () => {
|
|||||||
|
|
||||||
await goToGridDoc(page);
|
await goToGridDoc(page);
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'download' })).toBeVisible();
|
await expect(
|
||||||
|
page.locator('h2').getByText('Mocked document'),
|
||||||
|
).toHaveAttribute('contenteditable');
|
||||||
|
|
||||||
await page.getByLabel('Open the document options').click();
|
await page.getByLabel('Open the document options').click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('button', { name: 'Export' })).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Delete document' }),
|
page.getByRole('button', { name: 'Delete document' }),
|
||||||
).toBeDisabled();
|
).toBeHidden();
|
||||||
|
|
||||||
// Click somewhere else to close the options
|
// Click somewhere else to close the options
|
||||||
await page.click('body', { position: { x: 0, y: 0 } });
|
await page.click('body', { position: { x: 0, y: 0 } });
|
||||||
@@ -241,24 +290,36 @@ test.describe('Doc Header', () => {
|
|||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
const shareModal = page.getByLabel('Share modal');
|
const shareModal = page.getByLabel('Share modal');
|
||||||
await expect(page.getByText('Share the document')).toBeVisible();
|
|
||||||
|
|
||||||
await expect(page.getByPlaceholder('Type a name or email')).toBeHidden();
|
await expect(
|
||||||
|
shareModal.getByRole('combobox', {
|
||||||
|
name: 'Visibility',
|
||||||
|
}),
|
||||||
|
).toHaveAttribute('disabled');
|
||||||
|
await expect(shareModal.getByText('Search by email')).toBeHidden();
|
||||||
|
|
||||||
const invitationCard = shareModal.getByLabel('List invitation card');
|
const invitationCard = shareModal.getByLabel('List invitation card');
|
||||||
await expect(
|
await expect(
|
||||||
invitationCard.getByText('test@invitation.test').first(),
|
invitationCard.getByText('test@invitation.test'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(invitationCard.getByLabel('doc-role-text')).toBeVisible();
|
|
||||||
await expect(
|
await expect(
|
||||||
invitationCard.getByRole('button', { name: 'more_horiz' }),
|
invitationCard.getByRole('combobox', { name: 'Role' }),
|
||||||
|
).toHaveAttribute('disabled');
|
||||||
|
await expect(
|
||||||
|
invitationCard.getByRole('button', {
|
||||||
|
name: 'delete',
|
||||||
|
}),
|
||||||
).toBeHidden();
|
).toBeHidden();
|
||||||
|
|
||||||
const memberCard = shareModal.getByLabel('List members card');
|
const memberCard = shareModal.getByLabel('List members card');
|
||||||
await expect(memberCard.getByText('test@accesses.test')).toBeVisible();
|
await expect(memberCard.getByText('test@accesses.test')).toBeVisible();
|
||||||
await expect(memberCard.getByLabel('doc-role-text')).toBeVisible();
|
|
||||||
await expect(
|
await expect(
|
||||||
memberCard.getByRole('button', { name: 'more_horiz' }),
|
memberCard.getByRole('combobox', { name: 'Role' }),
|
||||||
|
).toHaveAttribute('disabled');
|
||||||
|
await expect(
|
||||||
|
memberCard.getByRole('button', {
|
||||||
|
name: 'delete',
|
||||||
|
}),
|
||||||
).toBeHidden();
|
).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -290,12 +351,16 @@ test.describe('Doc Header', () => {
|
|||||||
|
|
||||||
await goToGridDoc(page);
|
await goToGridDoc(page);
|
||||||
|
|
||||||
await expect(page.getByRole('button', { name: 'download' })).toBeVisible();
|
await expect(
|
||||||
|
page.locator('h2').getByText('Mocked document'),
|
||||||
|
).not.toHaveAttribute('contenteditable');
|
||||||
|
|
||||||
await page.getByLabel('Open the document options').click();
|
await page.getByLabel('Open the document options').click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('button', { name: 'Export' })).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Delete document' }),
|
page.getByRole('button', { name: 'Delete document' }),
|
||||||
).toBeDisabled();
|
).toBeHidden();
|
||||||
|
|
||||||
// Click somewhere else to close the options
|
// Click somewhere else to close the options
|
||||||
await page.click('body', { position: { x: 0, y: 0 } });
|
await page.click('body', { position: { x: 0, y: 0 } });
|
||||||
@@ -303,24 +368,36 @@ test.describe('Doc Header', () => {
|
|||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
const shareModal = page.getByLabel('Share modal');
|
const shareModal = page.getByLabel('Share modal');
|
||||||
await expect(page.getByText('Share the document')).toBeVisible();
|
|
||||||
|
|
||||||
await expect(page.getByPlaceholder('Type a name or email')).toBeHidden();
|
await expect(
|
||||||
|
shareModal.getByRole('combobox', {
|
||||||
|
name: 'Visibility',
|
||||||
|
}),
|
||||||
|
).toHaveAttribute('disabled');
|
||||||
|
await expect(shareModal.getByText('Search by email')).toBeHidden();
|
||||||
|
|
||||||
const invitationCard = shareModal.getByLabel('List invitation card');
|
const invitationCard = shareModal.getByLabel('List invitation card');
|
||||||
await expect(
|
await expect(
|
||||||
invitationCard.getByText('test@invitation.test').first(),
|
invitationCard.getByText('test@invitation.test'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(invitationCard.getByLabel('doc-role-text')).toBeVisible();
|
|
||||||
await expect(
|
await expect(
|
||||||
invitationCard.getByRole('button', { name: 'more_horiz' }),
|
invitationCard.getByRole('combobox', { name: 'Role' }),
|
||||||
|
).toHaveAttribute('disabled');
|
||||||
|
await expect(
|
||||||
|
invitationCard.getByRole('button', {
|
||||||
|
name: 'delete',
|
||||||
|
}),
|
||||||
).toBeHidden();
|
).toBeHidden();
|
||||||
|
|
||||||
const memberCard = shareModal.getByLabel('List members card');
|
const memberCard = shareModal.getByLabel('List members card');
|
||||||
await expect(memberCard.getByText('test@accesses.test')).toBeVisible();
|
await expect(memberCard.getByText('test@accesses.test')).toBeVisible();
|
||||||
await expect(memberCard.getByLabel('doc-role-text')).toBeVisible();
|
|
||||||
await expect(
|
await expect(
|
||||||
memberCard.getByRole('button', { name: 'more_horiz' }),
|
memberCard.getByRole('combobox', { name: 'Role' }),
|
||||||
|
).toHaveAttribute('disabled');
|
||||||
|
await expect(
|
||||||
|
memberCard.getByRole('button', {
|
||||||
|
name: 'delete',
|
||||||
|
}),
|
||||||
).toBeHidden();
|
).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -337,7 +414,7 @@ test.describe('Doc Header', () => {
|
|||||||
// create page and navigate to it
|
// create page and navigate to it
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
name: 'New doc',
|
name: 'Create a new document',
|
||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
@@ -372,7 +449,7 @@ test.describe('Doc Header', () => {
|
|||||||
// create page and navigate to it
|
// create page and navigate to it
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
name: 'New doc',
|
name: 'Create a new document',
|
||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
@@ -394,7 +471,9 @@ test.describe('Doc Header', () => {
|
|||||||
navigator.clipboard.readText(),
|
navigator.clipboard.readText(),
|
||||||
);
|
);
|
||||||
const clipboardContent = await handle.jsonValue();
|
const clipboardContent = await handle.jsonValue();
|
||||||
expect(clipboardContent.trim()).toBe(`<h1>Hello World</h1><p></p>`);
|
expect(clipboardContent.trim()).toBe(
|
||||||
|
`<h1 data-level="1">Hello World</h1><p></p>`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -422,7 +501,6 @@ test.describe('Documents Header mobile', () => {
|
|||||||
|
|
||||||
await goToGridDoc(page);
|
await goToGridDoc(page);
|
||||||
|
|
||||||
await page.getByLabel('Open the document options').click();
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
await expect(page.getByLabel('Share modal')).toBeVisible();
|
await expect(page.getByLabel('Share modal')).toBeVisible();
|
||||||
|
|||||||
@@ -16,82 +16,163 @@ test.describe('Document create member', () => {
|
|||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
const inputSearch = page.getByRole('combobox', {
|
const inputSearch = page.getByLabel(/Find a member to add to the document/);
|
||||||
name: 'Quick search input',
|
|
||||||
});
|
|
||||||
await expect(inputSearch).toBeVisible();
|
await expect(inputSearch).toBeVisible();
|
||||||
|
|
||||||
// Select user 1 and verify tag
|
// Select user 1
|
||||||
await inputSearch.fill('user');
|
await inputSearch.fill('user');
|
||||||
|
|
||||||
const response = await responsePromise;
|
const response = await responsePromise;
|
||||||
const users = (await response.json()).results as {
|
const users = (await response.json()).results as {
|
||||||
email: string;
|
email: string;
|
||||||
full_name: string;
|
|
||||||
}[];
|
}[];
|
||||||
|
|
||||||
const list = page.getByTestId('doc-share-add-member-list');
|
await page.getByRole('option', { name: users[0].email }).click();
|
||||||
await expect(list).toBeHidden();
|
|
||||||
const quickSearchContent = page.getByTestId('doc-share-quick-search');
|
|
||||||
await quickSearchContent
|
|
||||||
.getByTestId(`search-user-row-${users[0].email}`)
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(list).toBeVisible();
|
// Select user 2
|
||||||
await expect(
|
|
||||||
list.getByTestId(`doc-share-add-member-${users[0].email}`),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(list.getByText(`${users[0].full_name}`)).toBeVisible();
|
|
||||||
|
|
||||||
// Select user 2 and verify tag
|
|
||||||
await inputSearch.fill('user');
|
await inputSearch.fill('user');
|
||||||
await quickSearchContent
|
await page.getByRole('option', { name: users[1].email }).click();
|
||||||
.getByTestId(`search-user-row-${users[1].email}`)
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(
|
// Select email
|
||||||
list.getByTestId(`doc-share-add-member-${users[1].email}`),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(list.getByText(`${users[1].full_name}`)).toBeVisible();
|
|
||||||
|
|
||||||
// Select email and verify tag
|
|
||||||
const email = randomName('test@test.fr', browserName, 1)[0];
|
const email = randomName('test@test.fr', browserName, 1)[0];
|
||||||
await inputSearch.fill(email);
|
await inputSearch.fill(email);
|
||||||
await quickSearchContent.getByText(email).click();
|
await page.getByRole('option', { name: email }).click();
|
||||||
await expect(list.getByText(email)).toBeVisible();
|
|
||||||
|
// Check user 1 tag
|
||||||
|
await expect(
|
||||||
|
page.getByText(`${users[0].email}`, { exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.getByLabel(`Remove ${users[0].email}`)).toBeVisible();
|
||||||
|
|
||||||
|
// Check user 2 tag
|
||||||
|
await expect(
|
||||||
|
page.getByText(`${users[1].email}`, { exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.getByLabel(`Remove ${users[1].email}`)).toBeVisible();
|
||||||
|
|
||||||
|
// Check invitation tag
|
||||||
|
await expect(page.getByText(email, { exact: true })).toBeVisible();
|
||||||
|
await expect(page.getByLabel(`Remove ${email}`)).toBeVisible();
|
||||||
|
|
||||||
// Check roles are displayed
|
// Check roles are displayed
|
||||||
await list.getByLabel('doc-role-dropdown').click();
|
await page.getByRole('combobox', { name: /Choose a role/ }).click();
|
||||||
await expect(page.getByRole('button', { name: 'Reader' })).toBeVisible();
|
|
||||||
await expect(page.getByRole('button', { name: 'Editor' })).toBeVisible();
|
|
||||||
await expect(page.getByRole('button', { name: 'Owner' })).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
page.getByRole('button', { name: 'Administrator' }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
// Validate
|
await expect(page.getByRole('option', { name: 'Reader' })).toBeVisible();
|
||||||
await page.getByRole('button', { name: 'Administrator' }).click();
|
await expect(page.getByRole('option', { name: 'Editor' })).toBeVisible();
|
||||||
await page.getByRole('button', { name: 'Invite' }).click();
|
|
||||||
|
|
||||||
// Check invitation added
|
|
||||||
await expect(
|
await expect(
|
||||||
quickSearchContent.getByText('Pending invitations'),
|
page.getByRole('option', { name: 'Administrator' }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
await expect(quickSearchContent.getByText(email).first()).toBeVisible();
|
await expect(page.getByRole('option', { name: 'Owner' })).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it sends a new invitation and adds a new user', async ({
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
}) => {
|
||||||
|
const responsePromiseSearchUser = page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/users/?q=user') && response.status() === 200,
|
||||||
|
);
|
||||||
|
|
||||||
|
await createDoc(page, 'user-invitation', browserName, 1);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
|
const inputSearch = page.getByLabel(/Find a member to add to the document/);
|
||||||
|
|
||||||
|
const email = randomName('test@test.fr', browserName, 1)[0];
|
||||||
|
await inputSearch.fill(email);
|
||||||
|
await page.getByRole('option', { name: email }).click();
|
||||||
|
|
||||||
|
// Select a new user
|
||||||
|
await inputSearch.fill('user');
|
||||||
|
const responseSearchUser = await responsePromiseSearchUser;
|
||||||
|
const [user] = (await responseSearchUser.json()).results as {
|
||||||
|
email: string;
|
||||||
|
}[];
|
||||||
|
await page.getByRole('option', { name: user.email }).click();
|
||||||
|
|
||||||
|
// Choose a role
|
||||||
|
await page.getByRole('combobox', { name: /Choose a role/ }).click();
|
||||||
|
await page.getByRole('option', { name: 'Administrator' }).click();
|
||||||
|
|
||||||
|
const responsePromiseCreateInvitation = page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/invitations/') && response.status() === 201,
|
||||||
|
);
|
||||||
|
const responsePromiseAddUser = page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/accesses/') && response.status() === 201,
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Validate' }).click();
|
||||||
|
|
||||||
|
// Check invitation sent
|
||||||
|
await expect(page.getByText(`Invitation sent to ${email}`)).toBeVisible();
|
||||||
|
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
||||||
|
expect(responseCreateInvitation.ok()).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
responseCreateInvitation.request().headers()['content-language'],
|
||||||
|
).toBe('en-us');
|
||||||
|
|
||||||
// Check user added
|
// Check user added
|
||||||
await expect(page.getByText('Share with 3 users')).toBeVisible();
|
|
||||||
await expect(
|
await expect(
|
||||||
quickSearchContent.getByText(users[0].full_name).first(),
|
page.getByText(`User ${user.email} added to the document.`),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
const responseAddUser = await responsePromiseAddUser;
|
||||||
|
expect(responseAddUser.ok()).toBeTruthy();
|
||||||
|
expect(responseAddUser.request().headers()['content-language']).toBe(
|
||||||
|
'en-us',
|
||||||
|
);
|
||||||
|
|
||||||
|
const listInvitation = page.getByLabel('List invitation card');
|
||||||
|
await expect(listInvitation.locator('li').getByText(email)).toBeVisible();
|
||||||
await expect(
|
await expect(
|
||||||
quickSearchContent.getByText(users[0].email).first(),
|
listInvitation.locator('li').getByText('Invited'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
|
const listMember = page.getByLabel('List members card');
|
||||||
|
await expect(listMember.locator('li').getByText(user.email)).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it try to add twice the same user', async ({ page, browserName }) => {
|
||||||
|
const responsePromiseSearchUser = page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/users/?q=user') && response.status() === 200,
|
||||||
|
);
|
||||||
|
|
||||||
|
await createDoc(page, 'user-twice', browserName, 1);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
|
const inputSearch = page.getByLabel(/Find a member to add to the document/);
|
||||||
|
await inputSearch.fill('user');
|
||||||
|
const responseSearchUser = await responsePromiseSearchUser;
|
||||||
|
const [user] = (await responseSearchUser.json()).results as {
|
||||||
|
email: string;
|
||||||
|
}[];
|
||||||
|
await page.getByRole('option', { name: user.email }).click();
|
||||||
|
|
||||||
|
// Choose a role
|
||||||
|
await page.getByRole('combobox', { name: /Choose a role/ }).click();
|
||||||
|
await page.getByRole('option', { name: 'Owner' }).click();
|
||||||
|
|
||||||
|
const responsePromiseAddMember = page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes('/accesses/') && response.status() === 201,
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Validate' }).click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
quickSearchContent.getByText(users[1].email).first(),
|
page.getByText(`User ${user.email} added to the document.`),
|
||||||
).toBeVisible();
|
|
||||||
await expect(
|
|
||||||
quickSearchContent.getByText(users[1].full_name).first(),
|
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
const responseAddMember = await responsePromiseAddMember;
|
||||||
|
expect(responseAddMember.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
await inputSearch.fill('user');
|
||||||
|
await expect(page.getByText('Loading...')).toBeHidden();
|
||||||
|
await expect(page.getByRole('option', { name: user.email })).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it try to add twice the same invitation', async ({
|
test('it try to add twice the same invitation', async ({
|
||||||
@@ -102,43 +183,40 @@ test.describe('Document create member', () => {
|
|||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
const inputSearch = page.getByRole('combobox', {
|
const inputSearch = page.getByLabel(/Find a member to add to the document/);
|
||||||
name: 'Quick search input',
|
|
||||||
});
|
|
||||||
|
|
||||||
const [email] = randomName('test@test.fr', browserName, 1);
|
const [email] = randomName('test@test.fr', browserName, 1);
|
||||||
await inputSearch.fill(email);
|
await inputSearch.fill(email);
|
||||||
await page.getByTestId(`search-user-row-${email}`).click();
|
await page.getByRole('option', { name: email }).click();
|
||||||
|
|
||||||
// Choose a role
|
// Choose a role
|
||||||
const container = page.getByTestId('doc-share-add-member-list');
|
await page.getByRole('combobox', { name: /Choose a role/ }).click();
|
||||||
await container.getByLabel('doc-role-dropdown').click();
|
await page.getByRole('option', { name: 'Owner' }).click();
|
||||||
await page.getByRole('button', { name: 'Owner' }).click();
|
|
||||||
|
|
||||||
const responsePromiseCreateInvitation = page.waitForResponse(
|
const responsePromiseCreateInvitation = page.waitForResponse(
|
||||||
(response) =>
|
(response) =>
|
||||||
response.url().includes('/invitations/') && response.status() === 201,
|
response.url().includes('/invitations/') && response.status() === 201,
|
||||||
);
|
);
|
||||||
await page.getByRole('button', { name: 'Invite' }).click();
|
|
||||||
|
await page.getByRole('button', { name: 'Validate' }).click();
|
||||||
|
|
||||||
// Check invitation sent
|
// Check invitation sent
|
||||||
|
await expect(page.getByText(`Invitation sent to ${email}`)).toBeVisible();
|
||||||
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
||||||
expect(responseCreateInvitation.ok()).toBeTruthy();
|
expect(responseCreateInvitation.ok()).toBeTruthy();
|
||||||
|
|
||||||
await inputSearch.fill(email);
|
await inputSearch.fill(email);
|
||||||
await page.getByTestId(`search-user-row-${email}`).click();
|
await page.getByRole('option', { name: email }).click();
|
||||||
|
|
||||||
// Choose a role
|
// Choose a role
|
||||||
await container.getByLabel('doc-role-dropdown').click();
|
await page.getByRole('combobox', { name: /Choose a role/ }).click();
|
||||||
await page.getByRole('button', { name: 'Owner' }).click();
|
await page.getByRole('option', { name: 'Owner' }).click();
|
||||||
|
|
||||||
const responsePromiseCreateInvitationFail = page.waitForResponse(
|
const responsePromiseCreateInvitationFail = page.waitForResponse(
|
||||||
(response) =>
|
(response) =>
|
||||||
response.url().includes('/invitations/') && response.status() === 400,
|
response.url().includes('/invitations/') && response.status() === 400,
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Invite' }).click();
|
await page.getByRole('button', { name: 'Validate' }).click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(`"${email}" is already invited to the document.`),
|
page.getByText(`"${email}" is already invited to the document.`),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
@@ -155,32 +233,31 @@ test.describe('Document create member', () => {
|
|||||||
|
|
||||||
const header = page.locator('header').first();
|
const header = page.locator('header').first();
|
||||||
await header.getByRole('combobox').getByText('EN').click();
|
await header.getByRole('combobox').getByText('EN').click();
|
||||||
await header.getByRole('option', { name: 'translate Français' }).click();
|
await header.getByRole('option', { name: 'FR' }).click();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Partager' }).click();
|
await page.getByRole('button', { name: 'Partager' }).click();
|
||||||
|
|
||||||
const inputSearch = page.getByRole('combobox', {
|
const inputSearch = page.getByLabel(
|
||||||
name: 'Quick search input',
|
/Trouver un membre à ajouter au document/,
|
||||||
});
|
);
|
||||||
|
|
||||||
const email = randomName('test@test.fr', browserName, 1)[0];
|
const email = randomName('test@test.fr', browserName, 1)[0];
|
||||||
await inputSearch.fill(email);
|
await inputSearch.fill(email);
|
||||||
await page.getByTestId(`search-user-row-${email}`).click();
|
await page.getByRole('option', { name: email }).click();
|
||||||
|
|
||||||
// Choose a role
|
// Choose a role
|
||||||
const container = page.getByTestId('doc-share-add-member-list');
|
await page.getByRole('combobox', { name: /Choisissez un rôle/ }).click();
|
||||||
await container.getByLabel('doc-role-dropdown').click();
|
await page.getByRole('option', { name: 'Administrateur' }).click();
|
||||||
await page.getByRole('button', { name: 'Administrateur' }).click();
|
|
||||||
|
|
||||||
const responsePromiseCreateInvitation = page.waitForResponse(
|
const responsePromiseCreateInvitation = page.waitForResponse(
|
||||||
(response) =>
|
(response) =>
|
||||||
response.url().includes('/invitations/') && response.status() === 201,
|
response.url().includes('/invitations/') && response.status() === 201,
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Invite' }).click();
|
await page.getByRole('button', { name: 'Valider' }).click();
|
||||||
|
|
||||||
// Check invitation sent
|
// Check invitation sent
|
||||||
|
await expect(page.getByText(`Invitation envoyée à ${email}`)).toBeVisible();
|
||||||
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
||||||
expect(responseCreateInvitation.ok()).toBeTruthy();
|
expect(responseCreateInvitation.ok()).toBeTruthy();
|
||||||
expect(
|
expect(
|
||||||
@@ -193,46 +270,41 @@ test.describe('Document create member', () => {
|
|||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
const inputSearch = page.getByRole('combobox', {
|
const inputSearch = page.getByLabel(/Find a member to add to the document/);
|
||||||
name: 'Quick search input',
|
|
||||||
});
|
|
||||||
|
|
||||||
const email = randomName('test@test.fr', browserName, 1)[0];
|
const email = randomName('test@test.fr', browserName, 1)[0];
|
||||||
await inputSearch.fill(email);
|
await inputSearch.fill(email);
|
||||||
await page.getByTestId(`search-user-row-${email}`).click();
|
await page.getByRole('option', { name: email }).click();
|
||||||
|
|
||||||
// Choose a role
|
// Choose a role
|
||||||
const container = page.getByTestId('doc-share-add-member-list');
|
await page.getByRole('combobox', { name: /Choose a role/ }).click();
|
||||||
await container.getByLabel('doc-role-dropdown').click();
|
await page.getByRole('option', { name: 'Administrator' }).click();
|
||||||
await page.getByRole('button', { name: 'Administrator' }).click();
|
|
||||||
|
|
||||||
const responsePromiseCreateInvitation = page.waitForResponse(
|
const responsePromiseCreateInvitation = page.waitForResponse(
|
||||||
(response) =>
|
(response) =>
|
||||||
response.url().includes('/invitations/') && response.status() === 201,
|
response.url().includes('/invitations/') && response.status() === 201,
|
||||||
);
|
);
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Invite' }).click();
|
await page.getByRole('button', { name: 'Validate' }).click();
|
||||||
|
|
||||||
// Check invitation sent
|
// Check invitation sent
|
||||||
|
await expect(page.getByText(`Invitation sent to ${email}`)).toBeVisible();
|
||||||
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
const responseCreateInvitation = await responsePromiseCreateInvitation;
|
||||||
expect(responseCreateInvitation.ok()).toBeTruthy();
|
expect(responseCreateInvitation.ok()).toBeTruthy();
|
||||||
|
|
||||||
const listInvitation = page.getByTestId('doc-share-quick-search');
|
const listInvitation = page.getByLabel('List invitation card');
|
||||||
const userInvitation = listInvitation.getByTestId(
|
const li = listInvitation.locator('li').filter({
|
||||||
`doc-share-invitation-row-${email}`,
|
hasText: email,
|
||||||
);
|
|
||||||
await expect(userInvitation).toBeVisible();
|
|
||||||
|
|
||||||
await userInvitation.getByLabel('doc-role-dropdown').click();
|
|
||||||
await page.getByRole('button', { name: 'Reader' }).click();
|
|
||||||
|
|
||||||
const moreActions = userInvitation.getByRole('button', {
|
|
||||||
name: 'more_horiz',
|
|
||||||
});
|
});
|
||||||
await moreActions.click();
|
await expect(li.getByText(email)).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
await li.getByRole('combobox', { name: /Role/ }).click();
|
||||||
|
await li.getByRole('option', { name: 'Reader' }).click();
|
||||||
await expect(userInvitation).toBeHidden();
|
await expect(page.getByText(`The role has been updated.`)).toBeVisible();
|
||||||
|
await li.getByText('delete').click();
|
||||||
|
await expect(
|
||||||
|
page.getByText(`The invitation has been removed.`),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(listInvitation.locator('li').getByText(email)).toBeHidden();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { addNewMember, createDoc, goToGridDoc, verifyDocName } from './common';
|
import { waitForElementCount } from '../helpers';
|
||||||
|
|
||||||
|
import { addNewMember, createDoc, goToGridDoc } from './common';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@@ -13,11 +15,10 @@ test.describe('Document list members', () => {
|
|||||||
async (route) => {
|
async (route) => {
|
||||||
const request = route.request();
|
const request = route.request();
|
||||||
const url = new URL(request.url());
|
const url = new URL(request.url());
|
||||||
const pageId = url.searchParams.get('page') ?? '1';
|
const pageId = url.searchParams.get('page');
|
||||||
|
|
||||||
const accesses = {
|
const accesses = {
|
||||||
count: 40,
|
count: 100,
|
||||||
next: +pageId < 2 ? 'http://anything/?page=2' : undefined,
|
next: 'http://anything/?page=2',
|
||||||
previous: null,
|
previous: null,
|
||||||
results: Array.from({ length: 20 }, (_, i) => ({
|
results: Array.from({ length: 20 }, (_, i) => ({
|
||||||
id: `2ff1ec07-86c1-4534-a643-f41824a6c53a-${pageId}-${i}`,
|
id: `2ff1ec07-86c1-4534-a643-f41824a6c53a-${pageId}-${i}`,
|
||||||
@@ -46,23 +47,26 @@ test.describe('Document list members', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const docTitle = await goToGridDoc(page);
|
await goToGridDoc(page);
|
||||||
await verifyDocName(page, docTitle);
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
const prefix = 'doc-share-member-row';
|
const list = page.getByLabel('List members card').locator('ul');
|
||||||
const elements = page.locator(`[data-testid^="${prefix}"]`);
|
await expect(list.locator('li')).toHaveCount(20);
|
||||||
const loadMore = page.getByTestId('load-more-members');
|
await list.getByText(`impress@impress.world-page-${1}-18`).hover();
|
||||||
|
await page.mouse.wheel(0, 10);
|
||||||
|
|
||||||
await expect(elements).toHaveCount(20);
|
await waitForElementCount(list.locator('li'), 21, 10000);
|
||||||
await expect(page.getByText(`Impress World Page 1-16`)).toBeVisible();
|
|
||||||
|
|
||||||
await loadMore.click();
|
expect(await list.locator('li').count()).toBeGreaterThan(20);
|
||||||
await expect(elements).toHaveCount(40);
|
await expect(list.getByText(`Impress World Page 1-16`)).toBeVisible();
|
||||||
await expect(page.getByText(`Impress World Page 2-15`)).toBeVisible();
|
await expect(
|
||||||
|
list.getByText(`impress@impress.world-page-1-16`),
|
||||||
await expect(loadMore).toBeHidden();
|
).toBeVisible();
|
||||||
|
await expect(list.getByText(`Impress World Page 2-15`)).toBeVisible();
|
||||||
|
await expect(
|
||||||
|
list.getByText(`impress@impress.world-page-2-15`),
|
||||||
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it checks a big list of invitations', async ({ page }) => {
|
test('it checks a big list of invitations', async ({ page }) => {
|
||||||
@@ -71,10 +75,10 @@ test.describe('Document list members', () => {
|
|||||||
async (route) => {
|
async (route) => {
|
||||||
const request = route.request();
|
const request = route.request();
|
||||||
const url = new URL(request.url());
|
const url = new URL(request.url());
|
||||||
const pageId = url.searchParams.get('page') ?? '1';
|
const pageId = url.searchParams.get('page');
|
||||||
const accesses = {
|
const accesses = {
|
||||||
count: 40,
|
count: 100,
|
||||||
next: +pageId < 2 ? 'http://anything/?page=2' : null,
|
next: 'http://anything/?page=2',
|
||||||
previous: null,
|
previous: null,
|
||||||
results: Array.from({ length: 20 }, (_, i) => ({
|
results: Array.from({ length: 20 }, (_, i) => ({
|
||||||
id: `2ff1ec07-86c1-4534-a643-f41824a6c53a-${pageId}-${i}`,
|
id: `2ff1ec07-86c1-4534-a643-f41824a6c53a-${pageId}-${i}`,
|
||||||
@@ -100,128 +104,131 @@ test.describe('Document list members', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const docTitle = await goToGridDoc(page);
|
await goToGridDoc(page);
|
||||||
await verifyDocName(page, docTitle);
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
const prefix = 'doc-share-invitation';
|
const list = page.getByLabel('List invitation card').locator('ul');
|
||||||
const elements = page.locator(`[data-testid^="${prefix}"]`);
|
await expect(list.locator('li')).toHaveCount(20);
|
||||||
const loadMore = page.getByTestId('load-more-invitations');
|
await list.getByText(`impress@impress.world-page-${1}-18`).hover();
|
||||||
|
await page.mouse.wheel(0, 10);
|
||||||
|
|
||||||
await expect(elements).toHaveCount(20);
|
await waitForElementCount(list.locator('li'), 21, 10000);
|
||||||
|
|
||||||
|
expect(await list.locator('li').count()).toBeGreaterThan(20);
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(`impress@impress.world-page-1-16`).first(),
|
list.getByText(`impress@impress.world-page-1-16`),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await loadMore.click();
|
|
||||||
await expect(elements).toHaveCount(40);
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(`impress@impress.world-page-2-16`).first(),
|
list.getByText(`impress@impress.world-page-2-15`),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(loadMore).toBeHidden();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it checks the role rules', async ({ page, browserName }) => {
|
test('it checks the role rules', async ({ page, browserName }) => {
|
||||||
const [docTitle] = await createDoc(page, 'Doc role rules', browserName, 1);
|
const [docTitle] = await createDoc(page, 'Doc role rules', browserName, 1);
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
const list = page.getByTestId('doc-share-quick-search');
|
|
||||||
await expect(list).toBeVisible();
|
const list = page.getByLabel('List members card').locator('ul');
|
||||||
const currentUser = list.getByTestId(
|
|
||||||
`doc-share-member-row-user@chromium.e2e`,
|
await expect(list.getByText(`user@${browserName}.e2e`)).toBeVisible();
|
||||||
);
|
|
||||||
const currentUserRole = currentUser.getByLabel('doc-role-dropdown');
|
const soleOwner = list.getByText(
|
||||||
await expect(currentUser).toBeVisible();
|
|
||||||
await expect(currentUserRole).toBeVisible();
|
|
||||||
await currentUserRole.click();
|
|
||||||
const soloOwner = page.getByText(
|
|
||||||
`You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.`,
|
`You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.`,
|
||||||
);
|
);
|
||||||
await expect(soloOwner).toBeVisible();
|
|
||||||
await list.click();
|
|
||||||
const newUserEmail = await addNewMember(page, 0, 'Owner');
|
|
||||||
const newUser = list.getByTestId(`doc-share-member-row-${newUserEmail}`);
|
|
||||||
const newUserRoles = newUser.getByLabel('doc-role-dropdown');
|
|
||||||
|
|
||||||
await expect(newUser).toBeVisible();
|
await expect(soleOwner).toBeVisible();
|
||||||
|
|
||||||
await currentUserRole.click();
|
const username = await addNewMember(page, 0, 'Owner');
|
||||||
await expect(soloOwner).toBeHidden();
|
|
||||||
await list.click();
|
|
||||||
|
|
||||||
const otherOwner = page.getByText(
|
await expect(list.getByText(username)).toBeVisible();
|
||||||
|
|
||||||
|
await expect(soleOwner).toBeHidden();
|
||||||
|
|
||||||
|
const otherOwner = list.getByText(
|
||||||
`You cannot update the role or remove other owner.`,
|
`You cannot update the role or remove other owner.`,
|
||||||
);
|
);
|
||||||
|
|
||||||
await newUserRoles.click();
|
|
||||||
await expect(otherOwner).toBeVisible();
|
await expect(otherOwner).toBeVisible();
|
||||||
await list.click();
|
|
||||||
|
|
||||||
await currentUserRole.click();
|
const SelectRoleCurrentUser = list
|
||||||
await page.getByRole('button', { name: 'Administrator' }).click();
|
.locator('li')
|
||||||
await list.click();
|
.filter({
|
||||||
await expect(currentUserRole).toBeVisible();
|
hasText: `user@${browserName}.e2e`,
|
||||||
|
})
|
||||||
|
.getByRole('combobox', { name: 'Role' });
|
||||||
|
|
||||||
await currentUserRole.click();
|
await SelectRoleCurrentUser.click();
|
||||||
await page.getByRole('button', { name: 'Reader' }).click();
|
await page.getByRole('option', { name: 'Administrator' }).click();
|
||||||
await list.click();
|
await expect(page.getByText('The role has been updated')).toBeVisible();
|
||||||
await expect(currentUserRole).toBeHidden();
|
|
||||||
|
const shareModal = page.getByLabel('Share modal');
|
||||||
|
|
||||||
|
// Admin still have the right to share
|
||||||
|
await expect(
|
||||||
|
shareModal.getByRole('combobox', {
|
||||||
|
name: 'Visibility',
|
||||||
|
}),
|
||||||
|
).not.toHaveAttribute('disabled');
|
||||||
|
|
||||||
|
await SelectRoleCurrentUser.click();
|
||||||
|
await page.getByRole('option', { name: 'Reader' }).click();
|
||||||
|
await expect(page.getByText('The role has been updated')).toBeVisible();
|
||||||
|
|
||||||
|
// Reader does not have the right to share
|
||||||
|
await expect(
|
||||||
|
shareModal.getByRole('combobox', {
|
||||||
|
name: 'Visibility',
|
||||||
|
}),
|
||||||
|
).toHaveAttribute('disabled');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it checks the delete members', async ({ page, browserName }) => {
|
test('it checks the delete members', async ({ page, browserName }) => {
|
||||||
const [docTitle] = await createDoc(page, 'Doc role rules', browserName, 1);
|
const [docTitle] = await createDoc(page, 'Doc role rules', browserName, 1);
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
const list = page.getByTestId('doc-share-quick-search');
|
const list = page.getByLabel('List members card').locator('ul');
|
||||||
|
|
||||||
const emailMyself = `user@${browserName}.e2e`;
|
const nameMyself = `user@${browserName}.e2e`;
|
||||||
const mySelf = list.getByTestId(`doc-share-member-row-${emailMyself}`);
|
await expect(list.getByText(nameMyself)).toBeVisible();
|
||||||
const mySelfMoreActions = mySelf.getByRole('button', {
|
|
||||||
name: 'more_horiz',
|
|
||||||
});
|
|
||||||
|
|
||||||
const userOwnerEmail = await addNewMember(page, 0, 'Owner');
|
const userOwner = await addNewMember(page, 0, 'Owner');
|
||||||
const userOwner = list.getByTestId(
|
await expect(list.getByText(userOwner)).toBeVisible();
|
||||||
`doc-share-member-row-${userOwnerEmail}`,
|
|
||||||
);
|
|
||||||
const userOwnerMoreActions = userOwner.getByRole('button', {
|
|
||||||
name: 'more_horiz',
|
|
||||||
});
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'close' }).first().click();
|
const userReader = await addNewMember(page, 0, 'Reader');
|
||||||
await page.getByRole('button', { name: 'Share' }).first().click();
|
await expect(list.getByText(userReader)).toBeVisible();
|
||||||
|
|
||||||
const userReaderEmail = await addNewMember(page, 0, 'Reader');
|
await list
|
||||||
|
.locator('li')
|
||||||
|
.filter({
|
||||||
|
hasText: userReader,
|
||||||
|
})
|
||||||
|
.getByText('delete')
|
||||||
|
.click();
|
||||||
|
|
||||||
const userReader = list.getByTestId(
|
await expect(list.getByText(userReader)).toBeHidden();
|
||||||
`doc-share-member-row-${userReaderEmail}`,
|
|
||||||
);
|
|
||||||
const userReaderMoreActions = userReader.getByRole('button', {
|
|
||||||
name: 'more_horiz',
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(mySelf).toBeVisible();
|
await list
|
||||||
await expect(userOwner).toBeVisible();
|
.locator('li')
|
||||||
await expect(userReader).toBeVisible();
|
.filter({
|
||||||
|
hasText: nameMyself,
|
||||||
|
})
|
||||||
|
.getByText('delete')
|
||||||
|
.click();
|
||||||
|
|
||||||
await expect(userOwnerMoreActions).toBeVisible();
|
await expect(list.getByText(nameMyself)).toBeHidden();
|
||||||
await expect(userReaderMoreActions).toBeVisible();
|
|
||||||
await expect(mySelfMoreActions).toBeVisible();
|
|
||||||
|
|
||||||
await userReaderMoreActions.click();
|
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
|
||||||
await expect(userReader).toBeHidden();
|
|
||||||
|
|
||||||
await mySelfMoreActions.click();
|
|
||||||
await page.getByRole('button', { name: 'Delete' }).click();
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText('You do not have permission to perform this action.'),
|
page.getByText('The member has been removed from the document').first(),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('heading', { name: 'Share', level: 3 }),
|
||||||
|
).toBeHidden();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ test.describe('Doc Routing', () => {
|
|||||||
|
|
||||||
test('Check the presence of the meta tag noindex', async ({ page }) => {
|
test('Check the presence of the meta tag noindex', async ({ page }) => {
|
||||||
const buttonCreateHomepage = page.getByRole('button', {
|
const buttonCreateHomepage = page.getByRole('button', {
|
||||||
name: 'New doc',
|
name: 'Create a new document',
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(buttonCreateHomepage).toBeVisible();
|
await expect(buttonCreateHomepage).toBeVisible();
|
||||||
@@ -27,7 +27,7 @@ test.describe('Doc Routing', () => {
|
|||||||
await expect(page).toHaveURL('/');
|
await expect(page).toHaveURL('/');
|
||||||
|
|
||||||
const buttonCreateHomepage = page.getByRole('button', {
|
const buttonCreateHomepage = page.getByRole('button', {
|
||||||
name: 'New doc',
|
name: 'Create a new document',
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(buttonCreateHomepage).toBeVisible();
|
await expect(buttonCreateHomepage).toBeVisible();
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
|
|
||||||
import { createDoc, verifyDocName } from './common';
|
|
||||||
|
|
||||||
type SmallDoc = {
|
|
||||||
id: string;
|
|
||||||
title: string;
|
|
||||||
updated_at: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.goto('/');
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Document search', () => {
|
|
||||||
test('it checks all elements are visible', async ({ page }) => {
|
|
||||||
await page.getByRole('button', { name: 'search' }).click();
|
|
||||||
await expect(
|
|
||||||
page.getByRole('img', { name: 'No active search' }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.getByLabel('Search modal').getByText('search'),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page.getByPlaceholder('Type the name of a document'),
|
|
||||||
).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('it checks search for a document', async ({ page, browserName }) => {
|
|
||||||
const id = Math.random().toString(36).substring(7);
|
|
||||||
|
|
||||||
const doc1 = await createDoc(page, `My super ${id} doc`, browserName, 1);
|
|
||||||
await verifyDocName(page, doc1[0]);
|
|
||||||
await page.goto('/');
|
|
||||||
const doc2 = await createDoc(
|
|
||||||
page,
|
|
||||||
`My super ${id} very doc`,
|
|
||||||
browserName,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
await verifyDocName(page, doc2[0]);
|
|
||||||
await page.goto('/');
|
|
||||||
await page.getByRole('button', { name: 'search' }).click();
|
|
||||||
await page.getByPlaceholder('Type the name of a document').click();
|
|
||||||
await page
|
|
||||||
.getByPlaceholder('Type the name of a document')
|
|
||||||
.fill(`My super ${id}`);
|
|
||||||
|
|
||||||
let responsePromisePage = page.waitForResponse(
|
|
||||||
(response) =>
|
|
||||||
response.url().includes(`/documents/?page=1&title=My+super+${id}`) &&
|
|
||||||
response.status() === 200,
|
|
||||||
);
|
|
||||||
let response = await responsePromisePage;
|
|
||||||
let result = (await response.json()) as { results: SmallDoc[] };
|
|
||||||
let docs = result.results;
|
|
||||||
expect(docs.length).toEqual(2);
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
docs.map(async (doc: SmallDoc) => {
|
|
||||||
await expect(
|
|
||||||
page.getByTestId(`doc-search-item-${doc.id}`),
|
|
||||||
).toBeVisible();
|
|
||||||
const updatedAt = DateTime.fromISO(doc.updated_at ?? DateTime.now())
|
|
||||||
.setLocale('en')
|
|
||||||
.toRelative();
|
|
||||||
await expect(
|
|
||||||
page.getByTestId(`doc-search-item-${doc.id}`).getByText(updatedAt!),
|
|
||||||
).toBeVisible();
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const firstDoc = docs[0];
|
|
||||||
|
|
||||||
await expect(
|
|
||||||
page
|
|
||||||
.getByTestId(`doc-search-item-${firstDoc.id}`)
|
|
||||||
.getByText('keyboard_return'),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await page
|
|
||||||
.getByPlaceholder('Type the name of a document')
|
|
||||||
.press('ArrowDown');
|
|
||||||
|
|
||||||
const secondDoc = docs[1];
|
|
||||||
await expect(
|
|
||||||
page
|
|
||||||
.getByTestId(`doc-search-item-${secondDoc.id}`)
|
|
||||||
.getByText('keyboard_return'),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByPlaceholder('Type the name of a document').click();
|
|
||||||
await page
|
|
||||||
.getByPlaceholder('Type the name of a document')
|
|
||||||
.fill(`My super ${id} doc`);
|
|
||||||
|
|
||||||
responsePromisePage = page.waitForResponse(
|
|
||||||
(response) =>
|
|
||||||
response
|
|
||||||
.url()
|
|
||||||
.includes(`/documents/?page=1&title=My+super+${id}+doc`) &&
|
|
||||||
response.status() === 200,
|
|
||||||
);
|
|
||||||
|
|
||||||
response = await responsePromisePage;
|
|
||||||
result = (await response.json()) as { results: SmallDoc[] };
|
|
||||||
docs = result.results;
|
|
||||||
|
|
||||||
expect(docs.length).toEqual(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { createDoc, verifyDocName } from './common';
|
import { createDoc, goToGridDoc } from './common';
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@@ -17,29 +17,123 @@ test.describe('Doc Table Content', () => {
|
|||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
|
|
||||||
await verifyDocName(page, randomDoc);
|
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||||
|
|
||||||
await page.locator('.ProseMirror').click();
|
await page.getByLabel('Open the document options').click();
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Table of contents',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
await page.keyboard.type('# Level 1\n## Level 2\n### Level 3');
|
const panel = page.getByLabel('Document panel');
|
||||||
|
const editor = page.locator('.ProseMirror');
|
||||||
|
|
||||||
const summaryContainer = page.locator('#summaryContainer');
|
await editor.locator('.bn-block-outer').last().fill('/');
|
||||||
await summaryContainer.click();
|
await page.getByText('Heading 1').click();
|
||||||
|
await page.keyboard.type('Hello World');
|
||||||
|
await editor.getByText('Hello').dblclick();
|
||||||
|
await page.getByRole('button', { name: 'Strike' }).click();
|
||||||
|
|
||||||
const level1 = summaryContainer.getByText('Level 1');
|
await page.locator('.bn-block-outer').first().click();
|
||||||
const level2 = summaryContainer.getByText('Level 2');
|
await page.locator('.bn-block-outer').last().click();
|
||||||
const level3 = summaryContainer.getByText('Level 3');
|
|
||||||
|
|
||||||
await expect(level1).toBeVisible();
|
// Create space to fill the viewport
|
||||||
await expect(level1).toHaveCSS('padding', /4px 0px/);
|
for (let i = 0; i < 10; i++) {
|
||||||
await expect(level1).toHaveAttribute('aria-selected', 'true');
|
await page.keyboard.press('Enter');
|
||||||
|
}
|
||||||
|
|
||||||
await expect(level2).toBeVisible();
|
await editor.locator('.bn-block-outer').last().fill('/');
|
||||||
await expect(level2).toHaveCSS('padding-left', /14.4px/);
|
await page.getByText('Heading 2').click();
|
||||||
await expect(level2).toHaveAttribute('aria-selected', 'false');
|
await page.keyboard.type('Super World', { delay: 100 });
|
||||||
|
|
||||||
await expect(level3).toBeVisible();
|
await page.locator('.bn-block-outer').last().click();
|
||||||
await expect(level3).toHaveCSS('padding-left', /24px/);
|
|
||||||
await expect(level3).toHaveAttribute('aria-selected', 'false');
|
// Create space to fill the viewport
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
}
|
||||||
|
|
||||||
|
await editor.locator('.bn-block-outer').last().fill('/');
|
||||||
|
await page.getByText('Heading 3').click();
|
||||||
|
await page.keyboard.type('Another World');
|
||||||
|
|
||||||
|
const hello = panel.getByText('Hello World');
|
||||||
|
const superW = panel.getByText('Super World');
|
||||||
|
const another = panel.getByText('Another World');
|
||||||
|
|
||||||
|
await expect(hello).toBeVisible();
|
||||||
|
await expect(hello).toHaveCSS('font-size', /17/);
|
||||||
|
await expect(hello).toHaveAttribute('aria-selected', 'true');
|
||||||
|
|
||||||
|
await expect(superW).toBeVisible();
|
||||||
|
await expect(superW).toHaveCSS('font-size', /14/);
|
||||||
|
await expect(superW).toHaveAttribute('aria-selected', 'false');
|
||||||
|
|
||||||
|
await expect(another).toBeVisible();
|
||||||
|
await expect(another).toHaveCSS('font-size', /12/);
|
||||||
|
await expect(another).toHaveAttribute('aria-selected', 'false');
|
||||||
|
|
||||||
|
await hello.click();
|
||||||
|
|
||||||
|
await expect(editor.getByText('Hello World')).toBeInViewport();
|
||||||
|
await expect(hello).toHaveAttribute('aria-selected', 'true');
|
||||||
|
await expect(superW).toHaveAttribute('aria-selected', 'false');
|
||||||
|
|
||||||
|
await another.click();
|
||||||
|
|
||||||
|
await expect(editor.getByText('Hello World')).not.toBeInViewport();
|
||||||
|
await expect(hello).toHaveAttribute('aria-selected', 'false');
|
||||||
|
await expect(superW).toHaveAttribute('aria-selected', 'true');
|
||||||
|
|
||||||
|
await panel.getByText('Back to top').click();
|
||||||
|
await expect(editor.getByText('Hello World')).toBeInViewport();
|
||||||
|
await expect(hello).toHaveAttribute('aria-selected', 'true');
|
||||||
|
await expect(superW).toHaveAttribute('aria-selected', 'false');
|
||||||
|
|
||||||
|
await panel.getByText('Go to bottom').click();
|
||||||
|
await expect(editor.getByText('Hello World')).not.toBeInViewport();
|
||||||
|
await expect(superW).toHaveAttribute('aria-selected', 'true');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('it checks that table contents panel is opened automaticaly if more that 2 headings', async ({
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
}) => {
|
||||||
|
const [randomDoc] = await createDoc(
|
||||||
|
page,
|
||||||
|
'doc-table-content',
|
||||||
|
browserName,
|
||||||
|
1,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||||
|
await expect(page.getByLabel('Open the panel')).toBeHidden();
|
||||||
|
|
||||||
|
const editor = page.locator('.ProseMirror');
|
||||||
|
|
||||||
|
await editor.locator('.bn-block-outer').last().fill('/');
|
||||||
|
await page.getByText('Heading 1').click();
|
||||||
|
await page.keyboard.type('Hello World', { delay: 100 });
|
||||||
|
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
|
||||||
|
await editor.locator('.bn-block-outer').last().fill('/');
|
||||||
|
await page.getByText('Heading 2').click();
|
||||||
|
await page.keyboard.type('Super World', { delay: 100 });
|
||||||
|
|
||||||
|
await goToGridDoc(page, {
|
||||||
|
title: randomDoc,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(page.getByLabel('Close the panel')).toBeVisible();
|
||||||
|
|
||||||
|
const panel = page.getByLabel('Document panel');
|
||||||
|
await expect(panel.getByText('Hello World')).toBeVisible();
|
||||||
|
await expect(panel.getByText('Super World')).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByLabel('Close the panel').click();
|
||||||
|
|
||||||
|
await expect(panel).toHaveAttribute('aria-hidden', 'true');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import {
|
import { createDoc, goToGridDoc, mockedDocument } from './common';
|
||||||
createDoc,
|
|
||||||
goToGridDoc,
|
|
||||||
mockedDocument,
|
|
||||||
verifyDocName,
|
|
||||||
} from './common';
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
await page.goto('/');
|
await page.goto('/');
|
||||||
@@ -15,7 +10,7 @@ test.describe('Doc Version', () => {
|
|||||||
test('it displays the doc versions', async ({ page, browserName }) => {
|
test('it displays the doc versions', async ({ page, browserName }) => {
|
||||||
const [randomDoc] = await createDoc(page, 'doc-version', browserName, 1);
|
const [randomDoc] = await createDoc(page, 'doc-version', browserName, 1);
|
||||||
|
|
||||||
await verifyDocName(page, randomDoc);
|
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||||
|
|
||||||
await page.getByLabel('Open the document options').click();
|
await page.getByLabel('Open the document options').click();
|
||||||
await page
|
await page
|
||||||
@@ -23,29 +18,24 @@ test.describe('Doc Version', () => {
|
|||||||
name: 'Version history',
|
name: 'Version history',
|
||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
await expect(page.getByText('History', { exact: true })).toBeVisible();
|
|
||||||
|
|
||||||
const modal = page.getByLabel('version history modal');
|
const panel = page.getByLabel('Document panel');
|
||||||
const panel = modal.getByLabel('version list');
|
|
||||||
await expect(panel).toBeVisible();
|
|
||||||
await expect(modal.getByText('No versions')).toBeVisible();
|
|
||||||
|
|
||||||
const editor = page.locator('.ProseMirror');
|
await expect(panel.getByText('Current version')).toBeVisible();
|
||||||
await modal.getByRole('button', { name: 'close' }).click();
|
expect(await panel.locator('li').count()).toBe(1);
|
||||||
await editor.click();
|
|
||||||
await page.keyboard.type('# Hello World');
|
await page.locator('.ProseMirror.bn-editor').click();
|
||||||
|
await page.locator('.ProseMirror.bn-editor').last().fill('Hello World');
|
||||||
|
|
||||||
await goToGridDoc(page, {
|
await goToGridDoc(page, {
|
||||||
title: randomDoc,
|
title: randomDoc,
|
||||||
});
|
});
|
||||||
|
|
||||||
await expect(
|
await expect(page.getByText('Hello World')).toBeVisible();
|
||||||
page.getByRole('heading', { name: 'Hello World' }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.locator('.ProseMirror .bn-block')
|
.locator('.ProseMirror .bn-block')
|
||||||
.getByRole('heading', { name: 'Hello World' })
|
.getByText('Hello World')
|
||||||
.fill('It will create a version');
|
.fill('It will create a version');
|
||||||
|
|
||||||
await goToGridDoc(page, {
|
await goToGridDoc(page, {
|
||||||
@@ -53,9 +43,7 @@ test.describe('Doc Version', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(page.getByText('Hello World')).toBeHidden();
|
await expect(page.getByText('Hello World')).toBeHidden();
|
||||||
await expect(
|
await expect(page.getByText('It will create a version')).toBeVisible();
|
||||||
page.getByRole('heading', { name: 'It will create a version' }),
|
|
||||||
).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByLabel('Open the document options').click();
|
await page.getByLabel('Open the document options').click();
|
||||||
await page
|
await page
|
||||||
@@ -64,15 +52,19 @@ test.describe('Doc Version', () => {
|
|||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await expect(panel).toBeVisible();
|
await expect(panel.getByText('Current version')).toBeVisible();
|
||||||
await expect(page.getByText('History', { exact: true })).toBeVisible();
|
expect(await panel.locator('li').count()).toBe(2);
|
||||||
await expect(page.getByRole('status')).toBeHidden();
|
|
||||||
const items = await panel.locator('.version-item').all();
|
|
||||||
expect(items.length).toBe(1);
|
|
||||||
await items[0].click();
|
|
||||||
|
|
||||||
await expect(modal.getByText('Hello World')).toBeVisible();
|
await panel.locator('li').nth(1).click();
|
||||||
await expect(modal.getByText('It will create a version')).toBeHidden();
|
await expect(
|
||||||
|
page.getByText('Read only, you cannot edit document versions.'),
|
||||||
|
).toBeVisible();
|
||||||
|
await expect(page.getByText('Hello World')).toBeVisible();
|
||||||
|
await expect(page.getByText('It will create a version')).toBeHidden();
|
||||||
|
|
||||||
|
await panel.getByText('Current version').click();
|
||||||
|
await expect(page.getByText('Hello World')).toBeHidden();
|
||||||
|
await expect(page.getByText('It will create a version')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it does not display the doc versions if not allowed', async ({
|
test('it does not display the doc versions if not allowed', async ({
|
||||||
@@ -87,17 +79,24 @@ test.describe('Doc Version', () => {
|
|||||||
|
|
||||||
await goToGridDoc(page);
|
await goToGridDoc(page);
|
||||||
|
|
||||||
await verifyDocName(page, 'Mocked document');
|
await expect(page.locator('h2').getByText('Mocked document')).toBeVisible();
|
||||||
|
|
||||||
await page.getByLabel('Open the document options').click();
|
await page.getByLabel('Open the document options').click();
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', { name: 'Version history' }),
|
page.getByRole('button', { name: 'Version history' }),
|
||||||
).toBeDisabled();
|
).toBeHidden();
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Table of content' }).click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByLabel('Document panel').getByText('Versions'),
|
||||||
|
).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('it restores the doc version', async ({ page, browserName }) => {
|
test('it restores the doc version', async ({ page, browserName }) => {
|
||||||
const [randomDoc] = await createDoc(page, 'doc-version', browserName, 1);
|
const [randomDoc] = await createDoc(page, 'doc-version', browserName, 1);
|
||||||
await verifyDocName(page, randomDoc);
|
|
||||||
|
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||||
|
|
||||||
await page.locator('.bn-block-outer').last().click();
|
await page.locator('.bn-block-outer').last().click();
|
||||||
await page.locator('.bn-block-outer').last().fill('Hello');
|
await page.locator('.bn-block-outer').last().fill('Hello');
|
||||||
@@ -125,26 +124,84 @@ test.describe('Doc Version', () => {
|
|||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
const modal = page.getByLabel('version history modal');
|
const panel = page.getByLabel('Document panel');
|
||||||
const panel = modal.getByLabel('version list');
|
await panel.locator('li').nth(1).click();
|
||||||
await expect(panel).toBeVisible();
|
await expect(page.getByText('World')).toBeHidden();
|
||||||
|
|
||||||
await expect(page.getByText('History', { exact: true })).toBeVisible();
|
await panel.getByLabel('Open the version options').click();
|
||||||
await expect(page.getByRole('status')).toBeVisible();
|
await page.getByText('Restore the version').click();
|
||||||
await expect(page.getByRole('status')).toBeHidden();
|
|
||||||
const items = await panel.locator('.version-item').all();
|
|
||||||
expect(items.length).toBe(1);
|
|
||||||
await items[0].click();
|
|
||||||
|
|
||||||
await expect(modal.getByText('World')).toBeHidden();
|
await expect(page.getByText('Restore this version?')).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Restore' }).click();
|
await page
|
||||||
await expect(page.getByText('Your current document will')).toBeVisible();
|
.getByRole('button', {
|
||||||
await page.getByText('If a member is editing, his').click();
|
name: 'Restore',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
await page.getByLabel('Restore', { exact: true }).click();
|
await expect(panel.locator('li')).toHaveCount(3);
|
||||||
|
|
||||||
|
await panel.getByText('Current version').click();
|
||||||
await expect(page.getByText('Hello')).toBeVisible();
|
await expect(page.getByText('Hello')).toBeVisible();
|
||||||
await expect(page.getByText('World')).toBeHidden();
|
await expect(page.getByText('World')).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('it restores the doc version from button title', async ({
|
||||||
|
page,
|
||||||
|
browserName,
|
||||||
|
}) => {
|
||||||
|
const [randomDoc] = await createDoc(page, 'doc-version', browserName, 1);
|
||||||
|
|
||||||
|
await expect(page.locator('h2').getByText(randomDoc)).toBeVisible();
|
||||||
|
|
||||||
|
const editor = page.locator('.ProseMirror');
|
||||||
|
await editor.locator('.bn-block-outer').last().click();
|
||||||
|
await editor.locator('.bn-block-outer').last().fill('Hello');
|
||||||
|
|
||||||
|
await goToGridDoc(page, {
|
||||||
|
title: randomDoc,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(editor.getByText('Hello')).toBeVisible();
|
||||||
|
await editor.locator('.bn-block-outer').last().click();
|
||||||
|
await page.keyboard.press('Enter');
|
||||||
|
await editor.locator('.bn-block-outer').last().fill('World');
|
||||||
|
|
||||||
|
await goToGridDoc(page, {
|
||||||
|
title: randomDoc,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(editor.getByText('World')).toBeVisible();
|
||||||
|
|
||||||
|
await page.getByLabel('Open the document options').click();
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Version history',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
|
const panel = page.getByLabel('Document panel');
|
||||||
|
await panel.locator('li').nth(1).click();
|
||||||
|
await expect(editor.getByText('World')).toBeHidden();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Restore this version',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect(page.getByText('Restore this version?')).toBeVisible();
|
||||||
|
|
||||||
|
await page
|
||||||
|
.getByRole('button', {
|
||||||
|
name: 'Restore',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect(panel.locator('li')).toHaveCount(3);
|
||||||
|
|
||||||
|
await panel.getByText('Current version').click();
|
||||||
|
await expect(editor.getByText('Hello')).toBeVisible();
|
||||||
|
await expect(editor.getByText('World')).toBeHidden();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { createDoc, keyCloakSignIn, verifyDocName } from './common';
|
import { createDoc, keyCloakSignIn } from './common';
|
||||||
|
|
||||||
const browsersName = ['chromium', 'webkit', 'firefox'];
|
const browsersName = ['chromium', 'webkit', 'firefox'];
|
||||||
|
|
||||||
@@ -36,38 +36,42 @@ test.describe('Doc Visibility', () => {
|
|||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
const selectVisibility = page.getByLabel('Visibility', { exact: true });
|
const selectVisibility = page.getByRole('combobox', {
|
||||||
|
name: 'Visibility',
|
||||||
|
});
|
||||||
|
|
||||||
await expect(selectVisibility.getByText('Private')).toBeVisible();
|
await expect(selectVisibility.getByText('Restricted')).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByLabel('Read only')).toBeHidden();
|
await expect(page.getByLabel('Read only')).toBeHidden();
|
||||||
await expect(page.getByLabel('Can read and edit')).toBeHidden();
|
await expect(page.getByLabel('Can read and edit')).toBeHidden();
|
||||||
|
|
||||||
await selectVisibility.click();
|
await selectVisibility.click();
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('option', {
|
||||||
name: 'Connected',
|
name: 'Authenticated',
|
||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await expect(page.getByLabel('Visibility mode')).toBeVisible();
|
await expect(page.getByLabel('Read only')).toBeVisible();
|
||||||
|
await expect(page.getByLabel('Can read and edit')).toBeVisible();
|
||||||
|
|
||||||
await selectVisibility.click();
|
await selectVisibility.click();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('option', {
|
||||||
name: 'Public',
|
name: 'Public',
|
||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await expect(page.getByLabel('Visibility mode')).toBeVisible();
|
await expect(page.getByLabel('Read only')).toBeVisible();
|
||||||
|
await expect(page.getByLabel('Can read and edit')).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Doc Visibility: Restricted', () => {
|
test.describe('Doc Visibility: Restricted', () => {
|
||||||
test.use({ storageState: { cookies: [], origins: [] } });
|
test.use({ storageState: { cookies: [], origins: [] } });
|
||||||
|
|
||||||
test('A doc is not accessible when not authenticated.', async ({
|
test('A doc is not accessible when not authentified.', async ({
|
||||||
page,
|
page,
|
||||||
browserName,
|
browserName,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -81,7 +85,7 @@ test.describe('Doc Visibility: Restricted', () => {
|
|||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
|
||||||
|
|
||||||
const urlDoc = page.url();
|
const urlDoc = page.url();
|
||||||
|
|
||||||
@@ -98,7 +102,7 @@ test.describe('Doc Visibility: Restricted', () => {
|
|||||||
await expect(page.getByRole('textbox', { name: 'password' })).toBeVisible();
|
await expect(page.getByRole('textbox', { name: 'password' })).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('A doc is not accessible when authenticated but not member.', async ({
|
test('A doc is not accessible when authentified but not member.', async ({
|
||||||
page,
|
page,
|
||||||
browserName,
|
browserName,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -107,7 +111,7 @@ test.describe('Doc Visibility: Restricted', () => {
|
|||||||
|
|
||||||
const [docTitle] = await createDoc(page, 'Restricted auth', browserName, 1);
|
const [docTitle] = await createDoc(page, 'Restricted auth', browserName, 1);
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
|
||||||
|
|
||||||
const urlDoc = page.url();
|
const urlDoc = page.url();
|
||||||
|
|
||||||
@@ -135,13 +139,11 @@ test.describe('Doc Visibility: Restricted', () => {
|
|||||||
|
|
||||||
const [docTitle] = await createDoc(page, 'Restricted auth', browserName, 1);
|
const [docTitle] = await createDoc(page, 'Restricted auth', browserName, 1);
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
const inputSearch = page.getByRole('combobox', {
|
const inputSearch = page.getByLabel(/Find a member to add to the document/);
|
||||||
name: 'Quick search input',
|
|
||||||
});
|
|
||||||
|
|
||||||
const otherBrowser = browsersName.find((b) => b !== browserName);
|
const otherBrowser = browsersName.find((b) => b !== browserName);
|
||||||
const username = `user@${otherBrowser}.e2e`;
|
const username = `user@${otherBrowser}.e2e`;
|
||||||
@@ -149,11 +151,14 @@ test.describe('Doc Visibility: Restricted', () => {
|
|||||||
await page.getByRole('option', { name: username }).click();
|
await page.getByRole('option', { name: username }).click();
|
||||||
|
|
||||||
// Choose a role
|
// Choose a role
|
||||||
const container = page.getByTestId('doc-share-add-member-list');
|
await page.getByRole('combobox', { name: /Choose a role/ }).click();
|
||||||
await container.getByLabel('doc-role-dropdown').click();
|
await page.getByRole('option', { name: 'Administrator' }).click();
|
||||||
await page.getByRole('button', { name: 'Administrator' }).click();
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Invite' }).click();
|
await page.getByRole('button', { name: 'Validate' }).click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByText(`User ${username} added to the document.`),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
await page.locator('.c__modal__backdrop').click({
|
await page.locator('.c__modal__backdrop').click({
|
||||||
position: { x: 0, y: 0 },
|
position: { x: 0, y: 0 },
|
||||||
@@ -171,8 +176,8 @@ test.describe('Doc Visibility: Restricted', () => {
|
|||||||
|
|
||||||
await page.goto(urlDoc);
|
await page.goto(urlDoc);
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||||
await expect(page.getByLabel('Share button')).toBeVisible();
|
await expect(page.getByRole('button', { name: 'Share' })).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -193,14 +198,17 @@ test.describe('Doc Visibility: Public', () => {
|
|||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
const selectVisibility = page.getByLabel('Visibility', { exact: true });
|
await page
|
||||||
await selectVisibility.click();
|
.getByRole('combobox', {
|
||||||
|
name: 'Visibility',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('option', {
|
||||||
name: 'Public',
|
name: 'Public',
|
||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
@@ -209,27 +217,20 @@ test.describe('Doc Visibility: Public', () => {
|
|||||||
page.getByText('The document visibility has been updated.'),
|
page.getByText('The document visibility has been updated.'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await page.getByLabel('Visibility mode').click();
|
await page.getByLabel('Read only').click();
|
||||||
await page
|
|
||||||
.getByRole('button', {
|
|
||||||
name: 'Reading',
|
|
||||||
})
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText('The document visibility has been updated.').first(),
|
page.getByText('The document visibility has been updated.').first(),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'close' }).click();
|
await page.locator('.c__modal__backdrop').click({
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
const cardContainer = page.getByLabel(
|
});
|
||||||
'It is the card information about the document.',
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(cardContainer.getByTestId('public-icon')).toBeVisible();
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
cardContainer.getByText('Public document', { exact: true }),
|
page
|
||||||
|
.getByLabel('It is the card information about the document.')
|
||||||
|
.getByText('Public', { exact: true }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
const urlDoc = page.url();
|
const urlDoc = page.url();
|
||||||
@@ -246,10 +247,9 @@ test.describe('Doc Visibility: Public', () => {
|
|||||||
|
|
||||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||||
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
|
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
|
||||||
const card = page.getByLabel('It is the card information');
|
await expect(
|
||||||
await expect(card).toBeVisible();
|
page.getByText('Read only, you cannot edit this document'),
|
||||||
|
).toBeVisible();
|
||||||
await expect(card.getByText('Reader')).toBeVisible();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('It checks a public doc in editable mode', async ({
|
test('It checks a public doc in editable mode', async ({
|
||||||
@@ -261,14 +261,17 @@ test.describe('Doc Visibility: Public', () => {
|
|||||||
|
|
||||||
const [docTitle] = await createDoc(page, 'Public editable', browserName, 1);
|
const [docTitle] = await createDoc(page, 'Public editable', browserName, 1);
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
const selectVisibility = page.getByLabel('Visibility', { exact: true });
|
await page
|
||||||
await selectVisibility.click();
|
.getByRole('combobox', {
|
||||||
|
name: 'Visibility',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('option', {
|
||||||
name: 'Public',
|
name: 'Public',
|
||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
@@ -277,23 +280,20 @@ test.describe('Doc Visibility: Public', () => {
|
|||||||
page.getByText('The document visibility has been updated.'),
|
page.getByText('The document visibility has been updated.'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await page.getByLabel('Visibility mode').click();
|
await page.getByLabel('Can read and edit').click();
|
||||||
await page.getByLabel('Edition').click();
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText('The document visibility has been updated.').first(),
|
page.getByText('The document visibility has been updated.').first(),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'close' }).click();
|
await page.locator('.c__modal__backdrop').click({
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
const cardContainer = page.getByLabel(
|
});
|
||||||
'It is the card information about the document.',
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(cardContainer.getByTestId('public-icon')).toBeVisible();
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
cardContainer.getByText('Public document', { exact: true }),
|
page
|
||||||
|
.getByLabel('It is the card information about the document.')
|
||||||
|
.getByText('Public', { exact: true }),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
const urlDoc = page.url();
|
const urlDoc = page.url();
|
||||||
@@ -308,15 +308,18 @@ test.describe('Doc Visibility: Public', () => {
|
|||||||
|
|
||||||
await page.goto(urlDoc);
|
await page.goto(urlDoc);
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||||
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
|
await expect(page.getByRole('button', { name: 'Share' })).toBeHidden();
|
||||||
|
await expect(
|
||||||
|
page.getByText('Read only, you cannot edit this document'),
|
||||||
|
).toBeHidden();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Doc Visibility: Authenticated', () => {
|
test.describe('Doc Visibility: Authenticated', () => {
|
||||||
test.use({ storageState: { cookies: [], origins: [] } });
|
test.use({ storageState: { cookies: [], origins: [] } });
|
||||||
|
|
||||||
test('A doc is not accessible when unauthenticated.', async ({
|
test('A doc is not accessible when unauthentified.', async ({
|
||||||
page,
|
page,
|
||||||
browserName,
|
browserName,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -325,19 +328,22 @@ test.describe('Doc Visibility: Authenticated', () => {
|
|||||||
|
|
||||||
const [docTitle] = await createDoc(
|
const [docTitle] = await createDoc(
|
||||||
page,
|
page,
|
||||||
'Authenticated unauthenticated',
|
'Authenticated unauthentified',
|
||||||
browserName,
|
browserName,
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
const selectVisibility = page.getByLabel('Visibility', { exact: true });
|
|
||||||
await selectVisibility.click();
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('combobox', {
|
||||||
name: 'Connected',
|
name: 'Visibility',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
await page
|
||||||
|
.getByRole('option', {
|
||||||
|
name: 'Authenticated',
|
||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
@@ -345,7 +351,9 @@ test.describe('Doc Visibility: Authenticated', () => {
|
|||||||
page.getByText('The document visibility has been updated.'),
|
page.getByText('The document visibility has been updated.'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'close' }).click();
|
await page.locator('.c__modal__backdrop').click({
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
const urlDoc = page.url();
|
const urlDoc = page.url();
|
||||||
|
|
||||||
@@ -377,14 +385,17 @@ test.describe('Doc Visibility: Authenticated', () => {
|
|||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
const selectVisibility = page.getByLabel('Visibility', { exact: true });
|
|
||||||
await selectVisibility.click();
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('combobox', {
|
||||||
name: 'Connected',
|
name: 'Visibility',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
await page
|
||||||
|
.getByRole('option', {
|
||||||
|
name: 'Authenticated',
|
||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
@@ -392,7 +403,9 @@ test.describe('Doc Visibility: Authenticated', () => {
|
|||||||
page.getByText('The document visibility has been updated.'),
|
page.getByText('The document visibility has been updated.'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'close' }).click();
|
await page.locator('.c__modal__backdrop').click({
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
const urlDoc = page.url();
|
const urlDoc = page.url();
|
||||||
|
|
||||||
@@ -409,13 +422,19 @@ test.describe('Doc Visibility: Authenticated', () => {
|
|||||||
|
|
||||||
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
await expect(
|
||||||
|
page.getByText('Read only, you cannot edit this document'),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(selectVisibility).toBeHidden();
|
const shareModal = page.getByLabel('Share modal');
|
||||||
|
|
||||||
const inputSearch = page.getByRole('combobox', {
|
await expect(
|
||||||
name: 'Quick search input',
|
shareModal.getByRole('combobox', {
|
||||||
});
|
name: 'Visibility',
|
||||||
await expect(inputSearch).toBeHidden();
|
}),
|
||||||
|
).toHaveAttribute('disabled');
|
||||||
|
await expect(shareModal.getByText('Search by email')).toBeHidden();
|
||||||
|
await expect(shareModal.getByLabel('List members card')).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('It checks a authenticated doc in editable mode', async ({
|
test('It checks a authenticated doc in editable mode', async ({
|
||||||
@@ -432,14 +451,17 @@ test.describe('Doc Visibility: Authenticated', () => {
|
|||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await expect(page.getByRole('heading', { name: docTitle })).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
const selectVisibility = page.getByLabel('Visibility', { exact: true });
|
|
||||||
await selectVisibility.click();
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('combobox', {
|
||||||
name: 'Connected',
|
name: 'Visibility',
|
||||||
|
})
|
||||||
|
.click();
|
||||||
|
await page
|
||||||
|
.getByRole('option', {
|
||||||
|
name: 'Authenticated',
|
||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
@@ -447,15 +469,23 @@ test.describe('Doc Visibility: Authenticated', () => {
|
|||||||
page.getByText('The document visibility has been updated.'),
|
page.getByText('The document visibility has been updated.'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
|
await page.locator('.c__modal__backdrop').click({
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
const urlDoc = page.url();
|
const urlDoc = page.url();
|
||||||
await page.getByLabel('Visibility mode').click();
|
|
||||||
await page.getByLabel('Edition').click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
|
await page.getByLabel('Can read and edit').click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText('The document visibility has been updated.').first(),
|
page.getByText('The document visibility has been updated.').first(),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'close' }).click();
|
await page.locator('.c__modal__backdrop').click({
|
||||||
|
position: { x: 0, y: 0 },
|
||||||
|
});
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
@@ -468,14 +498,20 @@ test.describe('Doc Visibility: Authenticated', () => {
|
|||||||
|
|
||||||
await page.goto(urlDoc);
|
await page.goto(urlDoc);
|
||||||
|
|
||||||
await verifyDocName(page, docTitle);
|
await expect(page.locator('h2').getByText(docTitle)).toBeVisible();
|
||||||
await page.getByRole('button', { name: 'Share' }).click();
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
await expect(
|
||||||
|
page.getByText('Read only, you cannot edit this document'),
|
||||||
|
).toBeHidden();
|
||||||
|
|
||||||
await expect(selectVisibility).toBeHidden();
|
const shareModal = page.getByLabel('Share modal');
|
||||||
|
|
||||||
const inputSearch = page.getByRole('combobox', {
|
await expect(
|
||||||
name: 'Quick search input',
|
shareModal.getByRole('combobox', {
|
||||||
});
|
name: 'Visibility',
|
||||||
await expect(inputSearch).toBeHidden();
|
}),
|
||||||
|
).toHaveAttribute('disabled');
|
||||||
|
await expect(shareModal.getByText('Search by email')).toBeHidden();
|
||||||
|
await expect(shareModal.getByLabel('List members card')).toBeHidden();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -75,13 +75,29 @@ test.describe('Header mobile', () => {
|
|||||||
test('it checks the header when mobile', async ({ page }) => {
|
test('it checks the header when mobile', async ({ page }) => {
|
||||||
const header = page.locator('header').first();
|
const header = page.locator('header').first();
|
||||||
|
|
||||||
await expect(header.getByLabel('Open the header menu')).toBeVisible();
|
|
||||||
await expect(header.getByRole('link', { name: 'Docs Logo' })).toBeVisible();
|
|
||||||
await expect(
|
await expect(
|
||||||
header.getByRole('button', {
|
header.getByRole('button', {
|
||||||
name: 'Les services de La Suite numérique',
|
name: 'Les services de La Suite numérique',
|
||||||
}),
|
}),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', {
|
||||||
|
name: 'Logout',
|
||||||
|
}),
|
||||||
|
).toBeHidden();
|
||||||
|
|
||||||
|
await expect(page.getByText('English')).toBeHidden();
|
||||||
|
|
||||||
|
await header.getByLabel('Open the header menu').click();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
page.getByRole('button', {
|
||||||
|
name: 'Logout',
|
||||||
|
}),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
await expect(page.getByText('English')).toBeVisible();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,11 @@ test.beforeEach(async ({ page }) => {
|
|||||||
|
|
||||||
test.describe('Language', () => {
|
test.describe('Language', () => {
|
||||||
test('checks the language picker', async ({ page }) => {
|
test('checks the language picker', async ({ page }) => {
|
||||||
await expect(page.getByLabel('Logout')).toBeVisible();
|
await expect(
|
||||||
|
page.getByRole('button', {
|
||||||
|
name: 'Create a new document',
|
||||||
|
}),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
const header = page.locator('header').first();
|
const header = page.locator('header').first();
|
||||||
await header.getByRole('combobox').getByText('English').click();
|
await header.getByRole('combobox').getByText('English').click();
|
||||||
@@ -15,7 +19,11 @@ test.describe('Language', () => {
|
|||||||
header.getByRole('combobox').getByText('Français'),
|
header.getByRole('combobox').getByText('Français'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByLabel('Se déconnecter')).toBeVisible();
|
await expect(
|
||||||
|
page.getByRole('button', {
|
||||||
|
name: 'Créer un nouveau document',
|
||||||
|
}),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
await header.getByRole('combobox').getByText('Français').click();
|
await header.getByRole('combobox').getByText('Français').click();
|
||||||
await header.getByRole('option', { name: 'Deutsch' }).click();
|
await header.getByRole('option', { name: 'Deutsch' }).click();
|
||||||
@@ -23,7 +31,11 @@ test.describe('Language', () => {
|
|||||||
header.getByRole('combobox').getByText('Deutsch'),
|
header.getByRole('combobox').getByText('Deutsch'),
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByLabel('Abmelden')).toBeVisible();
|
await expect(
|
||||||
|
page.getByRole('button', {
|
||||||
|
name: 'Neues Dokument erstellen',
|
||||||
|
}),
|
||||||
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('checks that backend uses the same language as the frontend', async ({
|
test('checks that backend uses the same language as the frontend', async ({
|
||||||
|
|||||||
@@ -1,48 +0,0 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
|
||||||
|
|
||||||
test.describe('Left panel desktop', () => {
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.goto('/');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('checks all the elements are visible', async ({ page }) => {
|
|
||||||
await expect(page.getByTestId('left-panel-desktop')).toBeVisible();
|
|
||||||
await expect(page.getByTestId('left-panel-mobile')).toBeHidden();
|
|
||||||
await expect(page.getByRole('button', { name: 'house' })).toBeVisible();
|
|
||||||
await expect(page.getByRole('button', { name: 'New doc' })).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Left panel mobile', () => {
|
|
||||||
test.use({ viewport: { width: 500, height: 1200 } });
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.goto('/');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('checks all the desktop elements are hidden and all mobile elements are visible', async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
await expect(page.getByTestId('left-panel-desktop')).toBeHidden();
|
|
||||||
await expect(page.getByTestId('left-panel-mobile')).not.toBeInViewport();
|
|
||||||
|
|
||||||
const header = page.locator('header').first();
|
|
||||||
const homeButton = page.getByRole('button', { name: 'house' });
|
|
||||||
const newDocButton = page.getByRole('button', { name: 'New doc' });
|
|
||||||
const languageButton = page.getByRole('combobox', { name: 'Language' });
|
|
||||||
const logoutButton = page.getByRole('button', { name: 'Logout' });
|
|
||||||
|
|
||||||
await expect(homeButton).not.toBeInViewport();
|
|
||||||
await expect(newDocButton).not.toBeInViewport();
|
|
||||||
await expect(languageButton).not.toBeInViewport();
|
|
||||||
await expect(logoutButton).not.toBeInViewport();
|
|
||||||
|
|
||||||
await header.getByLabel('Open the header menu').click();
|
|
||||||
|
|
||||||
await expect(page.getByTestId('left-panel-mobile')).toBeInViewport();
|
|
||||||
await expect(homeButton).toBeInViewport();
|
|
||||||
await expect(newDocButton).toBeInViewport();
|
|
||||||
await expect(languageButton).toBeInViewport();
|
|
||||||
await expect(logoutButton).toBeInViewport();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -16,9 +16,7 @@
|
|||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"@types/pdf-parse": "1.1.4",
|
"@types/pdf-parse": "1.1.4",
|
||||||
"eslint-config-impress": "*",
|
"eslint-config-impress": "*",
|
||||||
"typescript": "*",
|
"typescript": "*"
|
||||||
"luxon": "3.5.0",
|
|
||||||
"@types/luxon": "3.4.2"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"convert-stream": "1.0.2",
|
"convert-stream": "1.0.2",
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ server {
|
|||||||
try_files $uri index.html $uri/ =404;
|
try_files $uri index.html $uri/ =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location ~ ^/docs/(.*)/versions/(.*)/$ {
|
||||||
|
error_page 404 /docs/[id]/versions/[versionId]/;
|
||||||
|
}
|
||||||
|
|
||||||
location /docs/ {
|
location /docs/ {
|
||||||
error_page 404 /docs/[id]/;
|
error_page 404 /docs/[id]/;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,60 +5,22 @@ const config = {
|
|||||||
colors: {
|
colors: {
|
||||||
'card-border': '#ededed',
|
'card-border': '#ededed',
|
||||||
'primary-bg': '#FAFAFA',
|
'primary-bg': '#FAFAFA',
|
||||||
'primary-050': '#F5F5FE',
|
|
||||||
'primary-100': '#EDF5FA',
|
'primary-100': '#EDF5FA',
|
||||||
'primary-150': '#E5EEFA',
|
'primary-150': '#E5EEFA',
|
||||||
'primary-950': '#1B1B35',
|
|
||||||
'info-150': '#E5EEFA',
|
'info-150': '#E5EEFA',
|
||||||
'greyscale-000': '#fff',
|
|
||||||
'greyscale-1000': '#161616',
|
|
||||||
'blue-400': '#7AB1E8',
|
|
||||||
'blue-500': '#417DC4',
|
|
||||||
'blue-600': '#3558A2',
|
|
||||||
'brown-400': '#E6BE92',
|
|
||||||
'brown-500': '#BD987A',
|
|
||||||
'brown-600': '#745B47',
|
|
||||||
'cyan-400': '#34BAB5',
|
|
||||||
'cyan-500': '#009099',
|
|
||||||
'cyan-600': '#006A6F',
|
|
||||||
'gold-400': '#FFCA00',
|
|
||||||
'gold-500': '#C3992A',
|
|
||||||
'gold-600': '#695240',
|
|
||||||
'green-400': '#34CB6A',
|
|
||||||
'green-500': '#00A95F',
|
|
||||||
'green-600': '#297254',
|
|
||||||
'olive-400': '#99C221',
|
|
||||||
'olive-500': '#68A532',
|
|
||||||
'olive-600': '#447049',
|
|
||||||
'orange-400': '#FF732C',
|
|
||||||
'orange-500': '#E4794A',
|
|
||||||
'orange-600': '#755348',
|
|
||||||
'pink-400': '#FFB7AE',
|
|
||||||
'pink-500': '#E18B76',
|
|
||||||
'pink-600': '#8D533E',
|
|
||||||
'purple-400': '#CE70CC',
|
|
||||||
'purple-500': '#A558A0',
|
|
||||||
'purple-600': '#6E445A',
|
|
||||||
'yellow-400': '#D8C634',
|
|
||||||
'yellow-500': '#B7A73F',
|
|
||||||
'yellow-600': '#66673D',
|
|
||||||
},
|
},
|
||||||
font: {
|
font: {
|
||||||
sizes: {
|
sizes: {
|
||||||
xs: '0.75rem',
|
|
||||||
sm: '0.875rem',
|
|
||||||
md: '1rem',
|
|
||||||
lg: '1.125rem',
|
|
||||||
ml: '0.938rem',
|
ml: '0.938rem',
|
||||||
xl: '1.25rem',
|
xl: '1.50rem',
|
||||||
t: '0.6875rem',
|
t: '0.6875rem',
|
||||||
s: '0.75rem',
|
s: '0.75rem',
|
||||||
h1: '2rem',
|
h1: '2.2rem',
|
||||||
h2: '1.75rem',
|
h2: '1.7rem',
|
||||||
h3: '1.5rem',
|
h3: '1.37rem',
|
||||||
h4: '1.375rem',
|
h4: '1.15rem',
|
||||||
h5: '1.25rem',
|
h5: '1rem',
|
||||||
h6: '1.125rem',
|
h6: '0.87rem',
|
||||||
},
|
},
|
||||||
weights: {
|
weights: {
|
||||||
thin: 100,
|
thin: 100,
|
||||||
@@ -72,21 +34,6 @@ const config = {
|
|||||||
auto: 'auto',
|
auto: 'auto',
|
||||||
bx: '2.2rem',
|
bx: '2.2rem',
|
||||||
full: '100%',
|
full: '100%',
|
||||||
'4xs': '0.125rem',
|
|
||||||
'3xs': '0.25rem',
|
|
||||||
'2xs': '0.375rem',
|
|
||||||
xs: '0.5rem',
|
|
||||||
sm: '0.75rem',
|
|
||||||
base: '1rem',
|
|
||||||
md: '1.5rem',
|
|
||||||
lg: '2rem',
|
|
||||||
xl: '2.5rem',
|
|
||||||
xxl: '3rem',
|
|
||||||
xxxl: '3.5rem',
|
|
||||||
'4xl': '4rem',
|
|
||||||
'5xl': '4.5rem',
|
|
||||||
'6xl': '6rem',
|
|
||||||
'7xl': '7.5rem',
|
|
||||||
},
|
},
|
||||||
breakpoints: {
|
breakpoints: {
|
||||||
xxs: '320px',
|
xxs: '320px',
|
||||||
@@ -157,7 +104,7 @@ const config = {
|
|||||||
focus: 'var(--c--components--forms-select--border-radius)',
|
focus: 'var(--c--components--forms-select--border-radius)',
|
||||||
},
|
},
|
||||||
'font-size': 'var(--c--theme--font--sizes--ml)',
|
'font-size': 'var(--c--theme--font--sizes--ml)',
|
||||||
'menu-background-color': '#fff',
|
'menu-background-color': '#ffffff',
|
||||||
'item-background-color': {
|
'item-background-color': {
|
||||||
hover: 'var(--c--theme--colors--primary-300)',
|
hover: 'var(--c--theme--colors--primary-300)',
|
||||||
},
|
},
|
||||||
@@ -179,7 +126,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
modal: {
|
modal: {
|
||||||
'background-color': '#fff',
|
'background-color': '#ffffff',
|
||||||
},
|
},
|
||||||
button: {
|
button: {
|
||||||
'border-radius': {
|
'border-radius': {
|
||||||
@@ -200,8 +147,8 @@ const config = {
|
|||||||
danger: {
|
danger: {
|
||||||
'color-hover': 'white',
|
'color-hover': 'white',
|
||||||
background: {
|
background: {
|
||||||
color: 'var(--c--theme--colors--danger-600)',
|
color: 'var(--c--theme--colors--danger-400)',
|
||||||
'color-hover': '#FF2725',
|
'color-hover': 'var(--c--theme--colors--danger-500)',
|
||||||
'color-disabled': 'var(--c--theme--colors--danger-100)',
|
'color-disabled': 'var(--c--theme--colors--danger-100)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -231,9 +178,7 @@ const config = {
|
|||||||
color: 'var(--c--theme--colors--primary-text)',
|
color: 'var(--c--theme--colors--primary-text)',
|
||||||
'color-disabled': 'var(--c--theme--colors--greyscale-600)',
|
'color-disabled': 'var(--c--theme--colors--greyscale-600)',
|
||||||
background: {
|
background: {
|
||||||
color: 'var(--c--theme--colors--primary-100)',
|
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||||
'color-hover': 'var(--c--theme--colors--primary-300)',
|
|
||||||
'color-active': 'var(--c--theme--colors--primary-100)',
|
|
||||||
'color-disabled': 'var(--c--theme--colors--greyscale-200)',
|
'color-disabled': 'var(--c--theme--colors--greyscale-200)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -252,19 +197,19 @@ const config = {
|
|||||||
dsfr: {
|
dsfr: {
|
||||||
theme: {
|
theme: {
|
||||||
colors: {
|
colors: {
|
||||||
'card-border': '#E5E5E5',
|
'card-border': '#ededed',
|
||||||
'primary-text': '#000091',
|
'primary-text': '#000091',
|
||||||
'primary-100': '#ECECFE',
|
'primary-100': '#f5f5fe',
|
||||||
'primary-150': '#F4F4FD',
|
'primary-150': '#F4F4FD',
|
||||||
'primary-200': '#E3E3FD',
|
'primary-200': '#ececfe',
|
||||||
'primary-300': '#CACAFB',
|
'primary-300': '#e3e3fd',
|
||||||
'primary-400': '#8585F6',
|
'primary-400': '#cacafb',
|
||||||
'primary-500': '#6A6AF4',
|
'primary-500': '#6a6af4',
|
||||||
'primary-600': '#313178',
|
'primary-600': '#000091',
|
||||||
'primary-700': '#272747',
|
'primary-700': '#272747',
|
||||||
'primary-800': '#000091',
|
'primary-800': '#21213f',
|
||||||
'primary-900': '#21213F',
|
'primary-900': '#1c1a36',
|
||||||
'secondary-text': '#fff',
|
'secondary-text': '#FFFFFF',
|
||||||
'secondary-100': '#fee9ea',
|
'secondary-100': '#fee9ea',
|
||||||
'secondary-200': '#fedfdf',
|
'secondary-200': '#fedfdf',
|
||||||
'secondary-300': '#fdbfbf',
|
'secondary-300': '#fdbfbf',
|
||||||
@@ -275,22 +220,16 @@ const config = {
|
|||||||
'secondary-800': '#341f1f',
|
'secondary-800': '#341f1f',
|
||||||
'secondary-900': '#2b1919',
|
'secondary-900': '#2b1919',
|
||||||
'greyscale-text': '#303C4B',
|
'greyscale-text': '#303C4B',
|
||||||
'greyscale-000': '#fff',
|
'greyscale-000': '#f6f6f6',
|
||||||
'greyscale-050': '#F6F6F6',
|
'greyscale-100': '#eeeeee',
|
||||||
'greyscale-100': '#eee',
|
'greyscale-200': '#e5e5e5',
|
||||||
'greyscale-200': '#E5E5E5',
|
'greyscale-300': '#e1e1e1',
|
||||||
'greyscale-250': '#ddd',
|
'greyscale-400': '#dddddd',
|
||||||
'greyscale-300': '#CECECE',
|
'greyscale-500': '#cecece',
|
||||||
'greyscale-350': '#ddd',
|
'greyscale-600': '#7b7b7b',
|
||||||
'greyscale-400': '#929292',
|
'greyscale-700': '#666666',
|
||||||
'greyscale-500': '#7C7C7C',
|
'greyscale-800': '#2a2a2a',
|
||||||
'greyscale-600': '#666666',
|
'greyscale-900': '#1e1e1e',
|
||||||
'greyscale-700': '#3A3A3A',
|
|
||||||
'greyscale-750': '#353535',
|
|
||||||
'greyscale-800': '#2A2A2A',
|
|
||||||
'greyscale-900': '#242424',
|
|
||||||
'greyscale-950': '#1E1E1E',
|
|
||||||
'greyscale-1000': '#161616',
|
|
||||||
'success-text': '#1f8d49',
|
'success-text': '#1f8d49',
|
||||||
'success-100': '#dffee6',
|
'success-100': '#dffee6',
|
||||||
'success-200': '#b8fec9',
|
'success-200': '#b8fec9',
|
||||||
@@ -302,15 +241,15 @@ const config = {
|
|||||||
'success-800': '#1e2e22',
|
'success-800': '#1e2e22',
|
||||||
'success-900': '#19281d',
|
'success-900': '#19281d',
|
||||||
'info-text': '#0078f3',
|
'info-text': '#0078f3',
|
||||||
'info-100': '#E8EDFF',
|
'info-100': '#f4f6ff',
|
||||||
'info-200': '#DDE5FF',
|
'info-200': '#e8edff',
|
||||||
'info-300': '#BCCDFF',
|
'info-300': '#dde5ff',
|
||||||
'info-400': '#518FFF',
|
'info-400': '#bdcdff',
|
||||||
'info-500': '#0078F3',
|
'info-500': '#0078f3',
|
||||||
'info-600': '#0063CB',
|
'info-600': '#0063cb',
|
||||||
'info-700': '#273961',
|
'info-700': '#f4f6ff',
|
||||||
'info-800': '#222A3F',
|
'info-800': '#222a3f',
|
||||||
'info-900': '#1D2437',
|
'info-900': '#1d2437',
|
||||||
'warning-text': '#d64d00',
|
'warning-text': '#d64d00',
|
||||||
'warning-100': '#fff4f3',
|
'warning-100': '#fff4f3',
|
||||||
'warning-200': '#ffe9e6',
|
'warning-200': '#ffe9e6',
|
||||||
@@ -321,16 +260,16 @@ const config = {
|
|||||||
'warning-700': '#5e2c21',
|
'warning-700': '#5e2c21',
|
||||||
'warning-800': '#3e241e',
|
'warning-800': '#3e241e',
|
||||||
'warning-900': '#361e19',
|
'warning-900': '#361e19',
|
||||||
'danger-text': '#FFF',
|
'danger-text': '#e1000f',
|
||||||
'danger-100': '#FFE9E9',
|
'danger-100': '#fef4f4',
|
||||||
'danger-200': '#FFDDDD',
|
'danger-200': '#fee9e9',
|
||||||
'danger-300': '#FFBDBD',
|
'danger-300': '#fddede',
|
||||||
'danger-400': '#FF5655',
|
'danger-400': '#fcbfbf',
|
||||||
'danger-500': '#F60700',
|
'danger-500': '#e1000f',
|
||||||
'danger-600': '#CE0500',
|
'danger-600': '#c9191e',
|
||||||
'danger-700': '#642626',
|
'danger-700': '#642727',
|
||||||
'danger-800': '#412121',
|
'danger-800': '#412121',
|
||||||
'danger-900': '#391C1C',
|
'danger-900': '#3a1c1c',
|
||||||
},
|
},
|
||||||
font: {
|
font: {
|
||||||
families: {
|
families: {
|
||||||
@@ -349,12 +288,8 @@ const config = {
|
|||||||
alert: {
|
alert: {
|
||||||
'border-radius': '0',
|
'border-radius': '0',
|
||||||
},
|
},
|
||||||
modal: {
|
|
||||||
'width-small': '342px',
|
|
||||||
},
|
|
||||||
button: {
|
button: {
|
||||||
'medium-height': '40px',
|
'medium-height': '48px',
|
||||||
'medium-text-height': '40px',
|
|
||||||
'border-radius': '4px',
|
'border-radius': '4px',
|
||||||
primary: {
|
primary: {
|
||||||
background: {
|
background: {
|
||||||
@@ -362,9 +297,9 @@ const config = {
|
|||||||
'color-hover': '#1212ff',
|
'color-hover': '#1212ff',
|
||||||
'color-active': '#2323ff',
|
'color-active': '#2323ff',
|
||||||
},
|
},
|
||||||
color: '#fff',
|
color: '#ffffff',
|
||||||
'color-hover': '#fff',
|
'color-hover': '#ffffff',
|
||||||
'color-active': '#fff',
|
'color-active': '#ffffff',
|
||||||
},
|
},
|
||||||
'primary-text': {
|
'primary-text': {
|
||||||
background: {
|
background: {
|
||||||
@@ -386,7 +321,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
'tertiary-text': {
|
'tertiary-text': {
|
||||||
background: {
|
background: {
|
||||||
'color-hover': 'var(--c--theme--colors--greyscale-100)',
|
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||||
},
|
},
|
||||||
'color-hover': 'var(--c--theme--colors--primary-text)',
|
'color-hover': 'var(--c--theme--colors--primary-text)',
|
||||||
color: 'var(--c--theme--colors--primary-600)',
|
color: 'var(--c--theme--colors--primary-600)',
|
||||||
@@ -428,7 +363,7 @@ const config = {
|
|||||||
},
|
},
|
||||||
'forms-input': {
|
'forms-input': {
|
||||||
'border-radius': '4px',
|
'border-radius': '4px',
|
||||||
'background-color': '#fff',
|
'background-color': '#ffffff',
|
||||||
'border-color': 'var(--c--theme--colors--primary-text)',
|
'border-color': 'var(--c--theme--colors--primary-text)',
|
||||||
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
|
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
|
||||||
'value-color': 'var(--c--theme--colors--primary-text)',
|
'value-color': 'var(--c--theme--colors--primary-text)',
|
||||||
@@ -446,7 +381,7 @@ const config = {
|
|||||||
'item-font-size': '14px',
|
'item-font-size': '14px',
|
||||||
'border-radius': '4px',
|
'border-radius': '4px',
|
||||||
'border-radius-hover': '4px',
|
'border-radius-hover': '4px',
|
||||||
'background-color': '#fff',
|
'background-color': '#ffffff',
|
||||||
'border-color': 'var(--c--theme--colors--primary-text)',
|
'border-color': 'var(--c--theme--colors--primary-text)',
|
||||||
'border-color-hover': 'var(--c--theme--colors--primary-text)',
|
'border-color-hover': 'var(--c--theme--colors--primary-text)',
|
||||||
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
|
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
|
||||||
|
|||||||
@@ -15,37 +15,34 @@
|
|||||||
"test:watch": "jest --watch"
|
"test:watch": "jest --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@blocknote/core": "0.22.0",
|
"@blocknote/core": "*",
|
||||||
"@blocknote/mantine": "0.22.0",
|
"@blocknote/mantine": "*",
|
||||||
"@blocknote/react": "0.22.0",
|
"@blocknote/react": "*",
|
||||||
"@gouvfr-lasuite/integration": "1.0.2",
|
"@gouvfr-lasuite/integration": "1.0.2",
|
||||||
"@hocuspocus/provider": "2.15.0",
|
"@hocuspocus/provider": "2.15.0",
|
||||||
"@openfun/cunningham-react": "2.9.4",
|
"@openfun/cunningham-react": "2.9.4",
|
||||||
"@sentry/nextjs": "8.47.0",
|
"@sentry/nextjs": "8.45.1",
|
||||||
"@tanstack/react-query": "5.62.11",
|
"@tanstack/react-query": "5.62.7",
|
||||||
"cmdk": "1.0.4",
|
|
||||||
"crisp-sdk-web": "1.0.25",
|
"crisp-sdk-web": "1.0.25",
|
||||||
"i18next": "24.2.0",
|
"i18next": "24.1.0",
|
||||||
"i18next-browser-languagedetector": "8.0.2",
|
"i18next-browser-languagedetector": "8.0.2",
|
||||||
"idb": "8.0.1",
|
"idb": "8.0.1",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"luxon": "3.5.0",
|
"luxon": "3.5.0",
|
||||||
"next": "15.1.3",
|
"next": "15.1.0",
|
||||||
"react": "*",
|
"react": "*",
|
||||||
"react-aria-components": "1.5.0",
|
"react-aria-components": "1.5.0",
|
||||||
"react-dom": "*",
|
"react-dom": "*",
|
||||||
"react-i18next": "15.4.0",
|
"react-i18next": "15.2.0",
|
||||||
"react-intersection-observer": "9.13.1",
|
|
||||||
"react-select": "5.9.0",
|
"react-select": "5.9.0",
|
||||||
"styled-components": "6.1.13",
|
"styled-components": "6.1.13",
|
||||||
"use-debounce": "10.0.4",
|
|
||||||
"y-protocols": "1.0.6",
|
"y-protocols": "1.0.6",
|
||||||
"yjs": "13.6.21",
|
"yjs": "*",
|
||||||
"zustand": "5.0.2"
|
"zustand": "5.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@svgr/webpack": "8.1.0",
|
"@svgr/webpack": "8.1.0",
|
||||||
"@tanstack/react-query-devtools": "5.62.11",
|
"@tanstack/react-query-devtools": "5.62.7",
|
||||||
"@testing-library/dom": "10.4.0",
|
"@testing-library/dom": "10.4.0",
|
||||||
"@testing-library/jest-dom": "6.6.3",
|
"@testing-library/jest-dom": "6.6.3",
|
||||||
"@testing-library/react": "16.1.0",
|
"@testing-library/react": "16.1.0",
|
||||||
@@ -56,7 +53,7 @@
|
|||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"@types/react": "18.3.12",
|
"@types/react": "18.3.12",
|
||||||
"@types/react-dom": "*",
|
"@types/react-dom": "*",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "*",
|
||||||
"dotenv": "16.4.7",
|
"dotenv": "16.4.7",
|
||||||
"eslint-config-impress": "*",
|
"eslint-config-impress": "*",
|
||||||
"fetch-mock": "9.11.0",
|
"fetch-mock": "9.11.0",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 279 KiB |
@@ -0,0 +1,32 @@
|
|||||||
|
import '@testing-library/jest-dom';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { AppWrapper } from '@/tests/utils';
|
||||||
|
|
||||||
|
import Page from '../pages';
|
||||||
|
|
||||||
|
jest.mock('next/router', () => ({
|
||||||
|
useRouter() {
|
||||||
|
return {
|
||||||
|
push: jest.fn(),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('@sentry/nextjs', () => ({
|
||||||
|
captureException: jest.fn(),
|
||||||
|
captureMessage: jest.fn(),
|
||||||
|
setUser: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Page', () => {
|
||||||
|
it('checks Page rendering', () => {
|
||||||
|
render(<Page />, { wrapper: AppWrapper });
|
||||||
|
|
||||||
|
expect(
|
||||||
|
screen.getByRole('button', {
|
||||||
|
name: /Create a new document/i,
|
||||||
|
}),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -29,7 +29,7 @@ describe('fetchAPI', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('check the versioning', () => {
|
it('check the versionning', () => {
|
||||||
fetchMock.mock('http://test.jest/api/v2.0/some/url', 200);
|
fetchMock.mock('http://test.jest/api/v2.0/some/url', 200);
|
||||||
|
|
||||||
void fetchAPI('some/url', {}, '2.0');
|
void fetchAPI('some/url', {}, '2.0');
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,13 +1,9 @@
|
|||||||
import { forwardRef } from 'react';
|
import { ComponentPropsWithRef, forwardRef } from 'react';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
import { Box, BoxType } from './Box';
|
import { Box, BoxType } from './Box';
|
||||||
|
|
||||||
export type BoxButtonType = BoxType & {
|
export type BoxButtonType = ComponentPropsWithRef<typeof BoxButton>;
|
||||||
disabled?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Styleless button that extends the Box component.
|
* Styleless button that extends the Box component.
|
||||||
@@ -22,7 +18,7 @@ export type BoxButtonType = BoxType & {
|
|||||||
* </BoxButton>
|
* </BoxButton>
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
|
const BoxButton = forwardRef<HTMLDivElement, BoxType>(
|
||||||
({ $css, ...props }, ref) => {
|
({ $css, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@@ -32,24 +28,14 @@ const BoxButton = forwardRef<HTMLDivElement, BoxButtonType>(
|
|||||||
$margin="none"
|
$margin="none"
|
||||||
$padding="none"
|
$padding="none"
|
||||||
$css={css`
|
$css={css`
|
||||||
cursor: ${props.disabled ? 'not-allowed' : 'pointer'};
|
cursor: pointer;
|
||||||
border: none;
|
border: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
|
||||||
color: ${props.disabled
|
|
||||||
? 'var(--c--theme--colors--greyscale-400) !important'
|
|
||||||
: 'inherit'};
|
|
||||||
${$css || ''}
|
${$css || ''}
|
||||||
`}
|
`}
|
||||||
{...props}
|
{...props}
|
||||||
onClick={(event: React.MouseEvent<HTMLDivElement>) => {
|
|
||||||
if (props.disabled) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
props.onClick?.(event);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,7 +17,8 @@ export const Card = ({
|
|||||||
$background="white"
|
$background="white"
|
||||||
$radius="4px"
|
$radius="4px"
|
||||||
$css={css`
|
$css={css`
|
||||||
border: 1px solid ${colorsTokens()['greyscale-200']};
|
box-shadow: 2px 2px 5px ${colorsTokens()['greyscale-300']};
|
||||||
|
border: 1px solid ${colorsTokens()['card-border']};
|
||||||
${$css}
|
${$css}
|
||||||
`}
|
`}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
import {
|
import React, {
|
||||||
PropsWithChildren,
|
PropsWithChildren,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useEffect,
|
useEffect,
|
||||||
useRef,
|
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { Button, Popover } from 'react-aria-components';
|
import { Button, DialogTrigger, Popover } from 'react-aria-components';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
|
|
||||||
const StyledPopover = styled(Popover)`
|
const StyledPopover = styled(Popover)`
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
|
box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);
|
||||||
|
padding: 0.5rem;
|
||||||
border: 1px solid #dddddd;
|
border: 1px solid #dddddd;
|
||||||
|
opacity: 0;
|
||||||
transition: opacity 0.2s ease-in-out;
|
transition: opacity 0.2s ease-in-out;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -27,15 +26,13 @@ const StyledButton = styled(Button)`
|
|||||||
font-family: Marianne, Arial, serif;
|
font-family: Marianne, Arial, serif;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.938rem;
|
font-size: 0.938rem;
|
||||||
padding: 0;
|
|
||||||
text-wrap: nowrap;
|
text-wrap: nowrap;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export interface DropButtonProps {
|
interface DropButtonProps {
|
||||||
button: ReactNode;
|
button: ReactNode;
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
onOpenChange?: (isOpen: boolean) => void;
|
onOpenChange?: (isOpen: boolean) => void;
|
||||||
label?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DropButton = ({
|
export const DropButton = ({
|
||||||
@@ -43,12 +40,10 @@ export const DropButton = ({
|
|||||||
isOpen = false,
|
isOpen = false,
|
||||||
onOpenChange,
|
onOpenChange,
|
||||||
children,
|
children,
|
||||||
label,
|
|
||||||
}: PropsWithChildren<DropButtonProps>) => {
|
}: PropsWithChildren<DropButtonProps>) => {
|
||||||
|
const [opacity, setOpacity] = useState(false);
|
||||||
const [isLocalOpen, setIsLocalOpen] = useState(isOpen);
|
const [isLocalOpen, setIsLocalOpen] = useState(isOpen);
|
||||||
|
|
||||||
const triggerRef = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLocalOpen(isOpen);
|
setIsLocalOpen(isOpen);
|
||||||
}, [isOpen]);
|
}, [isOpen]);
|
||||||
@@ -56,25 +51,21 @@ export const DropButton = ({
|
|||||||
const onOpenChangeHandler = (isOpen: boolean) => {
|
const onOpenChangeHandler = (isOpen: boolean) => {
|
||||||
setIsLocalOpen(isOpen);
|
setIsLocalOpen(isOpen);
|
||||||
onOpenChange?.(isOpen);
|
onOpenChange?.(isOpen);
|
||||||
|
setTimeout(() => {
|
||||||
|
setOpacity(isOpen);
|
||||||
|
}, 10);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<DialogTrigger onOpenChange={onOpenChangeHandler} isOpen={isLocalOpen}>
|
||||||
<StyledButton
|
<StyledButton>{button}</StyledButton>
|
||||||
ref={triggerRef}
|
|
||||||
onPress={() => onOpenChangeHandler(true)}
|
|
||||||
aria-label={label}
|
|
||||||
>
|
|
||||||
{button}
|
|
||||||
</StyledButton>
|
|
||||||
|
|
||||||
<StyledPopover
|
<StyledPopover
|
||||||
triggerRef={triggerRef}
|
style={{ opacity: opacity ? 1 : 0 }}
|
||||||
isOpen={isLocalOpen}
|
isOpen={isLocalOpen}
|
||||||
onOpenChange={onOpenChangeHandler}
|
onOpenChange={onOpenChangeHandler}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</StyledPopover>
|
</StyledPopover>
|
||||||
</>
|
</DialogTrigger>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
import { PropsWithChildren, useState } from 'react';
|
|
||||||
import { css } from 'styled-components';
|
|
||||||
|
|
||||||
import { Box, BoxButton, BoxProps, DropButton, Icon, Text } from '@/components';
|
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
|
||||||
|
|
||||||
export type DropdownMenuOption = {
|
|
||||||
icon?: string;
|
|
||||||
label: string;
|
|
||||||
testId?: string;
|
|
||||||
callback?: () => void | Promise<unknown>;
|
|
||||||
danger?: boolean;
|
|
||||||
isSelected?: boolean;
|
|
||||||
disabled?: boolean;
|
|
||||||
show?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DropdownMenuProps = {
|
|
||||||
options: DropdownMenuOption[];
|
|
||||||
showArrow?: boolean;
|
|
||||||
label?: string;
|
|
||||||
arrowCss?: BoxProps['$css'];
|
|
||||||
topMessage?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const DropdownMenu = ({
|
|
||||||
options,
|
|
||||||
children,
|
|
||||||
showArrow = false,
|
|
||||||
arrowCss,
|
|
||||||
label,
|
|
||||||
topMessage,
|
|
||||||
}: PropsWithChildren<DropdownMenuProps>) => {
|
|
||||||
const theme = useCunninghamTheme();
|
|
||||||
const spacings = theme.spacingsTokens();
|
|
||||||
const colors = theme.colorsTokens();
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
|
|
||||||
const onOpenChange = (isOpen: boolean) => {
|
|
||||||
setIsOpen(isOpen);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropButton
|
|
||||||
isOpen={isOpen}
|
|
||||||
onOpenChange={onOpenChange}
|
|
||||||
label={label}
|
|
||||||
button={
|
|
||||||
showArrow ? (
|
|
||||||
<Box $direction="row" $align="center">
|
|
||||||
<div>{children}</div>
|
|
||||||
<Icon
|
|
||||||
$variation="600"
|
|
||||||
$css={
|
|
||||||
arrowCss ??
|
|
||||||
css`
|
|
||||||
color: var(--c--theme--colors--primary-600);
|
|
||||||
`
|
|
||||||
}
|
|
||||||
iconName={isOpen ? 'arrow_drop_up' : 'arrow_drop_down'}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
children
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Box $maxWidth="320px">
|
|
||||||
{topMessage && (
|
|
||||||
<Text
|
|
||||||
$variation="700"
|
|
||||||
$wrap="wrap"
|
|
||||||
$size="xs"
|
|
||||||
$weight="bold"
|
|
||||||
$padding={{ vertical: 'xs', horizontal: 'base' }}
|
|
||||||
>
|
|
||||||
{topMessage}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{options.map((option, index) => {
|
|
||||||
if (option.show !== undefined && !option.show) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const isDisabled = option.disabled !== undefined && option.disabled;
|
|
||||||
return (
|
|
||||||
<BoxButton
|
|
||||||
aria-label={option.label}
|
|
||||||
data-testid={option.testId}
|
|
||||||
$direction="row"
|
|
||||||
disabled={isDisabled}
|
|
||||||
onClick={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
onOpenChange?.(false);
|
|
||||||
void option.callback?.();
|
|
||||||
}}
|
|
||||||
key={option.label}
|
|
||||||
$align="center"
|
|
||||||
$justify="space-between"
|
|
||||||
$background={colors['greyscale-000']}
|
|
||||||
$color={colors['primary-600']}
|
|
||||||
$padding={{ vertical: 'xs', horizontal: 'base' }}
|
|
||||||
$width="100%"
|
|
||||||
$gap={spacings['base']}
|
|
||||||
$css={css`
|
|
||||||
border: none;
|
|
||||||
${index === 0 &&
|
|
||||||
css`
|
|
||||||
border-top-left-radius: 4px;
|
|
||||||
border-top-right-radius: 4px;
|
|
||||||
`}
|
|
||||||
${index === options.length - 1 &&
|
|
||||||
css`
|
|
||||||
border-bottom-left-radius: 4px;
|
|
||||||
border-bottom-right-radius: 4px;
|
|
||||||
`}
|
|
||||||
font-size: var(--c--theme--font--sizes--sm);
|
|
||||||
color: var(--c--theme--colors--greyscale-1000);
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: ${isDisabled ? 'not-allowed' : 'pointer'};
|
|
||||||
user-select: none;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background-color: var(--c--theme--colors--greyscale-050);
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<Box $direction="row" $align="center" $gap={spacings['base']}>
|
|
||||||
{option.icon && (
|
|
||||||
<Icon
|
|
||||||
$size="20px"
|
|
||||||
$theme="greyscale"
|
|
||||||
$variation={isDisabled ? '400' : '1000'}
|
|
||||||
iconName={option.icon}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Text $variation={isDisabled ? '400' : '1000'}>
|
|
||||||
{option.label}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
{option.isSelected && (
|
|
||||||
<Icon iconName="check" $size="20px" $theme="greyscale" />
|
|
||||||
)}
|
|
||||||
</BoxButton>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Box>
|
|
||||||
</DropButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,19 +1,6 @@
|
|||||||
import { css } from 'styled-components';
|
|
||||||
|
|
||||||
import { Text, TextType } from '@/components';
|
import { Text, TextType } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
|
|
||||||
type IconProps = TextType & {
|
|
||||||
iconName: string;
|
|
||||||
};
|
|
||||||
export const Icon = ({ iconName, ...textProps }: IconProps) => {
|
|
||||||
return (
|
|
||||||
<Text $isMaterialIcon {...textProps}>
|
|
||||||
{iconName}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IconBGProps extends TextType {
|
interface IconBGProps extends TextType {
|
||||||
iconName: string;
|
iconName: string;
|
||||||
}
|
}
|
||||||
@@ -42,21 +29,23 @@ export const IconBG = ({ iconName, ...textProps }: IconBGProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
type IconOptionsProps = TextType & {
|
interface IconOptionsProps {
|
||||||
isHorizontal?: boolean;
|
isOpen: boolean;
|
||||||
};
|
'aria-label': string;
|
||||||
|
}
|
||||||
|
|
||||||
export const IconOptions = ({ isHorizontal, ...props }: IconOptionsProps) => {
|
export const IconOptions = ({ isOpen, ...props }: IconOptionsProps) => {
|
||||||
return (
|
return (
|
||||||
<Text
|
<Text
|
||||||
{...props}
|
aria-label={props['aria-label']}
|
||||||
$isMaterialIcon
|
$isMaterialIcon
|
||||||
$css={css`
|
$css={`
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
transform: rotate(${isOpen ? '90' : '0'}deg);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
${props.$css}
|
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{isHorizontal ? 'more_horiz' : 'more_vert'}
|
more_vert
|
||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
import { Button } from '@openfun/cunningham-react';
|
import { PropsWithChildren, useEffect, useRef } from 'react';
|
||||||
import { PropsWithChildren } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { InView } from 'react-intersection-observer';
|
|
||||||
|
|
||||||
import { Box, BoxType, Icon } from '@/components';
|
import { Box, BoxType } from '@/components';
|
||||||
|
|
||||||
interface InfiniteScrollProps extends BoxType {
|
interface InfiniteScrollProps extends BoxType {
|
||||||
hasMore: boolean;
|
hasMore: boolean;
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
next: () => void;
|
next: () => void;
|
||||||
scrollContainer?: HTMLElement | null;
|
scrollContainer: HTMLElement | null;
|
||||||
buttonLabel?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InfiniteScroll = ({
|
export const InfiniteScroll = ({
|
||||||
@@ -18,31 +14,42 @@ export const InfiniteScroll = ({
|
|||||||
hasMore,
|
hasMore,
|
||||||
isLoading,
|
isLoading,
|
||||||
next,
|
next,
|
||||||
buttonLabel,
|
scrollContainer,
|
||||||
...boxProps
|
...boxProps
|
||||||
}: PropsWithChildren<InfiniteScrollProps>) => {
|
}: PropsWithChildren<InfiniteScrollProps>) => {
|
||||||
const { t } = useTranslation();
|
const timeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
const loadMore = (inView: boolean) => {
|
|
||||||
if (!inView || isLoading) {
|
useEffect(() => {
|
||||||
|
if (!scrollContainer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void next();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
const nextHandle = () => {
|
||||||
<Box {...boxProps}>
|
if (!hasMore || isLoading) {
|
||||||
{children}
|
return;
|
||||||
<InView onChange={loadMore}>
|
}
|
||||||
{!isLoading && hasMore && (
|
|
||||||
<Button
|
// To not wait until the end of the scroll to load more data
|
||||||
onClick={() => void next()}
|
const heightFromBottom = 150;
|
||||||
color="primary-text"
|
|
||||||
icon={<Icon iconName="arrow_downward" />}
|
const { scrollTop, clientHeight, scrollHeight } = scrollContainer;
|
||||||
>
|
if (scrollTop + clientHeight >= scrollHeight - heightFromBottom) {
|
||||||
{buttonLabel ?? t('Load more')}
|
next();
|
||||||
</Button>
|
}
|
||||||
)}
|
};
|
||||||
</InView>
|
|
||||||
</Box>
|
const handleScroll = () => {
|
||||||
);
|
if (timeout.current) {
|
||||||
|
clearTimeout(timeout.current);
|
||||||
|
}
|
||||||
|
timeout.current = setTimeout(nextHandle, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
scrollContainer.addEventListener('scroll', handleScroll);
|
||||||
|
return () => {
|
||||||
|
scrollContainer.removeEventListener('scroll', handleScroll);
|
||||||
|
};
|
||||||
|
}, [hasMore, isLoading, next, scrollContainer]);
|
||||||
|
|
||||||
|
return <Box {...boxProps}>{children}</Box>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import { Box } from './Box';
|
|
||||||
import { Icon } from './Icon';
|
|
||||||
import { Text } from './Text';
|
|
||||||
|
|
||||||
type LoadMoreTextProps = {
|
|
||||||
['data-testid']?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LoadMoreText = ({
|
|
||||||
'data-testid': dataTestId,
|
|
||||||
}: LoadMoreTextProps) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
data-testid={dataTestId}
|
|
||||||
$direction="row"
|
|
||||||
$align="center"
|
|
||||||
$gap="0.4rem"
|
|
||||||
$padding={{ horizontal: '2xs', vertical: 'sm' }}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
$theme="primary"
|
|
||||||
$variation="800"
|
|
||||||
iconName="arrow_downward"
|
|
||||||
$size="md"
|
|
||||||
/>
|
|
||||||
<Text $theme="primary" $variation="800">
|
|
||||||
{t('Load more')}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -33,7 +33,6 @@ export interface TextProps extends BoxProps {
|
|||||||
| 'greyscale';
|
| 'greyscale';
|
||||||
$variation?:
|
$variation?:
|
||||||
| 'text'
|
| 'text'
|
||||||
| '000'
|
|
||||||
| '100'
|
| '100'
|
||||||
| '200'
|
| '200'
|
||||||
| '300'
|
| '300'
|
||||||
@@ -42,8 +41,7 @@ export interface TextProps extends BoxProps {
|
|||||||
| '600'
|
| '600'
|
||||||
| '700'
|
| '700'
|
||||||
| '800'
|
| '800'
|
||||||
| '900'
|
| '900';
|
||||||
| '1000';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TextType = ComponentPropsWithRef<typeof Text>;
|
export type TextType = ComponentPropsWithRef<typeof Text>;
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ describe('<Box />', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByText('My Box')).toHaveStyle(`
|
expect(screen.getByText('My Box')).toHaveStyle(`
|
||||||
padding-left: 2.5rem;
|
padding-left: 4rem;
|
||||||
padding-right: 2.5rem;
|
padding-right: 4rem;
|
||||||
padding-top: 3rem;
|
padding-top: 3rem;
|
||||||
padding-bottom: 0.5rem;`);
|
padding-bottom: 0.5rem;`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,12 +2,9 @@ export * from './Box';
|
|||||||
export * from './BoxButton';
|
export * from './BoxButton';
|
||||||
export * from './Card';
|
export * from './Card';
|
||||||
export * from './DropButton';
|
export * from './DropButton';
|
||||||
export * from './DropdownMenu';
|
|
||||||
export * from './Icon';
|
export * from './Icon';
|
||||||
export * from './InfiniteScroll';
|
export * from './InfiniteScroll';
|
||||||
export * from './Link';
|
export * from './Link';
|
||||||
export * from './LoadMoreText';
|
|
||||||
export * from './SideModal';
|
export * from './SideModal';
|
||||||
export * from './separators';
|
|
||||||
export * from './Text';
|
export * from './Text';
|
||||||
export * from './TextErrors';
|
export * from './TextErrors';
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import { Command } from 'cmdk';
|
|
||||||
import { ReactNode, useRef } from 'react';
|
|
||||||
|
|
||||||
import { hasChildrens } from '@/utils/children';
|
|
||||||
|
|
||||||
import { Box } from '../Box';
|
|
||||||
|
|
||||||
import { QuickSearchInput } from './QuickSearchInput';
|
|
||||||
import { QuickSearchStyle } from './QuickSearchStyle';
|
|
||||||
|
|
||||||
export type QuickSearchAction = {
|
|
||||||
onSelect?: () => void;
|
|
||||||
content: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QuickSearchData<T> = {
|
|
||||||
groupName: string;
|
|
||||||
elements: T[];
|
|
||||||
emptyString?: string;
|
|
||||||
startActions?: QuickSearchAction[];
|
|
||||||
endActions?: QuickSearchAction[];
|
|
||||||
showWhenEmpty?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type QuickSearchProps = {
|
|
||||||
onFilter?: (str: string) => void;
|
|
||||||
inputValue?: string;
|
|
||||||
inputContent?: ReactNode;
|
|
||||||
showInput?: boolean;
|
|
||||||
loading?: boolean;
|
|
||||||
label?: string;
|
|
||||||
placeholder?: string;
|
|
||||||
children?: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const QuickSearch = ({
|
|
||||||
onFilter,
|
|
||||||
inputContent,
|
|
||||||
inputValue,
|
|
||||||
loading,
|
|
||||||
showInput = true,
|
|
||||||
label,
|
|
||||||
placeholder,
|
|
||||||
children,
|
|
||||||
}: QuickSearchProps) => {
|
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<QuickSearchStyle />
|
|
||||||
<div className="quick-search-container">
|
|
||||||
<Command label={label} shouldFilter={false} ref={ref}>
|
|
||||||
{showInput && (
|
|
||||||
<QuickSearchInput
|
|
||||||
loading={loading}
|
|
||||||
withSeparator={hasChildrens(children)}
|
|
||||||
inputValue={inputValue}
|
|
||||||
onFilter={onFilter}
|
|
||||||
placeholder={placeholder}
|
|
||||||
>
|
|
||||||
{inputContent}
|
|
||||||
</QuickSearchInput>
|
|
||||||
)}
|
|
||||||
<Command.List>
|
|
||||||
<Box>{children}</Box>
|
|
||||||
</Command.List>
|
|
||||||
</Command>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
import { Command } from 'cmdk';
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
|
|
||||||
import { Box } from '../Box';
|
|
||||||
|
|
||||||
import { QuickSearchData } from './QuickSearch';
|
|
||||||
import { QuickSearchItem } from './QuickSearchItem';
|
|
||||||
|
|
||||||
type Props<T> = {
|
|
||||||
group: QuickSearchData<T>;
|
|
||||||
renderElement?: (element: T) => ReactNode;
|
|
||||||
onSelect?: (element: T) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const QuickSearchGroup = <T,>({
|
|
||||||
group,
|
|
||||||
onSelect,
|
|
||||||
renderElement,
|
|
||||||
}: Props<T>) => {
|
|
||||||
return (
|
|
||||||
<Box $margin={{ top: 'base' }}>
|
|
||||||
<Command.Group
|
|
||||||
key={group.groupName}
|
|
||||||
heading={group.groupName}
|
|
||||||
forceMount={false}
|
|
||||||
>
|
|
||||||
{group.startActions?.map((action, index) => {
|
|
||||||
return (
|
|
||||||
<QuickSearchItem
|
|
||||||
key={`${group.groupName}-action-${index}`}
|
|
||||||
onSelect={action.onSelect}
|
|
||||||
>
|
|
||||||
{action.content}
|
|
||||||
</QuickSearchItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{group.elements.map((groupElement, index) => {
|
|
||||||
return (
|
|
||||||
<QuickSearchItem
|
|
||||||
id={`${group.groupName}-element-${index}`}
|
|
||||||
key={`${group.groupName}-element-${index}`}
|
|
||||||
onSelect={() => {
|
|
||||||
onSelect?.(groupElement);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{renderElement?.(groupElement)}
|
|
||||||
</QuickSearchItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{group.endActions?.map((action, index) => {
|
|
||||||
return (
|
|
||||||
<QuickSearchItem
|
|
||||||
key={`${group.groupName}-action-${index}`}
|
|
||||||
onSelect={action.onSelect}
|
|
||||||
>
|
|
||||||
{action.content}
|
|
||||||
</QuickSearchItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{group.emptyString && group.elements.length === 0 && (
|
|
||||||
<span className="ml-b clr-greyscale-500">{group.emptyString}</span>
|
|
||||||
)}
|
|
||||||
</Command.Group>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import { Loader } from '@openfun/cunningham-react';
|
|
||||||
import { Command } from 'cmdk';
|
|
||||||
import { ReactNode } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import { HorizontalSeparator } from '@/components';
|
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
|
||||||
|
|
||||||
import { Box } from '../Box';
|
|
||||||
import { Icon } from '../Icon';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
loading?: boolean;
|
|
||||||
inputValue?: string;
|
|
||||||
onFilter?: (str: string) => void;
|
|
||||||
placeholder?: string;
|
|
||||||
children?: ReactNode;
|
|
||||||
withSeparator?: boolean;
|
|
||||||
};
|
|
||||||
export const QuickSearchInput = ({
|
|
||||||
loading,
|
|
||||||
inputValue,
|
|
||||||
onFilter,
|
|
||||||
placeholder,
|
|
||||||
children,
|
|
||||||
withSeparator: separator = true,
|
|
||||||
}: Props) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { spacingsTokens } = useCunninghamTheme();
|
|
||||||
const spacing = spacingsTokens();
|
|
||||||
|
|
||||||
if (children) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{children}
|
|
||||||
{separator && <HorizontalSeparator />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Box
|
|
||||||
$direction="row"
|
|
||||||
$align="center"
|
|
||||||
className="quick-search-input"
|
|
||||||
$gap={spacing['2xs']}
|
|
||||||
$padding={{ all: 'base' }}
|
|
||||||
>
|
|
||||||
{!loading && <Icon iconName="search" $variation="600" />}
|
|
||||||
{loading && (
|
|
||||||
<div>
|
|
||||||
<Loader size="small" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Command.Input
|
|
||||||
/* eslint-disable-next-line jsx-a11y/no-autofocus */
|
|
||||||
autoFocus={true}
|
|
||||||
aria-label={t('Quick search input')}
|
|
||||||
value={inputValue}
|
|
||||||
role="combobox"
|
|
||||||
placeholder={placeholder ?? t('Search')}
|
|
||||||
onValueChange={onFilter}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
{separator && <HorizontalSeparator $withPadding={false} />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Command } from 'cmdk';
|
|
||||||
import { PropsWithChildren } from 'react';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
onSelect?: (value: string) => void;
|
|
||||||
id?: string;
|
|
||||||
};
|
|
||||||
export const QuickSearchItem = ({
|
|
||||||
children,
|
|
||||||
onSelect,
|
|
||||||
id,
|
|
||||||
}: PropsWithChildren<Props>) => {
|
|
||||||
return (
|
|
||||||
<Command.Item value={id} onSelect={onSelect}>
|
|
||||||
{children}
|
|
||||||
</Command.Item>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { ReactNode } from 'react';
|
|
||||||
|
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
|
||||||
import { useResponsiveStore } from '@/stores';
|
|
||||||
|
|
||||||
import { Box } from '../Box';
|
|
||||||
|
|
||||||
export type QuickSearchItemContentProps = {
|
|
||||||
alwaysShowRight?: boolean;
|
|
||||||
left: ReactNode;
|
|
||||||
right?: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const QuickSearchItemContent = ({
|
|
||||||
alwaysShowRight = false,
|
|
||||||
left,
|
|
||||||
right,
|
|
||||||
}: QuickSearchItemContentProps) => {
|
|
||||||
const { spacingsTokens } = useCunninghamTheme();
|
|
||||||
const spacings = spacingsTokens();
|
|
||||||
|
|
||||||
const { isDesktop } = useResponsiveStore();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
$direction="row"
|
|
||||||
$align="center"
|
|
||||||
$padding={{ horizontal: '2xs', vertical: '3xs' }}
|
|
||||||
$justify="space-between"
|
|
||||||
$width="100%"
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
$direction="row"
|
|
||||||
$align="center"
|
|
||||||
$gap={spacings['2xs']}
|
|
||||||
$width="100%"
|
|
||||||
>
|
|
||||||
{left}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{isDesktop && right && (
|
|
||||||
<Box
|
|
||||||
className={!alwaysShowRight ? 'show-right-on-focus' : ''}
|
|
||||||
$direction="row"
|
|
||||||
$align="center"
|
|
||||||
>
|
|
||||||
{right}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
import { createGlobalStyle } from 'styled-components';
|
|
||||||
|
|
||||||
export const QuickSearchStyle = createGlobalStyle`
|
|
||||||
.quick-search-container {
|
|
||||||
[cmdk-root] {
|
|
||||||
width: 100%;
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 12px;
|
|
||||||
overflow: hidden;
|
|
||||||
transition: transform 100ms ease;
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
[cmdk-input] {
|
|
||||||
border: none;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 17px;
|
|
||||||
padding: 8px;
|
|
||||||
background: white;
|
|
||||||
outline: none;
|
|
||||||
color: var(--c--theme--colors--greyscale-1000);
|
|
||||||
border-radius: 0;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: var(--c--theme--colors--greyscale-500);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[cmdk-item] {
|
|
||||||
content-visibility: auto;
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: var(--c--theme--spacings--xs);
|
|
||||||
font-size: 14px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
user-select: none;
|
|
||||||
will-change: background, color;
|
|
||||||
transition: all 150ms ease;
|
|
||||||
transition-property: none;
|
|
||||||
|
|
||||||
.show-right-on-focus {
|
|
||||||
opacity: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover,
|
|
||||||
&[data-selected='true'] {
|
|
||||||
background: var(--c--theme--colors--greyscale-100);
|
|
||||||
.show-right-on-focus {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-disabled='true'] {
|
|
||||||
color: var(--c--theme--colors--greyscale-500);
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
& + [cmdk-item] {
|
|
||||||
margin-top: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[cmdk-list] {
|
|
||||||
|
|
||||||
padding: 0 var(--c--theme--spacings--base) var(--c--theme--spacings--base)
|
|
||||||
var(--c--theme--spacings--base);
|
|
||||||
|
|
||||||
flex:1;
|
|
||||||
overflow-y: auto;
|
|
||||||
overscroll-behavior: contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
[cmdk-vercel-shortcuts] {
|
|
||||||
display: flex;
|
|
||||||
margin-left: auto;
|
|
||||||
gap: 8px;
|
|
||||||
|
|
||||||
kbd {
|
|
||||||
font-size: 12px;
|
|
||||||
min-width: 20px;
|
|
||||||
padding: 4px;
|
|
||||||
height: 20px;
|
|
||||||
border-radius: 4px;
|
|
||||||
color: white;
|
|
||||||
background: var(--c--theme--colors--greyscale-500);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[cmdk-separator] {
|
|
||||||
height: 1px;
|
|
||||||
width: 100%;
|
|
||||||
background: var(--c--theme--colors--greyscale-500);
|
|
||||||
margin: 4px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
*:not([hidden]) + [cmdk-group] {
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
[cmdk-group-heading] {
|
|
||||||
user-select: none;
|
|
||||||
font-size: var(--c--theme--font--sizes--sm);
|
|
||||||
color: var(--c--theme--colors--greyscale-700);
|
|
||||||
font-weight: bold;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: var(--c--theme--spacings--xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
[cmdk-empty] {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.c__modal__scroller:has(.quick-search-container),
|
|
||||||
.c__modal__scroller:has(.noPadding) {
|
|
||||||
padding: 0 !important;
|
|
||||||
|
|
||||||
.c__modal__close .c__button {
|
|
||||||
right: 5px;
|
|
||||||
top: 5px;
|
|
||||||
padding: 1.5rem 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.c__modal__title {
|
|
||||||
font-size: var(--c--theme--font--sizes--xs);
|
|
||||||
|
|
||||||
padding: var(--c--theme--spacings--base);
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
`;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export * from './QuickSearch';
|
|
||||||
export * from './QuickSearchGroup';
|
|
||||||
export * from './QuickSearchItem';
|
|
||||||
export * from './QuickSearchItemContent';
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { useCunninghamTheme } from '@/cunningham';
|
|
||||||
|
|
||||||
import { Box } from '../Box';
|
|
||||||
|
|
||||||
export enum SeparatorVariant {
|
|
||||||
LIGHT = 'light',
|
|
||||||
DARK = 'dark',
|
|
||||||
}
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
variant?: SeparatorVariant;
|
|
||||||
$withPadding?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const HorizontalSeparator = ({
|
|
||||||
variant = SeparatorVariant.LIGHT,
|
|
||||||
$withPadding = true,
|
|
||||||
}: Props) => {
|
|
||||||
const { colorsTokens } = useCunninghamTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
$height="1px"
|
|
||||||
$width="100%"
|
|
||||||
$margin={{ vertical: $withPadding ? 'base' : 'none' }}
|
|
||||||
$background={
|
|
||||||
variant === SeparatorVariant.DARK
|
|
||||||
? '#e5e5e533'
|
|
||||||
: colorsTokens()['greyscale-100']
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { PropsWithChildren } from 'react';
|
|
||||||
import { css } from 'styled-components';
|
|
||||||
|
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
|
||||||
|
|
||||||
import { Box } from '../Box';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
showSeparator?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SeparatedSection = ({
|
|
||||||
showSeparator = true,
|
|
||||||
children,
|
|
||||||
}: PropsWithChildren<Props>) => {
|
|
||||||
const theme = useCunninghamTheme();
|
|
||||||
const colors = theme.colorsTokens();
|
|
||||||
const spacings = theme.spacingsTokens();
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
$css={css`
|
|
||||||
width: 100%;
|
|
||||||
padding: ${spacings['sm']} 0;
|
|
||||||
${showSeparator &&
|
|
||||||
css`
|
|
||||||
border-bottom: 1px solid ${colors?.['greyscale-200']};
|
|
||||||
`}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export * from './HorizontalSeparator';
|
|
||||||
export * from './SeparatedSection';
|
|
||||||
@@ -33,7 +33,7 @@ export const Auth = ({ children }: PropsWithChildren) => {
|
|||||||
setPathAllowed(!regexpUrlsAuth.some((regexp) => !!asPath.match(regexp)));
|
setPathAllowed(!regexpUrlsAuth.some((regexp) => !!asPath.match(regexp)));
|
||||||
}, [asPath]);
|
}, [asPath]);
|
||||||
|
|
||||||
// We force to log in except on allowed paths
|
// We force to login except on allowed paths
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!initiated || authenticated || pathAllowed) {
|
if (!initiated || authenticated || pathAllowed) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Button } from '@openfun/cunningham-react';
|
import { Button } from '@openfun/cunningham-react';
|
||||||
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { useAuthStore } from '@/core/auth';
|
import { useAuthStore } from '@/core/auth';
|
||||||
@@ -9,14 +10,24 @@ export const ButtonLogin = () => {
|
|||||||
|
|
||||||
if (!authenticated) {
|
if (!authenticated) {
|
||||||
return (
|
return (
|
||||||
<Button onClick={login} color="primary-text" aria-label={t('Login')}>
|
<Button
|
||||||
|
onClick={login}
|
||||||
|
color="primary-text"
|
||||||
|
icon={<span className="material-icons">login</span>}
|
||||||
|
aria-label={t('Login')}
|
||||||
|
>
|
||||||
{t('Login')}
|
{t('Login')}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={logout} color="primary-text" aria-label={t('Logout')}>
|
<Button
|
||||||
|
onClick={logout}
|
||||||
|
color="primary-text"
|
||||||
|
icon={<span className="material-icons">logout</span>}
|
||||||
|
aria-label={t('Logout')}
|
||||||
|
>
|
||||||
{t('Logout')}
|
{t('Logout')}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -46,8 +46,8 @@ export const useAuthStore = create<AuthStore>((set, get) => ({
|
|||||||
terminateCrispSession();
|
terminateCrispSession();
|
||||||
window.location.replace(`${baseApiUrl()}logout/`);
|
window.location.replace(`${baseApiUrl()}logout/`);
|
||||||
},
|
},
|
||||||
// If we try to access a specific page, and we are not authenticated
|
// If we try to access a specific page and we are not authenticated
|
||||||
// we store the path in the local storage to redirect to it after log in
|
// we store the path in the local storage to redirect to it after login
|
||||||
setAuthUrl() {
|
setAuthUrl() {
|
||||||
if (window.location.pathname !== '/') {
|
if (window.location.pathname !== '/') {
|
||||||
localStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.pathname);
|
localStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.pathname);
|
||||||
|
|||||||
@@ -351,19 +351,6 @@ input:-webkit-autofill:focus {
|
|||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c__button--nano {
|
|
||||||
padding: 0 var(--c--theme--spacings--3xs);
|
|
||||||
gap: var(--c--theme--spacings--3xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
.c__button--nano.c__button--icon-only {
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.c__button--nano.c__button--icon-only.c__button--full-width {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.c__button--medium {
|
.c__button--medium {
|
||||||
padding: 0.9rem var(--c--theme--spacings--s);
|
padding: 0.9rem var(--c--theme--spacings--s);
|
||||||
}
|
}
|
||||||
@@ -455,7 +442,6 @@ input:-webkit-autofill:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.c__button--tertiary {
|
.c__button--tertiary {
|
||||||
background-color: var(--c--components--button--tertiary--background--color);
|
|
||||||
color: var(--c--components--button--tertiary--color);
|
color: var(--c--components--button--tertiary--color);
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
@@ -468,13 +454,6 @@ input:-webkit-autofill:focus {
|
|||||||
color: var(--c--components--button--tertiary--color);
|
color: var(--c--components--button--tertiary--color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.c__button--tertiary:active {
|
|
||||||
background-color: var(
|
|
||||||
--c--components--button--tertiary--background--color-active
|
|
||||||
);
|
|
||||||
color: var(--c--components--button--tertiary--color-active);
|
|
||||||
}
|
|
||||||
|
|
||||||
.c__button--tertiary:disabled {
|
.c__button--tertiary:disabled {
|
||||||
background-color: var(
|
background-color: var(
|
||||||
--c--components--button--tertiary--background--color-disabled
|
--c--components--button--tertiary--background--color-disabled
|
||||||
@@ -533,32 +512,13 @@ input:-webkit-autofill:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.c__modal__close button {
|
.c__modal__close button {
|
||||||
padding: 0;
|
padding: 1.5rem 1rem;
|
||||||
font-size: 88px;
|
|
||||||
width: 28px !important;
|
|
||||||
height: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.c__modal__close button .material-icons {
|
|
||||||
padding: 0;
|
|
||||||
font-size: 24px;
|
|
||||||
color: var(--c--theme--colors--greyscale-600);
|
|
||||||
}
|
|
||||||
|
|
||||||
.c__modal__close .c__button {
|
|
||||||
padding: 0 !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.c__modal--full .c__modal__content {
|
.c__modal--full .c__modal__content {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.c__modal__title {
|
|
||||||
padding: 0;
|
|
||||||
font-size: 1.125rem;
|
|
||||||
margin-bottom: var(--c--theme--spacings--2xs);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (width <= 420px) {
|
@media screen and (width <= 420px) {
|
||||||
.c__modal__scroller {
|
.c__modal__scroller {
|
||||||
padding: 0.7rem;
|
padding: 0.7rem;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -13,7 +13,7 @@ export const tokens = {
|
|||||||
'secondary-700': '#97A3AE',
|
'secondary-700': '#97A3AE',
|
||||||
'secondary-800': '#757E87',
|
'secondary-800': '#757E87',
|
||||||
'secondary-900': '#596067',
|
'secondary-900': '#596067',
|
||||||
'info-text': '#fff',
|
'info-text': '#FFFFFF',
|
||||||
'info-100': '#EBF2FC',
|
'info-100': '#EBF2FC',
|
||||||
'info-200': '#8CB5EA',
|
'info-200': '#8CB5EA',
|
||||||
'info-300': '#5894E1',
|
'info-300': '#5894E1',
|
||||||
@@ -32,7 +32,7 @@ export const tokens = {
|
|||||||
'greyscale-700': '#555F6B',
|
'greyscale-700': '#555F6B',
|
||||||
'greyscale-800': '#303C4B',
|
'greyscale-800': '#303C4B',
|
||||||
'greyscale-900': '#0C1A2B',
|
'greyscale-900': '#0C1A2B',
|
||||||
'greyscale-000': '#fff',
|
'greyscale-000': '#FFFFFF',
|
||||||
'primary-100': '#EDF5FA',
|
'primary-100': '#EDF5FA',
|
||||||
'primary-200': '#8CB5EA',
|
'primary-200': '#8CB5EA',
|
||||||
'primary-300': '#5894E1',
|
'primary-300': '#5894E1',
|
||||||
@@ -69,65 +69,28 @@ export const tokens = {
|
|||||||
'danger-700': '#9B0000',
|
'danger-700': '#9B0000',
|
||||||
'danger-800': '#780000',
|
'danger-800': '#780000',
|
||||||
'danger-900': '#5C0000',
|
'danger-900': '#5C0000',
|
||||||
'primary-text': '#fff',
|
'primary-text': '#FFFFFF',
|
||||||
'success-text': '#fff',
|
'success-text': '#FFFFFF',
|
||||||
'warning-text': '#fff',
|
'warning-text': '#FFFFFF',
|
||||||
'danger-text': '#fff',
|
'danger-text': '#FFFFFF',
|
||||||
'card-border': '#ededed',
|
'card-border': '#ededed',
|
||||||
'primary-bg': '#FAFAFA',
|
'primary-bg': '#FAFAFA',
|
||||||
'primary-050': '#F5F5FE',
|
|
||||||
'primary-150': '#E5EEFA',
|
'primary-150': '#E5EEFA',
|
||||||
'primary-950': '#1B1B35',
|
|
||||||
'info-150': '#E5EEFA',
|
'info-150': '#E5EEFA',
|
||||||
'greyscale-1000': '#161616',
|
|
||||||
'blue-400': '#7AB1E8',
|
|
||||||
'blue-500': '#417DC4',
|
|
||||||
'blue-600': '#3558A2',
|
|
||||||
'brown-400': '#E6BE92',
|
|
||||||
'brown-500': '#BD987A',
|
|
||||||
'brown-600': '#745B47',
|
|
||||||
'cyan-400': '#34BAB5',
|
|
||||||
'cyan-500': '#009099',
|
|
||||||
'cyan-600': '#006A6F',
|
|
||||||
'gold-400': '#FFCA00',
|
|
||||||
'gold-500': '#C3992A',
|
|
||||||
'gold-600': '#695240',
|
|
||||||
'green-400': '#34CB6A',
|
|
||||||
'green-500': '#00A95F',
|
|
||||||
'green-600': '#297254',
|
|
||||||
'olive-400': '#99C221',
|
|
||||||
'olive-500': '#68A532',
|
|
||||||
'olive-600': '#447049',
|
|
||||||
'orange-400': '#FF732C',
|
|
||||||
'orange-500': '#E4794A',
|
|
||||||
'orange-600': '#755348',
|
|
||||||
'pink-400': '#FFB7AE',
|
|
||||||
'pink-500': '#E18B76',
|
|
||||||
'pink-600': '#8D533E',
|
|
||||||
'purple-400': '#CE70CC',
|
|
||||||
'purple-500': '#A558A0',
|
|
||||||
'purple-600': '#6E445A',
|
|
||||||
'yellow-400': '#D8C634',
|
|
||||||
'yellow-500': '#B7A73F',
|
|
||||||
'yellow-600': '#66673D',
|
|
||||||
},
|
},
|
||||||
font: {
|
font: {
|
||||||
sizes: {
|
sizes: {
|
||||||
h1: '2rem',
|
h1: '2.2rem',
|
||||||
h2: '1.75rem',
|
h2: '1.7rem',
|
||||||
h3: '1.5rem',
|
h3: '1.37rem',
|
||||||
h4: '1.375rem',
|
h4: '1.15rem',
|
||||||
h5: '1.25rem',
|
h5: '1rem',
|
||||||
h6: '1.125rem',
|
h6: '0.87rem',
|
||||||
l: '1rem',
|
l: '1rem',
|
||||||
m: '0.8125rem',
|
m: '0.8125rem',
|
||||||
s: '0.75rem',
|
s: '0.75rem',
|
||||||
xs: '0.75rem',
|
|
||||||
sm: '0.875rem',
|
|
||||||
md: '1rem',
|
|
||||||
lg: '1.125rem',
|
|
||||||
ml: '0.938rem',
|
ml: '0.938rem',
|
||||||
xl: '1.25rem',
|
xl: '1.50rem',
|
||||||
t: '0.6875rem',
|
t: '0.6875rem',
|
||||||
},
|
},
|
||||||
weights: {
|
weights: {
|
||||||
@@ -157,7 +120,7 @@ export const tokens = {
|
|||||||
},
|
},
|
||||||
spacings: {
|
spacings: {
|
||||||
'0': '0',
|
'0': '0',
|
||||||
xl: '2.5rem',
|
xl: '4rem',
|
||||||
l: '3rem',
|
l: '3rem',
|
||||||
b: '1.625rem',
|
b: '1.625rem',
|
||||||
s: '1rem',
|
s: '1rem',
|
||||||
@@ -167,20 +130,6 @@ export const tokens = {
|
|||||||
auto: 'auto',
|
auto: 'auto',
|
||||||
bx: '2.2rem',
|
bx: '2.2rem',
|
||||||
full: '100%',
|
full: '100%',
|
||||||
'4xs': '0.125rem',
|
|
||||||
'3xs': '0.25rem',
|
|
||||||
'2xs': '0.375rem',
|
|
||||||
xs: '0.5rem',
|
|
||||||
sm: '0.75rem',
|
|
||||||
base: '1rem',
|
|
||||||
md: '1.5rem',
|
|
||||||
lg: '2rem',
|
|
||||||
xxl: '3rem',
|
|
||||||
xxxl: '3.5rem',
|
|
||||||
'4xl': '4rem',
|
|
||||||
'5xl': '4.5rem',
|
|
||||||
'6xl': '6rem',
|
|
||||||
'7xl': '7.5rem',
|
|
||||||
},
|
},
|
||||||
transitions: {
|
transitions: {
|
||||||
'ease-in': 'cubic-bezier(0.32, 0, 0.67, 0)',
|
'ease-in': 'cubic-bezier(0.32, 0, 0.67, 0)',
|
||||||
@@ -253,7 +202,7 @@ export const tokens = {
|
|||||||
focus: 'var(--c--components--forms-select--border-radius)',
|
focus: 'var(--c--components--forms-select--border-radius)',
|
||||||
},
|
},
|
||||||
'font-size': 'var(--c--theme--font--sizes--ml)',
|
'font-size': 'var(--c--theme--font--sizes--ml)',
|
||||||
'menu-background-color': '#fff',
|
'menu-background-color': '#ffffff',
|
||||||
'item-background-color': {
|
'item-background-color': {
|
||||||
hover: 'var(--c--theme--colors--primary-300)',
|
hover: 'var(--c--theme--colors--primary-300)',
|
||||||
},
|
},
|
||||||
@@ -274,7 +223,7 @@ export const tokens = {
|
|||||||
'border-color-hover': 'var(--c--theme--colors--greyscale-200)',
|
'border-color-hover': 'var(--c--theme--colors--greyscale-200)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modal: { 'background-color': '#fff' },
|
modal: { 'background-color': '#ffffff' },
|
||||||
button: {
|
button: {
|
||||||
'border-radius': {
|
'border-radius': {
|
||||||
active: 'var(--c--components--button--border-radius)',
|
active: 'var(--c--components--button--border-radius)',
|
||||||
@@ -294,8 +243,8 @@ export const tokens = {
|
|||||||
danger: {
|
danger: {
|
||||||
'color-hover': 'white',
|
'color-hover': 'white',
|
||||||
background: {
|
background: {
|
||||||
color: 'var(--c--theme--colors--danger-600)',
|
color: 'var(--c--theme--colors--danger-400)',
|
||||||
'color-hover': '#FF2725',
|
'color-hover': 'var(--c--theme--colors--danger-500)',
|
||||||
'color-disabled': 'var(--c--theme--colors--danger-100)',
|
'color-disabled': 'var(--c--theme--colors--danger-100)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -321,9 +270,7 @@ export const tokens = {
|
|||||||
color: 'var(--c--theme--colors--primary-text)',
|
color: 'var(--c--theme--colors--primary-text)',
|
||||||
'color-disabled': 'var(--c--theme--colors--greyscale-600)',
|
'color-disabled': 'var(--c--theme--colors--greyscale-600)',
|
||||||
background: {
|
background: {
|
||||||
color: 'var(--c--theme--colors--primary-100)',
|
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||||
'color-hover': 'var(--c--theme--colors--primary-300)',
|
|
||||||
'color-active': 'var(--c--theme--colors--primary-100)',
|
|
||||||
'color-disabled': 'var(--c--theme--colors--greyscale-200)',
|
'color-disabled': 'var(--c--theme--colors--greyscale-200)',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -387,19 +334,19 @@ export const tokens = {
|
|||||||
dsfr: {
|
dsfr: {
|
||||||
theme: {
|
theme: {
|
||||||
colors: {
|
colors: {
|
||||||
'card-border': '#E5E5E5',
|
'card-border': '#ededed',
|
||||||
'primary-text': '#000091',
|
'primary-text': '#000091',
|
||||||
'primary-100': '#ECECFE',
|
'primary-100': '#f5f5fe',
|
||||||
'primary-150': '#F4F4FD',
|
'primary-150': '#F4F4FD',
|
||||||
'primary-200': '#E3E3FD',
|
'primary-200': '#ececfe',
|
||||||
'primary-300': '#CACAFB',
|
'primary-300': '#e3e3fd',
|
||||||
'primary-400': '#8585F6',
|
'primary-400': '#cacafb',
|
||||||
'primary-500': '#6A6AF4',
|
'primary-500': '#6a6af4',
|
||||||
'primary-600': '#313178',
|
'primary-600': '#000091',
|
||||||
'primary-700': '#272747',
|
'primary-700': '#272747',
|
||||||
'primary-800': '#000091',
|
'primary-800': '#21213f',
|
||||||
'primary-900': '#21213F',
|
'primary-900': '#1c1a36',
|
||||||
'secondary-text': '#fff',
|
'secondary-text': '#FFFFFF',
|
||||||
'secondary-100': '#fee9ea',
|
'secondary-100': '#fee9ea',
|
||||||
'secondary-200': '#fedfdf',
|
'secondary-200': '#fedfdf',
|
||||||
'secondary-300': '#fdbfbf',
|
'secondary-300': '#fdbfbf',
|
||||||
@@ -410,22 +357,16 @@ export const tokens = {
|
|||||||
'secondary-800': '#341f1f',
|
'secondary-800': '#341f1f',
|
||||||
'secondary-900': '#2b1919',
|
'secondary-900': '#2b1919',
|
||||||
'greyscale-text': '#303C4B',
|
'greyscale-text': '#303C4B',
|
||||||
'greyscale-000': '#fff',
|
'greyscale-000': '#f6f6f6',
|
||||||
'greyscale-050': '#F6F6F6',
|
'greyscale-100': '#eeeeee',
|
||||||
'greyscale-100': '#eee',
|
'greyscale-200': '#e5e5e5',
|
||||||
'greyscale-200': '#E5E5E5',
|
'greyscale-300': '#e1e1e1',
|
||||||
'greyscale-250': '#ddd',
|
'greyscale-400': '#dddddd',
|
||||||
'greyscale-300': '#CECECE',
|
'greyscale-500': '#cecece',
|
||||||
'greyscale-350': '#ddd',
|
'greyscale-600': '#7b7b7b',
|
||||||
'greyscale-400': '#929292',
|
'greyscale-700': '#666666',
|
||||||
'greyscale-500': '#7C7C7C',
|
'greyscale-800': '#2a2a2a',
|
||||||
'greyscale-600': '#666666',
|
'greyscale-900': '#1e1e1e',
|
||||||
'greyscale-700': '#3A3A3A',
|
|
||||||
'greyscale-750': '#353535',
|
|
||||||
'greyscale-800': '#2A2A2A',
|
|
||||||
'greyscale-900': '#242424',
|
|
||||||
'greyscale-950': '#1E1E1E',
|
|
||||||
'greyscale-1000': '#161616',
|
|
||||||
'success-text': '#1f8d49',
|
'success-text': '#1f8d49',
|
||||||
'success-100': '#dffee6',
|
'success-100': '#dffee6',
|
||||||
'success-200': '#b8fec9',
|
'success-200': '#b8fec9',
|
||||||
@@ -437,15 +378,15 @@ export const tokens = {
|
|||||||
'success-800': '#1e2e22',
|
'success-800': '#1e2e22',
|
||||||
'success-900': '#19281d',
|
'success-900': '#19281d',
|
||||||
'info-text': '#0078f3',
|
'info-text': '#0078f3',
|
||||||
'info-100': '#E8EDFF',
|
'info-100': '#f4f6ff',
|
||||||
'info-200': '#DDE5FF',
|
'info-200': '#e8edff',
|
||||||
'info-300': '#BCCDFF',
|
'info-300': '#dde5ff',
|
||||||
'info-400': '#518FFF',
|
'info-400': '#bdcdff',
|
||||||
'info-500': '#0078F3',
|
'info-500': '#0078f3',
|
||||||
'info-600': '#0063CB',
|
'info-600': '#0063cb',
|
||||||
'info-700': '#273961',
|
'info-700': '#f4f6ff',
|
||||||
'info-800': '#222A3F',
|
'info-800': '#222a3f',
|
||||||
'info-900': '#1D2437',
|
'info-900': '#1d2437',
|
||||||
'warning-text': '#d64d00',
|
'warning-text': '#d64d00',
|
||||||
'warning-100': '#fff4f3',
|
'warning-100': '#fff4f3',
|
||||||
'warning-200': '#ffe9e6',
|
'warning-200': '#ffe9e6',
|
||||||
@@ -456,16 +397,16 @@ export const tokens = {
|
|||||||
'warning-700': '#5e2c21',
|
'warning-700': '#5e2c21',
|
||||||
'warning-800': '#3e241e',
|
'warning-800': '#3e241e',
|
||||||
'warning-900': '#361e19',
|
'warning-900': '#361e19',
|
||||||
'danger-text': '#FFF',
|
'danger-text': '#e1000f',
|
||||||
'danger-100': '#FFE9E9',
|
'danger-100': '#fef4f4',
|
||||||
'danger-200': '#FFDDDD',
|
'danger-200': '#fee9e9',
|
||||||
'danger-300': '#FFBDBD',
|
'danger-300': '#fddede',
|
||||||
'danger-400': '#FF5655',
|
'danger-400': '#fcbfbf',
|
||||||
'danger-500': '#F60700',
|
'danger-500': '#e1000f',
|
||||||
'danger-600': '#CE0500',
|
'danger-600': '#c9191e',
|
||||||
'danger-700': '#642626',
|
'danger-700': '#642727',
|
||||||
'danger-800': '#412121',
|
'danger-800': '#412121',
|
||||||
'danger-900': '#391C1C',
|
'danger-900': '#3a1c1c',
|
||||||
},
|
},
|
||||||
font: { families: { accent: 'Marianne', base: 'Marianne' } },
|
font: { families: { accent: 'Marianne', base: 'Marianne' } },
|
||||||
logo: {
|
logo: {
|
||||||
@@ -477,10 +418,8 @@ export const tokens = {
|
|||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
alert: { 'border-radius': '0' },
|
alert: { 'border-radius': '0' },
|
||||||
modal: { 'width-small': '342px' },
|
|
||||||
button: {
|
button: {
|
||||||
'medium-height': '40px',
|
'medium-height': '48px',
|
||||||
'medium-text-height': '40px',
|
|
||||||
'border-radius': '4px',
|
'border-radius': '4px',
|
||||||
primary: {
|
primary: {
|
||||||
background: {
|
background: {
|
||||||
@@ -488,9 +427,9 @@ export const tokens = {
|
|||||||
'color-hover': '#1212ff',
|
'color-hover': '#1212ff',
|
||||||
'color-active': '#2323ff',
|
'color-active': '#2323ff',
|
||||||
},
|
},
|
||||||
color: '#fff',
|
color: '#ffffff',
|
||||||
'color-hover': '#fff',
|
'color-hover': '#ffffff',
|
||||||
'color-active': '#fff',
|
'color-active': '#ffffff',
|
||||||
},
|
},
|
||||||
'primary-text': {
|
'primary-text': {
|
||||||
background: {
|
background: {
|
||||||
@@ -509,7 +448,7 @@ export const tokens = {
|
|||||||
},
|
},
|
||||||
'tertiary-text': {
|
'tertiary-text': {
|
||||||
background: {
|
background: {
|
||||||
'color-hover': 'var(--c--theme--colors--greyscale-100)',
|
'color-hover': 'var(--c--theme--colors--primary-100)',
|
||||||
},
|
},
|
||||||
'color-hover': 'var(--c--theme--colors--primary-text)',
|
'color-hover': 'var(--c--theme--colors--primary-text)',
|
||||||
color: 'var(--c--theme--colors--primary-600)',
|
color: 'var(--c--theme--colors--primary-600)',
|
||||||
@@ -547,7 +486,7 @@ export const tokens = {
|
|||||||
},
|
},
|
||||||
'forms-input': {
|
'forms-input': {
|
||||||
'border-radius': '4px',
|
'border-radius': '4px',
|
||||||
'background-color': '#fff',
|
'background-color': '#ffffff',
|
||||||
'border-color': 'var(--c--theme--colors--primary-text)',
|
'border-color': 'var(--c--theme--colors--primary-text)',
|
||||||
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
|
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
|
||||||
'value-color': 'var(--c--theme--colors--primary-text)',
|
'value-color': 'var(--c--theme--colors--primary-text)',
|
||||||
@@ -563,7 +502,7 @@ export const tokens = {
|
|||||||
'item-font-size': '14px',
|
'item-font-size': '14px',
|
||||||
'border-radius': '4px',
|
'border-radius': '4px',
|
||||||
'border-radius-hover': '4px',
|
'border-radius-hover': '4px',
|
||||||
'background-color': '#fff',
|
'background-color': '#ffffff',
|
||||||
'border-color': 'var(--c--theme--colors--primary-text)',
|
'border-color': 'var(--c--theme--colors--primary-text)',
|
||||||
'border-color-hover': 'var(--c--theme--colors--primary-text)',
|
'border-color-hover': 'var(--c--theme--colors--primary-text)',
|
||||||
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
|
'box-shadow-color': 'var(--c--theme--colors--primary-text)',
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ import { tokens } from './cunningham-tokens';
|
|||||||
|
|
||||||
type Tokens = typeof tokens.themes.default & Partial<typeof tokens.themes.dsfr>;
|
type Tokens = typeof tokens.themes.default & Partial<typeof tokens.themes.dsfr>;
|
||||||
type ColorsTokens = Tokens['theme']['colors'];
|
type ColorsTokens = Tokens['theme']['colors'];
|
||||||
type FontSizesTokens = Tokens['theme']['font']['sizes'];
|
|
||||||
type SpacingsTokens = Tokens['theme']['spacings'];
|
|
||||||
type ComponentTokens = Tokens['components'];
|
type ComponentTokens = Tokens['components'];
|
||||||
export type Theme = keyof typeof tokens.themes;
|
export type Theme = keyof typeof tokens.themes;
|
||||||
|
|
||||||
@@ -15,8 +13,6 @@ interface AuthStore {
|
|||||||
setTheme: (theme: Theme) => void;
|
setTheme: (theme: Theme) => void;
|
||||||
themeTokens: () => Partial<Tokens['theme']>;
|
themeTokens: () => Partial<Tokens['theme']>;
|
||||||
colorsTokens: () => Partial<ColorsTokens>;
|
colorsTokens: () => Partial<ColorsTokens>;
|
||||||
fontSizesTokens: () => Partial<FontSizesTokens>;
|
|
||||||
spacingsTokens: () => Partial<SpacingsTokens>;
|
|
||||||
componentTokens: () => ComponentTokens;
|
componentTokens: () => ComponentTokens;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -32,8 +28,6 @@ export const useCunninghamTheme = create<AuthStore>((set, get) => {
|
|||||||
themeTokens: () => currentTheme().theme,
|
themeTokens: () => currentTheme().theme,
|
||||||
colorsTokens: () => currentTheme().theme.colors,
|
colorsTokens: () => currentTheme().theme.colors,
|
||||||
componentTokens: () => currentTheme().components,
|
componentTokens: () => currentTheme().components,
|
||||||
spacingsTokens: () => currentTheme().theme.spacings,
|
|
||||||
fontSizesTokens: () => currentTheme().theme.font.sizes,
|
|
||||||
setTheme: (theme: Theme) => {
|
setTheme: (theme: Theme) => {
|
||||||
set({ theme });
|
set({ theme });
|
||||||
},
|
},
|
||||||
|
|||||||
+26
-49
@@ -4,13 +4,13 @@ import { BlockNoteView } from '@blocknote/mantine';
|
|||||||
import '@blocknote/mantine/style.css';
|
import '@blocknote/mantine/style.css';
|
||||||
import { useCreateBlockNote } from '@blocknote/react';
|
import { useCreateBlockNote } from '@blocknote/react';
|
||||||
import { HocuspocusProvider } from '@hocuspocus/provider';
|
import { HocuspocusProvider } from '@hocuspocus/provider';
|
||||||
import { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
import { Box, TextErrors } from '@/components';
|
import { Box, TextErrors } from '@/components';
|
||||||
import { useAuthStore } from '@/core/auth';
|
import { useAuthStore } from '@/core/auth';
|
||||||
import { Doc } from '@/features/docs/doc-management';
|
import { Doc, Role, currentDocRole } from '@/features/docs/doc-management';
|
||||||
|
|
||||||
import { useUploadFile } from '../hook';
|
import { useUploadFile } from '../hook';
|
||||||
import { useHeadings } from '../hook/useHeadings';
|
import { useHeadings } from '../hook/useHeadings';
|
||||||
@@ -22,53 +22,12 @@ import { BlockNoteToolbar } from './BlockNoteToolbar';
|
|||||||
|
|
||||||
const cssEditor = (readonly: boolean) => `
|
const cssEditor = (readonly: boolean) => `
|
||||||
&, & > .bn-container, & .ProseMirror {
|
&, & > .bn-container, & .ProseMirror {
|
||||||
height:100%;
|
height:100%
|
||||||
|
};
|
||||||
.bn-side-menu[data-block-type=heading][data-level="1"] {
|
& .bn-editor {
|
||||||
height: 50px;
|
padding-right: 30px;
|
||||||
}
|
${readonly && `padding-left: 30px;`}
|
||||||
.bn-side-menu[data-block-type=heading][data-level="2"] {
|
|
||||||
height: 43px;
|
|
||||||
}
|
|
||||||
.bn-side-menu[data-block-type=heading][data-level="3"] {
|
|
||||||
height: 35px;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 1.875rem;
|
|
||||||
}
|
|
||||||
h2 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
h3 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: var(--c--theme--colors--greyscale-500);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.bn-block-group
|
|
||||||
.bn-block-group
|
|
||||||
.bn-block-outer:not([data-prev-depth-changed]):before {
|
|
||||||
border-left: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.bn-editor {
|
|
||||||
|
|
||||||
color: var(--c--theme--colors--greyscale-700);
|
|
||||||
}
|
|
||||||
.bn-block-outer:not(:first-child) {
|
|
||||||
&:has(h1) {
|
|
||||||
padding-top: 32px;
|
|
||||||
}
|
|
||||||
&:has(h2) {
|
|
||||||
padding-top: 24px;
|
|
||||||
}
|
|
||||||
&:has(h3) {
|
|
||||||
padding-top: 16px;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
& .bn-inline-content code {
|
& .bn-inline-content code {
|
||||||
background-color: gainsboro;
|
background-color: gainsboro;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
@@ -76,7 +35,8 @@ const cssEditor = (readonly: boolean) => `
|
|||||||
}
|
}
|
||||||
@media screen and (width <= 560px) {
|
@media screen and (width <= 560px) {
|
||||||
& .bn-editor {
|
& .bn-editor {
|
||||||
|
padding-left: 40px;
|
||||||
|
padding-right: 10px;
|
||||||
${readonly && `padding-left: 10px;`}
|
${readonly && `padding-left: 10px;`}
|
||||||
};
|
};
|
||||||
.bn-side-menu[data-block-type=heading][data-level="1"] {
|
.bn-side-menu[data-block-type=heading][data-level="1"] {
|
||||||
@@ -167,6 +127,23 @@ export const BlockNoteEditor = ({ doc, provider }: BlockNoteEditorProps) => {
|
|||||||
);
|
);
|
||||||
useHeadings(editor);
|
useHeadings(editor);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* With the collaboration it gets complicated to create the initial block
|
||||||
|
* better to let Blocknote manage, then we update the block with the content.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (doc.content || currentDocRole(doc.abilities) !== Role.OWNER) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
editor.updateBlock(editor.document[0], {
|
||||||
|
type: 'heading',
|
||||||
|
content: '',
|
||||||
|
});
|
||||||
|
}, 100);
|
||||||
|
}, [editor, doc.content, doc.abilities]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setEditor(editor);
|
setEditor(editor);
|
||||||
|
|
||||||
|
|||||||
@@ -1,36 +1,40 @@
|
|||||||
import { Loader } from '@openfun/cunningham-react';
|
import { Alert, Loader, VariantType } from '@openfun/cunningham-react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { css } from 'styled-components';
|
import { useTranslation } from 'react-i18next';
|
||||||
import * as Y from 'yjs';
|
import * as Y from 'yjs';
|
||||||
|
|
||||||
import { Box, Text, TextErrors } from '@/components';
|
import { Box, Card, Text, TextErrors } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
import { DocHeader, DocVersionHeader } from '@/features/docs/doc-header/';
|
import { DocHeader } from '@/features/docs/doc-header';
|
||||||
import {
|
import {
|
||||||
Doc,
|
Doc,
|
||||||
base64ToBlocknoteXmlFragment,
|
base64ToBlocknoteXmlFragment,
|
||||||
useProviderStore,
|
useDocStore,
|
||||||
} from '@/features/docs/doc-management';
|
} from '@/features/docs/doc-management';
|
||||||
import { TableContent } from '@/features/docs/doc-table-content/';
|
|
||||||
import { Versions, useDocVersion } from '@/features/docs/doc-versioning/';
|
import { Versions, useDocVersion } from '@/features/docs/doc-versioning/';
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
import { BlockNoteEditor, BlockNoteEditorVersion } from './BlockNoteEditor';
|
import { BlockNoteEditor, BlockNoteEditorVersion } from './BlockNoteEditor';
|
||||||
|
import { IconOpenPanelEditor, PanelEditor } from './PanelEditor';
|
||||||
|
|
||||||
interface DocEditorProps {
|
interface DocEditorProps {
|
||||||
doc: Doc;
|
doc: Doc;
|
||||||
versionId?: Versions['version_id'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
export const DocEditor = ({ doc }: DocEditorProps) => {
|
||||||
const { isDesktop } = useResponsiveStore();
|
const {
|
||||||
|
query: { versionId },
|
||||||
|
} = useRouter();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { isMobile } = useResponsiveStore();
|
||||||
|
|
||||||
const isVersion = !!versionId && typeof versionId === 'string';
|
const isVersion = versionId && typeof versionId === 'string';
|
||||||
|
|
||||||
const { colorsTokens } = useCunninghamTheme();
|
const { colorsTokens } = useCunninghamTheme();
|
||||||
|
|
||||||
const { provider } = useProviderStore();
|
const { providers } = useDocStore();
|
||||||
|
const provider = providers?.[doc.id];
|
||||||
|
|
||||||
if (!provider) {
|
if (!provider) {
|
||||||
return null;
|
return null;
|
||||||
@@ -38,41 +42,43 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isDesktop && !isVersion && (
|
<DocHeader doc={doc} />
|
||||||
<Box
|
{!doc.abilities.partial_update && (
|
||||||
$position="absolute"
|
<Box $margin={{ all: 'small', top: 'none' }}>
|
||||||
$css={css`
|
<Alert type={VariantType.WARNING}>
|
||||||
top: 72px;
|
{t(`Read only, you cannot edit this document.`)}
|
||||||
right: 20px;
|
</Alert>
|
||||||
`}
|
|
||||||
>
|
|
||||||
<TableContent />
|
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
<Box $maxWidth="868px" $width="100%" $height="100%">
|
{isVersion && (
|
||||||
<Box $padding={{ horizontal: isDesktop ? '54px' : 'base' }}>
|
<Box $margin={{ all: 'small', top: 'none' }}>
|
||||||
{isVersion ? (
|
<Alert type={VariantType.WARNING}>
|
||||||
<DocVersionHeader title={doc.title} />
|
{t(`Read only, you cannot edit document versions.`)}
|
||||||
) : (
|
</Alert>
|
||||||
<DocHeader doc={doc} />
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
<Box
|
<Box
|
||||||
$background={colorsTokens()['primary-bg']}
|
$background={colorsTokens()['primary-bg']}
|
||||||
$direction="row"
|
$height="100%"
|
||||||
$width="100%"
|
$direction="row"
|
||||||
$css="overflow-x: clip; flex: 1;"
|
$margin={{ all: isMobile ? 'tiny' : 'small', top: 'none' }}
|
||||||
|
$css="overflow-x: clip;"
|
||||||
|
$position="relative"
|
||||||
|
>
|
||||||
|
<Card
|
||||||
|
$padding={isMobile ? 'small' : 'big'}
|
||||||
|
$css="flex:1;"
|
||||||
|
$overflow="auto"
|
||||||
$position="relative"
|
$position="relative"
|
||||||
>
|
>
|
||||||
<Box $css="flex:1;" $overflow="auto" $position="relative">
|
{isVersion ? (
|
||||||
{isVersion ? (
|
<DocVersionEditor docId={doc.id} versionId={versionId} />
|
||||||
<DocVersionEditor docId={doc.id} versionId={versionId} />
|
) : (
|
||||||
) : (
|
<BlockNoteEditor doc={doc} provider={provider} />
|
||||||
<BlockNoteEditor doc={doc} provider={provider} />
|
)}
|
||||||
)}
|
{!isMobile && <IconOpenPanelEditor />}
|
||||||
</Box>
|
</Card>
|
||||||
</Box>
|
<PanelEditor doc={doc} />
|
||||||
</Box>
|
</Box>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import React, { PropsWithChildren, useEffect, useState } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { Box, BoxButton, Card, IconBG, Text } from '@/components';
|
||||||
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
|
import { Doc } from '@/features/docs/doc-management';
|
||||||
|
import { TableContent } from '@/features/docs/doc-table-content';
|
||||||
|
import { VersionList } from '@/features/docs/doc-versioning';
|
||||||
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
|
import { useHeadingStore, usePanelEditorStore } from '../stores';
|
||||||
|
|
||||||
|
interface PanelProps {
|
||||||
|
doc: Doc;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PanelEditor = ({ doc }: PropsWithChildren<PanelProps>) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { colorsTokens } = useCunninghamTheme();
|
||||||
|
const { isMobile } = useResponsiveStore();
|
||||||
|
const { isPanelTableContentOpen, setIsPanelTableContentOpen, isPanelOpen } =
|
||||||
|
usePanelEditorStore();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
$width="100%"
|
||||||
|
$maxWidth="20rem"
|
||||||
|
$position={isMobile ? 'absolute' : 'sticky'}
|
||||||
|
$height="100%"
|
||||||
|
$hasTransition="slow"
|
||||||
|
$css={`
|
||||||
|
top: 0vh;
|
||||||
|
right: 0;
|
||||||
|
transform: translateX(0%);
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 1rem;
|
||||||
|
${
|
||||||
|
!isPanelOpen &&
|
||||||
|
`
|
||||||
|
transform: translateX(200%);
|
||||||
|
opacity: 0;
|
||||||
|
flex: 0;
|
||||||
|
margin-left: 0rem;
|
||||||
|
max-width: 0rem;
|
||||||
|
`
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
aria-label={t('Document panel')}
|
||||||
|
aria-hidden={!isPanelOpen}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
$overflow="inherit"
|
||||||
|
$position="sticky"
|
||||||
|
$hasTransition="slow"
|
||||||
|
$css={`
|
||||||
|
top: 0;
|
||||||
|
opacity: ${isPanelOpen ? '1' : '0'};
|
||||||
|
`}
|
||||||
|
$maxHeight="99vh"
|
||||||
|
>
|
||||||
|
{isMobile && <IconOpenPanelEditor />}
|
||||||
|
<Box
|
||||||
|
$direction="row"
|
||||||
|
$justify="space-between"
|
||||||
|
$align="center"
|
||||||
|
$position="relative"
|
||||||
|
$background={colorsTokens()['primary-400']}
|
||||||
|
$margin={{ bottom: 'tiny' }}
|
||||||
|
$radius="4px 4px 0 0"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
$background="white"
|
||||||
|
$position="absolute"
|
||||||
|
$height="100%"
|
||||||
|
$width={doc.abilities.versions_list ? '50%' : '100%'}
|
||||||
|
$hasTransition="slow"
|
||||||
|
$css={`
|
||||||
|
border-top: 2px solid ${colorsTokens()['primary-600']};
|
||||||
|
border-radius: 0 4px 0 0;
|
||||||
|
${
|
||||||
|
isPanelTableContentOpen
|
||||||
|
? `
|
||||||
|
transform: translateX(0);
|
||||||
|
border-radius: 4px 0 0 0;
|
||||||
|
`
|
||||||
|
: `transform: translateX(100%);`
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
<BoxButton
|
||||||
|
$minWidth={doc.abilities.versions_list ? '50%' : '100%'}
|
||||||
|
onClick={() => setIsPanelTableContentOpen(true)}
|
||||||
|
$zIndex={1}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
$width="100%"
|
||||||
|
$weight="bold"
|
||||||
|
$size="m"
|
||||||
|
$theme="primary"
|
||||||
|
$variation="600"
|
||||||
|
$padding={{ vertical: 'small', horizontal: 'small' }}
|
||||||
|
>
|
||||||
|
{t('Table of content')}
|
||||||
|
</Text>
|
||||||
|
</BoxButton>
|
||||||
|
{doc.abilities.versions_list && (
|
||||||
|
<BoxButton
|
||||||
|
$minWidth="50%"
|
||||||
|
onClick={() => setIsPanelTableContentOpen(false)}
|
||||||
|
$zIndex={1}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
$width="100%"
|
||||||
|
$weight="bold"
|
||||||
|
$size="m"
|
||||||
|
$theme="primary"
|
||||||
|
$variation="600"
|
||||||
|
$padding={{ vertical: 'small', horizontal: 'small' }}
|
||||||
|
>
|
||||||
|
{t('Versions')}
|
||||||
|
</Text>
|
||||||
|
</BoxButton>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
{isPanelTableContentOpen && <TableContent />}
|
||||||
|
{!isPanelTableContentOpen && doc.abilities.versions_list && (
|
||||||
|
<VersionList doc={doc} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IconOpenPanelEditor = () => {
|
||||||
|
const { headings } = useHeadingStore();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { setIsPanelOpen, isPanelOpen, setIsPanelTableContentOpen } =
|
||||||
|
usePanelEditorStore();
|
||||||
|
const [hasBeenOpen, setHasBeenOpen] = useState(isPanelOpen);
|
||||||
|
const { isMobile } = useResponsiveStore();
|
||||||
|
|
||||||
|
const setClosePanel = () => {
|
||||||
|
setHasBeenOpen(true);
|
||||||
|
setIsPanelOpen(!isPanelOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Open the panel if there are more than 1 heading
|
||||||
|
useEffect(() => {
|
||||||
|
if (headings?.length && headings.length > 1 && !hasBeenOpen && !isMobile) {
|
||||||
|
setIsPanelTableContentOpen(true);
|
||||||
|
setIsPanelOpen(true);
|
||||||
|
setHasBeenOpen(true);
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
headings,
|
||||||
|
setIsPanelTableContentOpen,
|
||||||
|
setIsPanelOpen,
|
||||||
|
hasBeenOpen,
|
||||||
|
isMobile,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// If open from the doc header we set the state as well
|
||||||
|
useEffect(() => {
|
||||||
|
if (isPanelOpen && !hasBeenOpen) {
|
||||||
|
setHasBeenOpen(true);
|
||||||
|
}
|
||||||
|
}, [hasBeenOpen, isPanelOpen]);
|
||||||
|
|
||||||
|
// Close the panel unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
setIsPanelOpen(false);
|
||||||
|
};
|
||||||
|
}, [setIsPanelOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconBG
|
||||||
|
iconName="menu_open"
|
||||||
|
aria-label={isPanelOpen ? t('Close the panel') : t('Open the panel')}
|
||||||
|
$background="transparent"
|
||||||
|
$size="h2"
|
||||||
|
$zIndex={10}
|
||||||
|
$hasTransition="slow"
|
||||||
|
$css={`
|
||||||
|
cursor: pointer;
|
||||||
|
right: 0rem;
|
||||||
|
top: 0.1rem;
|
||||||
|
transform: rotate(${isPanelOpen ? '180deg' : '0deg'});
|
||||||
|
user-select: none;
|
||||||
|
${hasBeenOpen ? 'display:flex;' : 'display: none;'}
|
||||||
|
`}
|
||||||
|
$position="absolute"
|
||||||
|
onClick={setClosePanel}
|
||||||
|
$radius="2px"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -10,7 +10,7 @@ import { toBase64 } from '../utils';
|
|||||||
|
|
||||||
const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
|
const useSaveDoc = (docId: string, doc: Y.Doc, canSave: boolean) => {
|
||||||
const { mutate: updateDoc } = useUpdateDoc({
|
const { mutate: updateDoc } = useUpdateDoc({
|
||||||
listInvalidQueries: [KEY_LIST_DOC_VERSIONS],
|
listInvalideQueries: [KEY_LIST_DOC_VERSIONS],
|
||||||
});
|
});
|
||||||
const [initialDoc, setInitialDoc] = useState<string>(
|
const [initialDoc, setInitialDoc] = useState<string>(
|
||||||
toBase64(Y.encodeStateAsUpdate(doc)),
|
toBase64(Y.encodeStateAsUpdate(doc)),
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
import { DateTime } from 'luxon';
|
import { Fragment } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
import { Box, HorizontalSeparator, Icon, Text } from '@/components';
|
import { Box, Card, StyledLink, Text } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
import {
|
import { Doc, currentDocRole, useTrans } from '@/features/docs/doc-management';
|
||||||
Doc,
|
import { useDate } from '@/hook';
|
||||||
LinkReach,
|
|
||||||
currentDocRole,
|
|
||||||
useTrans,
|
|
||||||
} from '@/features/docs/doc-management';
|
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
|
import { DocTagPublic } from './DocTagPublic';
|
||||||
import { DocTitle } from './DocTitle';
|
import { DocTitle } from './DocTitle';
|
||||||
import { DocToolBox } from './DocToolBox';
|
import { DocToolBox } from './DocToolBox';
|
||||||
|
|
||||||
@@ -20,86 +17,89 @@ interface DocHeaderProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DocHeader = ({ doc }: DocHeaderProps) => {
|
export const DocHeader = ({ doc }: DocHeaderProps) => {
|
||||||
const { colorsTokens, spacingsTokens } = useCunninghamTheme();
|
const { colorsTokens } = useCunninghamTheme();
|
||||||
const { isDesktop } = useResponsiveStore();
|
|
||||||
const spacings = spacingsTokens();
|
|
||||||
const colors = colorsTokens();
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const docIsPublic = doc.link_reach === LinkReach.PUBLIC;
|
const { formatDate } = useDate();
|
||||||
|
|
||||||
const { transRole } = useTrans();
|
const { transRole } = useTrans();
|
||||||
|
const { isMobile, isSmallMobile } = useResponsiveStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box
|
<Card
|
||||||
$width="100%"
|
$margin={isMobile ? 'tiny' : 'small'}
|
||||||
$padding={{ top: isDesktop ? '4xl' : 'md' }}
|
|
||||||
$gap={spacings['base']}
|
|
||||||
aria-label={t('It is the card information about the document.')}
|
aria-label={t('It is the card information about the document.')}
|
||||||
>
|
>
|
||||||
{docIsPublic && (
|
<Box
|
||||||
<Box
|
$padding={isMobile ? 'tiny' : 'small'}
|
||||||
aria-label={t('Public document')}
|
$direction="row"
|
||||||
$color={colors['primary-800']}
|
$align="center"
|
||||||
$background={colors['primary-100']}
|
>
|
||||||
$radius={spacings['3xs']}
|
<StyledLink href="/">
|
||||||
$direction="row"
|
<Text
|
||||||
$padding="xs"
|
$isMaterialIcon
|
||||||
$flex={1}
|
|
||||||
$align="center"
|
|
||||||
$gap={spacings['3xs']}
|
|
||||||
$css={css`
|
|
||||||
border: 1px solid var(--c--theme--colors--primary-300, #e3e3fd);
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
$theme="primary"
|
$theme="primary"
|
||||||
$variation="800"
|
$variation="600"
|
||||||
data-testid="public-icon"
|
$size="2rem"
|
||||||
iconName="public"
|
$css={css`
|
||||||
/>
|
&:hover {
|
||||||
<Text $theme="primary" $variation="800">
|
background-color: ${colorsTokens()['primary-100']};
|
||||||
{t('Public document')}
|
}
|
||||||
|
`}
|
||||||
|
$hasTransition
|
||||||
|
$radius="5px"
|
||||||
|
$padding="tiny"
|
||||||
|
>
|
||||||
|
home
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</StyledLink>
|
||||||
)}
|
<Box
|
||||||
<Box $direction="row" $align="center" $width="100%">
|
$width="1px"
|
||||||
|
$height="70%"
|
||||||
|
$background={colorsTokens()['greyscale-100']}
|
||||||
|
$margin={{ horizontal: 'tiny' }}
|
||||||
|
/>
|
||||||
<Box
|
<Box
|
||||||
$direction="row"
|
$direction="row"
|
||||||
$justify="space-between"
|
$justify="space-between"
|
||||||
$css="flex:1;"
|
$css="flex:1;"
|
||||||
$gap="0.5rem 1rem"
|
$gap="0.5rem 1rem"
|
||||||
|
$wrap="wrap"
|
||||||
$align="center"
|
$align="center"
|
||||||
>
|
>
|
||||||
<Box $gap={spacings['3xs']}>
|
<DocTitle doc={doc} />
|
||||||
<DocTitle doc={doc} />
|
|
||||||
|
|
||||||
<Box $direction="row">
|
|
||||||
{isDesktop && (
|
|
||||||
<>
|
|
||||||
<Text $variation="600" $size="s" $weight="bold">
|
|
||||||
{transRole(currentDocRole(doc.abilities))} ·
|
|
||||||
</Text>
|
|
||||||
<Text $variation="600" $size="s">
|
|
||||||
{t('Last update: {{update}}', {
|
|
||||||
update: DateTime.fromISO(doc.updated_at).toRelative(),
|
|
||||||
})}
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!isDesktop && (
|
|
||||||
<Text $variation="400" $size="s">
|
|
||||||
{DateTime.fromISO(doc.updated_at).toRelative()}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
<DocToolBox doc={doc} />
|
<DocToolBox doc={doc} />
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
<HorizontalSeparator $withPadding={true} />
|
<Box
|
||||||
</Box>
|
$direction={isSmallMobile ? 'column' : 'row'}
|
||||||
|
$align={isSmallMobile ? 'start' : 'center'}
|
||||||
|
$css="border-top:1px solid #eee"
|
||||||
|
$padding={{
|
||||||
|
horizontal: isMobile ? 'tiny' : 'big',
|
||||||
|
vertical: 'tiny',
|
||||||
|
}}
|
||||||
|
$gap="0.5rem 2rem"
|
||||||
|
$justify="space-between"
|
||||||
|
$wrap="wrap"
|
||||||
|
$position="relative"
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
$direction={isSmallMobile ? 'column' : 'row'}
|
||||||
|
$align={isSmallMobile ? 'start' : 'center'}
|
||||||
|
$gap="0.5rem 2rem"
|
||||||
|
$wrap="wrap"
|
||||||
|
>
|
||||||
|
<DocTagPublic doc={doc} />
|
||||||
|
<Text $size="s" $display="inline">
|
||||||
|
{t('Created at')} <strong>{formatDate(doc.created_at)}</strong>
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Text $size="s" $display="inline">
|
||||||
|
{t('Your role:')}{' '}
|
||||||
|
<strong>{transRole(currentDocRole(doc.abilities))}</strong>
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ import {
|
|||||||
VariantType,
|
VariantType,
|
||||||
useToastProvider,
|
useToastProvider,
|
||||||
} from '@openfun/cunningham-react';
|
} from '@openfun/cunningham-react';
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { css } from 'styled-components';
|
|
||||||
|
|
||||||
import { Box, Text } from '@/components';
|
import { Box, Text } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
|
import { useHeadingStore } from '@/features/docs/doc-editor';
|
||||||
import {
|
import {
|
||||||
Doc,
|
Doc,
|
||||||
KEY_DOC,
|
KEY_DOC,
|
||||||
@@ -19,50 +19,45 @@ import {
|
|||||||
useUpdateDoc,
|
useUpdateDoc,
|
||||||
} from '@/features/docs/doc-management';
|
} from '@/features/docs/doc-management';
|
||||||
import { useBroadcastStore, useResponsiveStore } from '@/stores';
|
import { useBroadcastStore, useResponsiveStore } from '@/stores';
|
||||||
|
import { isFirefox } from '@/utils/userAgent';
|
||||||
|
|
||||||
interface DocTitleProps {
|
interface DocTitleProps {
|
||||||
doc: Doc;
|
doc: Doc;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DocTitle = ({ doc }: DocTitleProps) => {
|
export const DocTitle = ({ doc }: DocTitleProps) => {
|
||||||
|
const { isMobile } = useResponsiveStore();
|
||||||
|
|
||||||
if (!doc.abilities.partial_update) {
|
if (!doc.abilities.partial_update) {
|
||||||
return <DocTitleText title={doc.title} />;
|
return (
|
||||||
|
<Text
|
||||||
|
as="h2"
|
||||||
|
$margin={{ all: 'none', left: 'tiny' }}
|
||||||
|
$size={isMobile ? 'h4' : 'h2'}
|
||||||
|
>
|
||||||
|
{doc.title}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return <DocTitleInput doc={doc} />;
|
return <DocTitleInput doc={doc} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface DocTitleTextProps {
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DocTitleText = ({ title }: DocTitleTextProps) => {
|
|
||||||
const { isMobile } = useResponsiveStore();
|
|
||||||
return (
|
|
||||||
<Text
|
|
||||||
as="h2"
|
|
||||||
$margin={{ all: 'none', left: 'none' }}
|
|
||||||
$size={isMobile ? 'h4' : 'h2'}
|
|
||||||
$variation="1000"
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const DocTitleInput = ({ doc }: DocTitleProps) => {
|
const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||||
const { isDesktop } = useResponsiveStore();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { colorsTokens } = useCunninghamTheme();
|
const { colorsTokens } = useCunninghamTheme();
|
||||||
const [titleDisplay, setTitleDisplay] = useState(doc.title);
|
const [titleDisplay, setTitleDisplay] = useState(doc.title);
|
||||||
const { toast } = useToastProvider();
|
const { toast } = useToastProvider();
|
||||||
const { untitledDocument } = useTrans();
|
const { untitledDocument } = useTrans();
|
||||||
const isUntitled = titleDisplay === untitledDocument;
|
const isUntitled = titleDisplay === untitledDocument;
|
||||||
|
const { headings } = useHeadingStore();
|
||||||
|
const headingText = headings?.[0]?.contentText;
|
||||||
|
const debounceRef = useRef<NodeJS.Timeout>();
|
||||||
|
const { isMobile } = useResponsiveStore();
|
||||||
const { broadcast } = useBroadcastStore();
|
const { broadcast } = useBroadcastStore();
|
||||||
|
|
||||||
const { mutate: updateDoc } = useUpdateDoc({
|
const { mutate: updateDoc } = useUpdateDoc({
|
||||||
listInvalidQueries: [KEY_DOC, KEY_LIST_DOC],
|
listInvalideQueries: [KEY_LIST_DOC],
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
if (data.title !== untitledDocument) {
|
if (data.title !== untitledDocument) {
|
||||||
toast(t('Document title updated successfully'), VariantType.SUCCESS);
|
toast(t('Document title updated successfully'), VariantType.SUCCESS);
|
||||||
@@ -86,7 +81,10 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
|||||||
|
|
||||||
// If mutation we update
|
// If mutation we update
|
||||||
if (sanitizedTitle !== doc.title) {
|
if (sanitizedTitle !== doc.title) {
|
||||||
setTitleDisplay(sanitizedTitle);
|
if (debounceRef.current) {
|
||||||
|
clearTimeout(debounceRef.current);
|
||||||
|
debounceRef.current = undefined;
|
||||||
|
}
|
||||||
updateDoc({ id: doc.id, title: sanitizedTitle });
|
updateDoc({ id: doc.id, title: sanitizedTitle });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -100,42 +98,74 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleOnClick = () => {
|
||||||
|
if (isUntitled) {
|
||||||
|
setTitleDisplay('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTitleDisplay(doc.title);
|
setTitleDisplay(doc.title);
|
||||||
}, [doc]);
|
}, [doc.title]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if ((!debounceRef.current && !isUntitled) || !headingText) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setTitleDisplay(headingText);
|
||||||
|
|
||||||
|
if (debounceRef.current) {
|
||||||
|
clearTimeout(debounceRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
debounceRef.current = setTimeout(() => {
|
||||||
|
handleTitleSubmit(headingText);
|
||||||
|
debounceRef.current = undefined;
|
||||||
|
}, 3000);
|
||||||
|
}, [isUntitled, handleTitleSubmit, headingText]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip content={t('Rename')} placement="top">
|
<Tooltip content={t('Rename')} placement="top">
|
||||||
<Box
|
<Box
|
||||||
as="span"
|
as="h2"
|
||||||
role="textbox"
|
$radius="4px"
|
||||||
contentEditable
|
$padding={{ horizontal: 'tiny', vertical: '4px' }}
|
||||||
defaultValue={isUntitled ? undefined : titleDisplay}
|
$margin="none"
|
||||||
|
$minWidth="200px"
|
||||||
|
contentEditable={isFirefox() ? 'true' : 'plaintext-only'}
|
||||||
|
onClick={handleOnClick}
|
||||||
|
onBlurCapture={(e) =>
|
||||||
|
handleTitleSubmit(e.currentTarget.textContent || '')
|
||||||
|
}
|
||||||
onKeyDownCapture={handleKeyDown}
|
onKeyDownCapture={handleKeyDown}
|
||||||
suppressContentEditableWarning={true}
|
suppressContentEditableWarning={true}
|
||||||
aria-label="doc title input"
|
$color={
|
||||||
onBlurCapture={(event) =>
|
isUntitled
|
||||||
handleTitleSubmit(event.target.textContent || '')
|
? colorsTokens()['greyscale-200']
|
||||||
|
: colorsTokens()['greyscale-text']
|
||||||
}
|
}
|
||||||
$color={colorsTokens()['greyscale-1000']}
|
$css={`
|
||||||
$margin={{ left: '-2px', right: '10px' }}
|
${isUntitled && 'font-style: italic;'}
|
||||||
$css={css`
|
cursor: text;
|
||||||
&[contenteditable='true']:empty:not(:focus):before {
|
font-size: ${isMobile ? '1.2rem' : '1.5rem'};
|
||||||
content: '${untitledDocument}';
|
transition: box-shadow 0.5s, border-color 0.5s;
|
||||||
color: grey;
|
border: 1px dashed transparent;
|
||||||
pointer-events: none;
|
|
||||||
font-style: italic;
|
&:hover {
|
||||||
|
border-color: rgba(0, 123, 255, 0.25);
|
||||||
|
border-style: dashed;
|
||||||
}
|
}
|
||||||
font-size: ${isDesktop
|
|
||||||
? css`var(--c--theme--font--sizes--h2)`
|
|
||||||
: css`var(--c--theme--font--sizes--sm)`};
|
|
||||||
font-weight: 700;
|
|
||||||
|
|
||||||
outline: none;
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: transparent;
|
||||||
|
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
|
||||||
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{isUntitled ? '' : titleDisplay}
|
{titleDisplay}
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
|
|||||||
+131
-173
@@ -1,30 +1,24 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
VariantType,
|
VariantType,
|
||||||
useModal,
|
|
||||||
useToastProvider,
|
useToastProvider,
|
||||||
} from '@openfun/cunningham-react';
|
} from '@openfun/cunningham-react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect, useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { css } from 'styled-components';
|
|
||||||
|
|
||||||
import {
|
import { Box, DropButton, IconOptions } from '@/components';
|
||||||
Box,
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuOption,
|
|
||||||
Icon,
|
|
||||||
IconOptions,
|
|
||||||
} from '@/components';
|
|
||||||
import { useAuthStore } from '@/core';
|
import { useAuthStore } from '@/core';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
|
||||||
import { useEditorStore } from '@/features/docs/doc-editor/';
|
|
||||||
import { Doc, ModalRemoveDoc } from '@/features/docs/doc-management';
|
|
||||||
import { DocShareModal } from '@/features/docs/doc-share';
|
|
||||||
import {
|
import {
|
||||||
KEY_LIST_DOC_VERSIONS,
|
useEditorStore,
|
||||||
ModalSelectVersion,
|
usePanelEditorStore,
|
||||||
} from '@/features/docs/doc-versioning';
|
} from '@/features/docs/doc-editor/';
|
||||||
|
import {
|
||||||
|
Doc,
|
||||||
|
ModalRemoveDoc,
|
||||||
|
ModalShare,
|
||||||
|
} from '@/features/docs/doc-management';
|
||||||
|
import { ModalVersion } from '@/features/docs/doc-versioning';
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
import { ModalPDF } from './ModalExport';
|
import { ModalPDF } from './ModalExport';
|
||||||
@@ -34,78 +28,21 @@ interface DocToolBoxProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||||
|
const {
|
||||||
|
query: { versionId },
|
||||||
|
} = useRouter();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const hasAccesses = doc.nb_accesses > 1;
|
const [isModalShareOpen, setIsModalShareOpen] = useState(false);
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
|
||||||
|
|
||||||
const spacings = spacingsTokens();
|
|
||||||
const colors = colorsTokens();
|
|
||||||
|
|
||||||
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
|
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
|
||||||
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
|
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
|
||||||
const selectHistoryModal = useModal();
|
const [isDropOpen, setIsDropOpen] = useState(false);
|
||||||
const modalShare = useModal();
|
const { setIsPanelOpen, setIsPanelTableContentOpen } = usePanelEditorStore();
|
||||||
|
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false);
|
||||||
const { isSmallMobile, isDesktop } = useResponsiveStore();
|
const { isSmallMobile } = useResponsiveStore();
|
||||||
const { authenticated } = useAuthStore();
|
const { authenticated } = useAuthStore();
|
||||||
const { editor } = useEditorStore();
|
const { editor } = useEditorStore();
|
||||||
const { toast } = useToastProvider();
|
const { toast } = useToastProvider();
|
||||||
|
|
||||||
const options: DropdownMenuOption[] = [
|
|
||||||
...(isSmallMobile
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
label: t('Share'),
|
|
||||||
icon: 'upload',
|
|
||||||
callback: () => {
|
|
||||||
modalShare.open();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('Export'),
|
|
||||||
icon: 'download',
|
|
||||||
callback: () => {
|
|
||||||
setIsModalPDFOpen(true);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
|
|
||||||
{
|
|
||||||
label: t('Version history'),
|
|
||||||
icon: 'history',
|
|
||||||
disabled: !doc.abilities.versions_list,
|
|
||||||
callback: () => {
|
|
||||||
selectHistoryModal.open();
|
|
||||||
},
|
|
||||||
show: isDesktop,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
label: t('Copy as {{format}}', { format: 'Markdown' }),
|
|
||||||
icon: 'content_copy',
|
|
||||||
callback: () => {
|
|
||||||
void copyCurrentEditorToClipboard('markdown');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('Copy as {{format}}', { format: 'HTML' }),
|
|
||||||
icon: 'content_copy',
|
|
||||||
callback: () => {
|
|
||||||
void copyCurrentEditorToClipboard('html');
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: t('Delete document'),
|
|
||||||
icon: 'delete',
|
|
||||||
disabled: !doc.abilities.destroy,
|
|
||||||
callback: () => {
|
|
||||||
setIsModalRemoveOpen(true);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const copyCurrentEditorToClipboard = async (
|
const copyCurrentEditorToClipboard = async (
|
||||||
asFormat: 'html' | 'markdown',
|
asFormat: 'html' | 'markdown',
|
||||||
) => {
|
) => {
|
||||||
@@ -129,16 +66,6 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (selectHistoryModal.isOpen) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
void queryClient.resetQueries({
|
|
||||||
queryKey: [KEY_LIST_DOC_VERSIONS],
|
|
||||||
});
|
|
||||||
}, [selectHistoryModal.isOpen, queryClient]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
$margin={{ left: 'auto' }}
|
$margin={{ left: 'auto' }}
|
||||||
@@ -147,88 +74,118 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
$gap="0.5rem 1.5rem"
|
$gap="0.5rem 1.5rem"
|
||||||
$wrap={isSmallMobile ? 'wrap' : 'nowrap'}
|
$wrap={isSmallMobile ? 'wrap' : 'nowrap'}
|
||||||
>
|
>
|
||||||
<Box
|
{versionId && (
|
||||||
$direction="row"
|
<Box $margin={{ left: 'auto' }}>
|
||||||
$align="center"
|
|
||||||
$margin={{ left: 'auto' }}
|
|
||||||
$gap={spacings['2xs']}
|
|
||||||
>
|
|
||||||
{authenticated && !isSmallMobile && (
|
|
||||||
<>
|
|
||||||
{!hasAccesses && (
|
|
||||||
<Button
|
|
||||||
color="tertiary-text"
|
|
||||||
onClick={() => {
|
|
||||||
modalShare.open();
|
|
||||||
}}
|
|
||||||
size={isSmallMobile ? 'small' : 'medium'}
|
|
||||||
>
|
|
||||||
{t('Share')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{hasAccesses && (
|
|
||||||
<Box
|
|
||||||
$css={css`
|
|
||||||
.c__button--medium {
|
|
||||||
height: 32px;
|
|
||||||
padding: 10px var(--c--theme--spacings--xs);
|
|
||||||
gap: 7px;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
<Button
|
|
||||||
color="tertiary"
|
|
||||||
aria-label="Share button"
|
|
||||||
icon={
|
|
||||||
<Icon iconName="group" $theme="primary" $variation="800" />
|
|
||||||
}
|
|
||||||
onClick={() => {
|
|
||||||
modalShare.open();
|
|
||||||
}}
|
|
||||||
size={isSmallMobile ? 'small' : 'medium'}
|
|
||||||
>
|
|
||||||
{doc.nb_accesses}
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!isSmallMobile && (
|
|
||||||
<Button
|
<Button
|
||||||
color="tertiary-text"
|
|
||||||
icon={
|
|
||||||
<Icon iconName="download" $theme="primary" $variation="800" />
|
|
||||||
}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsModalPDFOpen(true);
|
setIsModalVersionOpen(true);
|
||||||
|
}}
|
||||||
|
color="secondary"
|
||||||
|
size={isSmallMobile ? 'small' : 'medium'}
|
||||||
|
>
|
||||||
|
{t('Restore this version')}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
<Box $direction="row" $margin={{ left: 'auto' }} $gap="1rem">
|
||||||
|
{authenticated && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setIsModalShareOpen(true);
|
||||||
}}
|
}}
|
||||||
size={isSmallMobile ? 'small' : 'medium'}
|
size={isSmallMobile ? 'small' : 'medium'}
|
||||||
/>
|
>
|
||||||
|
{t('Share')}
|
||||||
|
</Button>
|
||||||
)}
|
)}
|
||||||
<DropdownMenu options={options}>
|
<DropButton
|
||||||
<IconOptions
|
button={
|
||||||
isHorizontal
|
<IconOptions
|
||||||
$theme="primary"
|
isOpen={isDropOpen}
|
||||||
$padding={{ all: 'xs' }}
|
aria-label={t('Open the document options')}
|
||||||
$css={css`
|
/>
|
||||||
border-radius: 4px;
|
}
|
||||||
&:hover {
|
onOpenChange={(isOpen) => setIsDropOpen(isOpen)}
|
||||||
background-color: ${colors['greyscale-100']};
|
isOpen={isDropOpen}
|
||||||
}
|
>
|
||||||
${isSmallMobile
|
<Box>
|
||||||
? css`
|
{doc.abilities.versions_list && (
|
||||||
padding: 10px;
|
<Button
|
||||||
border: 1px solid ${colors['greyscale-300']};
|
onClick={() => {
|
||||||
`
|
setIsPanelOpen(true);
|
||||||
: ''}
|
setIsPanelTableContentOpen(false);
|
||||||
`}
|
setIsDropOpen(false);
|
||||||
aria-label={t('Open the document options')}
|
}}
|
||||||
/>
|
color="primary-text"
|
||||||
</DropdownMenu>
|
icon={<span className="material-icons">history</span>}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{t('Version history')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setIsPanelOpen(true);
|
||||||
|
setIsPanelTableContentOpen(true);
|
||||||
|
setIsDropOpen(false);
|
||||||
|
}}
|
||||||
|
color="primary-text"
|
||||||
|
icon={<span className="material-icons">summarize</span>}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{t('Table of contents')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setIsModalPDFOpen(true);
|
||||||
|
setIsDropOpen(false);
|
||||||
|
}}
|
||||||
|
color="primary-text"
|
||||||
|
icon={<span className="material-icons">file_download</span>}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{t('Export')}
|
||||||
|
</Button>
|
||||||
|
{doc.abilities.destroy && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setIsModalRemoveOpen(true);
|
||||||
|
setIsDropOpen(false);
|
||||||
|
}}
|
||||||
|
color="primary-text"
|
||||||
|
icon={<span className="material-icons">delete</span>}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{t('Delete document')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setIsDropOpen(false);
|
||||||
|
void copyCurrentEditorToClipboard('markdown');
|
||||||
|
}}
|
||||||
|
color="primary-text"
|
||||||
|
icon={<span className="material-icons">content_copy</span>}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{t('Copy as {{format}}', { format: 'Markdown' })}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
setIsDropOpen(false);
|
||||||
|
void copyCurrentEditorToClipboard('html');
|
||||||
|
}}
|
||||||
|
color="primary-text"
|
||||||
|
icon={<span className="material-icons">content_copy</span>}
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
{t('Copy as {{format}}', { format: 'HTML' })}
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</DropButton>
|
||||||
</Box>
|
</Box>
|
||||||
|
{isModalShareOpen && (
|
||||||
{modalShare.isOpen && (
|
<ModalShare onClose={() => setIsModalShareOpen(false)} doc={doc} />
|
||||||
<DocShareModal onClose={() => modalShare.close()} doc={doc} />
|
|
||||||
)}
|
)}
|
||||||
{isModalPDFOpen && (
|
{isModalPDFOpen && (
|
||||||
<ModalPDF onClose={() => setIsModalPDFOpen(false)} doc={doc} />
|
<ModalPDF onClose={() => setIsModalPDFOpen(false)} doc={doc} />
|
||||||
@@ -236,10 +193,11 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
|||||||
{isModalRemoveOpen && (
|
{isModalRemoveOpen && (
|
||||||
<ModalRemoveDoc onClose={() => setIsModalRemoveOpen(false)} doc={doc} />
|
<ModalRemoveDoc onClose={() => setIsModalRemoveOpen(false)} doc={doc} />
|
||||||
)}
|
)}
|
||||||
{selectHistoryModal.isOpen && (
|
{isModalVersionOpen && versionId && (
|
||||||
<ModalSelectVersion
|
<ModalVersion
|
||||||
onClose={() => selectHistoryModal.close()}
|
onClose={() => setIsModalVersionOpen(false)}
|
||||||
doc={doc}
|
docId={doc.id}
|
||||||
|
versionId={versionId as string}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
-31
@@ -1,31 +0,0 @@
|
|||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import { Box, HorizontalSeparator } from '@/components';
|
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
|
||||||
|
|
||||||
import { DocTitleText } from './DocTitle';
|
|
||||||
|
|
||||||
interface DocVersionHeaderProps {
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DocVersionHeader = ({ title }: DocVersionHeaderProps) => {
|
|
||||||
const { spacingsTokens } = useCunninghamTheme();
|
|
||||||
|
|
||||||
const spacings = spacingsTokens();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Box
|
|
||||||
$width="100%"
|
|
||||||
$padding={{ vertical: 'base' }}
|
|
||||||
$gap={spacings['base']}
|
|
||||||
aria-label={t('It is the document title')}
|
|
||||||
>
|
|
||||||
<DocTitleText title={title} />
|
|
||||||
<HorizontalSeparator />
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
import {
|
import {
|
||||||
|
Alert,
|
||||||
Button,
|
Button,
|
||||||
Loader,
|
Loader,
|
||||||
Modal,
|
Modal,
|
||||||
ModalSize,
|
ModalSize,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
Select,
|
Select,
|
||||||
VariantType,
|
VariantType,
|
||||||
useToastProvider,
|
useToastProvider,
|
||||||
@@ -18,11 +21,6 @@ import { useExport } from '../api/useExport';
|
|||||||
import { TemplatesOrdering, useTemplates } from '../api/useTemplates';
|
import { TemplatesOrdering, useTemplates } from '../api/useTemplates';
|
||||||
import { adaptBlockNoteHTML, downloadFile } from '../utils';
|
import { adaptBlockNoteHTML, downloadFile } from '../utils';
|
||||||
|
|
||||||
export enum DocDownloadFormat {
|
|
||||||
PDF = 'pdf',
|
|
||||||
DOCX = 'docx',
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ModalPDFProps {
|
interface ModalPDFProps {
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
doc: Doc;
|
doc: Doc;
|
||||||
@@ -43,9 +41,7 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
|
|||||||
error,
|
error,
|
||||||
} = useExport();
|
} = useExport();
|
||||||
const [templateIdSelected, setTemplateIdSelected] = useState<string>();
|
const [templateIdSelected, setTemplateIdSelected] = useState<string>();
|
||||||
const [format, setFormat] = useState<DocDownloadFormat>(
|
const [format, setFormat] = useState<'pdf' | 'docx'>('pdf');
|
||||||
DocDownloadFormat.PDF,
|
|
||||||
);
|
|
||||||
|
|
||||||
const templateOptions = useMemo(() => {
|
const templateOptions = useMemo(() => {
|
||||||
if (!templates?.pages) {
|
if (!templates?.pages) {
|
||||||
@@ -127,48 +123,61 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
data-testid="modal-export"
|
|
||||||
isOpen
|
isOpen
|
||||||
closeOnClickOutside
|
closeOnClickOutside
|
||||||
|
hideCloseButton
|
||||||
|
leftActions={
|
||||||
|
<Button
|
||||||
|
aria-label={t('Close the modal')}
|
||||||
|
color="secondary"
|
||||||
|
fullWidth
|
||||||
|
onClick={() => onClose()}
|
||||||
|
>
|
||||||
|
{t('Cancel')}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
onClose={() => onClose()}
|
onClose={() => onClose()}
|
||||||
rightActions={
|
rightActions={
|
||||||
<>
|
<Button
|
||||||
<Button
|
aria-label={t('Download')}
|
||||||
aria-label={t('Close the modal')}
|
color="primary"
|
||||||
color="secondary"
|
fullWidth
|
||||||
fullWidth
|
onClick={() => void onSubmit()}
|
||||||
onClick={() => onClose()}
|
disabled={isPending || !templateIdSelected}
|
||||||
>
|
>
|
||||||
{t('Cancel')}
|
{t('Download')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
aria-label={t('Download')}
|
|
||||||
color="primary"
|
|
||||||
fullWidth
|
|
||||||
onClick={() => void onSubmit()}
|
|
||||||
disabled={isPending || !templateIdSelected}
|
|
||||||
>
|
|
||||||
{t('Download')}
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
size={ModalSize.MEDIUM}
|
size={ModalSize.MEDIUM}
|
||||||
title={
|
title={
|
||||||
<Text $size="h6" $variation="1000" $align="flex-start">
|
<Box $align="center" $gap="1rem">
|
||||||
{t('Download')}
|
<Text
|
||||||
</Text>
|
className="material-icons"
|
||||||
|
$size="3.5rem"
|
||||||
|
$theme="primary"
|
||||||
|
$variation="600"
|
||||||
|
>
|
||||||
|
picture_as_pdf
|
||||||
|
</Text>
|
||||||
|
<Text as="h2" $size="h3" $margin="none" $theme="primary">
|
||||||
|
{t('Export')}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Box
|
<Box
|
||||||
$margin={{ bottom: 'xl' }}
|
$margin={{ bottom: 'xl' }}
|
||||||
aria-label={t('Content modal to export the document')}
|
aria-label={t('Content modal to export the document')}
|
||||||
$gap="1rem"
|
$gap="1.5rem"
|
||||||
>
|
>
|
||||||
<Text $variation="600" $size="sm">
|
<Alert canClose={false} type={VariantType.INFO}>
|
||||||
{t(
|
<Text>
|
||||||
'Upload your docs to a Microsoft Word, Open Office or PDF document.',
|
{t(
|
||||||
)}
|
'Export your document, it will be inserted in the selected template.',
|
||||||
</Text>
|
)}
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
clearable={false}
|
clearable={false}
|
||||||
label={t('Template')}
|
label={t('Template')}
|
||||||
@@ -178,19 +187,22 @@ export const ModalPDF = ({ onClose, doc }: ModalPDFProps) => {
|
|||||||
setTemplateIdSelected(options.target.value as string)
|
setTemplateIdSelected(options.target.value as string)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<Select
|
|
||||||
clearable={false}
|
<RadioGroup>
|
||||||
fullWidth
|
<Radio
|
||||||
label={t('Format')}
|
label={t('PDF')}
|
||||||
options={[
|
value="pdf"
|
||||||
{ label: t('Word / Open Office'), value: DocDownloadFormat.DOCX },
|
name="format"
|
||||||
{ label: t('PDF'), value: DocDownloadFormat.PDF },
|
onChange={(evt) => setFormat(evt.target.value as 'pdf')}
|
||||||
]}
|
defaultChecked={true}
|
||||||
value={format}
|
/>
|
||||||
onChange={(options) =>
|
<Radio
|
||||||
setFormat(options.target.value as DocDownloadFormat)
|
label={t('Docx')}
|
||||||
}
|
value="docx"
|
||||||
/>
|
name="format"
|
||||||
|
onChange={(evt) => setFormat(evt.target.value as 'docx')}
|
||||||
|
/>
|
||||||
|
</RadioGroup>
|
||||||
|
|
||||||
{isPending && (
|
{isPending && (
|
||||||
<Box $align="center" $margin={{ top: 'big' }}>
|
<Box $align="center" $margin={{ top: 'big' }}>
|
||||||
|
|||||||
@@ -1,2 +1 @@
|
|||||||
export * from './DocHeader';
|
export * from './DocHeader';
|
||||||
export * from './DocVersionHeader';
|
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ const convertToImg = (html: string) => {
|
|||||||
const doc = parser.parseFromString(html, 'text/html');
|
const doc = parser.parseFromString(html, 'text/html');
|
||||||
const divs = doc.querySelectorAll('div[data-content-type="image"]');
|
const divs = doc.querySelectorAll('div[data-content-type="image"]');
|
||||||
|
|
||||||
// Loop through each div and replace it with an img
|
// Loop through each div and replace it with a img
|
||||||
divs.forEach((div) => {
|
divs.forEach((div) => {
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
export * from './useCreateDoc';
|
export * from './useCreateDoc';
|
||||||
export * from './useDeleteFavoriteDoc';
|
|
||||||
export * from './useDoc';
|
export * from './useDoc';
|
||||||
export * from './useDocOptions';
|
export * from './useDocOptions';
|
||||||
export * from './useDocs';
|
export * from './useDocs';
|
||||||
export * from './useCreateFavoriteDoc';
|
|
||||||
export * from './useUpdateDoc';
|
export * from './useUpdateDoc';
|
||||||
export * from './useUpdateDocLink';
|
export * from './useUpdateDocLink';
|
||||||
|
|||||||
-42
@@ -1,42 +0,0 @@
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
|
||||||
import { Doc } from '@/features/docs';
|
|
||||||
|
|
||||||
export type CreateFavoriteDocParams = Pick<Doc, 'id'>;
|
|
||||||
|
|
||||||
export const createFavoriteDoc = async ({ id }: CreateFavoriteDocParams) => {
|
|
||||||
const response = await fetchAPI(`documents/${id}/favorite/`, {
|
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new APIError(
|
|
||||||
'Failed to make the doc as favorite',
|
|
||||||
await errorCauses(response),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
interface CreateFavoriteDocProps {
|
|
||||||
onSuccess?: () => void;
|
|
||||||
listInvalidQueries?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCreateFavoriteDoc({
|
|
||||||
onSuccess,
|
|
||||||
listInvalidQueries,
|
|
||||||
}: CreateFavoriteDocProps) {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation<void, APIError, CreateFavoriteDocParams>({
|
|
||||||
mutationFn: createFavoriteDoc,
|
|
||||||
onSuccess: () => {
|
|
||||||
listInvalidQueries?.forEach((queryKey) => {
|
|
||||||
void queryClient.invalidateQueries({
|
|
||||||
queryKey: [queryKey],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
onSuccess?.();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
-42
@@ -1,42 +0,0 @@
|
|||||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
|
||||||
import { Doc } from '@/features/docs';
|
|
||||||
|
|
||||||
export type DeleteFavoriteDocParams = Pick<Doc, 'id'>;
|
|
||||||
|
|
||||||
export const deleteFavoriteDoc = async ({ id }: DeleteFavoriteDocParams) => {
|
|
||||||
const response = await fetchAPI(`documents/${id}/favorite/`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new APIError(
|
|
||||||
'Failed to remove the doc as favorite',
|
|
||||||
await errorCauses(response),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
interface DeleteFavoriteDocProps {
|
|
||||||
onSuccess?: () => void;
|
|
||||||
listInvalidQueries?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useDeleteFavoriteDoc({
|
|
||||||
onSuccess,
|
|
||||||
listInvalidQueries,
|
|
||||||
}: DeleteFavoriteDocProps) {
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
return useMutation<void, APIError, DeleteFavoriteDocParams>({
|
|
||||||
mutationFn: deleteFavoriteDoc,
|
|
||||||
onSuccess: () => {
|
|
||||||
listInvalidQueries?.forEach((queryKey) => {
|
|
||||||
void queryClient.invalidateQueries({
|
|
||||||
queryKey: [queryKey],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
onSuccess?.();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user