Compare commits

...

15 Commits

Author SHA1 Message Date
Henry
e48f16ade6 fix: migration order 2026-03-03 17:38:49 +00:00
Henry
eeae23a24c docs: add migrate service to self hosting guide (#425)
* docs: add migrate service to self hosting guide

* chore: remove build from readme compose
2026-02-28 23:08:59 +00:00
Henry
d81950b8bd fix: publish migrate image (#423) 2026-02-27 22:38:01 +00:00
hjball
e530f39360 chore: update translations 2026-02-27 13:16:47 +00:00
Nick Meinhold
bd25fb33f7 feat(api): add webhook delivery utility and card event integration (#392)
* feat(api): add webhook delivery utility and card event integration

Add the core webhook delivery logic and wire it into card mutations:

- Add sendWebhookToUrl() with HMAC-SHA256 signing, 10s timeout
- Add sendWebhooksForWorkspace() for fan-out delivery (fire-and-forget)
- Add createCardWebhookPayload() for building webhook payloads
- Fire webhooks on card create, update, move, and delete events
- Add unit tests for webhook utility functions

Depends on #391 (DB schema & repository).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(api): use correct boardId in webhook payloads and add rejection safety

- Fix bug where workspaceId was incorrectly passed as boardId in all
  webhook payloads — now uses board's publicId via boardPublicId
- Replace void sendWebhooksForWorkspace() with .catch() to prevent
  unhandled promise rejections if the DB query inside fails

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(api): add SSRF protection to webhook delivery

Block webhook URLs targeting internal networks:
- Require HTTPS (reject HTTP)
- Block localhost, 127.0.0.1, ::1, 0.0.0.0
- Block cloud metadata endpoints (169.254.169.254, metadata.google.internal)
- Block private IP ranges (10.x, 172.16-31.x, 192.168.x)
- Add tests for all blocked URL patterns

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(api): use WebhookEvent type from schema instead of duplicating

Replace the hardcoded WebhookEventType union with the canonical
WebhookEvent type from @kan/db/schema, addressing reviewer feedback
on PR #392.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(api): improve webhook delivery safety and validation

Cherry-pick delivery-related changes from b2cc9ac:
- Extract URL validation into reusable webhookUrlSchema zod validator
  for SSRF checks
- Wrap sendWebhooksForWorkspace in try/catch to prevent unhandled
  promise rejections
- Document SSRF risk mitigation on sendWebhookToUrl
- Add corresponding tests

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 13:15:35 +00:00
hjball
93f2816b37 chore: compile translations 2026-02-26 21:41:09 +00:00
hjball
2292b972c4 chore: update translations 2026-02-26 21:41:05 +00:00
Eliott Herbert-Byrnes
af36fb133a feat: archived boards (#386)
* init: schema migration, isArchived added to
board table. refactor: board repo for isArchived filtering

* init: archived, unarchived API procedures. refactor: all query

* fix: migration error

* feat: add tabbed navigation for boards view

* Implemented a Listbox for mobile and a tabbed navigation for desktop to switch between "Boards" and "Archived" views.
* Introduced state management for active tab selection.
* Updated UI components to reflect the new navigation structure.

* init: frontend/boards lists & tabs

* chore: fixed font styling and spacing

* init:boardDropdown / boardView.

* chore:added translations

* Remove .cursor plan file from repo

* fix:build erros

* revert: remove locales changes

* fix:reverted changes under locales, replaced the archive and unarchive endpoints. Reorder migrations

* fix:migration issue

* fix: update journal.json

---------

Co-authored-by: Henry <henry_ball@hotmail.co.uk>
2026-02-26 21:39:15 +00:00
hjball
9cb1fb5218 chore: update translations 2026-02-26 21:26:17 +00:00
Nick Meinhold
5f190b92de fix: allow invited users to sign up when registration is disabled (#418)
* fix: allow invited users to sign up when registration is disabled

Move sign-up restriction logic from better-auth's disableSignUp config
to the existing user.create.before database hook, which already checks
for pending invitations. The frontend signup and login pages now detect
invite flows (?next=/invite/...) and bypass the disabled UI accordingly.

Closes #411

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add regression tests for sign-up hook invite bypass

Verify that the user.create.before database hook correctly:
- allows sign-up when registration is not disabled
- blocks sign-up when disabled and no invitation exists
- allows sign-up when disabled but a pending invitation exists
- respects BETTER_AUTH_ALLOWED_DOMAINS in combination with invites

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add OIDC/social sign-up path coverage for invite bypass

Address review suggestion: add explicit tests verifying the
user.create.before hook handles OIDC/social sign-ups the same way as
email/password — invited users are allowed, uninvited users are blocked.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:24:58 +00:00
Nick Meinhold
48a1f39588 fix(api): add missing openapi meta to checklist.update (#417)
The checklist.update procedure was inheriting the default
protectedProcedure meta (GET /protected) instead of declaring
its own OpenAPI route, making it unreachable via the REST API.

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 21:12:02 +00:00
hjball
c7fce10fa7 chore: update translations 2026-02-26 21:05:45 +00:00
Matej
094e1d139d fix: make links clickable in comments (#376) (#415)
Move Link extension before Markdown in the TipTap extensions array
to ensure URL detection happens before markdown processing. Also
explicitly enable linkOnPaste for paste detection.

Fixes #376

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-26 21:04:31 +00:00
Henry
fae169c691 fix: ensure deps are installed during translate workflow 2026-02-23 23:11:52 +00:00
Henry
91bc319cf1 fix: prevent cross-workspace access on member and permission endpoints (#413) 2026-02-23 22:56:36 +00:00
50 changed files with 9107 additions and 456 deletions

View File

@@ -26,6 +26,7 @@ env:
REGISTRY: ghcr.io
# github.repository as <account>/<repo>
IMAGE_NAME: ${{ github.repository }}
MIGRATE_IMAGE_NAME: ${{ github.repository }}-migrate
jobs:
build:
@@ -85,6 +86,19 @@ jobs:
type=semver,pattern={{major}}
type=raw,value=latest,enable={{is_default_branch}}
- name: Extract Docker metadata (migrate)
id: meta-migrate
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.MIGRATE_IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable={{is_default_branch}}
# Extract version from git tag or ref
# Uses git describe to get latest tag + commit hash in SemVer format: 1.2.3+abc1234
- name: Extract version
@@ -138,6 +152,22 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push migrate Docker image
id: build-and-push-migrate
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
with:
context: .
file: apps/web/Dockerfile
target: migrate
push: ${{ github.event_name != 'pull_request' }}
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta-migrate.outputs.tags }}
labels: ${{ steps.meta-migrate.outputs.labels }}
build-args: |
APP_VERSION=${{ steps.version.outputs.version }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Sign the resulting Docker image digest except on PRs.
# This will only write to the public Rekor transparency log when the Docker
# repository is public to avoid leaking data. If you would like to publish
@@ -152,3 +182,10 @@ jobs:
# This step uses the identity token to provision an ephemeral certificate
# against the sigstore community Fulcio instance.
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
- name: Sign the published migrate Docker image
if: ${{ github.event_name != 'pull_request' }}
env:
TAGS: ${{ steps.meta-migrate.outputs.tags }}
DIGEST: ${{ steps.build-and-push-migrate.outputs.digest }}
run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}

View File

@@ -14,16 +14,24 @@ jobs:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 9
- name: Install dependencies
run: pnpm install --filter @kan/web...
- name: Extract translations
working-directory: apps/web
run: npx --yes @lingui/cli extract
run: pnpm lingui:extract
- name: Lingo.dev
uses: lingodotdev/lingo.dev@main
with:
@@ -33,11 +41,11 @@ jobs:
commit-author-name: "${{ github.actor }}"
commit-author-email: "${{ github.actor }}@users.noreply.github.com"
parallel: true
- name: Compile translations
working-directory: apps/web
run: npx --yes @lingui/cli compile --typescript
run: pnpm lingui:compile
- name: Commit compiled translations
run: |
git config --local user.email "${{ github.actor }}@users.noreply.github.com"
@@ -46,4 +54,3 @@ jobs:
git add apps/web/src/locales/*/messages.ts
git diff --staged --quiet || (git commit -m "chore: compile translations" && git push)
fi

View File

@@ -57,39 +57,61 @@ The easiest way to deploy Kan is through Railway. We've partnered with Railway t
### Docker Compose
Alternatively, you can self-host Kan with Docker Compose. This will set up everything for you including your postgres database.
Alternatively, you can self-host Kan with Docker Compose. This will set up everything for you including your postgres database and automatically run migrations.
1. Create a new file called `docker-compose.yml` and paste the following configuration:
1. Create a `.env` file with your environment variables (see [Environment Variables](#environment-variables-) section below)
2. Use the provided `docker-compose.yml` file or create your own with the following configuration:
```yaml
services:
migrate:
image: ghcr.io/kanbn/kan-migrate:latest
container_name: kan-migrate
networks:
- kan-network
environment:
- POSTGRES_URL=${POSTGRES_URL}
depends_on:
postgres:
condition: service_healthy
restart: "no"
web:
image: ghcr.io/kanbn/kan:latest
container_name: kan-web
ports:
- "3000:3000"
- "${WEB_PORT:-3000}:3000"
networks:
- kan-network
env_file:
- .env
environment:
NEXT_PUBLIC_BASE_URL: http://localhost:3000
BETTER_AUTH_SECRET: your_auth_secret
POSTGRES_URL: postgresql://kan:your_postgres_password@postgres:5432/kan_db
NEXT_PUBLIC_ALLOW_CREDENTIALS: true
- NEXT_PUBLIC_BASE_URL=${NEXT_PUBLIC_BASE_URL}
- BETTER_AUTH_SECRET=${BETTER_AUTH_SECRET}
- POSTGRES_URL=${POSTGRES_URL}
- NEXT_PUBLIC_ALLOW_CREDENTIALS=true
depends_on:
- postgres
migrate:
condition: service_completed_successfully
restart: unless-stopped
postgres:
image: postgres:15
container_name: kan-db
environment:
POSTGRES_DB: kan_db
POSTGRES_USER: kan
POSTGRES_PASSWORD: your_postgres_password
- POSTGRES_DB=kan_db
- POSTGRES_USER=kan
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
ports:
- 5432:5432
volumes:
- kan_postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U kan -d kan_db"]
interval: 5s
timeout: 5s
retries: 10
restart: unless-stopped
networks:
- kan-network
@@ -101,23 +123,23 @@ volumes:
kan_postgres_data:
```
2. Start the containers in detached mode:
3. Start the containers in detached mode:
```bash
docker compose up -d
```
3. Access Kan at http://localhost:3000
The `migrate` service will automatically run database migrations before the web service starts. The application will be available at http://localhost:3000 (or the port specified in `WEB_PORT`).
The application will be running in the background. You can manage the containers using these commands:
**Managing containers:**
- To stop the containers: `docker compose down`
- To view logs: `docker compose logs -f`
- To view logs for a specific service: `docker compose logs -f web` or `docker compose logs -f migrate`
- To restart the containers: `docker compose restart`
- To rebuild after code changes: `docker compose up -d --build`
For the complete Docker Compose configuration, see [docker-compose.yml](./docker-compose.yml) in the repository.
> **Note**: The Docker Compose configuration shown above is a minimal example. For a complete setup with all features (email, OAuth, file uploads, etc.), you'll need to create a `.env` file with the required environment variables. See the Environment Variables section below for the full list of available options.
For the complete Docker Compose configuration with all optional features, see [docker-compose.yml](./docker-compose.yml) in the repository.
## Local Development 🧑‍💻

View File

@@ -81,6 +81,9 @@ checksums:
7L01XJ/message: c46571856723b03262fd33f511116298
7L01XJ/origin/0/0: feacdaf2791aaddec128f463ffa2e807
7L01XJ/translation: c46571856723b03262fd33f511116298
F6pfE9/translation: 3e1ec025c4a50830bbb9ad57a176630a
F6pfE9/message: 3e1ec025c4a50830bbb9ad57a176630a
F6pfE9/origin/0/0: 4b563046ba98abb3777fb1c3560866aa
XJOV1Y/message: 1948763de8e531483a798b68195e297e
XJOV1Y/origin/0/0: 15fd9c05fc09ccbbb54aadc4aa5e5501
XJOV1Y/origin/1/0: 9c8b544f5db130cd7c5eda9064ba3957
@@ -245,6 +248,12 @@ checksums:
yb%2Ffjw/message: 2f5426445800ecd5a3102aac940d459c
yb%2Ffjw/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
yb%2Ffjw/translation: 2f5426445800ecd5a3102aac940d459c
aKJHG%2B/translation: 63de765f7fd7bc5238a3e36532306b6b
aKJHG%2B/message: 63de765f7fd7bc5238a3e36532306b6b
aKJHG%2B/origin/0/0: 0ad2af981579a62b084b3d41462c5546
TdfEV7/translation: cf5127ecfd7e43a35466a1ba5fe16450
TdfEV7/message: cf5127ecfd7e43a35466a1ba5fe16450
TdfEV7/origin/0/0: 4b563046ba98abb3777fb1c3560866aa
lo8xBK/message: f9b67026c5ef86ac4d5944380d571961
lo8xBK/placeholders/entityLabel/0: 5745fa1af326b9ac2b140cbbef90cdeb
lo8xBK/origin/0/0: 68450df287fb46ea629e818732c1291a
@@ -332,6 +341,9 @@ checksums:
YhvWXb/origin/0/0: ff4a160eca5f8da4f075fd1dd6061fa6
YhvWXb/origin/1/0: dbad0c8d7863cb41f5495264f82e7081
YhvWXb/translation: 336a8c70a91adc49c15266167ee84cc0
i0ZMQl/translation: f739058266a2cb6a3d7cca14de09c69a
i0ZMQl/message: f739058266a2cb6a3d7cca14de09c69a
i0ZMQl/origin/0/0: 0ad2af981579a62b084b3d41462c5546
wewm3j/message: 985c0177744ec56d8dac84f66ec284ca
wewm3j/origin/0/0: bcd95e286f12800a3e7ee0a71feb3ca5
wewm3j/translation: 985c0177744ec56d8dac84f66ec284ca
@@ -344,6 +356,9 @@ checksums:
XNxYxq/message: b0e6a79e1751b734e1060a1034a893d6
XNxYxq/origin/0/0: c0b59968b2b234167c488a05cc38710b
XNxYxq/translation: b0e6a79e1751b734e1060a1034a893d6
tbNcu4/translation: 3817a16b436504bb8d96ca5ee62078cc
tbNcu4/message: 3817a16b436504bb8d96ca5ee62078cc
tbNcu4/origin/0/0: 0ad2af981579a62b084b3d41462c5546
Xid3K6/message: ba018946ffdf85e55d701a9c27595b4d
Xid3K6/origin/0/0: 2e937f7560901c3de71a592b4fd80f34
Xid3K6/translation: ba018946ffdf85e55d701a9c27595b4d
@@ -368,6 +383,9 @@ checksums:
8TveY%2B/origin/0/0: 94d3e20c29bb517dd394711d7524e478
8TveY%2B/origin/1/0: c0b59968b2b234167c488a05cc38710b
8TveY%2B/translation: 8fff51c5872e051beb9b718336665e80
0RE1AJ/translation: c9a1699f5037acf87aaa6515d0f16910
0RE1AJ/message: c9a1699f5037acf87aaa6515d0f16910
0RE1AJ/origin/0/0: f78c0a7870158f5dab5216bcd6092e49
ZDo4HN/message: 736332f2e4488609e42d2be8547d296e
ZDo4HN/origin/0/0: eb63312c63f6c8d2a5b6520c89012123
ZDo4HN/translation: 736332f2e4488609e42d2be8547d296e
@@ -1500,6 +1518,9 @@ checksums:
"%2FglL9Q/placeholders/0/0": 07bd981d42ffe27ee81d1dffa7dc0d43
"%2FglL9Q/origin/0/0": f78c0a7870158f5dab5216bcd6092e49
"%2FglL9Q/translation": a7a95e1dfdb4396da9e2be0e27f42323
tlhgDC/translation: b96a6f8fa3267ec01e68e61656382b2e
tlhgDC/message: b96a6f8fa3267ec01e68e61656382b2e
tlhgDC/origin/0/0: f78c0a7870158f5dab5216bcd6092e49
Eg99Sc/message: f718c896810da3612202887f9a4374fa
Eg99Sc/origin/0/0: d96af8a06c2e38a72238bcd16e426859
Eg99Sc/translation: f718c896810da3612202887f9a4374fa
@@ -2052,6 +2073,12 @@ checksums:
nhMhRr/message: 07edd8c50685a52c0969d711df26d768
nhMhRr/origin/0/0: 1fd8ccf586705354bcae6e1186108fc7
nhMhRr/translation: 07edd8c50685a52c0969d711df26d768
NcFrgC/translation: bd15a7b3d8b4bce18cd42682ea1ec164
NcFrgC/message: bd15a7b3d8b4bce18cd42682ea1ec164
NcFrgC/origin/0/0: 0ad2af981579a62b084b3d41462c5546
C6gv54/translation: 1573c77059bc92df17e661befda8d9b7
C6gv54/message: 1573c77059bc92df17e661befda8d9b7
C6gv54/origin/0/0: 0ad2af981579a62b084b3d41462c5546
jlB2RY/message: 67a76bf346ab4b48563269e9d941a64a
jlB2RY/origin/0/0: 0627e0040ca4939c9a57349e98c51797
jlB2RY/translation: 67a76bf346ab4b48563269e9d941a64a
@@ -2249,6 +2276,9 @@ checksums:
TFv5tM/message: 4f2240aeff6f7275feb33ca3f608e48c
TFv5tM/origin/0/0: 7493592314bc3b96ad96127a949ba1ba
TFv5tM/translation: 4f2240aeff6f7275feb33ca3f608e48c
It4%2FFS/translation: ae1527c2765592f13980acb66997e570
It4%2FFS/message: ae1527c2765592f13980acb66997e570
It4%2FFS/origin/0/0: 0ad2af981579a62b084b3d41462c5546
E8zYtd/message: b683cc07092348c1e75dba40b1262b85
E8zYtd/placeholders/0/0: 182720128698512d43d1bf43824cd7f8
E8zYtd/origin/0/0: 1d6b5f88cdad160a67842f6b65387448

View File

@@ -463,13 +463,6 @@ export default function Editor({
StarterKit.configure({
heading: disableHeadings ? false : undefined,
}),
Markdown,
Placeholder.configure({
placeholder: readOnly
? ""
: placeholder ??
t`Add description... (type '/' to open commands or '@' to mention)`,
}),
Link.configure({
openOnClick: true,
HTMLAttributes: {
@@ -479,6 +472,14 @@ export default function Editor({
},
validate: (href) => /^https?:\/\//.test(href),
autolink: true,
linkOnPaste: true,
}),
Markdown,
Placeholder.configure({
placeholder: readOnly
? ""
: placeholder ??
t`Add description... (type '/' to open commands or '@' to mention)`,
}),
SlashCommands.configure({
commandItems: getCommandItems(disableHeadings),

View File

@@ -19,6 +19,7 @@ interface UsePermissionsResult {
canCreateBoard: boolean;
canEditBoard: boolean;
canDeleteBoard: boolean;
canArchiveBoard: boolean;
canViewComment: boolean;
canCreateComment: boolean;
canEditComment: boolean;
@@ -33,7 +34,7 @@ interface UsePermissionsResult {
export function usePermissions(): UsePermissionsResult {
// Check if WorkspaceProvider is available (for public board views, it may not be)
const workspaceContext = useContext(WorkspaceContext);
// If WorkspaceProvider is not available, return safe defaults
if (!workspaceContext) {
const emptyPermissions: UsePermissionsResult = {
@@ -51,6 +52,7 @@ export function usePermissions(): UsePermissionsResult {
canCreateBoard: false,
canEditBoard: false,
canDeleteBoard: false,
canArchiveBoard: false,
canViewComment: false,
canCreateComment: false,
canEditComment: false,
@@ -95,6 +97,7 @@ export function usePermissions(): UsePermissionsResult {
canCreateBoard: hasPermission("board:create"),
canEditBoard: hasPermission("board:edit"),
canDeleteBoard: hasPermission("board:delete"),
canArchiveBoard: hasPermission("board:edit"),
canViewComment: hasPermission("comment:view"),
canCreateComment: hasPermission("comment:create"),
canEditComment: hasPermission("comment:edit"),

View File

@@ -19,7 +19,7 @@
"0": ["isTemplate ? \"Templates\" : \"Boards\""]
},
"comments": [],
"origin": [["src/views/boards/index.tsx", 40]],
"origin": [["src/views/boards/index.tsx", 53]],
"translation": "{0}"
},
"JArWcF": {
@@ -33,7 +33,7 @@
},
"comments": [],
"origin": [
["src/views/boards/index.tsx", 35],
["src/views/boards/index.tsx", 48],
["src/views/card/index.tsx", 309]
],
"translation": "{0} | {1}"
@@ -44,7 +44,7 @@
"0": ["boardName ?? \"Board\""]
},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 54]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 56]],
"translation": "{0} wurde zu deinen Favoriten hinzugefügt."
},
"bDI6VI": {
@@ -53,7 +53,7 @@
"0": ["boardName ?? \"Board\""]
},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 55]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 57]],
"translation": "{0} wurde aus deinen Favoriten entfernt."
},
"CJukzS": {
@@ -185,6 +185,13 @@
"origin": [["src/providers/keyboard-shortcuts.tsx", 56]],
"translation": "Aktionen"
},
"F6pfE9": {
"message": "Active",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 26]],
"translation": "Aktiv"
},
"XJOV1Y": {
"message": "Activity",
"placeholders": {},
@@ -246,7 +253,7 @@
"message": "Add description... (type '/' to open commands or '@' to mention)",
"placeholders": {},
"comments": [],
"origin": [["src/components/Editor.tsx", 471]],
"origin": [["src/components/Editor.tsx", 482]],
"translation": "Beschreibung hinzufügen... (/' für Befehle oder @' zum Erwähnen eingeben)"
},
"abUZlY": {
@@ -280,7 +287,7 @@
"message": "Add to favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 93]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 122]],
"translation": "Zu Favoriten hinzufügen"
},
"bmXKoX": {
@@ -368,7 +375,7 @@
"message": "Added to favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 51]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 53]],
"translation": "Zu Favoriten hinzugefügt"
},
"14Xi3Z": {
@@ -482,7 +489,7 @@
"message": "Already have an account? <0><1>Sign in</1></0>",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 88]],
"origin": [["src/views/auth/signup/index.tsx", 90]],
"translation": "Haben Sie bereits ein Konto? <0><1>Anmelden</1></0>"
},
"WmPhYW": {
@@ -574,6 +581,20 @@
"origin": [["src/views/boards/components/TemplateBoards.tsx", 46]],
"translation": "Genehmigung"
},
"aKJHG+": {
"message": "Archive board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 111]],
"translation": "Board archivieren"
},
"TdfEV7": {
"message": "Archived",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 27]],
"translation": "Archiviert"
},
"lo8xBK": {
"message": "Are you sure want to remove {entityLabel}?",
"placeholders": {
@@ -785,6 +806,13 @@
],
"translation": "Board-Analyse"
},
"i0ZMQl": {
"message": "Board archived",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 44]],
"translation": "Board archiviert"
},
"wewm3j": {
"message": "Board name cannot exceed 100 characters",
"placeholders": {},
@@ -815,6 +843,13 @@
],
"translation": "Board-Vorlagen"
},
"tbNcu4": {
"message": "Board unarchived",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 44]],
"translation": "Board wiederhergestellt"
},
"Xid3K6": {
"message": "Board URL can only contain letters, numbers, and hyphens",
"placeholders": {},
@@ -873,6 +908,13 @@
],
"translation": "Boards"
},
"0RE1AJ": {
"message": "Boards you archive will appear here.",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 66]],
"translation": "Boards, die du archivierst, werden hier angezeigt."
},
"ZDo4HN": {
"message": "Brainstorming",
"placeholders": {},
@@ -1237,7 +1279,7 @@
"comments": [],
"origin": [
["src/views/auth/login/index.tsx", 43],
["src/views/auth/signup/index.tsx", 69]
["src/views/auth/signup/index.tsx", 71]
],
"translation": "Überprüfen Sie Ihren Posteingang"
},
@@ -1305,7 +1347,7 @@
"comments": [],
"origin": [
["src/views/auth/login/index.tsx", 48],
["src/views/auth/signup/index.tsx", 74]
["src/views/auth/signup/index.tsx", 76]
],
"translation": "Klicken Sie auf den Link, den wir an {magicLinkRecipient} gesendet haben, um sich anzumelden."
},
@@ -1612,8 +1654,8 @@
},
"comments": [],
"origin": [
["src/views/boards/components/BoardsList.tsx", 79],
["src/views/boards/index.tsx", 28]
["src/views/boards/components/BoardsList.tsx", 80],
["src/views/boards/index.tsx", 41]
],
"translation": "Neue {0} erstellen"
},
@@ -1835,7 +1877,7 @@
"message": "Delete board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 104]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 133]],
"translation": "Board löschen"
},
"nabda1": {
@@ -1863,7 +1905,7 @@
"message": "Delete template",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 104]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 133]],
"translation": "Vorlage löschen"
},
"kYu0eF": {
@@ -2079,7 +2121,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/BoardDropdown.tsx", 84],
["src/views/board/components/BoardDropdown.tsx", 102],
["src/views/board/components/UpdateBoardSlugForm.tsx", 116]
],
"translation": "Board-URL bearbeiten"
@@ -2622,7 +2664,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/auth/signup/index.tsx", 69],
["src/views/auth/signup/index.tsx", 71],
["src/views/home/components/Cta.tsx", 61],
["src/views/home/components/Header.tsx", 106],
["src/views/pricing/components/PricingTiers.tsx", 29],
@@ -2647,7 +2689,7 @@
"0": ["isTemplate ? \"template\" : \"board\""]
},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 65]],
"origin": [["src/views/boards/components/BoardsList.tsx", 66]],
"translation": "Legen Sie los, indem Sie ein neues {0} erstellen"
},
"zC8Whw": {
@@ -2863,7 +2905,7 @@
"message": "Import",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 60]],
"origin": [["src/views/boards/index.tsx", 73]],
"translation": "Importieren"
},
"2MPcep": {
@@ -3299,7 +3341,7 @@
"message": "Make template",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 73]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 91]],
"translation": "Vorlage erstellen"
},
"IDvohQ": {
@@ -3432,7 +3474,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/boards/index.tsx", 82],
["src/views/boards/index.tsx", 95],
["src/views/home/components/Features.tsx", 73]
],
"translation": "Neu"
@@ -3550,9 +3592,16 @@
"0": ["isTemplate ? \"templates\" : \"boards\""]
},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 62]],
"origin": [["src/views/boards/components/BoardsList.tsx", 63]],
"translation": "Keine {0}"
},
"tlhgDC": {
"message": "No archived boards",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 63]],
"translation": "Keine archivierten Boards"
},
"Eg99Sc": {
"message": "No authentication methods are currently available",
"placeholders": {},
@@ -3963,7 +4012,7 @@
["src/components/DeleteLabelConfirmation.tsx", 26],
["src/components/FeedbackModal.tsx", 41],
["src/components/NewWorkspaceForm.tsx", 158],
["src/views/board/components/BoardDropdown.tsx", 63],
["src/views/board/components/BoardDropdown.tsx", 65],
["src/views/board/components/NewCardForm.tsx", 184],
["src/views/board/components/NewListForm.tsx", 79],
["src/views/board/components/NewTemplateForm.tsx", 61],
@@ -4178,7 +4227,7 @@
"message": "Remove from favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 92]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 121]],
"translation": "Aus Favoriten entfernen"
},
"99VIgC": {
@@ -4232,7 +4281,7 @@
"message": "Removed from favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 52]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 54]],
"translation": "Aus Favoriten entfernt"
},
"YVT40D": {
@@ -4570,8 +4619,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/auth/signup/index.tsx", 34],
["src/views/auth/signup/index.tsx", 59]
["src/views/auth/signup/index.tsx", 36],
["src/views/auth/signup/index.tsx", 61]
],
"translation": "Registrieren | kan.bn"
},
@@ -4579,14 +4628,14 @@
"message": "Sign up disabled",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 44]],
"origin": [["src/views/auth/signup/index.tsx", 46]],
"translation": "Registrierung deaktiviert"
},
"s6iE/c": {
"message": "Sign up is currently disabled. Please try again later.",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 47]],
"origin": [["src/views/auth/signup/index.tsx", 49]],
"translation": "Die Registrierung ist derzeit deaktiviert. Bitte versuchen Sie es später erneut."
},
"6FDocz": {
@@ -4809,6 +4858,20 @@
"origin": [["src/components/FeedbackModal.tsx", 34]],
"translation": "Vielen Dank für Ihr Feedback!"
},
"NcFrgC": {
"message": "The board has been archived.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 46]],
"translation": "Das Board wurde archiviert."
},
"C6gv54": {
"message": "The board has been unarchived.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 47]],
"translation": "Das Board wurde wiederhergestellt."
},
"jlB2RY": {
"message": "The current password you entered is incorrect.",
"placeholders": {},
@@ -5196,7 +5259,7 @@
"message": "Unable to update board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 62]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 64]],
"translation": "Board konnte nicht aktualisiert werden"
},
"XpcjLO": {
@@ -5292,6 +5355,13 @@
"origin": [["src/views/members/index.tsx", 65]],
"translation": "Rolle konnte nicht aktualisiert werden"
},
"It4/FS": {
"message": "Unarchive board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 111]],
"translation": "Board wiederherstellen"
},
"E8zYtd": {
"message": "unassigned <0>{0}</0> from the card",
"placeholders": {
@@ -5671,7 +5741,7 @@
"origin": [
["src/components/SettingsLayout.tsx", 46],
["src/views/board/index.tsx", 400],
["src/views/boards/index.tsx", 35],
["src/views/boards/index.tsx", 48],
["src/views/members/index.tsx", 258],
["src/views/public/board/index.tsx", 125],
["src/views/public/boards/index.tsx", 65]
@@ -5883,9 +5953,9 @@
["src/views/board/components/VisibilityButton.tsx", 72],
["src/views/board/index.tsx", 474],
["src/views/board/index.tsx", 532],
["src/views/boards/components/BoardsList.tsx", 70],
["src/views/boards/index.tsx", 46],
["src/views/boards/index.tsx", 67]
["src/views/boards/components/BoardsList.tsx", 71],
["src/views/boards/index.tsx", 59],
["src/views/boards/index.tsx", 80]
],
"translation": "Sie haben keine Berechtigung"
},

File diff suppressed because one or more lines are too long

View File

@@ -34,7 +34,7 @@
"origin": [
[
"src/views/boards/index.tsx",
40
53
]
],
"translation": "{0}"
@@ -55,7 +55,7 @@
"origin": [
[
"src/views/boards/index.tsx",
35
48
],
[
"src/views/card/index.tsx",
@@ -75,7 +75,7 @@
"origin": [
[
"src/views/board/components/BoardDropdown.tsx",
54
56
]
],
"translation": "{0} has been added to your favorites."
@@ -91,7 +91,7 @@
"origin": [
[
"src/views/board/components/BoardDropdown.tsx",
55
57
]
],
"translation": "{0} has been removed from your favorites."
@@ -312,6 +312,18 @@
],
"translation": "Actions"
},
"F6pfE9": {
"message": "Active",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/boards/index.tsx",
26
]
],
"translation": "Active"
},
"XJOV1Y": {
"message": "Activity",
"placeholders": {},
@@ -411,7 +423,7 @@
"origin": [
[
"src/components/Editor.tsx",
471
482
]
],
"translation": "Add description... (type '/' to open commands or '@' to mention)"
@@ -467,7 +479,7 @@
"origin": [
[
"src/views/board/components/BoardDropdown.tsx",
93
122
]
],
"translation": "Add to favorites"
@@ -622,7 +634,7 @@
"origin": [
[
"src/views/board/components/BoardDropdown.tsx",
51
53
]
],
"translation": "Added to favorites"
@@ -813,7 +825,7 @@
"origin": [
[
"src/views/auth/signup/index.tsx",
88
90
]
],
"translation": "Already have an account? <0><1>Sign in</1></0>"
@@ -966,6 +978,30 @@
],
"translation": "Approval"
},
"aKJHG+": {
"message": "Archive board",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/BoardDropdown.tsx",
111
]
],
"translation": "Archive board"
},
"TdfEV7": {
"message": "Archived",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/boards/index.tsx",
27
]
],
"translation": "Archived"
},
"lo8xBK": {
"message": "Are you sure want to remove {entityLabel}?",
"placeholders": {
@@ -1314,6 +1350,18 @@
],
"translation": "Board analytics"
},
"i0ZMQl": {
"message": "Board archived",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/BoardDropdown.tsx",
44
]
],
"translation": "Board archived"
},
"wewm3j": {
"message": "Board name cannot exceed 100 characters",
"placeholders": {},
@@ -1362,6 +1410,18 @@
],
"translation": "Board templates"
},
"tbNcu4": {
"message": "Board unarchived",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/BoardDropdown.tsx",
44
]
],
"translation": "Board unarchived"
},
"Xid3K6": {
"message": "Board URL can only contain letters, numbers, and hyphens",
"placeholders": {},
@@ -1458,6 +1518,18 @@
],
"translation": "Boards"
},
"0RE1AJ": {
"message": "Boards you archive will appear here.",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/boards/components/BoardsList.tsx",
66
]
],
"translation": "Boards you archive will appear here."
},
"ZDo4HN": {
"message": "Brainstorming",
"placeholders": {},
@@ -2077,7 +2149,7 @@
],
[
"src/views/auth/signup/index.tsx",
69
71
]
],
"translation": "Check your inbox"
@@ -2177,7 +2249,7 @@
],
[
"src/views/auth/signup/index.tsx",
74
76
]
],
"translation": "Click on the link we've sent to {magicLinkRecipient} to sign in."
@@ -2693,11 +2765,11 @@
"origin": [
[
"src/views/boards/components/BoardsList.tsx",
79
80
],
[
"src/views/boards/index.tsx",
28
41
]
],
"translation": "Create new {0}"
@@ -3077,7 +3149,7 @@
"origin": [
[
"src/views/board/components/BoardDropdown.tsx",
104
133
]
],
"translation": "Delete board"
@@ -3125,7 +3197,7 @@
"origin": [
[
"src/views/board/components/BoardDropdown.tsx",
104
133
]
],
"translation": "Delete template"
@@ -3513,7 +3585,7 @@
"origin": [
[
"src/views/board/components/BoardDropdown.tsx",
84
102
],
[
"src/views/board/components/UpdateBoardSlugForm.tsx",
@@ -4409,7 +4481,7 @@
"origin": [
[
"src/views/auth/signup/index.tsx",
69
71
],
[
"src/views/home/components/Cta.tsx",
@@ -4461,7 +4533,7 @@
"origin": [
[
"src/views/boards/components/BoardsList.tsx",
65
66
]
],
"translation": "Get started by creating a new {0}"
@@ -4821,7 +4893,7 @@
"origin": [
[
"src/views/boards/index.tsx",
60
73
]
],
"translation": "Import"
@@ -5557,7 +5629,7 @@
"origin": [
[
"src/views/board/components/BoardDropdown.tsx",
73
91
]
],
"translation": "Make template"
@@ -5792,7 +5864,7 @@
"origin": [
[
"src/views/boards/index.tsx",
82
95
],
[
"src/views/home/components/Features.tsx",
@@ -5988,11 +6060,23 @@
"origin": [
[
"src/views/boards/components/BoardsList.tsx",
62
63
]
],
"translation": "No {0}"
},
"tlhgDC": {
"message": "No archived boards",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/boards/components/BoardsList.tsx",
63
]
],
"translation": "No archived boards"
},
"Eg99Sc": {
"message": "No authentication methods are currently available",
"placeholders": {},
@@ -6672,7 +6756,7 @@
],
[
"src/views/board/components/BoardDropdown.tsx",
63
65
],
[
"src/views/board/components/NewCardForm.tsx",
@@ -7144,7 +7228,7 @@
"origin": [
[
"src/views/board/components/BoardDropdown.tsx",
92
121
]
],
"translation": "Remove from favorites"
@@ -7239,7 +7323,7 @@
"origin": [
[
"src/views/board/components/BoardDropdown.tsx",
52
54
]
],
"translation": "Removed from favorites"
@@ -7807,11 +7891,11 @@
"origin": [
[
"src/views/auth/signup/index.tsx",
34
36
],
[
"src/views/auth/signup/index.tsx",
59
61
]
],
"translation": "Sign up | kan.bn"
@@ -7823,7 +7907,7 @@
"origin": [
[
"src/views/auth/signup/index.tsx",
44
46
]
],
"translation": "Sign up disabled"
@@ -7835,7 +7919,7 @@
"origin": [
[
"src/views/auth/signup/index.tsx",
47
49
]
],
"translation": "Sign up is currently disabled. Please try again later."
@@ -8200,6 +8284,30 @@
],
"translation": "Thank you for your feedback!"
},
"NcFrgC": {
"message": "The board has been archived.",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/BoardDropdown.tsx",
46
]
],
"translation": "The board has been archived."
},
"C6gv54": {
"message": "The board has been unarchived.",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/BoardDropdown.tsx",
47
]
],
"translation": "The board has been unarchived."
},
"jlB2RY": {
"message": "The current password you entered is incorrect.",
"placeholders": {},
@@ -8827,7 +8935,7 @@
"origin": [
[
"src/views/board/components/BoardDropdown.tsx",
62
64
]
],
"translation": "Unable to update board"
@@ -8988,6 +9096,18 @@
],
"translation": "Unable to update role"
},
"It4/FS": {
"message": "Unarchive board",
"placeholders": {},
"comments": [],
"origin": [
[
"src/views/board/components/BoardDropdown.tsx",
111
]
],
"translation": "Unarchive board"
},
"E8zYtd": {
"message": "unassigned <0>{0}</0> from the card",
"placeholders": {
@@ -9659,7 +9779,7 @@
],
[
"src/views/boards/index.tsx",
35
48
],
[
"src/views/members/index.tsx",
@@ -10011,15 +10131,15 @@
],
[
"src/views/boards/components/BoardsList.tsx",
70
71
],
[
"src/views/boards/index.tsx",
46
59
],
[
"src/views/boards/index.tsx",
67
80
]
],
"translation": "You don't have permission"

File diff suppressed because one or more lines are too long

View File

@@ -19,7 +19,7 @@
"0": ["isTemplate ? \"Templates\" : \"Boards\""]
},
"comments": [],
"origin": [["src/views/boards/index.tsx", 40]],
"origin": [["src/views/boards/index.tsx", 53]],
"translation": "{0}"
},
"JArWcF": {
@@ -33,7 +33,7 @@
},
"comments": [],
"origin": [
["src/views/boards/index.tsx", 35],
["src/views/boards/index.tsx", 48],
["src/views/card/index.tsx", 309]
],
"translation": "{0} | {1}"
@@ -44,7 +44,7 @@
"0": ["boardName ?? \"Board\""]
},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 54]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 56]],
"translation": "{0} se ha añadido a tus favoritos."
},
"bDI6VI": {
@@ -53,7 +53,7 @@
"0": ["boardName ?? \"Board\""]
},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 55]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 57]],
"translation": "{0} se ha eliminado de tus favoritos."
},
"CJukzS": {
@@ -185,6 +185,13 @@
"origin": [["src/providers/keyboard-shortcuts.tsx", 56]],
"translation": "Acciones"
},
"F6pfE9": {
"message": "Active",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 26]],
"translation": "Activo"
},
"XJOV1Y": {
"message": "Activity",
"placeholders": {},
@@ -246,7 +253,7 @@
"message": "Add description... (type '/' to open commands or '@' to mention)",
"placeholders": {},
"comments": [],
"origin": [["src/components/Editor.tsx", 471]],
"origin": [["src/components/Editor.tsx", 482]],
"translation": "Añadir descripción... (escribe '/' para abrir comandos o '@' para mencionar)"
},
"abUZlY": {
@@ -280,7 +287,7 @@
"message": "Add to favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 93]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 122]],
"translation": "Añadir a favoritos"
},
"bmXKoX": {
@@ -368,7 +375,7 @@
"message": "Added to favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 51]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 53]],
"translation": "Añadido a favoritos"
},
"14Xi3Z": {
@@ -482,7 +489,7 @@
"message": "Already have an account? <0><1>Sign in</1></0>",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 88]],
"origin": [["src/views/auth/signup/index.tsx", 90]],
"translation": "¿Ya tienes una cuenta? <0><1>Inicia sesión</1></0>"
},
"WmPhYW": {
@@ -574,6 +581,20 @@
"origin": [["src/views/boards/components/TemplateBoards.tsx", 46]],
"translation": "Aprobación"
},
"aKJHG+": {
"message": "Archive board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 111]],
"translation": "Archivar tablero"
},
"TdfEV7": {
"message": "Archived",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 27]],
"translation": "Archivado"
},
"lo8xBK": {
"message": "Are you sure want to remove {entityLabel}?",
"placeholders": {
@@ -785,6 +806,13 @@
],
"translation": "Analíticas del tablero"
},
"i0ZMQl": {
"message": "Board archived",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 44]],
"translation": "Tablero archivado"
},
"wewm3j": {
"message": "Board name cannot exceed 100 characters",
"placeholders": {},
@@ -815,6 +843,13 @@
],
"translation": "Plantillas de tablero"
},
"tbNcu4": {
"message": "Board unarchived",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 44]],
"translation": "Tablero desarchivado"
},
"Xid3K6": {
"message": "Board URL can only contain letters, numbers, and hyphens",
"placeholders": {},
@@ -873,6 +908,13 @@
],
"translation": "Tableros"
},
"0RE1AJ": {
"message": "Boards you archive will appear here.",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 66]],
"translation": "Los tableros que archives aparecerán aquí."
},
"ZDo4HN": {
"message": "Brainstorming",
"placeholders": {},
@@ -1237,7 +1279,7 @@
"comments": [],
"origin": [
["src/views/auth/login/index.tsx", 43],
["src/views/auth/signup/index.tsx", 69]
["src/views/auth/signup/index.tsx", 71]
],
"translation": "Revisa tu bandeja de entrada"
},
@@ -1305,7 +1347,7 @@
"comments": [],
"origin": [
["src/views/auth/login/index.tsx", 48],
["src/views/auth/signup/index.tsx", 74]
["src/views/auth/signup/index.tsx", 76]
],
"translation": "Haz clic en el enlace que hemos enviado a {magicLinkRecipient} para iniciar sesión."
},
@@ -1612,8 +1654,8 @@
},
"comments": [],
"origin": [
["src/views/boards/components/BoardsList.tsx", 79],
["src/views/boards/index.tsx", 28]
["src/views/boards/components/BoardsList.tsx", 80],
["src/views/boards/index.tsx", 41]
],
"translation": "Crear nuevo {0}"
},
@@ -1835,7 +1877,7 @@
"message": "Delete board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 104]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 133]],
"translation": "Eliminar tablero"
},
"nabda1": {
@@ -1863,7 +1905,7 @@
"message": "Delete template",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 104]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 133]],
"translation": "Eliminar plantilla"
},
"kYu0eF": {
@@ -2079,7 +2121,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/BoardDropdown.tsx", 84],
["src/views/board/components/BoardDropdown.tsx", 102],
["src/views/board/components/UpdateBoardSlugForm.tsx", 116]
],
"translation": "Editar URL del tablero"
@@ -2622,7 +2664,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/auth/signup/index.tsx", 69],
["src/views/auth/signup/index.tsx", 71],
["src/views/home/components/Cta.tsx", 61],
["src/views/home/components/Header.tsx", 106],
["src/views/pricing/components/PricingTiers.tsx", 29],
@@ -2647,7 +2689,7 @@
"0": ["isTemplate ? \"template\" : \"board\""]
},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 65]],
"origin": [["src/views/boards/components/BoardsList.tsx", 66]],
"translation": "Comienza creando un nuevo {0}"
},
"zC8Whw": {
@@ -2863,7 +2905,7 @@
"message": "Import",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 60]],
"origin": [["src/views/boards/index.tsx", 73]],
"translation": "Importar"
},
"2MPcep": {
@@ -3299,7 +3341,7 @@
"message": "Make template",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 73]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 91]],
"translation": "Crear plantilla"
},
"IDvohQ": {
@@ -3432,7 +3474,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/boards/index.tsx", 82],
["src/views/boards/index.tsx", 95],
["src/views/home/components/Features.tsx", 73]
],
"translation": "Nuevo"
@@ -3550,9 +3592,16 @@
"0": ["isTemplate ? \"templates\" : \"boards\""]
},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 62]],
"origin": [["src/views/boards/components/BoardsList.tsx", 63]],
"translation": "No hay {0}"
},
"tlhgDC": {
"message": "No archived boards",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 63]],
"translation": "No hay tableros archivados"
},
"Eg99Sc": {
"message": "No authentication methods are currently available",
"placeholders": {},
@@ -3963,7 +4012,7 @@
["src/components/DeleteLabelConfirmation.tsx", 26],
["src/components/FeedbackModal.tsx", 41],
["src/components/NewWorkspaceForm.tsx", 158],
["src/views/board/components/BoardDropdown.tsx", 63],
["src/views/board/components/BoardDropdown.tsx", 65],
["src/views/board/components/NewCardForm.tsx", 184],
["src/views/board/components/NewListForm.tsx", 79],
["src/views/board/components/NewTemplateForm.tsx", 61],
@@ -4178,7 +4227,7 @@
"message": "Remove from favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 92]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 121]],
"translation": "Eliminar de favoritos"
},
"99VIgC": {
@@ -4232,7 +4281,7 @@
"message": "Removed from favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 52]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 54]],
"translation": "Eliminado de favoritos"
},
"YVT40D": {
@@ -4570,8 +4619,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/auth/signup/index.tsx", 34],
["src/views/auth/signup/index.tsx", 59]
["src/views/auth/signup/index.tsx", 36],
["src/views/auth/signup/index.tsx", 61]
],
"translation": "Registrarse | kan.bn"
},
@@ -4579,14 +4628,14 @@
"message": "Sign up disabled",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 44]],
"origin": [["src/views/auth/signup/index.tsx", 46]],
"translation": "Registro deshabilitado"
},
"s6iE/c": {
"message": "Sign up is currently disabled. Please try again later.",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 47]],
"origin": [["src/views/auth/signup/index.tsx", 49]],
"translation": "El registro está actualmente deshabilitado. Por favor, inténtalo de nuevo más tarde."
},
"6FDocz": {
@@ -4809,6 +4858,20 @@
"origin": [["src/components/FeedbackModal.tsx", 34]],
"translation": "¡Gracias por tus comentarios!"
},
"NcFrgC": {
"message": "The board has been archived.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 46]],
"translation": "El tablero ha sido archivado."
},
"C6gv54": {
"message": "The board has been unarchived.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 47]],
"translation": "El tablero ha sido desarchivado."
},
"jlB2RY": {
"message": "The current password you entered is incorrect.",
"placeholders": {},
@@ -5196,7 +5259,7 @@
"message": "Unable to update board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 62]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 64]],
"translation": "No se pudo actualizar el tablero"
},
"XpcjLO": {
@@ -5292,6 +5355,13 @@
"origin": [["src/views/members/index.tsx", 65]],
"translation": "No se pudo actualizar el rol"
},
"It4/FS": {
"message": "Unarchive board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 111]],
"translation": "Desarchivar tablero"
},
"E8zYtd": {
"message": "unassigned <0>{0}</0> from the card",
"placeholders": {
@@ -5671,7 +5741,7 @@
"origin": [
["src/components/SettingsLayout.tsx", 46],
["src/views/board/index.tsx", 400],
["src/views/boards/index.tsx", 35],
["src/views/boards/index.tsx", 48],
["src/views/members/index.tsx", 258],
["src/views/public/board/index.tsx", 125],
["src/views/public/boards/index.tsx", 65]
@@ -5883,9 +5953,9 @@
["src/views/board/components/VisibilityButton.tsx", 72],
["src/views/board/index.tsx", 474],
["src/views/board/index.tsx", 532],
["src/views/boards/components/BoardsList.tsx", 70],
["src/views/boards/index.tsx", 46],
["src/views/boards/index.tsx", 67]
["src/views/boards/components/BoardsList.tsx", 71],
["src/views/boards/index.tsx", 59],
["src/views/boards/index.tsx", 80]
],
"translation": "No tienes permiso"
},

File diff suppressed because one or more lines are too long

View File

@@ -19,7 +19,7 @@
"0": ["isTemplate ? \"Templates\" : \"Boards\""]
},
"comments": [],
"origin": [["src/views/boards/index.tsx", 40]],
"origin": [["src/views/boards/index.tsx", 53]],
"translation": "{0}"
},
"JArWcF": {
@@ -33,7 +33,7 @@
},
"comments": [],
"origin": [
["src/views/boards/index.tsx", 35],
["src/views/boards/index.tsx", 48],
["src/views/card/index.tsx", 309]
],
"translation": "{0} | {1}"
@@ -44,7 +44,7 @@
"0": ["boardName ?? \"Board\""]
},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 54]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 56]],
"translation": "{0} a été ajouté à vos favoris."
},
"bDI6VI": {
@@ -53,7 +53,7 @@
"0": ["boardName ?? \"Board\""]
},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 55]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 57]],
"translation": "{0} a été retiré de vos favoris."
},
"CJukzS": {
@@ -185,6 +185,13 @@
"origin": [["src/providers/keyboard-shortcuts.tsx", 56]],
"translation": "Actions"
},
"F6pfE9": {
"message": "Active",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 26]],
"translation": "Actif"
},
"XJOV1Y": {
"message": "Activity",
"placeholders": {},
@@ -246,7 +253,7 @@
"message": "Add description... (type '/' to open commands or '@' to mention)",
"placeholders": {},
"comments": [],
"origin": [["src/components/Editor.tsx", 471]],
"origin": [["src/components/Editor.tsx", 482]],
"translation": "Ajouter une description... (tapez « / » pour ouvrir les commandes ou « @ » pour mentionner)"
},
"abUZlY": {
@@ -280,7 +287,7 @@
"message": "Add to favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 93]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 122]],
"translation": "Ajouter aux favoris"
},
"bmXKoX": {
@@ -368,7 +375,7 @@
"message": "Added to favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 51]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 53]],
"translation": "Ajouté aux favoris"
},
"14Xi3Z": {
@@ -482,7 +489,7 @@
"message": "Already have an account? <0><1>Sign in</1></0>",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 88]],
"origin": [["src/views/auth/signup/index.tsx", 90]],
"translation": "Vous avez déjà un compte ? <0><1>Se connecter</1></0>"
},
"WmPhYW": {
@@ -574,6 +581,20 @@
"origin": [["src/views/boards/components/TemplateBoards.tsx", 46]],
"translation": "Approbation"
},
"aKJHG+": {
"message": "Archive board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 111]],
"translation": "Archiver le tableau"
},
"TdfEV7": {
"message": "Archived",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 27]],
"translation": "Archivé"
},
"lo8xBK": {
"message": "Are you sure want to remove {entityLabel}?",
"placeholders": {
@@ -785,6 +806,13 @@
],
"translation": "Analytiques du tableau"
},
"i0ZMQl": {
"message": "Board archived",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 44]],
"translation": "Tableau archivé"
},
"wewm3j": {
"message": "Board name cannot exceed 100 characters",
"placeholders": {},
@@ -815,6 +843,13 @@
],
"translation": "Modèles de tableau"
},
"tbNcu4": {
"message": "Board unarchived",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 44]],
"translation": "Tableau désarchivé"
},
"Xid3K6": {
"message": "Board URL can only contain letters, numbers, and hyphens",
"placeholders": {},
@@ -873,6 +908,13 @@
],
"translation": "Tableaux"
},
"0RE1AJ": {
"message": "Boards you archive will appear here.",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 66]],
"translation": "Les tableaux que vous archivez apparaîtront ici."
},
"ZDo4HN": {
"message": "Brainstorming",
"placeholders": {},
@@ -1237,7 +1279,7 @@
"comments": [],
"origin": [
["src/views/auth/login/index.tsx", 43],
["src/views/auth/signup/index.tsx", 69]
["src/views/auth/signup/index.tsx", 71]
],
"translation": "Consultez votre boîte de réception"
},
@@ -1305,7 +1347,7 @@
"comments": [],
"origin": [
["src/views/auth/login/index.tsx", 48],
["src/views/auth/signup/index.tsx", 74]
["src/views/auth/signup/index.tsx", 76]
],
"translation": "Cliquez sur le lien que nous avons envoyé à {magicLinkRecipient} pour vous connecter."
},
@@ -1612,8 +1654,8 @@
},
"comments": [],
"origin": [
["src/views/boards/components/BoardsList.tsx", 79],
["src/views/boards/index.tsx", 28]
["src/views/boards/components/BoardsList.tsx", 80],
["src/views/boards/index.tsx", 41]
],
"translation": "Créer un nouveau {0}"
},
@@ -1835,7 +1877,7 @@
"message": "Delete board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 104]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 133]],
"translation": "Supprimer le tableau"
},
"nabda1": {
@@ -1863,7 +1905,7 @@
"message": "Delete template",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 104]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 133]],
"translation": "Supprimer le modèle"
},
"kYu0eF": {
@@ -2079,7 +2121,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/BoardDropdown.tsx", 84],
["src/views/board/components/BoardDropdown.tsx", 102],
["src/views/board/components/UpdateBoardSlugForm.tsx", 116]
],
"translation": "Modifier l'URL du tableau"
@@ -2622,7 +2664,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/auth/signup/index.tsx", 69],
["src/views/auth/signup/index.tsx", 71],
["src/views/home/components/Cta.tsx", 61],
["src/views/home/components/Header.tsx", 106],
["src/views/pricing/components/PricingTiers.tsx", 29],
@@ -2647,7 +2689,7 @@
"0": ["isTemplate ? \"template\" : \"board\""]
},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 65]],
"origin": [["src/views/boards/components/BoardsList.tsx", 66]],
"translation": "Commencez en créant un nouveau {0}"
},
"zC8Whw": {
@@ -2863,7 +2905,7 @@
"message": "Import",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 60]],
"origin": [["src/views/boards/index.tsx", 73]],
"translation": "Importer"
},
"2MPcep": {
@@ -3299,7 +3341,7 @@
"message": "Make template",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 73]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 91]],
"translation": "Créer un modèle"
},
"IDvohQ": {
@@ -3432,7 +3474,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/boards/index.tsx", 82],
["src/views/boards/index.tsx", 95],
["src/views/home/components/Features.tsx", 73]
],
"translation": "Nouveau"
@@ -3550,9 +3592,16 @@
"0": ["isTemplate ? \"templates\" : \"boards\""]
},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 62]],
"origin": [["src/views/boards/components/BoardsList.tsx", 63]],
"translation": "Aucun {0}"
},
"tlhgDC": {
"message": "No archived boards",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 63]],
"translation": "Aucun tableau archivé"
},
"Eg99Sc": {
"message": "No authentication methods are currently available",
"placeholders": {},
@@ -3963,7 +4012,7 @@
["src/components/DeleteLabelConfirmation.tsx", 26],
["src/components/FeedbackModal.tsx", 41],
["src/components/NewWorkspaceForm.tsx", 158],
["src/views/board/components/BoardDropdown.tsx", 63],
["src/views/board/components/BoardDropdown.tsx", 65],
["src/views/board/components/NewCardForm.tsx", 184],
["src/views/board/components/NewListForm.tsx", 79],
["src/views/board/components/NewTemplateForm.tsx", 61],
@@ -4178,7 +4227,7 @@
"message": "Remove from favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 92]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 121]],
"translation": "Retirer des favoris"
},
"99VIgC": {
@@ -4232,7 +4281,7 @@
"message": "Removed from favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 52]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 54]],
"translation": "Retiré des favoris"
},
"YVT40D": {
@@ -4570,8 +4619,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/auth/signup/index.tsx", 34],
["src/views/auth/signup/index.tsx", 59]
["src/views/auth/signup/index.tsx", 36],
["src/views/auth/signup/index.tsx", 61]
],
"translation": "S'inscrire | kan.bn"
},
@@ -4579,14 +4628,14 @@
"message": "Sign up disabled",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 44]],
"origin": [["src/views/auth/signup/index.tsx", 46]],
"translation": "Inscription désactivée"
},
"s6iE/c": {
"message": "Sign up is currently disabled. Please try again later.",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 47]],
"origin": [["src/views/auth/signup/index.tsx", 49]],
"translation": "L'inscription est actuellement désactivée. Veuillez réessayer plus tard."
},
"6FDocz": {
@@ -4809,6 +4858,20 @@
"origin": [["src/components/FeedbackModal.tsx", 34]],
"translation": "Merci pour votre retour !"
},
"NcFrgC": {
"message": "The board has been archived.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 46]],
"translation": "Le tableau a été archivé."
},
"C6gv54": {
"message": "The board has been unarchived.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 47]],
"translation": "Le tableau a été désarchivé."
},
"jlB2RY": {
"message": "The current password you entered is incorrect.",
"placeholders": {},
@@ -5196,7 +5259,7 @@
"message": "Unable to update board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 62]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 64]],
"translation": "Impossible de mettre à jour le tableau"
},
"XpcjLO": {
@@ -5292,6 +5355,13 @@
"origin": [["src/views/members/index.tsx", 65]],
"translation": "Impossible de mettre à jour le rôle"
},
"It4/FS": {
"message": "Unarchive board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 111]],
"translation": "Désarchiver le tableau"
},
"E8zYtd": {
"message": "unassigned <0>{0}</0> from the card",
"placeholders": {
@@ -5671,7 +5741,7 @@
"origin": [
["src/components/SettingsLayout.tsx", 46],
["src/views/board/index.tsx", 400],
["src/views/boards/index.tsx", 35],
["src/views/boards/index.tsx", 48],
["src/views/members/index.tsx", 258],
["src/views/public/board/index.tsx", 125],
["src/views/public/boards/index.tsx", 65]
@@ -5883,9 +5953,9 @@
["src/views/board/components/VisibilityButton.tsx", 72],
["src/views/board/index.tsx", 474],
["src/views/board/index.tsx", 532],
["src/views/boards/components/BoardsList.tsx", 70],
["src/views/boards/index.tsx", 46],
["src/views/boards/index.tsx", 67]
["src/views/boards/components/BoardsList.tsx", 71],
["src/views/boards/index.tsx", 59],
["src/views/boards/index.tsx", 80]
],
"translation": "Vous n'avez pas la permission"
},

File diff suppressed because one or more lines are too long

View File

@@ -7,7 +7,7 @@ export const locales = [
"nl",
"ru",
"pl",
"pt-BR"
"ptbr"
] as const;
export type Locale = (typeof locales)[number];
@@ -23,5 +23,5 @@ export const localeNames: Record<Locale, string> = {
nl: "Nederlands",
ru: "Русский",
pl: "Polski",
"pt-BR": "Português",
ptbr: "Português",
};

View File

@@ -19,7 +19,7 @@
"0": ["isTemplate ? \"Templates\" : \"Boards\""]
},
"comments": [],
"origin": [["src/views/boards/index.tsx", 40]],
"origin": [["src/views/boards/index.tsx", 53]],
"translation": "{0}"
},
"JArWcF": {
@@ -33,7 +33,7 @@
},
"comments": [],
"origin": [
["src/views/boards/index.tsx", 35],
["src/views/boards/index.tsx", 48],
["src/views/card/index.tsx", 309]
],
"translation": "{0} | {1}"
@@ -44,7 +44,7 @@
"0": ["boardName ?? \"Board\""]
},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 54]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 56]],
"translation": "{0} è stato aggiunto ai tuoi preferiti."
},
"bDI6VI": {
@@ -53,7 +53,7 @@
"0": ["boardName ?? \"Board\""]
},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 55]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 57]],
"translation": "{0} è stato rimosso dai tuoi preferiti."
},
"CJukzS": {
@@ -185,6 +185,13 @@
"origin": [["src/providers/keyboard-shortcuts.tsx", 56]],
"translation": "Azioni"
},
"F6pfE9": {
"message": "Active",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 26]],
"translation": "Attivo"
},
"XJOV1Y": {
"message": "Activity",
"placeholders": {},
@@ -246,7 +253,7 @@
"message": "Add description... (type '/' to open commands or '@' to mention)",
"placeholders": {},
"comments": [],
"origin": [["src/components/Editor.tsx", 471]],
"origin": [["src/components/Editor.tsx", 482]],
"translation": "Aggiungi descrizione... (digita '/' per aprire i comandi o '@' per menzionare)"
},
"abUZlY": {
@@ -280,7 +287,7 @@
"message": "Add to favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 93]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 122]],
"translation": "Aggiungi ai preferiti"
},
"bmXKoX": {
@@ -368,7 +375,7 @@
"message": "Added to favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 51]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 53]],
"translation": "Aggiunto ai preferiti"
},
"14Xi3Z": {
@@ -482,7 +489,7 @@
"message": "Already have an account? <0><1>Sign in</1></0>",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 88]],
"origin": [["src/views/auth/signup/index.tsx", 90]],
"translation": "Hai già un account? <0><1>Accedi</1></0>"
},
"WmPhYW": {
@@ -574,6 +581,20 @@
"origin": [["src/views/boards/components/TemplateBoards.tsx", 46]],
"translation": "Approvazione"
},
"aKJHG+": {
"message": "Archive board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 111]],
"translation": "Archivia bacheca"
},
"TdfEV7": {
"message": "Archived",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 27]],
"translation": "Archiviato"
},
"lo8xBK": {
"message": "Are you sure want to remove {entityLabel}?",
"placeholders": {
@@ -785,6 +806,13 @@
],
"translation": "Analisi della board"
},
"i0ZMQl": {
"message": "Board archived",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 44]],
"translation": "Bacheca archiviata"
},
"wewm3j": {
"message": "Board name cannot exceed 100 characters",
"placeholders": {},
@@ -815,6 +843,13 @@
],
"translation": "Template di board"
},
"tbNcu4": {
"message": "Board unarchived",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 44]],
"translation": "Bacheca ripristinata"
},
"Xid3K6": {
"message": "Board URL can only contain letters, numbers, and hyphens",
"placeholders": {},
@@ -873,6 +908,13 @@
],
"translation": "Board"
},
"0RE1AJ": {
"message": "Boards you archive will appear here.",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 66]],
"translation": "Le bacheche che archivi appariranno qui."
},
"ZDo4HN": {
"message": "Brainstorming",
"placeholders": {},
@@ -1237,7 +1279,7 @@
"comments": [],
"origin": [
["src/views/auth/login/index.tsx", 43],
["src/views/auth/signup/index.tsx", 69]
["src/views/auth/signup/index.tsx", 71]
],
"translation": "Controlla la tua casella di posta"
},
@@ -1305,7 +1347,7 @@
"comments": [],
"origin": [
["src/views/auth/login/index.tsx", 48],
["src/views/auth/signup/index.tsx", 74]
["src/views/auth/signup/index.tsx", 76]
],
"translation": "Clicca sul link che abbiamo inviato a {magicLinkRecipient} per accedere."
},
@@ -1612,8 +1654,8 @@
},
"comments": [],
"origin": [
["src/views/boards/components/BoardsList.tsx", 79],
["src/views/boards/index.tsx", 28]
["src/views/boards/components/BoardsList.tsx", 80],
["src/views/boards/index.tsx", 41]
],
"translation": "Crea nuovo {0}"
},
@@ -1835,7 +1877,7 @@
"message": "Delete board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 104]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 133]],
"translation": "Elimina board"
},
"nabda1": {
@@ -1863,7 +1905,7 @@
"message": "Delete template",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 104]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 133]],
"translation": "Elimina template"
},
"kYu0eF": {
@@ -2079,7 +2121,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/BoardDropdown.tsx", 84],
["src/views/board/components/BoardDropdown.tsx", 102],
["src/views/board/components/UpdateBoardSlugForm.tsx", 116]
],
"translation": "Modifica URL bacheca"
@@ -2622,7 +2664,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/auth/signup/index.tsx", 69],
["src/views/auth/signup/index.tsx", 71],
["src/views/home/components/Cta.tsx", 61],
["src/views/home/components/Header.tsx", 106],
["src/views/pricing/components/PricingTiers.tsx", 29],
@@ -2647,7 +2689,7 @@
"0": ["isTemplate ? \"template\" : \"board\""]
},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 65]],
"origin": [["src/views/boards/components/BoardsList.tsx", 66]],
"translation": "Inizia creando un nuovo {0}"
},
"zC8Whw": {
@@ -2863,7 +2905,7 @@
"message": "Import",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 60]],
"origin": [["src/views/boards/index.tsx", 73]],
"translation": "Importa"
},
"2MPcep": {
@@ -3299,7 +3341,7 @@
"message": "Make template",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 73]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 91]],
"translation": "Crea template"
},
"IDvohQ": {
@@ -3432,7 +3474,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/boards/index.tsx", 82],
["src/views/boards/index.tsx", 95],
["src/views/home/components/Features.tsx", 73]
],
"translation": "Nuovo"
@@ -3550,9 +3592,16 @@
"0": ["isTemplate ? \"templates\" : \"boards\""]
},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 62]],
"origin": [["src/views/boards/components/BoardsList.tsx", 63]],
"translation": "Nessun {0}"
},
"tlhgDC": {
"message": "No archived boards",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 63]],
"translation": "Nessuna bacheca archiviata"
},
"Eg99Sc": {
"message": "No authentication methods are currently available",
"placeholders": {},
@@ -3963,7 +4012,7 @@
["src/components/DeleteLabelConfirmation.tsx", 26],
["src/components/FeedbackModal.tsx", 41],
["src/components/NewWorkspaceForm.tsx", 158],
["src/views/board/components/BoardDropdown.tsx", 63],
["src/views/board/components/BoardDropdown.tsx", 65],
["src/views/board/components/NewCardForm.tsx", 184],
["src/views/board/components/NewListForm.tsx", 79],
["src/views/board/components/NewTemplateForm.tsx", 61],
@@ -4178,7 +4227,7 @@
"message": "Remove from favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 92]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 121]],
"translation": "Rimuovi dai preferiti"
},
"99VIgC": {
@@ -4232,7 +4281,7 @@
"message": "Removed from favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 52]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 54]],
"translation": "Rimosso dai preferiti"
},
"YVT40D": {
@@ -4570,8 +4619,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/auth/signup/index.tsx", 34],
["src/views/auth/signup/index.tsx", 59]
["src/views/auth/signup/index.tsx", 36],
["src/views/auth/signup/index.tsx", 61]
],
"translation": "Registrati | kan.bn"
},
@@ -4579,14 +4628,14 @@
"message": "Sign up disabled",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 44]],
"origin": [["src/views/auth/signup/index.tsx", 46]],
"translation": "Registrazione disabilitata"
},
"s6iE/c": {
"message": "Sign up is currently disabled. Please try again later.",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 47]],
"origin": [["src/views/auth/signup/index.tsx", 49]],
"translation": "La registrazione è attualmente disabilitata. Riprova più tardi."
},
"6FDocz": {
@@ -4809,6 +4858,20 @@
"origin": [["src/components/FeedbackModal.tsx", 34]],
"translation": "Grazie per il tuo feedback!"
},
"NcFrgC": {
"message": "The board has been archived.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 46]],
"translation": "La bacheca è stata archiviata."
},
"C6gv54": {
"message": "The board has been unarchived.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 47]],
"translation": "La bacheca è stata ripristinata."
},
"jlB2RY": {
"message": "The current password you entered is incorrect.",
"placeholders": {},
@@ -5196,7 +5259,7 @@
"message": "Unable to update board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 62]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 64]],
"translation": "Impossibile aggiornare la board"
},
"XpcjLO": {
@@ -5292,6 +5355,13 @@
"origin": [["src/views/members/index.tsx", 65]],
"translation": "Impossibile aggiornare il ruolo"
},
"It4/FS": {
"message": "Unarchive board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 111]],
"translation": "Ripristina bacheca"
},
"E8zYtd": {
"message": "unassigned <0>{0}</0> from the card",
"placeholders": {
@@ -5671,7 +5741,7 @@
"origin": [
["src/components/SettingsLayout.tsx", 46],
["src/views/board/index.tsx", 400],
["src/views/boards/index.tsx", 35],
["src/views/boards/index.tsx", 48],
["src/views/members/index.tsx", 258],
["src/views/public/board/index.tsx", 125],
["src/views/public/boards/index.tsx", 65]
@@ -5883,9 +5953,9 @@
["src/views/board/components/VisibilityButton.tsx", 72],
["src/views/board/index.tsx", 474],
["src/views/board/index.tsx", 532],
["src/views/boards/components/BoardsList.tsx", 70],
["src/views/boards/index.tsx", 46],
["src/views/boards/index.tsx", 67]
["src/views/boards/components/BoardsList.tsx", 71],
["src/views/boards/index.tsx", 59],
["src/views/boards/index.tsx", 80]
],
"translation": "Non hai i permessi necessari"
},

File diff suppressed because one or more lines are too long

View File

@@ -19,7 +19,7 @@
"0": ["isTemplate ? \"Templates\" : \"Boards\""]
},
"comments": [],
"origin": [["src/views/boards/index.tsx", 40]],
"origin": [["src/views/boards/index.tsx", 53]],
"translation": "{0}"
},
"JArWcF": {
@@ -33,7 +33,7 @@
},
"comments": [],
"origin": [
["src/views/boards/index.tsx", 35],
["src/views/boards/index.tsx", 48],
["src/views/card/index.tsx", 309]
],
"translation": "{0} | {1}"
@@ -44,7 +44,7 @@
"0": ["boardName ?? \"Board\""]
},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 54]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 56]],
"translation": "{0} is toegevoegd aan je favorieten."
},
"bDI6VI": {
@@ -53,7 +53,7 @@
"0": ["boardName ?? \"Board\""]
},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 55]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 57]],
"translation": "{0} is verwijderd uit je favorieten."
},
"CJukzS": {
@@ -185,6 +185,13 @@
"origin": [["src/providers/keyboard-shortcuts.tsx", 56]],
"translation": "Acties"
},
"F6pfE9": {
"message": "Active",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 26]],
"translation": "Actief"
},
"XJOV1Y": {
"message": "Activity",
"placeholders": {},
@@ -246,7 +253,7 @@
"message": "Add description... (type '/' to open commands or '@' to mention)",
"placeholders": {},
"comments": [],
"origin": [["src/components/Editor.tsx", 471]],
"origin": [["src/components/Editor.tsx", 482]],
"translation": "Voeg beschrijving toe... (typ '/' om commando's te openen of '@' om te vermelden)"
},
"abUZlY": {
@@ -280,7 +287,7 @@
"message": "Add to favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 93]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 122]],
"translation": "Toevoegen aan favorieten"
},
"bmXKoX": {
@@ -368,7 +375,7 @@
"message": "Added to favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 51]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 53]],
"translation": "Toegevoegd aan favorieten"
},
"14Xi3Z": {
@@ -482,7 +489,7 @@
"message": "Already have an account? <0><1>Sign in</1></0>",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 88]],
"origin": [["src/views/auth/signup/index.tsx", 90]],
"translation": "Heb je al een account? <0><1>Inloggen</1></0>"
},
"WmPhYW": {
@@ -574,6 +581,20 @@
"origin": [["src/views/boards/components/TemplateBoards.tsx", 46]],
"translation": "Goedkeuring"
},
"aKJHG+": {
"message": "Archive board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 111]],
"translation": "Board archiveren"
},
"TdfEV7": {
"message": "Archived",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 27]],
"translation": "Gearchiveerd"
},
"lo8xBK": {
"message": "Are you sure want to remove {entityLabel}?",
"placeholders": {
@@ -785,6 +806,13 @@
],
"translation": "Bordanalyse"
},
"i0ZMQl": {
"message": "Board archived",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 44]],
"translation": "Board gearchiveerd"
},
"wewm3j": {
"message": "Board name cannot exceed 100 characters",
"placeholders": {},
@@ -815,6 +843,13 @@
],
"translation": "Bordsjablonen"
},
"tbNcu4": {
"message": "Board unarchived",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 44]],
"translation": "Board gedearchiveerd"
},
"Xid3K6": {
"message": "Board URL can only contain letters, numbers, and hyphens",
"placeholders": {},
@@ -873,6 +908,13 @@
],
"translation": "Boards"
},
"0RE1AJ": {
"message": "Boards you archive will appear here.",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 66]],
"translation": "Boards die je archiveert verschijnen hier."
},
"ZDo4HN": {
"message": "Brainstorming",
"placeholders": {},
@@ -1237,7 +1279,7 @@
"comments": [],
"origin": [
["src/views/auth/login/index.tsx", 43],
["src/views/auth/signup/index.tsx", 69]
["src/views/auth/signup/index.tsx", 71]
],
"translation": "Controleer je inbox"
},
@@ -1305,7 +1347,7 @@
"comments": [],
"origin": [
["src/views/auth/login/index.tsx", 48],
["src/views/auth/signup/index.tsx", 74]
["src/views/auth/signup/index.tsx", 76]
],
"translation": "Klik op de link die we naar {magicLinkRecipient} hebben gestuurd om in te loggen."
},
@@ -1612,8 +1654,8 @@
},
"comments": [],
"origin": [
["src/views/boards/components/BoardsList.tsx", 79],
["src/views/boards/index.tsx", 28]
["src/views/boards/components/BoardsList.tsx", 80],
["src/views/boards/index.tsx", 41]
],
"translation": "Maak nieuwe {0}"
},
@@ -1835,7 +1877,7 @@
"message": "Delete board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 104]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 133]],
"translation": "Bord verwijderen"
},
"nabda1": {
@@ -1863,7 +1905,7 @@
"message": "Delete template",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 104]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 133]],
"translation": "Sjabloon verwijderen"
},
"kYu0eF": {
@@ -2079,7 +2121,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/BoardDropdown.tsx", 84],
["src/views/board/components/BoardDropdown.tsx", 102],
["src/views/board/components/UpdateBoardSlugForm.tsx", 116]
],
"translation": "Bord-URL bewerken"
@@ -2622,7 +2664,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/auth/signup/index.tsx", 69],
["src/views/auth/signup/index.tsx", 71],
["src/views/home/components/Cta.tsx", 61],
["src/views/home/components/Header.tsx", 106],
["src/views/pricing/components/PricingTiers.tsx", 29],
@@ -2647,7 +2689,7 @@
"0": ["isTemplate ? \"template\" : \"board\""]
},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 65]],
"origin": [["src/views/boards/components/BoardsList.tsx", 66]],
"translation": "Ga aan de slag door een nieuwe {0} te maken"
},
"zC8Whw": {
@@ -2863,7 +2905,7 @@
"message": "Import",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 60]],
"origin": [["src/views/boards/index.tsx", 73]],
"translation": "Importeren"
},
"2MPcep": {
@@ -3299,7 +3341,7 @@
"message": "Make template",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 73]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 91]],
"translation": "Sjabloon maken"
},
"IDvohQ": {
@@ -3432,7 +3474,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/boards/index.tsx", 82],
["src/views/boards/index.tsx", 95],
["src/views/home/components/Features.tsx", 73]
],
"translation": "Nieuw"
@@ -3550,9 +3592,16 @@
"0": ["isTemplate ? \"templates\" : \"boards\""]
},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 62]],
"origin": [["src/views/boards/components/BoardsList.tsx", 63]],
"translation": "Geen {0}"
},
"tlhgDC": {
"message": "No archived boards",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 63]],
"translation": "Geen gearchiveerde boards"
},
"Eg99Sc": {
"message": "No authentication methods are currently available",
"placeholders": {},
@@ -3963,7 +4012,7 @@
["src/components/DeleteLabelConfirmation.tsx", 26],
["src/components/FeedbackModal.tsx", 41],
["src/components/NewWorkspaceForm.tsx", 158],
["src/views/board/components/BoardDropdown.tsx", 63],
["src/views/board/components/BoardDropdown.tsx", 65],
["src/views/board/components/NewCardForm.tsx", 184],
["src/views/board/components/NewListForm.tsx", 79],
["src/views/board/components/NewTemplateForm.tsx", 61],
@@ -4178,7 +4227,7 @@
"message": "Remove from favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 92]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 121]],
"translation": "Verwijderen uit favorieten"
},
"99VIgC": {
@@ -4232,7 +4281,7 @@
"message": "Removed from favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 52]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 54]],
"translation": "Verwijderd uit favorieten"
},
"YVT40D": {
@@ -4570,8 +4619,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/auth/signup/index.tsx", 34],
["src/views/auth/signup/index.tsx", 59]
["src/views/auth/signup/index.tsx", 36],
["src/views/auth/signup/index.tsx", 61]
],
"translation": "Registreren | kan.bn"
},
@@ -4579,14 +4628,14 @@
"message": "Sign up disabled",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 44]],
"origin": [["src/views/auth/signup/index.tsx", 46]],
"translation": "Registratie uitgeschakeld"
},
"s6iE/c": {
"message": "Sign up is currently disabled. Please try again later.",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 47]],
"origin": [["src/views/auth/signup/index.tsx", 49]],
"translation": "Registratie is momenteel uitgeschakeld. Probeer het later opnieuw."
},
"6FDocz": {
@@ -4809,6 +4858,20 @@
"origin": [["src/components/FeedbackModal.tsx", 34]],
"translation": "Bedankt voor je feedback!"
},
"NcFrgC": {
"message": "The board has been archived.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 46]],
"translation": "Het board is gearchiveerd."
},
"C6gv54": {
"message": "The board has been unarchived.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 47]],
"translation": "Het board is gedearchiveerd."
},
"jlB2RY": {
"message": "The current password you entered is incorrect.",
"placeholders": {},
@@ -5196,7 +5259,7 @@
"message": "Unable to update board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 62]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 64]],
"translation": "Kan bord niet bijwerken"
},
"XpcjLO": {
@@ -5292,6 +5355,13 @@
"origin": [["src/views/members/index.tsx", 65]],
"translation": "Kan rol niet bijwerken"
},
"It4/FS": {
"message": "Unarchive board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 111]],
"translation": "Board dearchiveren"
},
"E8zYtd": {
"message": "unassigned <0>{0}</0> from the card",
"placeholders": {
@@ -5671,7 +5741,7 @@
"origin": [
["src/components/SettingsLayout.tsx", 46],
["src/views/board/index.tsx", 400],
["src/views/boards/index.tsx", 35],
["src/views/boards/index.tsx", 48],
["src/views/members/index.tsx", 258],
["src/views/public/board/index.tsx", 125],
["src/views/public/boards/index.tsx", 65]
@@ -5883,9 +5953,9 @@
["src/views/board/components/VisibilityButton.tsx", 72],
["src/views/board/index.tsx", 474],
["src/views/board/index.tsx", 532],
["src/views/boards/components/BoardsList.tsx", 70],
["src/views/boards/index.tsx", 46],
["src/views/boards/index.tsx", 67]
["src/views/boards/components/BoardsList.tsx", 71],
["src/views/boards/index.tsx", 59],
["src/views/boards/index.tsx", 80]
],
"translation": "Je hebt geen toestemming"
},

File diff suppressed because one or more lines are too long

View File

@@ -19,7 +19,7 @@
"0": ["isTemplate ? \"Templates\" : \"Boards\""]
},
"comments": [],
"origin": [["src/views/boards/index.tsx", 40]],
"origin": [["src/views/boards/index.tsx", 53]],
"translation": "{0}"
},
"JArWcF": {
@@ -33,7 +33,7 @@
},
"comments": [],
"origin": [
["src/views/boards/index.tsx", 35],
["src/views/boards/index.tsx", 48],
["src/views/card/index.tsx", 309]
],
"translation": "{0} | {1}"
@@ -44,7 +44,7 @@
"0": ["boardName ?? \"Board\""]
},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 54]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 56]],
"translation": "{0} został dodany do ulubionych."
},
"bDI6VI": {
@@ -53,7 +53,7 @@
"0": ["boardName ?? \"Board\""]
},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 55]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 57]],
"translation": "{0} został usunięty z ulubionych."
},
"CJukzS": {
@@ -185,6 +185,13 @@
"origin": [["src/providers/keyboard-shortcuts.tsx", 56]],
"translation": "Akcje"
},
"F6pfE9": {
"message": "Active",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 26]],
"translation": "Aktywna"
},
"XJOV1Y": {
"message": "Activity",
"placeholders": {},
@@ -246,7 +253,7 @@
"message": "Add description... (type '/' to open commands or '@' to mention)",
"placeholders": {},
"comments": [],
"origin": [["src/components/Editor.tsx", 471]],
"origin": [["src/components/Editor.tsx", 482]],
"translation": "Dodaj opis... (wpisz '/' aby otworzyć polecenia lub '@', aby wspomnieć)"
},
"abUZlY": {
@@ -280,7 +287,7 @@
"message": "Add to favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 93]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 122]],
"translation": "Dodaj do ulubionych"
},
"bmXKoX": {
@@ -368,7 +375,7 @@
"message": "Added to favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 51]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 53]],
"translation": "Dodano do ulubionych"
},
"14Xi3Z": {
@@ -482,7 +489,7 @@
"message": "Already have an account? <0><1>Sign in</1></0>",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 88]],
"origin": [["src/views/auth/signup/index.tsx", 90]],
"translation": "Masz już konto? <0><1>Zaloguj się</1></0>"
},
"WmPhYW": {
@@ -574,6 +581,20 @@
"origin": [["src/views/boards/components/TemplateBoards.tsx", 46]],
"translation": "Zatwierdzenie"
},
"aKJHG+": {
"message": "Archive board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 111]],
"translation": "Zarchiwizuj tablicę"
},
"TdfEV7": {
"message": "Archived",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 27]],
"translation": "Zarchiwizowana"
},
"lo8xBK": {
"message": "Are you sure want to remove {entityLabel}?",
"placeholders": {
@@ -785,6 +806,13 @@
],
"translation": "Analiza tablicy"
},
"i0ZMQl": {
"message": "Board archived",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 44]],
"translation": "Tablica zarchiwizowana"
},
"wewm3j": {
"message": "Board name cannot exceed 100 characters",
"placeholders": {},
@@ -815,6 +843,13 @@
],
"translation": "Szablony tablic"
},
"tbNcu4": {
"message": "Board unarchived",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 44]],
"translation": "Tablica przywrócona z archiwum"
},
"Xid3K6": {
"message": "Board URL can only contain letters, numbers, and hyphens",
"placeholders": {},
@@ -873,6 +908,13 @@
],
"translation": "Tablice"
},
"0RE1AJ": {
"message": "Boards you archive will appear here.",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 66]],
"translation": "Zarchiwizowane tablice pojawią się tutaj."
},
"ZDo4HN": {
"message": "Brainstorming",
"placeholders": {},
@@ -1237,7 +1279,7 @@
"comments": [],
"origin": [
["src/views/auth/login/index.tsx", 43],
["src/views/auth/signup/index.tsx", 69]
["src/views/auth/signup/index.tsx", 71]
],
"translation": "Sprawdź swoją skrzynkę odbiorczą"
},
@@ -1305,7 +1347,7 @@
"comments": [],
"origin": [
["src/views/auth/login/index.tsx", 48],
["src/views/auth/signup/index.tsx", 74]
["src/views/auth/signup/index.tsx", 76]
],
"translation": "Kliknij w link, który wysłaliśmy do {magicLinkRecipient}, aby się zalogować."
},
@@ -1612,8 +1654,8 @@
},
"comments": [],
"origin": [
["src/views/boards/components/BoardsList.tsx", 79],
["src/views/boards/index.tsx", 28]
["src/views/boards/components/BoardsList.tsx", 80],
["src/views/boards/index.tsx", 41]
],
"translation": "Utwórz nową {0}"
},
@@ -1835,7 +1877,7 @@
"message": "Delete board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 104]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 133]],
"translation": "Usuń tablicę"
},
"nabda1": {
@@ -1863,7 +1905,7 @@
"message": "Delete template",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 104]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 133]],
"translation": "Usuń szablon"
},
"kYu0eF": {
@@ -2079,7 +2121,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/BoardDropdown.tsx", 84],
["src/views/board/components/BoardDropdown.tsx", 102],
["src/views/board/components/UpdateBoardSlugForm.tsx", 116]
],
"translation": "Edytuj URL tablicy"
@@ -2622,7 +2664,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/auth/signup/index.tsx", 69],
["src/views/auth/signup/index.tsx", 71],
["src/views/home/components/Cta.tsx", 61],
["src/views/home/components/Header.tsx", 106],
["src/views/pricing/components/PricingTiers.tsx", 29],
@@ -2647,7 +2689,7 @@
"0": ["isTemplate ? \"template\" : \"board\""]
},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 65]],
"origin": [["src/views/boards/components/BoardsList.tsx", 66]],
"translation": "Rozpocznij, tworząc nowy {0}"
},
"zC8Whw": {
@@ -2863,7 +2905,7 @@
"message": "Import",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 60]],
"origin": [["src/views/boards/index.tsx", 73]],
"translation": "Importuj"
},
"2MPcep": {
@@ -3299,7 +3341,7 @@
"message": "Make template",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 73]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 91]],
"translation": "Utwórz szablon"
},
"IDvohQ": {
@@ -3432,7 +3474,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/boards/index.tsx", 82],
["src/views/boards/index.tsx", 95],
["src/views/home/components/Features.tsx", 73]
],
"translation": "Nowy"
@@ -3550,9 +3592,16 @@
"0": ["isTemplate ? \"templates\" : \"boards\""]
},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 62]],
"origin": [["src/views/boards/components/BoardsList.tsx", 63]],
"translation": "Brak {0}"
},
"tlhgDC": {
"message": "No archived boards",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 63]],
"translation": "Brak zarchiwizowanych tablic"
},
"Eg99Sc": {
"message": "No authentication methods are currently available",
"placeholders": {},
@@ -3963,7 +4012,7 @@
["src/components/DeleteLabelConfirmation.tsx", 26],
["src/components/FeedbackModal.tsx", 41],
["src/components/NewWorkspaceForm.tsx", 158],
["src/views/board/components/BoardDropdown.tsx", 63],
["src/views/board/components/BoardDropdown.tsx", 65],
["src/views/board/components/NewCardForm.tsx", 184],
["src/views/board/components/NewListForm.tsx", 79],
["src/views/board/components/NewTemplateForm.tsx", 61],
@@ -4178,7 +4227,7 @@
"message": "Remove from favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 92]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 121]],
"translation": "Usuń z ulubionych"
},
"99VIgC": {
@@ -4232,7 +4281,7 @@
"message": "Removed from favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 52]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 54]],
"translation": "Usunięto z ulubionych"
},
"YVT40D": {
@@ -4570,8 +4619,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/auth/signup/index.tsx", 34],
["src/views/auth/signup/index.tsx", 59]
["src/views/auth/signup/index.tsx", 36],
["src/views/auth/signup/index.tsx", 61]
],
"translation": "Rejestracja | kan.bn"
},
@@ -4579,14 +4628,14 @@
"message": "Sign up disabled",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 44]],
"origin": [["src/views/auth/signup/index.tsx", 46]],
"translation": "Rejestracja wyłączona"
},
"s6iE/c": {
"message": "Sign up is currently disabled. Please try again later.",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 47]],
"origin": [["src/views/auth/signup/index.tsx", 49]],
"translation": "Rejestracja jest obecnie wyłączona. Spróbuj ponownie później."
},
"6FDocz": {
@@ -4809,6 +4858,20 @@
"origin": [["src/components/FeedbackModal.tsx", 34]],
"translation": "Dziękujemy za Twoją opinię!"
},
"NcFrgC": {
"message": "The board has been archived.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 46]],
"translation": "Tablica została zarchiwizowana."
},
"C6gv54": {
"message": "The board has been unarchived.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 47]],
"translation": "Tablica została przywrócona z archiwum."
},
"jlB2RY": {
"message": "The current password you entered is incorrect.",
"placeholders": {},
@@ -5196,7 +5259,7 @@
"message": "Unable to update board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 62]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 64]],
"translation": "Nie można zaktualizować tablicy"
},
"XpcjLO": {
@@ -5292,6 +5355,13 @@
"origin": [["src/views/members/index.tsx", 65]],
"translation": "Nie można zaktualizować roli"
},
"It4/FS": {
"message": "Unarchive board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 111]],
"translation": "Przywróć tablicę z archiwum"
},
"E8zYtd": {
"message": "unassigned <0>{0}</0> from the card",
"placeholders": {
@@ -5671,7 +5741,7 @@
"origin": [
["src/components/SettingsLayout.tsx", 46],
["src/views/board/index.tsx", 400],
["src/views/boards/index.tsx", 35],
["src/views/boards/index.tsx", 48],
["src/views/members/index.tsx", 258],
["src/views/public/board/index.tsx", 125],
["src/views/public/boards/index.tsx", 65]
@@ -5883,9 +5953,9 @@
["src/views/board/components/VisibilityButton.tsx", 72],
["src/views/board/index.tsx", 474],
["src/views/board/index.tsx", 532],
["src/views/boards/components/BoardsList.tsx", 70],
["src/views/boards/index.tsx", 46],
["src/views/boards/index.tsx", 67]
["src/views/boards/components/BoardsList.tsx", 71],
["src/views/boards/index.tsx", 59],
["src/views/boards/index.tsx", 80]
],
"translation": "Nie masz uprawnień"
},

File diff suppressed because one or more lines are too long

View File

@@ -19,7 +19,7 @@
"0": ["isTemplate ? \"Templates\" : \"Boards\""]
},
"comments": [],
"origin": [["src/views/boards/index.tsx", 40]],
"origin": [["src/views/boards/index.tsx", 53]],
"translation": "{0}"
},
"JArWcF": {
@@ -33,7 +33,7 @@
},
"comments": [],
"origin": [
["src/views/boards/index.tsx", 35],
["src/views/boards/index.tsx", 48],
["src/views/card/index.tsx", 309]
],
"translation": "{0} | {1}"
@@ -44,7 +44,7 @@
"0": ["boardName ?? \"Board\""]
},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 54]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 56]],
"translation": "{0} foi adicionado aos seus favoritos."
},
"bDI6VI": {
@@ -53,7 +53,7 @@
"0": ["boardName ?? \"Board\""]
},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 55]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 57]],
"translation": "{0} foi removido dos seus favoritos."
},
"CJukzS": {
@@ -185,6 +185,13 @@
"origin": [["src/providers/keyboard-shortcuts.tsx", 56]],
"translation": "Ações"
},
"F6pfE9": {
"message": "Active",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 26]],
"translation": "Ativo"
},
"XJOV1Y": {
"message": "Activity",
"placeholders": {},
@@ -246,7 +253,7 @@
"message": "Add description... (type '/' to open commands or '@' to mention)",
"placeholders": {},
"comments": [],
"origin": [["src/components/Editor.tsx", 471]],
"origin": [["src/components/Editor.tsx", 482]],
"translation": "Adicionar descrição... (digite '/' para abrir comandos ou '@' para mencionar)"
},
"abUZlY": {
@@ -280,7 +287,7 @@
"message": "Add to favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 93]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 122]],
"translation": "Adicionar aos favoritos"
},
"bmXKoX": {
@@ -368,7 +375,7 @@
"message": "Added to favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 51]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 53]],
"translation": "Adicionado aos favoritos"
},
"14Xi3Z": {
@@ -482,7 +489,7 @@
"message": "Already have an account? <0><1>Sign in</1></0>",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 88]],
"origin": [["src/views/auth/signup/index.tsx", 90]],
"translation": "Já tem uma conta? <0><1>Entrar</1></0>"
},
"WmPhYW": {
@@ -574,6 +581,20 @@
"origin": [["src/views/boards/components/TemplateBoards.tsx", 46]],
"translation": "Aprovação"
},
"aKJHG+": {
"message": "Archive board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 111]],
"translation": "Arquivar quadro"
},
"TdfEV7": {
"message": "Archived",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 27]],
"translation": "Arquivado"
},
"lo8xBK": {
"message": "Are you sure want to remove {entityLabel}?",
"placeholders": {
@@ -785,6 +806,13 @@
],
"translation": "Análise do quadro"
},
"i0ZMQl": {
"message": "Board archived",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 44]],
"translation": "Quadro arquivado"
},
"wewm3j": {
"message": "Board name cannot exceed 100 characters",
"placeholders": {},
@@ -815,6 +843,13 @@
],
"translation": "Modelos de quadro"
},
"tbNcu4": {
"message": "Board unarchived",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 44]],
"translation": "Quadro desarquivado"
},
"Xid3K6": {
"message": "Board URL can only contain letters, numbers, and hyphens",
"placeholders": {},
@@ -873,6 +908,13 @@
],
"translation": "Quadros"
},
"0RE1AJ": {
"message": "Boards you archive will appear here.",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 66]],
"translation": "Os quadros que você arquivar aparecerão aqui."
},
"ZDo4HN": {
"message": "Brainstorming",
"placeholders": {},
@@ -1237,7 +1279,7 @@
"comments": [],
"origin": [
["src/views/auth/login/index.tsx", 43],
["src/views/auth/signup/index.tsx", 69]
["src/views/auth/signup/index.tsx", 71]
],
"translation": "Verifique sua caixa de entrada"
},
@@ -1305,7 +1347,7 @@
"comments": [],
"origin": [
["src/views/auth/login/index.tsx", 48],
["src/views/auth/signup/index.tsx", 74]
["src/views/auth/signup/index.tsx", 76]
],
"translation": "Clique no link que enviamos para {magicLinkRecipient} para fazer login."
},
@@ -1612,8 +1654,8 @@
},
"comments": [],
"origin": [
["src/views/boards/components/BoardsList.tsx", 79],
["src/views/boards/index.tsx", 28]
["src/views/boards/components/BoardsList.tsx", 80],
["src/views/boards/index.tsx", 41]
],
"translation": "Criar novo {0}"
},
@@ -1835,7 +1877,7 @@
"message": "Delete board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 104]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 133]],
"translation": "Excluir quadro"
},
"nabda1": {
@@ -1863,7 +1905,7 @@
"message": "Delete template",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 104]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 133]],
"translation": "Excluir modelo"
},
"kYu0eF": {
@@ -2079,7 +2121,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/BoardDropdown.tsx", 84],
["src/views/board/components/BoardDropdown.tsx", 102],
["src/views/board/components/UpdateBoardSlugForm.tsx", 116]
],
"translation": "Editar URL do quadro"
@@ -2622,7 +2664,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/auth/signup/index.tsx", 69],
["src/views/auth/signup/index.tsx", 71],
["src/views/home/components/Cta.tsx", 61],
["src/views/home/components/Header.tsx", 106],
["src/views/pricing/components/PricingTiers.tsx", 29],
@@ -2647,7 +2689,7 @@
"0": ["isTemplate ? \"template\" : \"board\""]
},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 65]],
"origin": [["src/views/boards/components/BoardsList.tsx", 66]],
"translation": "Comece criando um novo {0}"
},
"zC8Whw": {
@@ -2863,7 +2905,7 @@
"message": "Import",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 60]],
"origin": [["src/views/boards/index.tsx", 73]],
"translation": "Importar"
},
"2MPcep": {
@@ -3299,7 +3341,7 @@
"message": "Make template",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 73]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 91]],
"translation": "Criar modelo"
},
"IDvohQ": {
@@ -3432,7 +3474,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/boards/index.tsx", 82],
["src/views/boards/index.tsx", 95],
["src/views/home/components/Features.tsx", 73]
],
"translation": "Novo"
@@ -3550,9 +3592,16 @@
"0": ["isTemplate ? \"templates\" : \"boards\""]
},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 62]],
"origin": [["src/views/boards/components/BoardsList.tsx", 63]],
"translation": "Nenhum {0}"
},
"tlhgDC": {
"message": "No archived boards",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 63]],
"translation": "Nenhum quadro arquivado"
},
"Eg99Sc": {
"message": "No authentication methods are currently available",
"placeholders": {},
@@ -3963,7 +4012,7 @@
["src/components/DeleteLabelConfirmation.tsx", 26],
["src/components/FeedbackModal.tsx", 41],
["src/components/NewWorkspaceForm.tsx", 158],
["src/views/board/components/BoardDropdown.tsx", 63],
["src/views/board/components/BoardDropdown.tsx", 65],
["src/views/board/components/NewCardForm.tsx", 184],
["src/views/board/components/NewListForm.tsx", 79],
["src/views/board/components/NewTemplateForm.tsx", 61],
@@ -4178,7 +4227,7 @@
"message": "Remove from favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 92]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 121]],
"translation": "Remover dos favoritos"
},
"99VIgC": {
@@ -4232,7 +4281,7 @@
"message": "Removed from favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 52]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 54]],
"translation": "Removido dos favoritos"
},
"YVT40D": {
@@ -4570,8 +4619,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/auth/signup/index.tsx", 34],
["src/views/auth/signup/index.tsx", 59]
["src/views/auth/signup/index.tsx", 36],
["src/views/auth/signup/index.tsx", 61]
],
"translation": "Cadastrar | kan.bn"
},
@@ -4579,14 +4628,14 @@
"message": "Sign up disabled",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 44]],
"origin": [["src/views/auth/signup/index.tsx", 46]],
"translation": "Cadastro desabilitado"
},
"s6iE/c": {
"message": "Sign up is currently disabled. Please try again later.",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 47]],
"origin": [["src/views/auth/signup/index.tsx", 49]],
"translation": "O cadastro está temporariamente desabilitado. Por favor, tente novamente mais tarde."
},
"6FDocz": {
@@ -4809,6 +4858,20 @@
"origin": [["src/components/FeedbackModal.tsx", 34]],
"translation": "Obrigado pelo seu feedback!"
},
"NcFrgC": {
"message": "The board has been archived.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 46]],
"translation": "O quadro foi arquivado."
},
"C6gv54": {
"message": "The board has been unarchived.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 47]],
"translation": "O quadro foi desarquivado."
},
"jlB2RY": {
"message": "The current password you entered is incorrect.",
"placeholders": {},
@@ -5196,7 +5259,7 @@
"message": "Unable to update board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 62]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 64]],
"translation": "Não foi possível atualizar o quadro"
},
"XpcjLO": {
@@ -5292,6 +5355,13 @@
"origin": [["src/views/members/index.tsx", 65]],
"translation": "Não foi possível atualizar a função"
},
"It4/FS": {
"message": "Unarchive board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 111]],
"translation": "Desarquivar quadro"
},
"E8zYtd": {
"message": "unassigned <0>{0}</0> from the card",
"placeholders": {
@@ -5671,7 +5741,7 @@
"origin": [
["src/components/SettingsLayout.tsx", 46],
["src/views/board/index.tsx", 400],
["src/views/boards/index.tsx", 35],
["src/views/boards/index.tsx", 48],
["src/views/members/index.tsx", 258],
["src/views/public/board/index.tsx", 125],
["src/views/public/boards/index.tsx", 65]
@@ -5883,9 +5953,9 @@
["src/views/board/components/VisibilityButton.tsx", 72],
["src/views/board/index.tsx", 474],
["src/views/board/index.tsx", 532],
["src/views/boards/components/BoardsList.tsx", 70],
["src/views/boards/index.tsx", 46],
["src/views/boards/index.tsx", 67]
["src/views/boards/components/BoardsList.tsx", 71],
["src/views/boards/index.tsx", 59],
["src/views/boards/index.tsx", 80]
],
"translation": "Você não tem permissão"
},

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -19,7 +19,7 @@
"0": ["isTemplate ? \"Templates\" : \"Boards\""]
},
"comments": [],
"origin": [["src/views/boards/index.tsx", 40]],
"origin": [["src/views/boards/index.tsx", 53]],
"translation": "{0}"
},
"JArWcF": {
@@ -33,7 +33,7 @@
},
"comments": [],
"origin": [
["src/views/boards/index.tsx", 35],
["src/views/boards/index.tsx", 48],
["src/views/card/index.tsx", 309]
],
"translation": "{0} | {1}"
@@ -44,7 +44,7 @@
"0": ["boardName ?? \"Board\""]
},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 54]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 56]],
"translation": "{0} добавлен в избранное."
},
"bDI6VI": {
@@ -53,7 +53,7 @@
"0": ["boardName ?? \"Board\""]
},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 55]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 57]],
"translation": "{0} удалён из избранного."
},
"CJukzS": {
@@ -185,6 +185,13 @@
"origin": [["src/providers/keyboard-shortcuts.tsx", 56]],
"translation": "Действия"
},
"F6pfE9": {
"message": "Active",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 26]],
"translation": "Активно"
},
"XJOV1Y": {
"message": "Activity",
"placeholders": {},
@@ -246,7 +253,7 @@
"message": "Add description... (type '/' to open commands or '@' to mention)",
"placeholders": {},
"comments": [],
"origin": [["src/components/Editor.tsx", 471]],
"origin": [["src/components/Editor.tsx", 482]],
"translation": "Добавить описание... (введите '/' для открытия команд или '@' для упоминания)"
},
"abUZlY": {
@@ -280,7 +287,7 @@
"message": "Add to favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 93]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 122]],
"translation": "Добавить в избранное"
},
"bmXKoX": {
@@ -368,7 +375,7 @@
"message": "Added to favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 51]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 53]],
"translation": "Добавлено в избранное"
},
"14Xi3Z": {
@@ -482,7 +489,7 @@
"message": "Already have an account? <0><1>Sign in</1></0>",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 88]],
"origin": [["src/views/auth/signup/index.tsx", 90]],
"translation": "Уже есть аккаунт? <0><1>Войти</1></0>"
},
"WmPhYW": {
@@ -574,6 +581,20 @@
"origin": [["src/views/boards/components/TemplateBoards.tsx", 46]],
"translation": "Одобрение"
},
"aKJHG+": {
"message": "Archive board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 111]],
"translation": "Архивировать доску"
},
"TdfEV7": {
"message": "Archived",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 27]],
"translation": "В архиве"
},
"lo8xBK": {
"message": "Are you sure want to remove {entityLabel}?",
"placeholders": {
@@ -785,6 +806,13 @@
],
"translation": "Аналитика доски"
},
"i0ZMQl": {
"message": "Board archived",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 44]],
"translation": "Доска архивирована"
},
"wewm3j": {
"message": "Board name cannot exceed 100 characters",
"placeholders": {},
@@ -815,6 +843,13 @@
],
"translation": "Шаблоны досок"
},
"tbNcu4": {
"message": "Board unarchived",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 44]],
"translation": "Доска восстановлена из архива"
},
"Xid3K6": {
"message": "Board URL can only contain letters, numbers, and hyphens",
"placeholders": {},
@@ -873,6 +908,13 @@
],
"translation": "Доски"
},
"0RE1AJ": {
"message": "Boards you archive will appear here.",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 66]],
"translation": "Здесь будут отображаться архивированные вами доски."
},
"ZDo4HN": {
"message": "Brainstorming",
"placeholders": {},
@@ -1237,7 +1279,7 @@
"comments": [],
"origin": [
["src/views/auth/login/index.tsx", 43],
["src/views/auth/signup/index.tsx", 69]
["src/views/auth/signup/index.tsx", 71]
],
"translation": "Проверьте свой почтовый ящик"
},
@@ -1305,7 +1347,7 @@
"comments": [],
"origin": [
["src/views/auth/login/index.tsx", 48],
["src/views/auth/signup/index.tsx", 74]
["src/views/auth/signup/index.tsx", 76]
],
"translation": "Перейдите по ссылке, которую мы отправили на {magicLinkRecipient}, чтобы войти."
},
@@ -1612,8 +1654,8 @@
},
"comments": [],
"origin": [
["src/views/boards/components/BoardsList.tsx", 79],
["src/views/boards/index.tsx", 28]
["src/views/boards/components/BoardsList.tsx", 80],
["src/views/boards/index.tsx", 41]
],
"translation": "Создать новый {0}"
},
@@ -1835,7 +1877,7 @@
"message": "Delete board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 104]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 133]],
"translation": "Удалить доску"
},
"nabda1": {
@@ -1863,7 +1905,7 @@
"message": "Delete template",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 104]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 133]],
"translation": "Удалить шаблон"
},
"kYu0eF": {
@@ -2079,7 +2121,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/board/components/BoardDropdown.tsx", 84],
["src/views/board/components/BoardDropdown.tsx", 102],
["src/views/board/components/UpdateBoardSlugForm.tsx", 116]
],
"translation": "Изменить URL доски"
@@ -2622,7 +2664,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/auth/signup/index.tsx", 69],
["src/views/auth/signup/index.tsx", 71],
["src/views/home/components/Cta.tsx", 61],
["src/views/home/components/Header.tsx", 106],
["src/views/pricing/components/PricingTiers.tsx", 29],
@@ -2647,7 +2689,7 @@
"0": ["isTemplate ? \"template\" : \"board\""]
},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 65]],
"origin": [["src/views/boards/components/BoardsList.tsx", 66]],
"translation": "Начните с создания нового {0}"
},
"zC8Whw": {
@@ -2863,7 +2905,7 @@
"message": "Import",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/index.tsx", 60]],
"origin": [["src/views/boards/index.tsx", 73]],
"translation": "Импорт"
},
"2MPcep": {
@@ -3299,7 +3341,7 @@
"message": "Make template",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 73]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 91]],
"translation": "Создать шаблон"
},
"IDvohQ": {
@@ -3432,7 +3474,7 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/boards/index.tsx", 82],
["src/views/boards/index.tsx", 95],
["src/views/home/components/Features.tsx", 73]
],
"translation": "Новый"
@@ -3550,9 +3592,16 @@
"0": ["isTemplate ? \"templates\" : \"boards\""]
},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 62]],
"origin": [["src/views/boards/components/BoardsList.tsx", 63]],
"translation": "Нет {0}"
},
"tlhgDC": {
"message": "No archived boards",
"placeholders": {},
"comments": [],
"origin": [["src/views/boards/components/BoardsList.tsx", 63]],
"translation": "Нет архивированных досок"
},
"Eg99Sc": {
"message": "No authentication methods are currently available",
"placeholders": {},
@@ -3963,7 +4012,7 @@
["src/components/DeleteLabelConfirmation.tsx", 26],
["src/components/FeedbackModal.tsx", 41],
["src/components/NewWorkspaceForm.tsx", 158],
["src/views/board/components/BoardDropdown.tsx", 63],
["src/views/board/components/BoardDropdown.tsx", 65],
["src/views/board/components/NewCardForm.tsx", 184],
["src/views/board/components/NewListForm.tsx", 79],
["src/views/board/components/NewTemplateForm.tsx", 61],
@@ -4178,7 +4227,7 @@
"message": "Remove from favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 92]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 121]],
"translation": "Удалить из избранного"
},
"99VIgC": {
@@ -4232,7 +4281,7 @@
"message": "Removed from favorites",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 52]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 54]],
"translation": "Удалено из избранного"
},
"YVT40D": {
@@ -4570,8 +4619,8 @@
"placeholders": {},
"comments": [],
"origin": [
["src/views/auth/signup/index.tsx", 34],
["src/views/auth/signup/index.tsx", 59]
["src/views/auth/signup/index.tsx", 36],
["src/views/auth/signup/index.tsx", 61]
],
"translation": "Регистрация | kan.bn"
},
@@ -4579,14 +4628,14 @@
"message": "Sign up disabled",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 44]],
"origin": [["src/views/auth/signup/index.tsx", 46]],
"translation": "Регистрация отключена"
},
"s6iE/c": {
"message": "Sign up is currently disabled. Please try again later.",
"placeholders": {},
"comments": [],
"origin": [["src/views/auth/signup/index.tsx", 47]],
"origin": [["src/views/auth/signup/index.tsx", 49]],
"translation": "Регистрация сейчас недоступна. Пожалуйста, попробуйте позже."
},
"6FDocz": {
@@ -4809,6 +4858,20 @@
"origin": [["src/components/FeedbackModal.tsx", 34]],
"translation": "Спасибо за ваш отзыв!"
},
"NcFrgC": {
"message": "The board has been archived.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 46]],
"translation": "Доска была архивирована."
},
"C6gv54": {
"message": "The board has been unarchived.",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 47]],
"translation": "Доска была восстановлена из архива."
},
"jlB2RY": {
"message": "The current password you entered is incorrect.",
"placeholders": {},
@@ -5196,7 +5259,7 @@
"message": "Unable to update board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 62]],
"origin": [["src/views/board/components/BoardDropdown.tsx", 64]],
"translation": "Не удалось обновить доску"
},
"XpcjLO": {
@@ -5292,6 +5355,13 @@
"origin": [["src/views/members/index.tsx", 65]],
"translation": "Не удалось обновить роль"
},
"It4/FS": {
"message": "Unarchive board",
"placeholders": {},
"comments": [],
"origin": [["src/views/board/components/BoardDropdown.tsx", 111]],
"translation": "Восстановить доску из архива"
},
"E8zYtd": {
"message": "unassigned <0>{0}</0> from the card",
"placeholders": {
@@ -5671,7 +5741,7 @@
"origin": [
["src/components/SettingsLayout.tsx", 46],
["src/views/board/index.tsx", 400],
["src/views/boards/index.tsx", 35],
["src/views/boards/index.tsx", 48],
["src/views/members/index.tsx", 258],
["src/views/public/board/index.tsx", 125],
["src/views/public/boards/index.tsx", 65]
@@ -5883,9 +5953,9 @@
["src/views/board/components/VisibilityButton.tsx", 72],
["src/views/board/index.tsx", 474],
["src/views/board/index.tsx", 532],
["src/views/boards/components/BoardsList.tsx", 70],
["src/views/boards/index.tsx", 46],
["src/views/boards/index.tsx", 67]
["src/views/boards/components/BoardsList.tsx", 71],
["src/views/boards/index.tsx", 59],
["src/views/boards/index.tsx", 80]
],
"translation": "У вас нет прав доступа"
},

File diff suppressed because one or more lines are too long

View File

@@ -58,7 +58,7 @@ export default function LoginPage() {
</div>
</div>
)}
{!isSignUpDisabled && (
{(!isSignUpDisabled || redirect?.startsWith("/invite/")) && (
<p className="mt-4 text-sm text-light-1000 dark:text-dark-1000">
<Trans>
Don't have an account?{" "}

View File

@@ -28,7 +28,9 @@ export default function SignUpPage() {
setMagicLinkRecipient(recipient);
};
if (isSignUpDisabled) {
const isInviteFlow = redirect?.startsWith("/invite/");
if (isSignUpDisabled && !isInviteFlow) {
return (
<>
<PageHead title={t`Sign up | kan.bn`} />

View File

@@ -7,7 +7,7 @@ import {
HiOutlineStar,
HiStar,
} from "react-icons/hi2";
import { IoArchiveOutline } from "react-icons/io5";
import Dropdown from "~/components/Dropdown";
import { usePermissions } from "~/hooks/usePermissions";
import { useModal } from "~/providers/modal";
@@ -17,6 +17,7 @@ import { api } from "~/utils/api";
export default function BoardDropdown({
isTemplate,
isLoading,
isArchived,
boardPublicId,
isFavorite,
boardName,
@@ -24,28 +25,29 @@ export default function BoardDropdown({
isTemplate: boolean;
isLoading: boolean;
boardPublicId: string;
isArchived?: boolean;
isFavorite?: boolean;
boardName?: string;
}) {
const { openModal } = useModal();
const { canEditBoard, canDeleteBoard, canCreateBoard } = usePermissions();
const { showPopup } = usePopup();
const { canEditBoard, canDeleteBoard, canCreateBoard, canArchiveBoard } =
usePermissions();
const utils = api.useUtils();
const handleToggleFavorite = () => {
updateBoard.mutate({
boardPublicId,
favorite: !isFavorite,
});
};
const updateBoard = api.board.update.useMutation({
onSuccess: (data, variables) => {
onSuccess: (_data, variables) => {
void utils.board.all.invalidate();
void utils.board.byId.invalidate();
// Show popup notification
if (variables.favorite !== undefined) {
if (variables.isArchived !== undefined) {
showPopup({
header: variables.isArchived ? t`Board archived` : t`Board unarchived`,
message: variables.isArchived
? t`The board has been archived.`
: t`The board has been unarchived.`,
icon: "success",
});
} else if (variables.favorite !== undefined) {
showPopup({
header: variables.favorite
? t`Added to favorites`
@@ -65,27 +67,54 @@ export default function BoardDropdown({
});
},
});
const handleToggleFavorite = () => {
updateBoard.mutate({
boardPublicId,
favorite: !isFavorite,
});
};
const handleArchiveOrUnarchive = () => {
updateBoard.mutate({
boardPublicId,
isArchived: !isArchived,
});
};
const isArchiveActionPending = updateBoard.isPending;
const items = [
...(isTemplate && canCreateBoard
? [
{
label: t`Make template`,
action: () => openModal("CREATE_TEMPLATE"),
icon: (
<HiOutlineDocumentDuplicate className="h-[16px] w-[16px] text-dark-900" />
),
},
]
{
label: t`Make template`,
action: () => openModal("CREATE_TEMPLATE"),
icon: (
<HiOutlineDocumentDuplicate className="h-[16px] w-[16px] text-dark-900" />
),
},
]
: []),
...(!isTemplate && canEditBoard
? [
{
label: t`Edit board URL`,
action: () => openModal("UPDATE_BOARD_SLUG"),
icon: <HiLink className="h-[16px] w-[16px] text-dark-900" />,
},
]
{
label: t`Edit board URL`,
action: () => openModal("UPDATE_BOARD_SLUG"),
icon: <HiLink className="h-[16px] w-[16px] text-dark-900" />,
},
]
: []),
...(!isTemplate && canArchiveBoard
? [
{
label: isArchived ? t`Unarchive board` : t`Archive board`,
action: handleArchiveOrUnarchive,
icon: (
<IoArchiveOutline className="h-[16px] w-[16px] text-dark-900" />
),
},
]
: []),
{
label: isFavorite
@@ -100,22 +129,26 @@ export default function BoardDropdown({
},
...(canDeleteBoard
? [
{
label: isTemplate ? t`Delete template` : t`Delete board`,
action: () => openModal("DELETE_BOARD"),
icon: <HiOutlineTrash className="h-[16px] w-[16px] text-dark-900" />,
},
]
{
label: isTemplate ? t`Delete template` : t`Delete board`,
action: () => openModal("DELETE_BOARD"),
icon: (
<HiOutlineTrash className="h-[16px] w-[16px] text-dark-900" />
),
},
]
: []),
];
if (items.length === 0) {
return null;
}
return (
<Dropdown disabled={isLoading} items={items}>
<Dropdown
disabled={isLoading || isArchiveActionPending}
items={items}
>
<HiEllipsisHorizontal className="h-5 w-5 text-dark-900" />
</Dropdown>
);

View File

@@ -494,7 +494,7 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
isTemplate={!!isTemplate}
isLoading={!boardData}
boardPublicId={boardId ?? ""}
workspacePublicId={workspace.publicId}
isArchived={boardData?.isArchived ?? false}
isFavorite={boardData?.favorite}
boardName={boardData?.name}
/>
@@ -598,13 +598,12 @@ export default function BoardPage({ isTemplate }: { isTemplate?: boolean }) {
? `/templates/${boardId}/cards/${card.publicId}`
: `/cards/${card.publicId}`
}
className={`mb-2 flex !cursor-pointer flex-col ${
card.publicId.startsWith(
"PLACEHOLDER",
)
? "pointer-events-none"
: ""
}`}
className={`mb-2 flex !cursor-pointer flex-col ${card.publicId.startsWith(
"PLACEHOLDER",
)
? "pointer-events-none"
: ""
}`}
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}

View File

@@ -10,7 +10,7 @@ import { useModal } from "~/providers/modal";
import { useWorkspace } from "~/providers/workspace";
import { api } from "~/utils/api";
export function BoardsList({ isTemplate }: { isTemplate?: boolean }) {
export function BoardsList({ isTemplate, archived = false }: { isTemplate?: boolean; archived?: boolean }) {
const { workspace } = useWorkspace();
const { openModal } = useModal();
const { canCreateBoard } = usePermissions();
@@ -26,6 +26,7 @@ export function BoardsList({ isTemplate }: { isTemplate?: boolean }) {
{
workspacePublicId: workspace.publicId,
type: isTemplate ? "template" : "regular",
archived: archived,
},
{ enabled: workspace.publicId ? true : false },
);
@@ -59,10 +60,10 @@ export function BoardsList({ isTemplate }: { isTemplate?: boolean }) {
<div className="flex flex-col items-center">
<HiOutlineRectangleStack className="h-10 w-10 text-light-800 dark:text-dark-800" />
<p className="mb-2 mt-4 text-[14px] font-bold text-light-1000 dark:text-dark-950">
{t`No ${isTemplate ? "templates" : "boards"}`}
{archived ? t`No archived boards` : t`No ${isTemplate ? "templates" : "boards"}`}
</p>
<p className="text-[14px] text-light-900 dark:text-dark-900">
{t`Get started by creating a new ${isTemplate ? "template" : "board"}`}
{archived ? t`Boards you archive will appear here.` : t`Get started by creating a new ${isTemplate ? "template" : "board"}`}
</p>
</div>
<Tooltip
@@ -109,18 +110,18 @@ export function BoardsList({ isTemplate }: { isTemplate?: boolean }) {
>
<div className="group relative mr-5 flex h-[150px] w-full items-center justify-center rounded-md border border-dashed border-light-400 bg-light-50 shadow-sm hover:bg-light-200 dark:border-dark-600 dark:bg-dark-50 dark:hover:bg-dark-100">
<PatternedBackground />
<button
onClick={(e) => handleToggleFavorite(e, board.publicId, board.favorite)}
className={`absolute right-3 top-3 z-10 rounded p-1 transition-all hover:bg-light-300 dark:hover:bg-dark-200 ${board.favorite ? "" : "md:opacity-0 md:group-hover:opacity-100"
}`}
aria-label={board.favorite ? "Remove from favorites" : "Add to favorites"}
>
{board.favorite ? (
<HiStar className="h-5 w-5 text-neutral-700 dark:text-dark-1000" />
) : (
<HiOutlineStar className="h-5 w-5 text-neutral-700 dark:text-dark-800" />
)}
</button>
<button
onClick={(e) => handleToggleFavorite(e, board.publicId, board.favorite)}
className={`absolute right-3 top-3 z-10 rounded p-1 transition-all hover:bg-light-300 dark:hover:bg-dark-200 ${board.favorite ? "" : "md:opacity-0 md:group-hover:opacity-100"
}`}
aria-label={board.favorite ? "Remove from favorites" : "Add to favorites"}
>
{board.favorite ? (
<HiStar className="h-5 w-5 text-neutral-700 dark:text-dark-1000" />
) : (
<HiOutlineStar className="h-5 w-5 text-neutral-700 dark:text-dark-800" />
)}
</button>
<p className="px-4 text-[14px] font-bold text-neutral-700 dark:text-dark-1000">
{board.name}
</p>

View File

@@ -1,5 +1,12 @@
import {
Listbox,
ListboxButton,
ListboxOption,
ListboxOptions,
} from "@headlessui/react";
import { t } from "@lingui/core/macro";
import { HiArrowDownTray, HiOutlinePlusSmall } from "react-icons/hi2";
import { HiArrowDownTray, HiChevronDown, HiOutlinePlusSmall } from "react-icons/hi2";
import { useState } from "react";
import Button from "~/components/Button";
import FeedbackModal from "~/components/FeedbackModal";
@@ -15,9 +22,15 @@ import { BoardsList } from "./components/BoardsList";
import { ImportBoardsForm } from "./components/ImportBoardsForm";
import { NewBoardForm } from "./components/NewBoardForm";
const boardsTabs = [
{ key: "boards" as const, label: t`Active` },
{ key: "archived" as const, label: t`Archived` },
];
export default function BoardsPage({ isTemplate }: { isTemplate?: boolean }) {
const { openModal, modalContentType, isOpen } = useModal();
const { workspace } = useWorkspace();
const [activeTab, setActiveTab] = useState<"boards" | "archived">("boards");
const { canCreateBoard } = usePermissions();
const { tooltipContent: createModalShortcutTooltipContent } =
@@ -34,7 +47,7 @@ export default function BoardsPage({ isTemplate }: { isTemplate?: boolean }) {
<PageHead
title={t`${isTemplate ? "Templates" : "Boards"} | ${workspace.name ?? t`Workspace`}`}
/>
<div className="m-auto h-full max-w-[1100px] p-6 px-5 md:px-28 md:py-12">
<div className="m-auto h-full max-w-[1100px] p-8 px-5 md:px-28 md:py-12">
<div className="relative z-10 mb-8 flex w-full items-center justify-between">
<h1 className="font-bold tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
{t`${isTemplate ? "Templates" : "Boards"}`}
@@ -115,9 +128,79 @@ export default function BoardsPage({ isTemplate }: { isTemplate?: boolean }) {
</Modal>
</>
<div className="flex h-full flex-row">
<BoardsList isTemplate={!!isTemplate} />
</div>
{!isTemplate ? (
<div className="flex h-full w-full flex-col">
<div className="focus:outline-none">
<div className="sm:hidden">
<Listbox
value={activeTab}
onChange={(tab) => setActiveTab(tab)}
>
<div className="relative mb-4">
<ListboxButton className="w-full appearance-none rounded-md border-0 bg-light-50 py-3 pl-3 pr-10 text-left text-sm font-semibold text-light-1000 shadow-sm ring-1 ring-inset ring-light-300 dark:bg-dark-50 dark:text-dark-1000 dark:ring-dark-300 dark:focus:ring-dark-500">
{boardsTabs.find((tab) => tab.key === activeTab)?.label ??
"Select a tab"}
<HiChevronDown
aria-hidden="true"
className="pointer-events-none absolute right-3 top-1/2 h-4 w-4 -translate-y-1/2 text-light-900 dark:text-dark-900"
/>
</ListboxButton>
<ListboxOptions className="absolute z-10 mt-1 w-full rounded-md bg-light-50 py-1 text-sm shadow-lg ring-1 ring-inset ring-light-300 dark:bg-dark-50 dark:ring-dark-300">
{boardsTabs.map((tab) => (
<ListboxOption
key={tab.key}
value={tab.key}
className={({ selected }) =>
`relative cursor-pointer select-none py-2 pl-3 pr-9 ${selected
? "font-bold text-light-1000 dark:text-dark-1000"
: "font-normal text-light-1000 dark:text-dark-1000"
}`
}
>
{tab.label}
</ListboxOption>
))}
</ListboxOptions>
</div>
</Listbox>
</div>
<div className="hidden sm:block">
<div>
<nav
aria-label="Tabs"
className="-mb-px flex space-x-8 focus:outline-none"
>
{boardsTabs.map((tab) => (
<button
key={tab.key}
type="button"
onClick={() => setActiveTab(tab.key)}
className={`whitespace-nowrap px-1 py-0 mt-2 mb-8 text-sm font-semibold transition-colors focus:outline-none ${activeTab === tab.key
? "border-light-1000 text-light-1000 dark:border-dark-1000 dark:text-dark-1000"
: "border-transparent text-light-900 hover:border-light-950 hover:text-light-950 dark:text-dark-900 dark:hover:border-white/20 dark:hover:text-dark-950"
}`}
>
{tab.label}
</button>
))}
</nav>
</div>
</div>
</div>
<div className="flex h-full flex-row focus:outline-none">
{activeTab === "boards" && (
<BoardsList isTemplate={false} archived={false} />
)}
{activeTab === "archived" && (
<BoardsList isTemplate={false} archived={true} />
)}
</div>
</div>
) : (
<div className="flex h-full flex-row">
<BoardsList isTemplate={!!isTemplate} />
</div>
)}
</div>
</>
);

View File

@@ -34,6 +34,7 @@ export const boardRouter = createTRPCRouter({
z.object({
workspacePublicId: z.string().min(12),
type: z.enum(["regular", "template"]).optional(),
archived: z.boolean().optional(),
}),
)
.output(
@@ -65,7 +66,10 @@ export const boardRouter = createTRPCRouter({
ctx.db,
workspace.id,
userId,
{ type: input.type }
{
type: input.type,
archived: input.archived ?? false,
}
);
return result;
@@ -153,24 +157,24 @@ export const boardRouter = createTRPCRouter({
// Generate presigned URLs for workspace member avatars
const workspaceWithAvatarUrls = result.workspace
? {
...result.workspace,
members: await Promise.all(
result.workspace.members.map(async (member) => {
if (!member.user?.image) {
return member;
}
...result.workspace,
members: await Promise.all(
result.workspace.members.map(async (member) => {
if (!member.user?.image) {
return member;
}
const avatarUrl = await generateAvatarUrl(member.user.image);
return {
...member,
user: {
...member.user,
image: avatarUrl,
},
};
}),
),
}
const avatarUrl = await generateAvatarUrl(member.user.image);
return {
...member,
user: {
...member.user,
image: avatarUrl,
},
};
}),
),
}
: result.workspace;
// Generate presigned URLs for card member avatars
@@ -461,7 +465,8 @@ export const boardRouter = createTRPCRouter({
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/)
.optional(),
visibility: z.enum(["public", "private"]).optional(),
favorite: z.boolean().optional()
favorite: z.boolean().optional(),
isArchived: z.boolean().optional(),
}),
)
.output(z.object({ success: z.boolean() }).or(z.custom<Awaited<ReturnType<typeof boardRepo.update>>>()))
@@ -503,7 +508,7 @@ export const boardRouter = createTRPCRouter({
}
// Handle other updates (name, slug, visibility)
const hasOtherUpdates = input.name || input.slug || input.visibility !== undefined;
const hasOtherUpdates = input.name || input.slug || input.visibility !== undefined || input.isArchived !== undefined;
if (!hasOtherUpdates) {
// Only favorite was updated, return success
@@ -530,6 +535,7 @@ export const boardRouter = createTRPCRouter({
slug: input.slug,
boardPublicId: input.boardPublicId,
visibility: input.visibility,
isArchived: input.isArchived,
});
if (!result)

View File

@@ -13,6 +13,10 @@ import { mergeActivities } from "../utils/activities";
import { sendMentionEmails } from "../utils/notifications";
import { assertCanDelete, assertCanEdit, assertPermission } from "../utils/permissions";
import { generateAttachmentUrl, generateAvatarUrl } from "@kan/shared/utils";
import {
createCardWebhookPayload,
sendWebhooksForWorkspace,
} from "../utils/webhook";
export const cardRouter = createTRPCRouter({
create: protectedProcedure
@@ -165,6 +169,32 @@ export const cardRouter = createTRPCRouter({
});
}
// Fire webhooks (non-blocking)
sendWebhooksForWorkspace(
ctx.db,
list.workspaceId,
createCardWebhookPayload(
"card.created",
{
id: String(newCard.id),
title: input.title,
description: input.description,
dueDate: input.dueDate ?? null,
listId: String(newCard.listId),
},
{
boardId: list.boardPublicId,
boardName: list.boardName,
listName: list.name,
user: ctx.user
? { id: ctx.user.id, name: ctx.user.name }
: undefined,
},
),
).catch((error) => {
console.error("Webhook delivery failed:", error);
});
return newCard;
}),
addComment: protectedProcedure
@@ -1007,6 +1037,59 @@ export const cardRouter = createTRPCRouter({
await cardActivityRepo.bulkCreate(ctx.db, activities);
}
// Build changes object for webhook
const webhookChanges: Record<string, { from: unknown; to: unknown }> = {};
if (input.title && existingCard.title !== input.title) {
webhookChanges.title = { from: existingCard.title, to: input.title };
}
if (input.description && existingCard.description !== input.description) {
webhookChanges.description = {
from: existingCard.description,
to: input.description,
};
}
if (
input.dueDate !== undefined &&
previousDueDate?.getTime() !== input.dueDate?.getTime()
) {
webhookChanges.dueDate = { from: previousDueDate, to: input.dueDate };
}
if (newListId && existingCard.listId !== newListId) {
webhookChanges.listId = { from: existingCard.listId, to: newListId };
}
// Fire webhooks (non-blocking)
sendWebhooksForWorkspace(
ctx.db,
card.workspaceId,
createCardWebhookPayload(
newListId && existingCard.listId !== newListId
? "card.moved"
: "card.updated",
{
id: String(result.id),
title: result.title,
description: result.description,
dueDate: result.dueDate,
listId: String(newListId ?? existingCard.listId),
},
{
boardId: card.boardPublicId,
boardName: card.boardName,
listName: card.listName,
user: ctx.user
? { id: ctx.user.id, name: ctx.user.name }
: undefined,
changes:
Object.keys(webhookChanges).length > 0
? webhookChanges
: undefined,
},
),
).catch((error) => {
console.error("Webhook delivery failed:", error);
});
return result;
}),
delete: protectedProcedure
@@ -1054,6 +1137,9 @@ export const cardRouter = createTRPCRouter({
card.createdBy,
);
// Fetch full card data before delete for webhook
const fullCard = await cardRepo.getByPublicId(ctx.db, input.cardPublicId);
const deletedAt = new Date();
await cardRepo.softDelete(ctx.db, {
@@ -1068,6 +1154,34 @@ export const cardRouter = createTRPCRouter({
createdBy: userId,
});
// Fire webhooks (non-blocking)
if (fullCard) {
sendWebhooksForWorkspace(
ctx.db,
card.workspaceId,
createCardWebhookPayload(
"card.deleted",
{
id: String(fullCard.id),
title: fullCard.title,
description: fullCard.description,
dueDate: fullCard.dueDate,
listId: String(fullCard.listId),
},
{
boardId: card.boardPublicId,
boardName: card.boardName,
listName: card.listName,
user: ctx.user
? { id: ctx.user.id, name: ctx.user.name }
: undefined,
},
),
).catch((error) => {
console.error("Webhook delivery failed:", error);
});
}
return { success: true };
}),
});

View File

@@ -81,6 +81,16 @@ export const checklistRouter = createTRPCRouter({
return newChecklist;
}),
update: protectedProcedure
.meta({
openapi: {
summary: "Update a checklist",
method: "PUT",
path: "/checklists/{checklistPublicId}",
description: "Updates a checklist by its public ID",
tags: ["Cards"],
protect: true,
},
})
.input(
z.object({
checklistPublicId: z.string().length(12),

View File

@@ -210,11 +210,12 @@ export const memberRouter = createTRPCRouter({
input.memberPublicId,
);
if (!member)
if (!member || member.workspaceId !== workspace.id) {
throw new TRPCError({
message: `Member with public ID ${input.memberPublicId} not found`,
code: "NOT_FOUND",
});
}
const deletedMember = await memberRepo.softDelete(ctx.db, {
memberId: member.id,
@@ -720,7 +721,7 @@ export const memberRouter = createTRPCRouter({
input.memberPublicId,
);
if (!member) {
if (!member || member.workspaceId !== workspace.id) {
throw new TRPCError({
message: "Member not found",
code: "NOT_FOUND",

View File

@@ -133,7 +133,7 @@ export const permissionRouter = createTRPCRouter({
input.memberPublicId,
);
if (!member) {
if (!member || member.workspaceId !== workspace.id) {
throw new TRPCError({
message: "Member not found",
code: "NOT_FOUND",
@@ -209,7 +209,7 @@ export const permissionRouter = createTRPCRouter({
input.memberPublicId,
);
if (!member) {
if (!member || member.workspaceId !== workspace.id) {
throw new TRPCError({
message: "Member not found",
code: "NOT_FOUND",
@@ -274,7 +274,7 @@ export const permissionRouter = createTRPCRouter({
input.memberPublicId,
);
if (!member) {
if (!member || member.workspaceId !== workspace.id) {
throw new TRPCError({
message: "Member not found",
code: "NOT_FOUND",
@@ -339,7 +339,7 @@ export const permissionRouter = createTRPCRouter({
input.memberPublicId,
);
if (!member) {
if (!member || member.workspaceId !== workspace.id) {
throw new TRPCError({
message: "Member not found",
code: "NOT_FOUND",

View File

@@ -0,0 +1,528 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
vi.mock("@kan/db/repository/webhook.repo", () => ({
getActiveByWorkspaceId: vi.fn(),
}));
import * as webhookRepo from "@kan/db/repository/webhook.repo";
import {
sendWebhookToUrl,
sendWebhooksForWorkspace,
createCardWebhookPayload,
webhookUrlSchema,
type WebhookPayload,
} from "./webhook";
const mockGetActiveByWorkspaceId = webhookRepo.getActiveByWorkspaceId as ReturnType<typeof vi.fn>;
describe("webhook utilities", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.useFakeTimers();
vi.setSystemTime(new Date("2024-01-15T12:00:00.000Z"));
});
afterEach(() => {
vi.useRealTimers();
});
describe("createCardWebhookPayload", () => {
it("creates a payload with required card fields", () => {
const payload = createCardWebhookPayload(
"card.created",
{
id: "card-123",
title: "Test Card",
listId: "list-456",
},
{
boardId: "board-789",
},
);
expect(payload.event).toBe("card.created");
expect(payload.timestamp).toBe("2024-01-15T12:00:00.000Z");
expect(payload.data.card).toEqual({
id: "card-123",
title: "Test Card",
description: undefined,
dueDate: null,
listId: "list-456",
boardId: "board-789",
});
});
it("includes optional card fields when provided", () => {
const dueDate = new Date("2024-02-01T10:00:00.000Z");
const payload = createCardWebhookPayload(
"card.updated",
{
id: "card-123",
title: "Test Card",
description: "A description",
dueDate,
listId: "list-456",
},
{
boardId: "board-789",
},
);
expect(payload.data.card.description).toBe("A description");
expect(payload.data.card.dueDate).toBe("2024-02-01T10:00:00.000Z");
});
it("includes board context when provided", () => {
const payload = createCardWebhookPayload(
"card.created",
{
id: "card-123",
title: "Test Card",
listId: "list-456",
},
{
boardId: "board-789",
boardName: "My Board",
},
);
expect(payload.data.board).toEqual({
id: "board-789",
name: "My Board",
});
});
it("includes list context when provided", () => {
const payload = createCardWebhookPayload(
"card.created",
{
id: "card-123",
title: "Test Card",
listId: "list-456",
},
{
boardId: "board-789",
listName: "To Do",
},
);
expect(payload.data.list).toEqual({
id: "list-456",
name: "To Do",
});
});
it("includes user context when provided", () => {
const payload = createCardWebhookPayload(
"card.created",
{
id: "card-123",
title: "Test Card",
listId: "list-456",
},
{
boardId: "board-789",
user: {
id: "user-111",
name: "John Doe",
},
},
);
expect(payload.data.user).toEqual({
id: "user-111",
name: "John Doe",
});
});
it("includes changes for card.updated events", () => {
const payload = createCardWebhookPayload(
"card.updated",
{
id: "card-123",
title: "Updated Title",
listId: "list-456",
},
{
boardId: "board-789",
changes: {
title: { from: "Old Title", to: "Updated Title" },
},
},
);
expect(payload.data.changes).toEqual({
title: { from: "Old Title", to: "Updated Title" },
});
});
});
describe("sendWebhookToUrl", () => {
const originalFetch = global.fetch;
beforeEach(() => {
global.fetch = vi.fn();
});
afterEach(() => {
global.fetch = originalFetch;
});
const mockPayload: WebhookPayload = {
event: "card.created",
timestamp: "2024-01-15T12:00:00.000Z",
data: {
card: {
id: "card-123",
title: "Test Card",
listId: "list-456",
boardId: "board-789",
},
},
};
describe("SSRF protection", () => {
it("blocks HTTP URLs", async () => {
const result = await sendWebhookToUrl("http://example.com/webhook", undefined, mockPayload);
expect(result.success).toBe(false);
expect(result.error).toContain("HTTPS");
expect(global.fetch).not.toHaveBeenCalled();
});
it("blocks localhost", async () => {
const result = await sendWebhookToUrl("https://localhost/webhook", undefined, mockPayload);
expect(result.success).toBe(false);
expect(result.error).toContain("Localhost");
expect(global.fetch).not.toHaveBeenCalled();
});
it("blocks 127.0.0.1", async () => {
const result = await sendWebhookToUrl("https://127.0.0.1/webhook", undefined, mockPayload);
expect(result.success).toBe(false);
expect(global.fetch).not.toHaveBeenCalled();
});
it("blocks cloud metadata endpoint", async () => {
const result = await sendWebhookToUrl("https://169.254.169.254/latest/meta-data/", undefined, mockPayload);
expect(result.success).toBe(false);
expect(result.error).toContain("metadata");
expect(global.fetch).not.toHaveBeenCalled();
});
it("blocks private 10.x.x.x IPs", async () => {
const result = await sendWebhookToUrl("https://10.0.0.1/webhook", undefined, mockPayload);
expect(result.success).toBe(false);
expect(result.error).toContain("Private");
expect(global.fetch).not.toHaveBeenCalled();
});
it("blocks private 192.168.x.x IPs", async () => {
const result = await sendWebhookToUrl("https://192.168.1.1/webhook", undefined, mockPayload);
expect(result.success).toBe(false);
expect(global.fetch).not.toHaveBeenCalled();
});
it("allows valid HTTPS URLs", async () => {
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({ ok: true, status: 200 });
const result = await sendWebhookToUrl("https://example.com/webhook", undefined, mockPayload);
expect(result.success).toBe(true);
expect(global.fetch).toHaveBeenCalled();
});
});
it("sends POST request with correct headers", async () => {
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
ok: true,
status: 200,
});
await sendWebhookToUrl("https://example.com/webhook", undefined, mockPayload);
expect(global.fetch).toHaveBeenCalledWith(
"https://example.com/webhook",
expect.objectContaining({
method: "POST",
headers: expect.objectContaining({
"Content-Type": "application/json",
"X-Webhook-Event": "card.created",
"X-Webhook-Timestamp": "2024-01-15T12:00:00.000Z",
}),
body: JSON.stringify(mockPayload),
}),
);
});
it("includes signature header when secret is provided", async () => {
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
ok: true,
status: 200,
});
await sendWebhookToUrl("https://example.com/webhook", "my-secret", mockPayload);
expect(global.fetch).toHaveBeenCalledWith(
"https://example.com/webhook",
expect.objectContaining({
headers: expect.objectContaining({
"X-Webhook-Signature": expect.stringMatching(/^[a-f0-9]{64}$/),
}),
}),
);
});
it("returns success for 2xx responses", async () => {
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
ok: true,
status: 200,
});
const result = await sendWebhookToUrl(
"https://example.com/webhook",
undefined,
mockPayload,
);
expect(result).toEqual({ success: true, statusCode: 200 });
});
it("returns failure for non-2xx responses", async () => {
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValueOnce({
ok: false,
status: 500,
statusText: "Internal Server Error",
});
const result = await sendWebhookToUrl(
"https://example.com/webhook",
undefined,
mockPayload,
);
expect(result).toEqual({
success: false,
statusCode: 500,
error: "500 Internal Server Error",
});
});
it("returns failure on network error", async () => {
(global.fetch as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
new Error("Network error"),
);
const result = await sendWebhookToUrl(
"https://example.com/webhook",
undefined,
mockPayload,
);
expect(result).toEqual({
success: false,
error: "Network error",
});
});
it("returns timeout error when request takes too long", async () => {
(global.fetch as ReturnType<typeof vi.fn>).mockImplementationOnce(
() =>
new Promise((_, reject) => {
setTimeout(() => {
const error = new Error("Aborted");
error.name = "AbortError";
reject(error);
}, 15000);
}),
);
const resultPromise = sendWebhookToUrl(
"https://example.com/webhook",
undefined,
mockPayload,
);
// Advance timers to trigger the abort
vi.advanceTimersByTime(15000);
const result = await resultPromise;
expect(result).toEqual({
success: false,
error: "Request timed out",
});
});
});
describe("sendWebhooksForWorkspace", () => {
const originalFetch = global.fetch;
beforeEach(() => {
global.fetch = vi.fn();
});
afterEach(() => {
global.fetch = originalFetch;
});
const mockDb = {} as Parameters<typeof sendWebhooksForWorkspace>[0];
const mockPayload: WebhookPayload = {
event: "card.created",
timestamp: "2024-01-15T12:00:00.000Z",
data: {
card: {
id: "card-123",
title: "Test Card",
listId: "list-456",
boardId: "board-789",
},
},
};
it("sends to all active webhooks subscribed to the event", async () => {
mockGetActiveByWorkspaceId.mockResolvedValueOnce([
{
id: 1,
publicId: "wh-1",
url: "https://example.com/webhook1",
secret: "secret1",
events: ["card.created", "card.updated"],
active: true,
},
{
id: 2,
publicId: "wh-2",
url: "https://example.com/webhook2",
secret: null,
events: ["card.created"],
active: true,
},
]);
(global.fetch as ReturnType<typeof vi.fn>).mockResolvedValue({
ok: true,
status: 200,
});
await sendWebhooksForWorkspace(mockDb, 1, mockPayload);
// Event filtering now happens at DB level
expect(mockGetActiveByWorkspaceId).toHaveBeenCalledWith(
mockDb,
1,
"card.created",
);
expect(global.fetch).toHaveBeenCalledTimes(2);
expect(global.fetch).toHaveBeenCalledWith(
"https://example.com/webhook1",
expect.any(Object),
);
expect(global.fetch).toHaveBeenCalledWith(
"https://example.com/webhook2",
expect.any(Object),
);
});
it("does not send when no webhooks match the event (DB-level filtering)", async () => {
// DB-level event filter returns empty array when no webhooks match
mockGetActiveByWorkspaceId.mockResolvedValueOnce([]);
await sendWebhooksForWorkspace(mockDb, 1, mockPayload);
expect(mockGetActiveByWorkspaceId).toHaveBeenCalledWith(
mockDb,
1,
"card.created",
);
expect(global.fetch).not.toHaveBeenCalled();
});
it("continues sending to other webhooks when one fails", async () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
mockGetActiveByWorkspaceId.mockResolvedValueOnce([
{
id: 1,
publicId: "wh-1",
url: "https://example.com/webhook1",
secret: null,
events: ["card.created"],
active: true,
},
{
id: 2,
publicId: "wh-2",
url: "https://example.com/webhook2",
secret: null,
events: ["card.created"],
active: true,
},
]);
(global.fetch as ReturnType<typeof vi.fn>)
.mockResolvedValueOnce({ ok: false, status: 500, statusText: "Error" })
.mockResolvedValueOnce({ ok: true, status: 200 });
await sendWebhooksForWorkspace(mockDb, 1, mockPayload);
expect(global.fetch).toHaveBeenCalledTimes(2);
expect(consoleSpy).toHaveBeenCalledWith(
expect.stringContaining("Webhook delivery failed"),
);
consoleSpy.mockRestore();
});
it("handles empty webhook list", async () => {
mockGetActiveByWorkspaceId.mockResolvedValueOnce([]);
await sendWebhooksForWorkspace(mockDb, 1, mockPayload);
expect(global.fetch).not.toHaveBeenCalled();
});
it("catches and logs DB errors without throwing", async () => {
const consoleSpy = vi
.spyOn(console, "error")
.mockImplementation(() => {});
mockGetActiveByWorkspaceId.mockRejectedValueOnce(
new Error("DB connection failed"),
);
// Should not throw — the try/catch absorbs the error
await expect(
sendWebhooksForWorkspace(mockDb, 1, mockPayload),
).resolves.toBeUndefined();
expect(consoleSpy).toHaveBeenCalledWith(
"Failed to send webhooks for workspace:",
expect.any(Error),
);
consoleSpy.mockRestore();
});
});
describe("webhookUrlSchema", () => {
it("accepts valid HTTPS URLs", () => {
expect(webhookUrlSchema.safeParse("https://example.com/webhook").success).toBe(true);
});
it("rejects HTTP URLs", () => {
const result = webhookUrlSchema.safeParse("http://example.com/webhook");
expect(result.success).toBe(false);
});
it("rejects localhost", () => {
const result = webhookUrlSchema.safeParse("https://localhost/webhook");
expect(result.success).toBe(false);
});
it("rejects private IPs", () => {
expect(webhookUrlSchema.safeParse("https://10.0.0.1/webhook").success).toBe(false);
expect(webhookUrlSchema.safeParse("https://192.168.1.1/webhook").success).toBe(false);
});
it("rejects cloud metadata endpoints", () => {
expect(webhookUrlSchema.safeParse("https://169.254.169.254/latest").success).toBe(false);
});
});
});

View File

@@ -0,0 +1,258 @@
import crypto from "crypto";
import { z } from "zod";
import type { dbClient } from "@kan/db/client";
import type { WebhookEvent } from "@kan/db/schema";
import * as webhookRepo from "@kan/db/repository/webhook.repo";
export type WebhookEventType = WebhookEvent;
export interface WebhookPayload {
event: WebhookEventType;
timestamp: string;
data: {
card: {
id: string;
title: string;
description?: string | null;
dueDate?: string | null; // ISO string after JSON serialization
listId: string;
boardId: string;
};
board?: {
id: string;
name: string;
};
list?: {
id: string;
name: string;
};
user?: {
id: string;
name: string | null;
};
changes?: Record<string, { from: unknown; to: unknown }>;
};
}
function generateSignature(payload: string, secret: string): string {
return crypto.createHmac("sha256", secret).update(payload).digest("hex");
}
/**
* Zod schema for webhook URLs with SSRF mitigation.
* Requires HTTPS and blocks private/internal IP ranges, localhost,
* and cloud metadata endpoints.
*/
export const webhookUrlSchema = z
.string()
.url()
.max(2048)
.refine(
(url) => {
try {
return new URL(url).protocol === "https:";
} catch {
return false;
}
},
{ message: "Only HTTPS URLs are allowed" },
)
.refine(
(url) => {
try {
const hostname = new URL(url).hostname.toLowerCase();
return !(
hostname === "localhost" ||
hostname === "127.0.0.1" ||
hostname === "::1" ||
hostname === "0.0.0.0"
);
} catch {
return false;
}
},
{ message: "Localhost URLs are not allowed" },
)
.refine(
(url) => {
try {
const hostname = new URL(url).hostname.toLowerCase();
return !(
hostname === "169.254.169.254" ||
hostname === "metadata.google.internal"
);
} catch {
return false;
}
},
{ message: "Cloud metadata endpoints are not allowed" },
)
.refine(
(url) => {
try {
const hostname = new URL(url).hostname.toLowerCase();
const ipv4Match = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(hostname);
if (ipv4Match) {
const [, a, b] = ipv4Match.map(Number);
if (
a === 10 ||
(a === 172 && b! >= 16 && b! <= 31) ||
(a === 192 && b === 168)
) {
return false;
}
}
return true;
} catch {
return false;
}
},
{ message: "Private IP addresses are not allowed" },
);
/**
* Send a webhook payload to a specific URL.
*
* SSRF note: URL validation (HTTPS-only, no private IPs, no cloud metadata)
* is enforced both here at delivery time and at webhook creation via
* webhookUrlSchema. This function is only reachable by workspace admins.
*/
export async function sendWebhookToUrl(
url: string,
secret: string | undefined,
payload: WebhookPayload,
): Promise<{ success: boolean; statusCode?: number; error?: string }> {
const result = webhookUrlSchema.safeParse(url);
if (!result.success) {
return { success: false, error: result.error.issues[0]?.message };
}
const body = JSON.stringify(payload);
const headers: Record<string, string> = {
"Content-Type": "application/json",
"X-Webhook-Event": payload.event,
"X-Webhook-Timestamp": payload.timestamp,
};
if (secret) {
headers["X-Webhook-Signature"] = generateSignature(body, secret);
}
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 10000);
try {
const response = await fetch(url, {
method: "POST",
headers,
body,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
return {
success: false,
statusCode: response.status,
error: `${response.status} ${response.statusText}`,
};
}
return { success: true, statusCode: response.status };
} catch (error) {
clearTimeout(timeoutId);
if (error instanceof Error && error.name === "AbortError") {
return { success: false, error: "Request timed out" };
}
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error",
};
}
}
/**
* Send webhook to all active webhooks for a workspace that are subscribed to the event.
* Wrapped in try/catch so callers using fire-and-forget don't risk unhandled rejections.
*/
export async function sendWebhooksForWorkspace(
db: dbClient,
workspaceId: number,
payload: WebhookPayload,
): Promise<void> {
try {
// Get active webhooks for this workspace
const webhooks = await webhookRepo.getActiveByWorkspaceId(db, workspaceId);
// Filter webhooks that are subscribed to this specific event
const webhooksForEvent = webhooks.filter((webhook) =>
webhook.events.includes(payload.event),
);
// Send to all subscribed webhooks in parallel (fire and forget)
const promises = webhooksForEvent.map((webhook) =>
sendWebhookToUrl(webhook.url, webhook.secret ?? undefined, payload).then(
(result) => {
if (!result.success) {
console.error(
`Webhook delivery failed to ${webhook.url}: ${result.error}`,
);
}
},
),
);
// Wait for all to complete but don't block on failures
await Promise.allSettled(promises);
} catch (error) {
console.error("Failed to send webhooks for workspace:", error);
}
}
export function createCardWebhookPayload(
event: WebhookEventType,
card: {
id: string;
title: string;
description?: string | null;
dueDate?: Date | null;
listId: string;
},
context: {
boardId: string;
boardName?: string;
listName?: string;
user?: {
id: string;
name: string | null;
};
changes?: Record<string, { from: unknown; to: unknown }>;
},
): WebhookPayload {
return {
event,
timestamp: new Date().toISOString(),
data: {
card: {
id: card.id,
title: card.title,
description: card.description,
dueDate: card.dueDate?.toISOString() ?? null,
listId: card.listId,
boardId: context.boardId,
},
board: context.boardName
? { id: context.boardId, name: context.boardName }
: undefined,
list: context.listName
? { id: card.listId, name: context.listName }
: undefined,
user: context.user,
changes: context.changes,
},
};
}

View File

@@ -21,6 +21,7 @@
"dev": "tsc",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"test": "vitest run",
"typecheck": "tsc --noEmit --emitDeclarationOnly false"
},
"devDependencies": {

View File

@@ -33,8 +33,10 @@ export const initAuth = (db: dbClient) => {
},
emailAndPassword: {
enabled: env("NEXT_PUBLIC_ALLOW_CREDENTIALS")?.toLowerCase() === "true",
disableSignUp:
env("NEXT_PUBLIC_DISABLE_SIGN_UP")?.toLowerCase() === "true",
// Sign-up restriction is handled by the user.create.before database
// hook which checks for pending invitations, allowing invited users
// to register even when public sign-up is disabled.
disableSignUp: false,
sendResetPassword: async (data) => {
await sendEmail(data.user.email, "Reset Password", "RESET_PASSWORD", {
resetPasswordUrl: data.url,

View File

@@ -0,0 +1,195 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
vi.mock("next-runtime-env", () => ({
env: vi.fn(),
}));
vi.mock("@kan/db/repository/member.repo", () => ({
getByEmailAndStatus: vi.fn(),
getByPublicId: vi.fn(),
acceptInvite: vi.fn(),
}));
vi.mock("@kan/db/repository/user.repo", () => ({
update: vi.fn(),
}));
vi.mock("@kan/email", () => ({
notificationClient: null,
}));
vi.mock("@kan/shared", () => ({
createEmailUnsubscribeLink: vi.fn(),
createS3Client: vi.fn(),
}));
vi.mock("@aws-sdk/client-s3", () => ({
PutObjectCommand: vi.fn(),
}));
vi.mock("@novu/api/models/components", () => ({
ChatOrPushProviderEnum: { Discord: "discord" },
}));
import { env } from "next-runtime-env";
import * as memberRepo from "@kan/db/repository/member.repo";
import { createDatabaseHooks } from "./hooks";
const mockEnv = env as ReturnType<typeof vi.fn>;
const mockGetByEmailAndStatus =
memberRepo.getByEmailAndStatus as ReturnType<typeof vi.fn>;
const db = {} as Parameters<typeof createDatabaseHooks>[0];
const fakeUser = {
id: "user-1",
createdAt: new Date(),
updatedAt: new Date(),
email: "test@example.com",
emailVerified: false,
name: "Test User",
};
describe("createDatabaseHooks", () => {
const hooks = createDatabaseHooks(db);
beforeEach(() => {
vi.clearAllMocks();
});
describe("user.create.before", () => {
it("allows sign-up when DISABLE_SIGN_UP is not set", async () => {
mockEnv.mockReturnValue(undefined);
const result = await hooks.user.create.before(fakeUser, {});
expect(result).toBe(true);
expect(mockGetByEmailAndStatus).not.toHaveBeenCalled();
});
it("allows sign-up when DISABLE_SIGN_UP is false", async () => {
mockEnv.mockImplementation((key: string) =>
key === "NEXT_PUBLIC_DISABLE_SIGN_UP" ? "false" : undefined,
);
const result = await hooks.user.create.before(fakeUser, {});
expect(result).toBe(true);
expect(mockGetByEmailAndStatus).not.toHaveBeenCalled();
});
it("blocks sign-up when disabled and user has no pending invitation", async () => {
mockEnv.mockImplementation((key: string) =>
key === "NEXT_PUBLIC_DISABLE_SIGN_UP" ? "true" : undefined,
);
mockGetByEmailAndStatus.mockResolvedValue(undefined);
const result = await hooks.user.create.before(fakeUser, {});
expect(result).toBe(false);
expect(mockGetByEmailAndStatus).toHaveBeenCalledWith(
db,
"test@example.com",
"invited",
);
});
it("allows sign-up when disabled but user has a pending invitation", async () => {
mockEnv.mockImplementation((key: string) =>
key === "NEXT_PUBLIC_DISABLE_SIGN_UP" ? "true" : undefined,
);
mockGetByEmailAndStatus.mockResolvedValue({
id: "member-1",
email: "test@example.com",
status: "invited",
});
const result = await hooks.user.create.before(fakeUser, {});
expect(result).toBe(true);
expect(mockGetByEmailAndStatus).toHaveBeenCalledWith(
db,
"test@example.com",
"invited",
);
});
it("blocks sign-up when disabled and invitation exists but domain is not allowed", async () => {
mockEnv.mockImplementation((key: string) =>
key === "NEXT_PUBLIC_DISABLE_SIGN_UP" ? "true" : undefined,
);
process.env.BETTER_AUTH_ALLOWED_DOMAINS = "acme.com";
mockGetByEmailAndStatus.mockResolvedValue({
id: "member-1",
email: "test@example.com",
status: "invited",
});
const result = await hooks.user.create.before(fakeUser, {});
expect(result).toBe(false);
delete process.env.BETTER_AUTH_ALLOWED_DOMAINS;
});
// The user.create.before hook fires for ALL sign-up paths including
// OIDC/social — verify invite bypass works regardless of auth method.
it("allows OIDC/social sign-up when disabled but user has a pending invitation", async () => {
mockEnv.mockImplementation((key: string) =>
key === "NEXT_PUBLIC_DISABLE_SIGN_UP" ? "true" : undefined,
);
const oidcUser = {
...fakeUser,
id: "user-oidc",
email: "sso@corp.com",
image: "https://provider.com/avatar.jpg",
};
mockGetByEmailAndStatus.mockResolvedValue({
id: "member-2",
email: "sso@corp.com",
status: "invited",
});
const result = await hooks.user.create.before(oidcUser, {});
expect(result).toBe(true);
expect(mockGetByEmailAndStatus).toHaveBeenCalledWith(
db,
"sso@corp.com",
"invited",
);
});
it("blocks OIDC/social sign-up when disabled and user has no pending invitation", async () => {
mockEnv.mockImplementation((key: string) =>
key === "NEXT_PUBLIC_DISABLE_SIGN_UP" ? "true" : undefined,
);
const oidcUser = {
...fakeUser,
id: "user-oidc",
email: "random@external.com",
image: "https://provider.com/avatar.jpg",
};
mockGetByEmailAndStatus.mockResolvedValue(undefined);
const result = await hooks.user.create.before(oidcUser, {});
expect(result).toBe(false);
expect(mockGetByEmailAndStatus).toHaveBeenCalledWith(
db,
"random@external.com",
"invited",
);
});
it("allows sign-up when disabled, invitation exists, and domain is allowed", async () => {
mockEnv.mockImplementation((key: string) =>
key === "NEXT_PUBLIC_DISABLE_SIGN_UP" ? "true" : undefined,
);
process.env.BETTER_AUTH_ALLOWED_DOMAINS = "example.com";
mockGetByEmailAndStatus.mockResolvedValue({
id: "member-1",
email: "test@example.com",
status: "invited",
});
const result = await hooks.user.create.before(fakeUser, {});
expect(result).toBe(true);
delete process.env.BETTER_AUTH_ALLOWED_DOMAINS;
});
});
});

View File

@@ -0,0 +1,14 @@
import { defineConfig } from "vitest/config";
import { resolve } from "path";
export default defineConfig({
test: {
root: __dirname,
include: ["src/**/*.test.ts"],
},
resolve: {
alias: {
"@kan/db": resolve(__dirname, "../db/src"),
},
},
});

View File

@@ -0,0 +1,2 @@
ALTER TABLE "board" ADD COLUMN "isArchived" boolean DEFAULT false NOT NULL;--> statement-breakpoint
CREATE INDEX IF NOT EXISTS "board_is_archived_idx" ON "board" USING btree ("isArchived");

File diff suppressed because it is too large Load Diff

View File

@@ -197,6 +197,13 @@
"when": 1771192000000,
"tag": "20260129210000_AddWorkspaceWebhooks",
"breakpoints": true
},
{
"idx": 28,
"version": "7",
"when": 1772049587901,
"tag": "20260225195947_AddIsArchivedToBoard",
"breakpoints": true
}
]
}
}

View File

@@ -44,7 +44,7 @@ export const getAllByWorkspaceId = async (
db: dbClient,
workspaceId: number,
userId: string,
opts?: { type?: "regular" | "template" },
opts?: { type?: "regular" | "template"; archived?: boolean },
) => {
const boardsData = await db.query.boards.findMany({
columns: {
@@ -78,6 +78,7 @@ export const getAllByWorkspaceId = async (
eq(boards.workspaceId, workspaceId),
isNull(boards.deletedAt),
opts?.type ? eq(boards.type, opts.type) : undefined,
opts?.archived !== undefined ? eq(boards.isArchived, opts.archived) : undefined,
),
});
@@ -102,6 +103,7 @@ export const getIdByPublicId = async (db: dbClient, boardPublicId: string) => {
columns: {
id: true,
type: true,
isArchived: true,
},
where: eq(boards.publicId, boardPublicId),
});
@@ -195,6 +197,7 @@ export const getByPublicId = async (
name: true,
slug: true,
visibility: true,
isArchived: true,
},
with: {
userFavorites: {
@@ -628,6 +631,7 @@ export const update = async (
slug: string | undefined;
visibility: BoardVisibilityStatus | undefined;
boardPublicId: string;
isArchived?: boolean;
},
) => {
const [result] = await db
@@ -637,6 +641,7 @@ export const update = async (
slug: boardInput.slug,
visibility: boardInput.visibility,
updatedAt: new Date(),
...(boardInput.isArchived !== undefined && { isArchived: boardInput.isArchived })
})
.where(eq(boards.publicId, boardInput.boardPublicId))
.returning({

View File

@@ -30,22 +30,22 @@ const SYSTEM_ROLES: {
description: string;
hierarchyLevel: number;
}[] = [
{
name: "admin",
description: "Full access to all workspace features",
hierarchyLevel: 100,
},
{
name: "member",
description: "Standard member with create and edit permissions",
hierarchyLevel: 50,
},
{
name: "guest",
description: "View-only access",
hierarchyLevel: 10,
},
];
{
name: "admin",
description: "Full access to all workspace features",
hierarchyLevel: 100,
},
{
name: "member",
description: "Standard member with create and edit permissions",
hierarchyLevel: 50,
},
{
name: "guest",
description: "View-only access",
hierarchyLevel: 10,
},
];
export const getCount = async (db: dbClient) => {
const result = await db
@@ -250,7 +250,7 @@ export const getBySlugWithBoards = (db: dbClient, workspaceSlug: string) => {
slug: true,
name: true,
},
where: and(isNull(boards.deletedAt), eq(boards.visibility, "public")),
where: and(isNull(boards.deletedAt), eq(boards.visibility, "public"), eq(boards.isArchived, false)),
orderBy: [asc(boards.name)]
},
},

View File

@@ -2,7 +2,6 @@ import { relations, sql } from "drizzle-orm";
import {
bigint,
bigserial,
boolean,
index,
pgEnum,
pgTable,
@@ -12,8 +11,8 @@ import {
uniqueIndex,
uuid,
varchar,
boolean,
} from "drizzle-orm/pg-core";
import { imports } from "./imports";
import { labels } from "./labels";
import { lists } from "./lists";
@@ -56,9 +55,11 @@ export const boards = pgTable(
.references(() => workspaces.id, { onDelete: "cascade" }),
visibility: boardVisibilityEnum("visibility").notNull().default("private"),
type: boardTypeEnum("type").notNull().default("regular"),
isArchived: boolean("isArchived").notNull().default(false),
sourceBoardId: bigint("sourceBoardId", { mode: "number" }),
},
(table) => [
index("board_is_archived_idx").on(table.isArchived),
index("board_visibility_idx").on(table.visibility),
index("board_type_idx").on(table.type),
index("board_source_idx").on(table.sourceBoardId),