@anchrd/intel-ui 0.42.0 → 0.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/attachment-viewer/attachment-viewer.tsx +12 -18
- package/src/board/board-assignee/board-assignee-names.tsx +61 -0
- package/src/board/board-assignee/board-assignee-picker.tsx +101 -0
- package/src/board/board-assignee/board-assignee.ts +11 -2
- package/src/board/board-panel/board-panel.tsx +64 -49
- package/src/board/board-task/board-task.tsx +18 -2
- package/src/data/intel-data-provider/intel-data-provider.ts +16 -0
- package/src/data/intel-data-provider/intel-data-provider.types.ts +16 -0
- package/src/file-preview/file-preview-view.tsx +82 -28
- package/src/i18n/de.json +49 -44
- package/src/i18n/en.json +49 -44
- package/src/i18n/es.json +49 -44
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anchrd/intel-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"repository": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"typecheck": "tsc --noEmit"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@anchrd/intel-contract": "^0.
|
|
36
|
+
"@anchrd/intel-contract": "^0.25.0",
|
|
37
37
|
"@blocknote/core": "^0.52.1",
|
|
38
38
|
"@blocknote/react": "^0.52.1",
|
|
39
39
|
"@blocknote/shadcn": "^0.52.1",
|
|
@@ -105,8 +105,6 @@ export function AttachmentViewer({ document }: { document: NodeDocument }) {
|
|
|
105
105
|
const kind = document.version
|
|
106
106
|
? attachmentPreviewKind(document.node.title, document.version.mediaType)
|
|
107
107
|
: "unsupported";
|
|
108
|
-
const hasViewerControls = kind !== "image" && kind !== "unsupported";
|
|
109
|
-
|
|
110
108
|
return (
|
|
111
109
|
<div className="flex min-h-0 flex-1 flex-col px-6 pb-6">
|
|
112
110
|
<ActionSlot name="title-actions">
|
|
@@ -115,12 +113,21 @@ export function AttachmentViewer({ document }: { document: NodeDocument }) {
|
|
|
115
113
|
aria-label={i18n.t(detailsOpen ? "attachment.detailsHide" : "attachment.detailsShow")}
|
|
116
114
|
aria-expanded={detailsOpen}
|
|
117
115
|
onClick={() => setDetailsOpen((current) => !current)}
|
|
118
|
-
className=
|
|
116
|
+
className={cn(
|
|
117
|
+
"inline-flex size-8 shrink-0 items-center justify-center rounded-md bg-transparent outline-none hover:bg-accent focus-visible:bg-accent focus-visible:ring-2 focus-visible:ring-ring",
|
|
118
|
+
detailsOpen && "bg-accent",
|
|
119
|
+
)}
|
|
119
120
|
>
|
|
120
121
|
<Info aria-hidden="true" className="size-4" />
|
|
121
122
|
</button>
|
|
122
123
|
</ActionSlot>
|
|
123
|
-
<div
|
|
124
|
+
<div
|
|
125
|
+
data-preview-kind={kind}
|
|
126
|
+
className={cn(
|
|
127
|
+
"relative flex min-h-0 flex-1 overflow-hidden rounded-xl",
|
|
128
|
+
kind === "image" ? "bg-muted/40" : "border bg-background",
|
|
129
|
+
)}
|
|
130
|
+
>
|
|
124
131
|
<div className="relative min-h-0 min-w-0 flex-1 bg-muted/40">
|
|
125
132
|
{document.version === null ? (
|
|
126
133
|
<PreviewMessage message={i18n.t("attachment.empty")} />
|
|
@@ -147,17 +154,10 @@ export function AttachmentViewer({ document }: { document: NodeDocument }) {
|
|
|
147
154
|
<AttachmentDetails
|
|
148
155
|
document={document}
|
|
149
156
|
file={file}
|
|
150
|
-
footerInset={hasViewerControls}
|
|
151
157
|
kind={kind}
|
|
152
158
|
close={() => setDetailsOpen(false)}
|
|
153
159
|
/>
|
|
154
160
|
) : null}
|
|
155
|
-
{detailsOpen && hasViewerControls ? (
|
|
156
|
-
<div
|
|
157
|
-
aria-hidden="true"
|
|
158
|
-
className="pointer-events-none absolute inset-x-0 bottom-14 z-10 hidden border-t md:block"
|
|
159
|
-
/>
|
|
160
|
-
) : null}
|
|
161
161
|
</div>
|
|
162
162
|
</div>
|
|
163
163
|
);
|
|
@@ -212,13 +212,11 @@ function ImagePreview({ file, title }: { file: File; title: string }) {
|
|
|
212
212
|
function AttachmentDetails({
|
|
213
213
|
document,
|
|
214
214
|
file,
|
|
215
|
-
footerInset,
|
|
216
215
|
kind,
|
|
217
216
|
close,
|
|
218
217
|
}: {
|
|
219
218
|
document: NodeDocument;
|
|
220
219
|
file: File | null;
|
|
221
|
-
footerInset: boolean;
|
|
222
220
|
kind: AttachmentPreviewKind;
|
|
223
221
|
close(): void;
|
|
224
222
|
}) {
|
|
@@ -257,11 +255,7 @@ function AttachmentDetails({
|
|
|
257
255
|
return (
|
|
258
256
|
<aside
|
|
259
257
|
aria-label={i18n.t("attachment.details.title")}
|
|
260
|
-
className=
|
|
261
|
-
"absolute inset-x-0 bottom-0 z-10 max-h-[80%] overflow-auto border-t bg-background shadow-lg md:static md:z-auto md:w-80 md:max-h-none md:shrink-0 md:border-l md:border-t-0 md:shadow-none",
|
|
262
|
-
// File renderers keep their controls in a shared footer; the panel ends beside that footer.
|
|
263
|
-
footerInset && "md:mb-14",
|
|
264
|
-
)}
|
|
258
|
+
className="absolute inset-x-0 bottom-0 z-10 max-h-[80%] overflow-auto border-t bg-background shadow-lg md:static md:z-auto md:w-80 md:max-h-none md:shrink-0 md:border-l md:border-t-0 md:shadow-none"
|
|
265
259
|
>
|
|
266
260
|
<div className="flex items-center justify-between border-b px-5 py-4">
|
|
267
261
|
<h3 className="font-semibold">{i18n.t("attachment.details.title")}</h3>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { createContext, type ReactNode, useContext, useMemo } from "react";
|
|
3
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The names behind the assignee ids a board is currently showing (#700).
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ **One request for the whole board, not one per card.** A hook that resolved a single id would
|
|
9
|
+
* be called once per row and once per card, and a board with forty cards would open forty requests
|
|
10
|
+
* for four distinct people. The ids are collected where they are already all in one place — the
|
|
11
|
+
* board view — and asked for together.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ **Absent is not an error.** An id that cannot be named is missing from the answer, and the
|
|
14
|
+
* map simply has no entry for it. Callers fall back to the same wording they used before names
|
|
15
|
+
* existed at all; nobody ever draws the raw id (#258).
|
|
16
|
+
*/
|
|
17
|
+
const AssigneeNames = createContext<ReadonlyMap<string, string> | null>(null);
|
|
18
|
+
|
|
19
|
+
export function AssigneeNamesProvider({
|
|
20
|
+
ids,
|
|
21
|
+
children,
|
|
22
|
+
}: {
|
|
23
|
+
ids: readonly (string | null)[];
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
}) {
|
|
26
|
+
const { data } = useIntelRouterContext();
|
|
27
|
+
// Sorted and de-duplicated so that the same board produces the same key twice running: an
|
|
28
|
+
// unstable key would refetch on every render that reorders cards, which a drag does constantly.
|
|
29
|
+
const wanted = useMemo(
|
|
30
|
+
() => [...new Set(ids.filter((id): id is string => id !== null))].sort(),
|
|
31
|
+
[ids],
|
|
32
|
+
);
|
|
33
|
+
const query = useQuery({
|
|
34
|
+
queryKey: ["board-assignee-names", wanted],
|
|
35
|
+
// ⚠️ Not `enabled: wanted.length > 0` alone — the queryFn must never be reached with an empty
|
|
36
|
+
// list, because the contract refuses it (`min(1)`) and the refusal would surface as a broken
|
|
37
|
+
// board rather than as the empty answer it means.
|
|
38
|
+
enabled: wanted.length > 0,
|
|
39
|
+
queryFn: async () => await data.resolveBoardAssignees(wanted),
|
|
40
|
+
staleTime: 5 * 60 * 1000,
|
|
41
|
+
});
|
|
42
|
+
const names = useMemo(() => {
|
|
43
|
+
const map = new Map<string, string>();
|
|
44
|
+
for (const person of query.data?.items ?? []) map.set(person.id, person.name);
|
|
45
|
+
return map;
|
|
46
|
+
}, [query.data]);
|
|
47
|
+
return <AssigneeNames.Provider value={names}>{children}</AssigneeNames.Provider>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The name behind one id, or `null` where this browser cannot say.
|
|
52
|
+
*
|
|
53
|
+
* ⚠️ Outside a provider this answers `null` for everybody, which is exactly the behaviour every
|
|
54
|
+
* caller already handles. A screen that forgets the provider shows what it showed before this
|
|
55
|
+
* existed; it does not break, and it does not leak an id.
|
|
56
|
+
*/
|
|
57
|
+
export function useResolvedName(id: string | null): string | null {
|
|
58
|
+
const names = useContext(AssigneeNames);
|
|
59
|
+
if (id === null) return null;
|
|
60
|
+
return names?.get(id) ?? null;
|
|
61
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useQuery } from "@tanstack/react-query";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import {
|
|
4
|
+
DropdownMenu,
|
|
5
|
+
DropdownMenuContent,
|
|
6
|
+
DropdownMenuItem,
|
|
7
|
+
DropdownMenuSeparator,
|
|
8
|
+
} from "@/components/ui/dropdown-menu.tsx";
|
|
9
|
+
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
10
|
+
import { useIntelRouterContext } from "@/router/router-context.ts";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Who to give this card to (#700, D70).
|
|
14
|
+
*
|
|
15
|
+
* ⚠️ **The list is the board's, not the installation's.** Only people who can actually open this
|
|
16
|
+
* board are offered, so handing somebody a card never also hands them a reason to ask why they
|
|
17
|
+
* cannot see it.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ **Under two characters the answer is empty on purpose**, and the wording says so rather than
|
|
20
|
+
* showing "nothing found": a picker that says "no matches" for one typed letter teaches the reader
|
|
21
|
+
* that the person is not there, which is the opposite of the truth.
|
|
22
|
+
*/
|
|
23
|
+
export function AssigneePicker({
|
|
24
|
+
boardId,
|
|
25
|
+
current,
|
|
26
|
+
onPick,
|
|
27
|
+
onClose,
|
|
28
|
+
}: {
|
|
29
|
+
boardId: string;
|
|
30
|
+
current: string | null;
|
|
31
|
+
onPick(assigneeId: string | null): void;
|
|
32
|
+
onClose(): void;
|
|
33
|
+
}) {
|
|
34
|
+
const i18n = useI18n();
|
|
35
|
+
const { data } = useIntelRouterContext();
|
|
36
|
+
const [query, setQuery] = useState("");
|
|
37
|
+
const trimmed = query.trim();
|
|
38
|
+
const search = useQuery({
|
|
39
|
+
queryKey: ["board-assignee-search", boardId, trimmed],
|
|
40
|
+
enabled: trimmed.length >= 2,
|
|
41
|
+
queryFn: async () => await data.searchBoardAssignees(boardId, trimmed),
|
|
42
|
+
});
|
|
43
|
+
const found = search.data?.items ?? [];
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<DropdownMenu open onOpenChange={(next) => !next && onClose()}>
|
|
47
|
+
<DropdownMenuContent align="start" className="w-64">
|
|
48
|
+
<div className="p-1">
|
|
49
|
+
<input
|
|
50
|
+
// biome-ignore lint/a11y/noAutofocus: the menu opens on a deliberate click, never on load
|
|
51
|
+
autoFocus
|
|
52
|
+
value={query}
|
|
53
|
+
aria-label={i18n.t("board.assignee.search")}
|
|
54
|
+
placeholder={i18n.t("board.assignee.search")}
|
|
55
|
+
onChange={(event) => setQuery(event.target.value)}
|
|
56
|
+
onKeyDown={(event) => event.key === "Escape" && onClose()}
|
|
57
|
+
className="w-full rounded-md border bg-background px-2 py-1 text-xs outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
58
|
+
/>
|
|
59
|
+
</div>
|
|
60
|
+
{current === null ? null : (
|
|
61
|
+
<>
|
|
62
|
+
<DropdownMenuItem
|
|
63
|
+
onSelect={() => {
|
|
64
|
+
onClose();
|
|
65
|
+
onPick(null);
|
|
66
|
+
}}
|
|
67
|
+
>
|
|
68
|
+
{i18n.t("board.assignee.clear")}
|
|
69
|
+
</DropdownMenuItem>
|
|
70
|
+
<DropdownMenuSeparator />
|
|
71
|
+
</>
|
|
72
|
+
)}
|
|
73
|
+
{trimmed.length < 2 ? (
|
|
74
|
+
<p className="px-2 py-1.5 text-muted-foreground text-xs">
|
|
75
|
+
{i18n.t("board.assignee.keepTyping")}
|
|
76
|
+
</p>
|
|
77
|
+
) : null}
|
|
78
|
+
{trimmed.length >= 2 && !search.isPending && found.length === 0 ? (
|
|
79
|
+
<p className="px-2 py-1.5 text-muted-foreground text-xs">
|
|
80
|
+
{i18n.t("board.assignee.nobody")}
|
|
81
|
+
</p>
|
|
82
|
+
) : null}
|
|
83
|
+
{found.map((person) => (
|
|
84
|
+
<DropdownMenuItem
|
|
85
|
+
key={person.id}
|
|
86
|
+
onSelect={() => {
|
|
87
|
+
onClose();
|
|
88
|
+
onPick(person.id);
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<span className="truncate">{person.name}</span>
|
|
92
|
+
{/* ⚠️ The address disambiguates two colleagues who share a first name. It is the
|
|
93
|
+
second line, not the first: a picker reads by NAME, and an address in the same
|
|
94
|
+
weight would make every row look like a form field. */}
|
|
95
|
+
<span className="ml-auto truncate text-muted-foreground text-xs">{person.email}</span>
|
|
96
|
+
</DropdownMenuItem>
|
|
97
|
+
))}
|
|
98
|
+
</DropdownMenuContent>
|
|
99
|
+
</DropdownMenu>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
2
2
|
import { useUserName } from "@/user-name/user-name.ts";
|
|
3
|
+
import { useResolvedName } from "./board-assignee-names.tsx";
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* What to call whoever a card is for.
|
|
@@ -12,7 +13,15 @@ import { useUserName } from "@/user-name/user-name.ts";
|
|
|
12
13
|
*/
|
|
13
14
|
export function useAssigneeLabel(id: string | null): string {
|
|
14
15
|
const i18n = useI18n();
|
|
15
|
-
|
|
16
|
+
// ⚠️ TWO sources, and the order matters. `useUserName` answers for the reader themselves out of
|
|
17
|
+
// the session the shell already holds, without a request; the board's resolved map answers for
|
|
18
|
+
// everybody else. Asking the map first would make the reader's own name depend on a round trip
|
|
19
|
+
// that has no reason to exist, and it would leave them nameless on any screen without a provider.
|
|
20
|
+
const own = useUserName(id);
|
|
21
|
+
const resolved = useResolvedName(id);
|
|
16
22
|
if (id === null) return i18n.t("board.unassigned");
|
|
17
|
-
|
|
23
|
+
// ⚠️ The fallback stays what it was: a person we cannot name is "a user account", never the
|
|
24
|
+
// principal id the row carries (#258). Names arriving is an improvement on this line, not a
|
|
25
|
+
// replacement for the rule underneath it.
|
|
26
|
+
return own ?? resolved ?? i18n.t("node.someUser");
|
|
18
27
|
}
|
|
@@ -5,6 +5,7 @@ import type { ColumnVisibilityState } from "@tanstack/react-table";
|
|
|
5
5
|
import { KanbanSquare, Settings, Table2 } from "lucide-react";
|
|
6
6
|
import { Suspense, useState } from "react";
|
|
7
7
|
import { ActionSlot } from "@/app/action-slot/action-slot.tsx";
|
|
8
|
+
import { AssigneeNamesProvider } from "@/board/board-assignee/board-assignee-names.tsx";
|
|
8
9
|
import { useBoard } from "@/board/board-data/board-data.ts";
|
|
9
10
|
import { BoardKanban } from "@/board/board-kanban/board-kanban.tsx";
|
|
10
11
|
import { useOpenTask } from "@/board/board-open-task/board-open-task.ts";
|
|
@@ -48,64 +49,78 @@ export function BoardPanel({ node }: { node: Node }) {
|
|
|
48
49
|
const board = useBoard(node.id);
|
|
49
50
|
const { openId, open } = useOpenTask();
|
|
50
51
|
|
|
52
|
+
/**
|
|
53
|
+
* ⚠️ Collected HERE and nowhere lower (#700). This component holds the one unfiltered board
|
|
54
|
+
* answer that both views and the open card read, so the ids are already all in one place. A hook
|
|
55
|
+
* per card would open one request per row for a handful of distinct people, and a hook per view
|
|
56
|
+
* would ask twice for the same names when somebody switches between them.
|
|
57
|
+
*/
|
|
58
|
+
const assigneeIds = (board.columns ?? []).flatMap((column) =>
|
|
59
|
+
column.cards.map((card) => card.assigneeId),
|
|
60
|
+
);
|
|
61
|
+
|
|
51
62
|
return (
|
|
52
|
-
<
|
|
53
|
-
<
|
|
54
|
-
|
|
63
|
+
<AssigneeNamesProvider ids={assigneeIds}>
|
|
64
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-hidden">
|
|
65
|
+
<ActionSlot name="title-actions">
|
|
66
|
+
{/* ⚠️ **Gone while a card is open.** The switch chooses between two views OF THE BOARD, and
|
|
55
67
|
an open card is neither — left standing it reports `aria-pressed` for a view nobody is
|
|
56
68
|
looking at and does nothing when pressed, which is a control that lies twice. */}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
69
|
+
{(openId === null ? views : []).map(({ name, Icon }) => (
|
|
70
|
+
<button
|
|
71
|
+
key={name}
|
|
72
|
+
type="button"
|
|
73
|
+
aria-pressed={view === name}
|
|
74
|
+
aria-label={i18n.t(`board.view.${name}`)}
|
|
75
|
+
onClick={() => setView(name)}
|
|
76
|
+
className="inline-flex size-8 items-center justify-center rounded-md outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring aria-pressed:bg-accent"
|
|
77
|
+
>
|
|
78
|
+
<Icon aria-hidden="true" className="size-4" />
|
|
79
|
+
</button>
|
|
80
|
+
))}
|
|
81
|
+
{/* ⚠️ Gone while a card is open, like the view switch above it. It configures the BOARD,
|
|
70
82
|
and the line it sits in belongs to the card then — a control that acts on something the
|
|
71
83
|
reader is not looking at is worse than a missing one, because it looks like it acts on
|
|
72
84
|
what they see. */}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
+
{openId === null ? (
|
|
86
|
+
<button
|
|
87
|
+
type="button"
|
|
88
|
+
aria-label={i18n.t("board.settings.open")}
|
|
89
|
+
onClick={() => setSettingsOpen(true)}
|
|
90
|
+
className="inline-flex size-8 items-center justify-center rounded-md outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"
|
|
91
|
+
>
|
|
92
|
+
<Settings aria-hidden="true" className="size-4" />
|
|
93
|
+
</button>
|
|
94
|
+
) : null}
|
|
95
|
+
</ActionSlot>
|
|
96
|
+
{/* ⚠️ Mounted only while open, so its draft is seeded once per opening. Handed an `open` flag
|
|
85
97
|
instead, a draft re-seeded from the board would lose what somebody typed the moment a
|
|
86
98
|
write from elsewhere answers. */}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
{settingsOpen ? (
|
|
100
|
+
<BoardSettings board={board} onClose={() => setSettingsOpen(false)} />
|
|
101
|
+
) : null}
|
|
102
|
+
{openId === null ? (
|
|
103
|
+
view === "kanban" ? (
|
|
104
|
+
<KanbanView board={board} onOpen={open} />
|
|
105
|
+
) : (
|
|
106
|
+
<TableView
|
|
107
|
+
nodeId={node.id}
|
|
108
|
+
hidden={hidden}
|
|
109
|
+
onHidden={setHidden}
|
|
110
|
+
onOpen={open}
|
|
111
|
+
filter={filter}
|
|
112
|
+
onFilter={setFilter}
|
|
113
|
+
/>
|
|
114
|
+
)
|
|
91
115
|
) : (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
)
|
|
101
|
-
) : (
|
|
102
|
-
// ⚠️ Drawn from the panel's own UNFILTERED board answer, which is also what the kanban
|
|
103
|
-
// reads — the open task is not a second read of the card. ⚠️ It is NOT the table's answer:
|
|
104
|
-
// the table keeps its own filtered query, so a card filtered out of the table still opens
|
|
105
|
-
// from a link. What this cannot find is a card that is gone, or one on another board.
|
|
106
|
-
<OpenTask board={board} taskId={openId} onOpen={open} />
|
|
107
|
-
)}
|
|
108
|
-
</div>
|
|
116
|
+
// ⚠️ Drawn from the panel's own UNFILTERED board answer, which is also what the kanban
|
|
117
|
+
// reads — the open task is not a second read of the card. ⚠️ It is NOT the table's answer:
|
|
118
|
+
// the table keeps its own filtered query, so a card filtered out of the table still opens
|
|
119
|
+
// from a link. What this cannot find is a card that is gone, or one on another board.
|
|
120
|
+
<OpenTask board={board} taskId={openId} onOpen={open} />
|
|
121
|
+
)}
|
|
122
|
+
</div>
|
|
123
|
+
</AssigneeNamesProvider>
|
|
109
124
|
);
|
|
110
125
|
}
|
|
111
126
|
|
|
@@ -2,6 +2,7 @@ import type { BoardTask } from "@anchrd/intel-contract/board";
|
|
|
2
2
|
import { Lock, Plus, Square, SquareCheck } from "lucide-react";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { AssigneeChip } from "@/board/board-assignee/board-assignee.tsx";
|
|
5
|
+
import { AssigneePicker } from "@/board/board-assignee/board-assignee-picker.tsx";
|
|
5
6
|
import { BoardChip } from "@/board/board-chip/board-chip.tsx";
|
|
6
7
|
import type { BoardHandle } from "@/board/board-data/board-data.types.ts";
|
|
7
8
|
import { StatusChip } from "@/board/board-status/board-status.tsx";
|
|
@@ -183,7 +184,10 @@ export function BoardTaskDocument({
|
|
|
183
184
|
* is no list to choose from anywhere in the data provider. An entry that opens nothing is worse than
|
|
184
185
|
* a missing one, because it looks like the feature is there (`#700`).
|
|
185
186
|
*/
|
|
186
|
-
|
|
187
|
+
// ⚠️ `assignee` is first, and that is Jacks Vorgabe 2026-08-21 read back into the menu: on a card
|
|
188
|
+
// the assignee chip stands LAST so the round element does not break the row, while in the menu the
|
|
189
|
+
// most-used entry stands first. The two orders answer different questions and are not a mismatch.
|
|
190
|
+
const addable = ["assignee", "label", "start", "due"] as const;
|
|
187
191
|
const linkable = ["subtask", "blocker"] as const;
|
|
188
192
|
type Addable = (typeof addable)[number] | (typeof linkable)[number];
|
|
189
193
|
|
|
@@ -213,6 +217,9 @@ function Adder({
|
|
|
213
217
|
.flatMap((entry) => entry.cards)
|
|
214
218
|
.filter((card) => card.id !== task.id && card.parentTaskId !== task.id);
|
|
215
219
|
|
|
220
|
+
// ⚠️ The picker is not an `<input>`, so it cannot ride the `draft` path below. It gets its own
|
|
221
|
+
// branch rather than a special case inside `commit`, because a "value" that is picked from a
|
|
222
|
+
// remote list has nothing in common with one that is typed.
|
|
216
223
|
const commit = () => {
|
|
217
224
|
const value = draft.trim();
|
|
218
225
|
if (value.length === 0 || board.isWriting) return close();
|
|
@@ -267,7 +274,16 @@ function Adder({
|
|
|
267
274
|
</DropdownMenuContent>
|
|
268
275
|
</DropdownMenu>
|
|
269
276
|
|
|
270
|
-
{kind
|
|
277
|
+
{kind !== "assignee" ? null : (
|
|
278
|
+
<AssigneePicker
|
|
279
|
+
boardId={board.boardId}
|
|
280
|
+
current={task.assigneeId}
|
|
281
|
+
onPick={(assigneeId) => write({ assigneeId })}
|
|
282
|
+
onClose={close}
|
|
283
|
+
/>
|
|
284
|
+
)}
|
|
285
|
+
|
|
286
|
+
{kind === null || kind === "blocker" || kind === "assignee" ? null : (
|
|
271
287
|
<input
|
|
272
288
|
// biome-ignore lint/a11y/noAutofocus: it opens on a deliberate click, never on load
|
|
273
289
|
autoFocus
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type NamedOrCounted, ProblemDetails, SessionUser } from "@anchrd/intel-contract";
|
|
2
2
|
import {
|
|
3
|
+
BoardAssigneeList,
|
|
3
4
|
BoardGetInput,
|
|
4
5
|
BoardTaskCreateInput,
|
|
5
6
|
BoardTaskUpdateInput,
|
|
@@ -394,6 +395,21 @@ export function createIntelDataProvider(
|
|
|
394
395
|
{ method: "PATCH", body: JSON.stringify(parsed) },
|
|
395
396
|
);
|
|
396
397
|
},
|
|
398
|
+
async searchBoardAssignees(boardId, query) {
|
|
399
|
+
return await request(
|
|
400
|
+
`/boards/${encodeURIComponent(boardId)}/assignees?q=${encodeURIComponent(query)}`,
|
|
401
|
+
BoardAssigneeList,
|
|
402
|
+
);
|
|
403
|
+
},
|
|
404
|
+
// ⚠️ A POST although it reads, mirroring the route: a hundred identifiers in a query string
|
|
405
|
+
// land in proxy logs and in browser history, and they would hit the length limit long before
|
|
406
|
+
// the hundredth.
|
|
407
|
+
async resolveBoardAssignees(ids) {
|
|
408
|
+
return await request("/boards/assignees/resolve", BoardAssigneeList, {
|
|
409
|
+
method: "POST",
|
|
410
|
+
body: JSON.stringify({ ids }),
|
|
411
|
+
});
|
|
412
|
+
},
|
|
397
413
|
async updateNode(input) {
|
|
398
414
|
const parsed = UpdateNodeInput.parse(input);
|
|
399
415
|
return await request(`/nodes/${encodeURIComponent(parsed.nodeId)}`, Node, {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SessionUser } from "@anchrd/intel-contract";
|
|
2
2
|
import type {
|
|
3
|
+
BoardAssigneeList,
|
|
3
4
|
BoardGetInput,
|
|
4
5
|
BoardTaskCreateInput,
|
|
5
6
|
BoardTaskUpdateInput,
|
|
@@ -117,6 +118,21 @@ export interface IntelDataProvider {
|
|
|
117
118
|
// and shifts nothing else, but a column's contents are what the screen draws, and re-deriving
|
|
118
119
|
// them from a single row is where a second, quietly different truth would start.
|
|
119
120
|
updateBoardTask(boardId: string, input: BoardTaskUpdateInput): Promise<BoardView>;
|
|
121
|
+
/**
|
|
122
|
+
* Who a card on THIS board may be given to (#700, D70).
|
|
123
|
+
*
|
|
124
|
+
* ⚠️ Board-scoped, and the same query against two boards can give two different answers. Only
|
|
125
|
+
* people who can actually open this one are offered.
|
|
126
|
+
*/
|
|
127
|
+
searchBoardAssignees(boardId: string, query: string): Promise<BoardAssigneeList>;
|
|
128
|
+
/**
|
|
129
|
+
* What the people already on cards are called (#258, #700).
|
|
130
|
+
*
|
|
131
|
+
* ⚠️ NOT board-scoped, unlike the search above: somebody whose access was withdrawn is still who
|
|
132
|
+
* the card belongs to, and a card that read as unassigned would be a worse answer than the truth.
|
|
133
|
+
* Ids that cannot be named are absent from the answer rather than reported.
|
|
134
|
+
*/
|
|
135
|
+
resolveBoardAssignees(ids: string[]): Promise<BoardAssigneeList>;
|
|
120
136
|
listNodes(input?: Partial<ListNodesInput>): Promise<NodeList>;
|
|
121
137
|
// One level of the shared tree: the documents and the flows filed in the same folder, in one
|
|
122
138
|
// sorted list. Per level rather than recursive, so opening a folder is what costs a request.
|
|
@@ -35,12 +35,14 @@ const viewerTheme = `
|
|
|
35
35
|
}
|
|
36
36
|
.pdf-shell,.pdf-wrapper{background:var(--muted)!important;color:var(--foreground)!important}
|
|
37
37
|
.pdf-nav-pane,.pdf-nav-tabs,.pdf-nav-head{background:var(--background)!important;border-color:var(--border)!important}
|
|
38
|
-
.pdf-
|
|
39
|
-
.pdf-nav-tabs
|
|
38
|
+
@media (min-width:721px){.pdf-shell:not(.pdf-shell--nav-hidden) .pdf-content{grid-template-columns:6rem minmax(0,1fr)!important}}
|
|
39
|
+
.pdf-nav-head{display:none!important}.pdf-nav-tabs{display:flex!important;justify-content:center;padding:6px!important}
|
|
40
|
+
.pdf-nav-tabs:not([data-outline-available="true"]){display:none!important}.pdf-nav-tabs button.active{display:none!important}
|
|
41
|
+
.pdf-nav-tabs button{width:30px;min-width:30px;padding:0;border:0!important;box-shadow:none!important;font-size:0;color:var(--muted-foreground)!important}
|
|
40
42
|
.pdf-nav-tabs button svg{width:15px;height:15px;margin:auto;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
|
41
|
-
.pdf-nav-tabs button:hover,.pdf-nav-tabs button.active{background:var(--accent)!important;border
|
|
42
|
-
.pdf-page-list{gap:4px!important;padding:6px!important}.pdf-page-button{display:flex!important;min-height:
|
|
43
|
-
.pdf-page-button:hover,.pdf-page-button--active{background:var(--accent)!important;border
|
|
43
|
+
.pdf-nav-tabs button:hover,.pdf-nav-tabs button.active{background:var(--accent)!important;border:0!important;color:var(--accent-foreground)!important}
|
|
44
|
+
.pdf-page-list{gap:4px!important;padding:6px!important}.pdf-page-button{display:flex!important;width:32px!important;min-width:32px!important;height:32px!important;min-height:32px!important;margin-inline:auto!important;padding:0!important;border:0!important;box-shadow:none!important;background:transparent!important;color:var(--foreground)!important;justify-content:center!important}
|
|
45
|
+
.pdf-page-button:hover,.pdf-page-button--active{background:var(--accent)!important;border:0!important;box-shadow:none!important}
|
|
44
46
|
.pdf-page-thumb{width:28px!important;height:28px!important;border:0!important;background:transparent!important;color:var(--foreground)!important}
|
|
45
47
|
.pdf-page-label{display:none!important}.pdf-outline-button{color:var(--foreground)!important}.pdf-outline-button:hover{background:var(--accent)!important;border-color:var(--border)!important}
|
|
46
48
|
`;
|
|
@@ -92,35 +94,83 @@ export function FilePreviewView({
|
|
|
92
94
|
style.dataset.intelViewerTheme = "true";
|
|
93
95
|
style.textContent = viewerTheme;
|
|
94
96
|
root.append(style);
|
|
95
|
-
const
|
|
97
|
+
const adaptNavigation = () => {
|
|
96
98
|
for (const button of root.querySelectorAll<HTMLButtonElement>(".pdf-page-button")) {
|
|
97
99
|
const label = button.querySelector(".pdf-page-label")?.textContent?.trim();
|
|
98
100
|
if (label) button.title = label;
|
|
99
101
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
102
|
+
const tabs = root.querySelector<HTMLElement>(".pdf-nav-tabs");
|
|
103
|
+
const buttons = tabs?.querySelectorAll<HTMLButtonElement>("button");
|
|
104
|
+
if (tabs && buttons?.length === 2) {
|
|
105
|
+
tabs.setAttribute("role", "group");
|
|
106
|
+
for (const [index, button] of buttons.entries()) {
|
|
107
|
+
button.setAttribute("role", "button");
|
|
108
|
+
button.removeAttribute("aria-selected");
|
|
109
|
+
if (!button.querySelector("[data-intel-navigation-icon]")) {
|
|
110
|
+
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
111
|
+
svg.dataset.intelNavigationIcon = "true";
|
|
112
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
113
|
+
svg.setAttribute("aria-hidden", "true");
|
|
114
|
+
const paths =
|
|
115
|
+
index === 0
|
|
116
|
+
? ["M8 6h13M8 12h13M8 18h13", "M3 6h.01M3 12h.01M3 18h.01"]
|
|
117
|
+
: ["M4 6h7M9 12h11M13 18h7", "M4 6v12h9M9 6v6M13 12v6"];
|
|
118
|
+
for (const pathData of paths) {
|
|
119
|
+
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
120
|
+
path.setAttribute("d", pathData);
|
|
121
|
+
svg.append(path);
|
|
122
|
+
}
|
|
123
|
+
button.prepend(svg);
|
|
124
|
+
}
|
|
125
|
+
if (button.dataset.intelNavigationAdapted !== "true") {
|
|
126
|
+
button.dataset.intelNavigationAdapted = "true";
|
|
127
|
+
button.addEventListener("click", () => {
|
|
128
|
+
if (root.activeElement === button) buttons[index === 0 ? 1 : 0]?.focus();
|
|
129
|
+
for (const tabButton of buttons) tabButton.removeAttribute("aria-selected");
|
|
130
|
+
});
|
|
131
|
+
}
|
|
116
132
|
}
|
|
117
|
-
button.prepend(svg);
|
|
118
133
|
}
|
|
119
134
|
};
|
|
120
|
-
|
|
121
|
-
|
|
135
|
+
let outlineObserver: MutationObserver;
|
|
136
|
+
let outlineTarget: HTMLElement | null = null;
|
|
137
|
+
const probeOutline = () => {
|
|
138
|
+
const tabs = root.querySelector<HTMLElement>(".pdf-nav-tabs");
|
|
139
|
+
const [pagesTab, outlineTab] = tabs?.querySelectorAll<HTMLButtonElement>("button") ?? [];
|
|
140
|
+
const navPane = root.querySelector<HTMLElement>(".pdf-nav-pane");
|
|
141
|
+
const navList = root.querySelector<HTMLElement>(".pdf-page-list,.pdf-outline-list");
|
|
142
|
+
if (!tabs || !pagesTab || !outlineTab || !navPane || !navList) return;
|
|
143
|
+
|
|
144
|
+
outlineObserver.disconnect();
|
|
145
|
+
const startedInOutline = outlineTab.classList.contains("active");
|
|
146
|
+
if (!startedInOutline) {
|
|
147
|
+
navPane.style.visibility = "hidden";
|
|
148
|
+
outlineTab.click();
|
|
149
|
+
}
|
|
150
|
+
tabs.dataset.outlineAvailable = String(root.querySelector(".pdf-outline-button") !== null);
|
|
151
|
+
if (!startedInOutline) pagesTab.click();
|
|
152
|
+
navPane.style.removeProperty("visibility");
|
|
153
|
+
adaptNavigation();
|
|
154
|
+
outlineTarget = navList;
|
|
155
|
+
outlineObserver.observe(navList, { childList: true });
|
|
156
|
+
};
|
|
157
|
+
adaptNavigation();
|
|
158
|
+
const observer = new MutationObserver(() => {
|
|
159
|
+
adaptNavigation();
|
|
160
|
+
if (!outlineTarget || !root.contains(outlineTarget)) probeOutline();
|
|
161
|
+
});
|
|
122
162
|
observer.observe(root, { childList: true, subtree: true });
|
|
123
|
-
|
|
163
|
+
outlineObserver = new MutationObserver(() => {
|
|
164
|
+
const tabs = root.querySelector<HTMLElement>(".pdf-nav-tabs");
|
|
165
|
+
if (tabs?.dataset.outlineAvailable !== "true") probeOutline();
|
|
166
|
+
});
|
|
167
|
+
// The pinned renderer exposes no outline count. Probe synchronously between paints, then let
|
|
168
|
+
// direct navigation-list mutations trigger another probe whenever a late outline read lands.
|
|
169
|
+
probeOutline();
|
|
170
|
+
return () => {
|
|
171
|
+
observer.disconnect();
|
|
172
|
+
outlineObserver.disconnect();
|
|
173
|
+
};
|
|
124
174
|
}, [state?.ready]);
|
|
125
175
|
|
|
126
176
|
useEffect(() => {
|
|
@@ -152,7 +202,7 @@ export function FilePreviewView({
|
|
|
152
202
|
</button>
|
|
153
203
|
</ActionSlot>
|
|
154
204
|
) : null}
|
|
155
|
-
<div className="absolute inset-
|
|
205
|
+
<div className="absolute inset-0 min-h-0">
|
|
156
206
|
<FileViewer
|
|
157
207
|
ref={viewer}
|
|
158
208
|
file={file}
|
|
@@ -165,7 +215,11 @@ export function FilePreviewView({
|
|
|
165
215
|
<div
|
|
166
216
|
role="toolbar"
|
|
167
217
|
aria-label={i18n.t("attachment.viewer.controls")}
|
|
168
|
-
className=
|
|
218
|
+
className={`absolute right-3 bottom-2 left-3 z-[5] mx-auto flex min-h-10 w-fit max-w-[calc(100%-1.5rem)] items-center gap-1 rounded-full border bg-background/80 p-1 shadow-lg backdrop-blur-md ${
|
|
219
|
+
viewState?.navigation?.visible
|
|
220
|
+
? "[@media(max-width:720px)]:hidden min-[721px]:left-[calc(6rem+0.75rem)]"
|
|
221
|
+
: ""
|
|
222
|
+
}`}
|
|
169
223
|
>
|
|
170
224
|
<span role="status" aria-live="polite" aria-atomic="true" className="sr-only">
|
|
171
225
|
{search?.query
|
package/src/i18n/de.json
CHANGED
|
@@ -29,9 +29,54 @@
|
|
|
29
29
|
"archive.restoring": "{title} wird wiederhergestellt",
|
|
30
30
|
"archive.restoringAction": "Wird wiederhergestellt",
|
|
31
31
|
"archive.title": "Archiv",
|
|
32
|
+
"attachment.details.checksum": "Prüfsumme",
|
|
33
|
+
"attachment.details.dimensions": "Abmessungen",
|
|
34
|
+
"attachment.details.format": "Format",
|
|
35
|
+
"attachment.details.mimeType": "MIME-Typ",
|
|
36
|
+
"attachment.details.name": "Name",
|
|
37
|
+
"attachment.details.pages": "Seiten",
|
|
38
|
+
"attachment.details.private": "Privat",
|
|
39
|
+
"attachment.details.sections": "Abschnitte",
|
|
40
|
+
"attachment.details.size": "Größe",
|
|
41
|
+
"attachment.details.slides": "Folien",
|
|
42
|
+
"attachment.details.storage": "Speicher",
|
|
43
|
+
"attachment.details.technical": "Technisch",
|
|
44
|
+
"attachment.details.title": "Dateidetails",
|
|
45
|
+
"attachment.details.type": "Typ",
|
|
46
|
+
"attachment.details.uploaded": "Hochgeladen",
|
|
47
|
+
"attachment.details.uploadedBy": "Hochgeladen von",
|
|
48
|
+
"attachment.details.version": "Version",
|
|
49
|
+
"attachment.details.worksheets": "Tabellenblätter",
|
|
50
|
+
"attachment.detailsHide": "Dateidetails ausblenden",
|
|
51
|
+
"attachment.detailsShow": "Dateidetails anzeigen",
|
|
52
|
+
"attachment.empty": "Es wurde noch keine Datei hochgeladen.",
|
|
53
|
+
"attachment.kind.image": "Bild",
|
|
54
|
+
"attachment.kind.pdf": "PDF-Dokument",
|
|
55
|
+
"attachment.kind.presentation": "Präsentation",
|
|
56
|
+
"attachment.kind.spreadsheet": "Tabelle",
|
|
57
|
+
"attachment.kind.unsupported": "Datei",
|
|
58
|
+
"attachment.kind.word": "Word-Dokument",
|
|
59
|
+
"attachment.loadFailed": "Die Datei konnte nicht geladen werden. Das Original lässt sich weiterhin über das Menü exportieren.",
|
|
60
|
+
"attachment.unsupported": "Dieser Dateityp kann nicht angezeigt werden. Das Original lässt sich weiterhin über das Menü exportieren.",
|
|
61
|
+
"attachment.viewer.controls": "Dateisteuerung",
|
|
62
|
+
"attachment.viewer.navigation": "Seitennavigation anzeigen",
|
|
63
|
+
"attachment.viewer.nextPage": "Nächste Seite",
|
|
64
|
+
"attachment.viewer.nextResult": "Nächster Suchtreffer",
|
|
65
|
+
"attachment.viewer.previousPage": "Vorherige Seite",
|
|
66
|
+
"attachment.viewer.previousResult": "Vorheriger Suchtreffer",
|
|
67
|
+
"attachment.viewer.resetZoom": "Zoom zurücksetzen",
|
|
68
|
+
"attachment.viewer.rotateLeft": "Nach links drehen",
|
|
69
|
+
"attachment.viewer.rotateRight": "Nach rechts drehen",
|
|
70
|
+
"attachment.viewer.search": "Dokument durchsuchen",
|
|
71
|
+
"attachment.viewer.searchNoResults": "Keine Treffer",
|
|
72
|
+
"attachment.viewer.searchPlaceholder": "Dokument durchsuchen",
|
|
73
|
+
"attachment.viewer.searchResults": "Suchtreffer {current} von {total}",
|
|
74
|
+
"attachment.viewer.zoomIn": "Vergrößern",
|
|
75
|
+
"attachment.viewer.zoomOut": "Verkleinern",
|
|
32
76
|
"auth.signOut": "Abmelden",
|
|
33
77
|
"auth.signOutFailed": "Das Abmelden ist fehlgeschlagen. Prüfe deine Verbindung und versuche es erneut.",
|
|
34
78
|
"board.add": "Etwas hinzufügen",
|
|
79
|
+
"board.add.assignee": "Zuständigkeit",
|
|
35
80
|
"board.add.due": "Fällig",
|
|
36
81
|
"board.add.label": "Label",
|
|
37
82
|
"board.add.start": "Start",
|
|
@@ -39,6 +84,10 @@
|
|
|
39
84
|
"board.addCardIn": "Eine Aufgabe zu {column} hinzufügen",
|
|
40
85
|
"board.addLabel": "Label hinzufügen",
|
|
41
86
|
"board.addLink": "Verlinkung",
|
|
87
|
+
"board.assignee.clear": "Zuständigkeit entfernen",
|
|
88
|
+
"board.assignee.keepTyping": "Mindestens zwei Zeichen tippen",
|
|
89
|
+
"board.assignee.nobody": "Niemand mit Zugriff auf dieses Board passt dazu",
|
|
90
|
+
"board.assignee.search": "Person suchen",
|
|
42
91
|
"board.backTo": "Zurück zu {board}",
|
|
43
92
|
"board.cardLabel": "{title} öffnen, in {column}. Alt und eine Pfeiltaste verschiebt sie.",
|
|
44
93
|
"board.clearAssignee": "Zuständigkeit von {name} entfernen",
|
|
@@ -228,50 +277,6 @@
|
|
|
228
277
|
"flows.validate": "Prüfen, ob er laufen würde",
|
|
229
278
|
"flows.validateReady": "Dieser Flow würde jetzt starten.",
|
|
230
279
|
"flows.validateWhen": "Geprüft {when}. Der Werkzeug-Zugriff wird jedes Mal mit deinem eigenen Token erfragt, diese Antwort ist also eine Momentaufnahme.",
|
|
231
|
-
"attachment.details.checksum": "Prüfsumme",
|
|
232
|
-
"attachment.details.dimensions": "Abmessungen",
|
|
233
|
-
"attachment.details.format": "Format",
|
|
234
|
-
"attachment.details.mimeType": "MIME-Typ",
|
|
235
|
-
"attachment.details.name": "Name",
|
|
236
|
-
"attachment.details.pages": "Seiten",
|
|
237
|
-
"attachment.details.private": "Privat",
|
|
238
|
-
"attachment.details.size": "Größe",
|
|
239
|
-
"attachment.details.sections": "Abschnitte",
|
|
240
|
-
"attachment.details.slides": "Folien",
|
|
241
|
-
"attachment.details.storage": "Speicher",
|
|
242
|
-
"attachment.details.technical": "Technisch",
|
|
243
|
-
"attachment.details.title": "Dateidetails",
|
|
244
|
-
"attachment.details.type": "Typ",
|
|
245
|
-
"attachment.details.uploaded": "Hochgeladen",
|
|
246
|
-
"attachment.details.uploadedBy": "Hochgeladen von",
|
|
247
|
-
"attachment.details.version": "Version",
|
|
248
|
-
"attachment.details.worksheets": "Tabellenblätter",
|
|
249
|
-
"attachment.kind.image": "Bild",
|
|
250
|
-
"attachment.kind.pdf": "PDF-Dokument",
|
|
251
|
-
"attachment.kind.presentation": "Präsentation",
|
|
252
|
-
"attachment.kind.spreadsheet": "Tabelle",
|
|
253
|
-
"attachment.kind.unsupported": "Datei",
|
|
254
|
-
"attachment.kind.word": "Word-Dokument",
|
|
255
|
-
"attachment.detailsHide": "Dateidetails ausblenden",
|
|
256
|
-
"attachment.detailsShow": "Dateidetails anzeigen",
|
|
257
|
-
"attachment.empty": "Es wurde noch keine Datei hochgeladen.",
|
|
258
|
-
"attachment.loadFailed": "Die Datei konnte nicht geladen werden. Das Original lässt sich weiterhin über das Menü exportieren.",
|
|
259
|
-
"attachment.unsupported": "Dieser Dateityp kann nicht angezeigt werden. Das Original lässt sich weiterhin über das Menü exportieren.",
|
|
260
|
-
"attachment.viewer.controls": "Dateisteuerung",
|
|
261
|
-
"attachment.viewer.navigation": "Seitennavigation anzeigen",
|
|
262
|
-
"attachment.viewer.nextPage": "Nächste Seite",
|
|
263
|
-
"attachment.viewer.nextResult": "Nächster Suchtreffer",
|
|
264
|
-
"attachment.viewer.previousPage": "Vorherige Seite",
|
|
265
|
-
"attachment.viewer.previousResult": "Vorheriger Suchtreffer",
|
|
266
|
-
"attachment.viewer.resetZoom": "Zoom zurücksetzen",
|
|
267
|
-
"attachment.viewer.rotateLeft": "Nach links drehen",
|
|
268
|
-
"attachment.viewer.rotateRight": "Nach rechts drehen",
|
|
269
|
-
"attachment.viewer.search": "Dokument durchsuchen",
|
|
270
|
-
"attachment.viewer.searchNoResults": "Keine Treffer",
|
|
271
|
-
"attachment.viewer.searchPlaceholder": "Dokument durchsuchen",
|
|
272
|
-
"attachment.viewer.searchResults": "Suchtreffer {current} von {total}",
|
|
273
|
-
"attachment.viewer.zoomIn": "Vergrößern",
|
|
274
|
-
"attachment.viewer.zoomOut": "Verkleinern",
|
|
275
280
|
"nav.primary": "Hauptnavigation",
|
|
276
281
|
"nav.tools": "Werkzeuge",
|
|
277
282
|
"node.access": "Zugriff",
|
package/src/i18n/en.json
CHANGED
|
@@ -29,9 +29,54 @@
|
|
|
29
29
|
"archive.restoring": "Restoring {title}",
|
|
30
30
|
"archive.restoringAction": "Restoring",
|
|
31
31
|
"archive.title": "Archive",
|
|
32
|
+
"attachment.details.checksum": "Checksum",
|
|
33
|
+
"attachment.details.dimensions": "Dimensions",
|
|
34
|
+
"attachment.details.format": "Format",
|
|
35
|
+
"attachment.details.mimeType": "MIME type",
|
|
36
|
+
"attachment.details.name": "Name",
|
|
37
|
+
"attachment.details.pages": "Pages",
|
|
38
|
+
"attachment.details.private": "Private",
|
|
39
|
+
"attachment.details.sections": "Sections",
|
|
40
|
+
"attachment.details.size": "Size",
|
|
41
|
+
"attachment.details.slides": "Slides",
|
|
42
|
+
"attachment.details.storage": "Storage",
|
|
43
|
+
"attachment.details.technical": "Technical",
|
|
44
|
+
"attachment.details.title": "File details",
|
|
45
|
+
"attachment.details.type": "Type",
|
|
46
|
+
"attachment.details.uploaded": "Uploaded",
|
|
47
|
+
"attachment.details.uploadedBy": "Uploaded by",
|
|
48
|
+
"attachment.details.version": "Version",
|
|
49
|
+
"attachment.details.worksheets": "Worksheets",
|
|
50
|
+
"attachment.detailsHide": "Hide file details",
|
|
51
|
+
"attachment.detailsShow": "Show file details",
|
|
52
|
+
"attachment.empty": "No file has been uploaded yet.",
|
|
53
|
+
"attachment.kind.image": "Image",
|
|
54
|
+
"attachment.kind.pdf": "PDF document",
|
|
55
|
+
"attachment.kind.presentation": "Presentation",
|
|
56
|
+
"attachment.kind.spreadsheet": "Spreadsheet",
|
|
57
|
+
"attachment.kind.unsupported": "File",
|
|
58
|
+
"attachment.kind.word": "Word document",
|
|
59
|
+
"attachment.loadFailed": "The file could not be loaded. You can still export the original from the menu.",
|
|
60
|
+
"attachment.unsupported": "This file type cannot be previewed. You can still export the original from the menu.",
|
|
61
|
+
"attachment.viewer.controls": "File controls",
|
|
62
|
+
"attachment.viewer.navigation": "Show page navigation",
|
|
63
|
+
"attachment.viewer.nextPage": "Next page",
|
|
64
|
+
"attachment.viewer.nextResult": "Next search result",
|
|
65
|
+
"attachment.viewer.previousPage": "Previous page",
|
|
66
|
+
"attachment.viewer.previousResult": "Previous search result",
|
|
67
|
+
"attachment.viewer.resetZoom": "Reset zoom",
|
|
68
|
+
"attachment.viewer.rotateLeft": "Rotate left",
|
|
69
|
+
"attachment.viewer.rotateRight": "Rotate right",
|
|
70
|
+
"attachment.viewer.search": "Search document",
|
|
71
|
+
"attachment.viewer.searchNoResults": "No results",
|
|
72
|
+
"attachment.viewer.searchPlaceholder": "Search document",
|
|
73
|
+
"attachment.viewer.searchResults": "Search result {current} of {total}",
|
|
74
|
+
"attachment.viewer.zoomIn": "Zoom in",
|
|
75
|
+
"attachment.viewer.zoomOut": "Zoom out",
|
|
32
76
|
"auth.signOut": "Sign out",
|
|
33
77
|
"auth.signOutFailed": "Signing out failed. Check your connection and try again.",
|
|
34
78
|
"board.add": "Add something",
|
|
79
|
+
"board.add.assignee": "Assignee",
|
|
35
80
|
"board.add.due": "Due",
|
|
36
81
|
"board.add.label": "Label",
|
|
37
82
|
"board.add.start": "Start",
|
|
@@ -39,6 +84,10 @@
|
|
|
39
84
|
"board.addCardIn": "Add a task to {column}",
|
|
40
85
|
"board.addLabel": "Add a label",
|
|
41
86
|
"board.addLink": "Link",
|
|
87
|
+
"board.assignee.clear": "Remove the assignee",
|
|
88
|
+
"board.assignee.keepTyping": "Type at least two characters",
|
|
89
|
+
"board.assignee.nobody": "Nobody with access to this board matches",
|
|
90
|
+
"board.assignee.search": "Find a person",
|
|
42
91
|
"board.backTo": "Back to {board}",
|
|
43
92
|
"board.cardLabel": "Open {title}, in {column}. Alt and an arrow key moves it.",
|
|
44
93
|
"board.clearAssignee": "Take the assignment off {name}",
|
|
@@ -228,50 +277,6 @@
|
|
|
228
277
|
"flows.validate": "Check whether it would run",
|
|
229
278
|
"flows.validateReady": "This flow would start now.",
|
|
230
279
|
"flows.validateWhen": "Checked {when}. Tool access is asked with your own token each time, so this answer is a snapshot.",
|
|
231
|
-
"attachment.details.checksum": "Checksum",
|
|
232
|
-
"attachment.details.dimensions": "Dimensions",
|
|
233
|
-
"attachment.details.format": "Format",
|
|
234
|
-
"attachment.details.mimeType": "MIME type",
|
|
235
|
-
"attachment.details.name": "Name",
|
|
236
|
-
"attachment.details.pages": "Pages",
|
|
237
|
-
"attachment.details.private": "Private",
|
|
238
|
-
"attachment.details.size": "Size",
|
|
239
|
-
"attachment.details.sections": "Sections",
|
|
240
|
-
"attachment.details.slides": "Slides",
|
|
241
|
-
"attachment.details.storage": "Storage",
|
|
242
|
-
"attachment.details.technical": "Technical",
|
|
243
|
-
"attachment.details.title": "File details",
|
|
244
|
-
"attachment.details.type": "Type",
|
|
245
|
-
"attachment.details.uploaded": "Uploaded",
|
|
246
|
-
"attachment.details.uploadedBy": "Uploaded by",
|
|
247
|
-
"attachment.details.version": "Version",
|
|
248
|
-
"attachment.details.worksheets": "Worksheets",
|
|
249
|
-
"attachment.kind.image": "Image",
|
|
250
|
-
"attachment.kind.pdf": "PDF document",
|
|
251
|
-
"attachment.kind.presentation": "Presentation",
|
|
252
|
-
"attachment.kind.spreadsheet": "Spreadsheet",
|
|
253
|
-
"attachment.kind.unsupported": "File",
|
|
254
|
-
"attachment.kind.word": "Word document",
|
|
255
|
-
"attachment.detailsHide": "Hide file details",
|
|
256
|
-
"attachment.detailsShow": "Show file details",
|
|
257
|
-
"attachment.empty": "No file has been uploaded yet.",
|
|
258
|
-
"attachment.loadFailed": "The file could not be loaded. You can still export the original from the menu.",
|
|
259
|
-
"attachment.unsupported": "This file type cannot be previewed. You can still export the original from the menu.",
|
|
260
|
-
"attachment.viewer.controls": "File controls",
|
|
261
|
-
"attachment.viewer.navigation": "Show page navigation",
|
|
262
|
-
"attachment.viewer.nextPage": "Next page",
|
|
263
|
-
"attachment.viewer.nextResult": "Next search result",
|
|
264
|
-
"attachment.viewer.previousPage": "Previous page",
|
|
265
|
-
"attachment.viewer.previousResult": "Previous search result",
|
|
266
|
-
"attachment.viewer.resetZoom": "Reset zoom",
|
|
267
|
-
"attachment.viewer.rotateLeft": "Rotate left",
|
|
268
|
-
"attachment.viewer.rotateRight": "Rotate right",
|
|
269
|
-
"attachment.viewer.search": "Search document",
|
|
270
|
-
"attachment.viewer.searchNoResults": "No results",
|
|
271
|
-
"attachment.viewer.searchPlaceholder": "Search document",
|
|
272
|
-
"attachment.viewer.searchResults": "Search result {current} of {total}",
|
|
273
|
-
"attachment.viewer.zoomIn": "Zoom in",
|
|
274
|
-
"attachment.viewer.zoomOut": "Zoom out",
|
|
275
280
|
"nav.primary": "Primary navigation",
|
|
276
281
|
"nav.tools": "Tools",
|
|
277
282
|
"node.access": "Access",
|
package/src/i18n/es.json
CHANGED
|
@@ -29,9 +29,54 @@
|
|
|
29
29
|
"archive.restoring": "Restaurando {title}",
|
|
30
30
|
"archive.restoringAction": "Restaurando",
|
|
31
31
|
"archive.title": "Archivo",
|
|
32
|
+
"attachment.details.checksum": "Suma de comprobación",
|
|
33
|
+
"attachment.details.dimensions": "Dimensiones",
|
|
34
|
+
"attachment.details.format": "Formato",
|
|
35
|
+
"attachment.details.mimeType": "Tipo MIME",
|
|
36
|
+
"attachment.details.name": "Nombre",
|
|
37
|
+
"attachment.details.pages": "Páginas",
|
|
38
|
+
"attachment.details.private": "Privado",
|
|
39
|
+
"attachment.details.sections": "Secciones",
|
|
40
|
+
"attachment.details.size": "Tamaño",
|
|
41
|
+
"attachment.details.slides": "Diapositivas",
|
|
42
|
+
"attachment.details.storage": "Almacenamiento",
|
|
43
|
+
"attachment.details.technical": "Técnico",
|
|
44
|
+
"attachment.details.title": "Detalles del archivo",
|
|
45
|
+
"attachment.details.type": "Tipo",
|
|
46
|
+
"attachment.details.uploaded": "Subido",
|
|
47
|
+
"attachment.details.uploadedBy": "Subido por",
|
|
48
|
+
"attachment.details.version": "Versión",
|
|
49
|
+
"attachment.details.worksheets": "Hojas",
|
|
50
|
+
"attachment.detailsHide": "Ocultar detalles del archivo",
|
|
51
|
+
"attachment.detailsShow": "Mostrar detalles del archivo",
|
|
52
|
+
"attachment.empty": "Todavía no se ha subido ningún archivo.",
|
|
53
|
+
"attachment.kind.image": "Imagen",
|
|
54
|
+
"attachment.kind.pdf": "Documento PDF",
|
|
55
|
+
"attachment.kind.presentation": "Presentación",
|
|
56
|
+
"attachment.kind.spreadsheet": "Hoja de cálculo",
|
|
57
|
+
"attachment.kind.unsupported": "Archivo",
|
|
58
|
+
"attachment.kind.word": "Documento de Word",
|
|
59
|
+
"attachment.loadFailed": "No se pudo cargar el archivo. Aún puedes exportar el original desde el menú.",
|
|
60
|
+
"attachment.unsupported": "No se puede previsualizar este tipo de archivo. Aún puedes exportar el original desde el menú.",
|
|
61
|
+
"attachment.viewer.controls": "Controles del archivo",
|
|
62
|
+
"attachment.viewer.navigation": "Mostrar navegación de páginas",
|
|
63
|
+
"attachment.viewer.nextPage": "Página siguiente",
|
|
64
|
+
"attachment.viewer.nextResult": "Siguiente resultado de búsqueda",
|
|
65
|
+
"attachment.viewer.previousPage": "Página anterior",
|
|
66
|
+
"attachment.viewer.previousResult": "Resultado de búsqueda anterior",
|
|
67
|
+
"attachment.viewer.resetZoom": "Restablecer zoom",
|
|
68
|
+
"attachment.viewer.rotateLeft": "Girar a la izquierda",
|
|
69
|
+
"attachment.viewer.rotateRight": "Girar a la derecha",
|
|
70
|
+
"attachment.viewer.search": "Buscar en el documento",
|
|
71
|
+
"attachment.viewer.searchNoResults": "Sin resultados",
|
|
72
|
+
"attachment.viewer.searchPlaceholder": "Buscar en el documento",
|
|
73
|
+
"attachment.viewer.searchResults": "Resultado {current} de {total}",
|
|
74
|
+
"attachment.viewer.zoomIn": "Ampliar",
|
|
75
|
+
"attachment.viewer.zoomOut": "Reducir",
|
|
32
76
|
"auth.signOut": "Cerrar sesión",
|
|
33
77
|
"auth.signOutFailed": "El cierre de sesión ha fallado. Comprueba tu conexión e inténtalo de nuevo.",
|
|
34
78
|
"board.add": "Añadir algo",
|
|
79
|
+
"board.add.assignee": "Responsable",
|
|
35
80
|
"board.add.due": "Vencimiento",
|
|
36
81
|
"board.add.label": "Etiqueta",
|
|
37
82
|
"board.add.start": "Inicio",
|
|
@@ -39,6 +84,10 @@
|
|
|
39
84
|
"board.addCardIn": "Añadir una tarea a {column}",
|
|
40
85
|
"board.addLabel": "Añadir etiqueta",
|
|
41
86
|
"board.addLink": "Vínculo",
|
|
87
|
+
"board.assignee.clear": "Quitar el responsable",
|
|
88
|
+
"board.assignee.keepTyping": "Escribe al menos dos caracteres",
|
|
89
|
+
"board.assignee.nobody": "Nadie con acceso a este tablero coincide",
|
|
90
|
+
"board.assignee.search": "Buscar una persona",
|
|
42
91
|
"board.backTo": "Volver a {board}",
|
|
43
92
|
"board.cardLabel": "Abrir {title}, en {column}. Alt y una flecha la mueve.",
|
|
44
93
|
"board.clearAssignee": "Quitar la responsabilidad de {name}",
|
|
@@ -228,50 +277,6 @@
|
|
|
228
277
|
"flows.validate": "Comprobar si se ejecutaría",
|
|
229
278
|
"flows.validateReady": "Este flujo empezaría ahora mismo.",
|
|
230
279
|
"flows.validateWhen": "Comprobado {when}. El acceso a las herramientas se pide cada vez con tu propio token, así que esta respuesta es una instantánea.",
|
|
231
|
-
"attachment.details.checksum": "Suma de comprobación",
|
|
232
|
-
"attachment.details.dimensions": "Dimensiones",
|
|
233
|
-
"attachment.details.format": "Formato",
|
|
234
|
-
"attachment.details.mimeType": "Tipo MIME",
|
|
235
|
-
"attachment.details.name": "Nombre",
|
|
236
|
-
"attachment.details.pages": "Páginas",
|
|
237
|
-
"attachment.details.private": "Privado",
|
|
238
|
-
"attachment.details.size": "Tamaño",
|
|
239
|
-
"attachment.details.sections": "Secciones",
|
|
240
|
-
"attachment.details.slides": "Diapositivas",
|
|
241
|
-
"attachment.details.storage": "Almacenamiento",
|
|
242
|
-
"attachment.details.technical": "Técnico",
|
|
243
|
-
"attachment.details.title": "Detalles del archivo",
|
|
244
|
-
"attachment.details.type": "Tipo",
|
|
245
|
-
"attachment.details.uploaded": "Subido",
|
|
246
|
-
"attachment.details.uploadedBy": "Subido por",
|
|
247
|
-
"attachment.details.version": "Versión",
|
|
248
|
-
"attachment.details.worksheets": "Hojas",
|
|
249
|
-
"attachment.kind.image": "Imagen",
|
|
250
|
-
"attachment.kind.pdf": "Documento PDF",
|
|
251
|
-
"attachment.kind.presentation": "Presentación",
|
|
252
|
-
"attachment.kind.spreadsheet": "Hoja de cálculo",
|
|
253
|
-
"attachment.kind.unsupported": "Archivo",
|
|
254
|
-
"attachment.kind.word": "Documento de Word",
|
|
255
|
-
"attachment.detailsHide": "Ocultar detalles del archivo",
|
|
256
|
-
"attachment.detailsShow": "Mostrar detalles del archivo",
|
|
257
|
-
"attachment.empty": "Todavía no se ha subido ningún archivo.",
|
|
258
|
-
"attachment.loadFailed": "No se pudo cargar el archivo. Aún puedes exportar el original desde el menú.",
|
|
259
|
-
"attachment.unsupported": "No se puede previsualizar este tipo de archivo. Aún puedes exportar el original desde el menú.",
|
|
260
|
-
"attachment.viewer.controls": "Controles del archivo",
|
|
261
|
-
"attachment.viewer.navigation": "Mostrar navegación de páginas",
|
|
262
|
-
"attachment.viewer.nextPage": "Página siguiente",
|
|
263
|
-
"attachment.viewer.nextResult": "Siguiente resultado de búsqueda",
|
|
264
|
-
"attachment.viewer.previousPage": "Página anterior",
|
|
265
|
-
"attachment.viewer.previousResult": "Resultado de búsqueda anterior",
|
|
266
|
-
"attachment.viewer.resetZoom": "Restablecer zoom",
|
|
267
|
-
"attachment.viewer.rotateLeft": "Girar a la izquierda",
|
|
268
|
-
"attachment.viewer.rotateRight": "Girar a la derecha",
|
|
269
|
-
"attachment.viewer.search": "Buscar en el documento",
|
|
270
|
-
"attachment.viewer.searchNoResults": "Sin resultados",
|
|
271
|
-
"attachment.viewer.searchPlaceholder": "Buscar en el documento",
|
|
272
|
-
"attachment.viewer.searchResults": "Resultado {current} de {total}",
|
|
273
|
-
"attachment.viewer.zoomIn": "Ampliar",
|
|
274
|
-
"attachment.viewer.zoomOut": "Reducir",
|
|
275
280
|
"nav.primary": "Navegación principal",
|
|
276
281
|
"nav.tools": "Herramientas",
|
|
277
282
|
"node.access": "Acceso",
|