texlite 0.9.2 → 0.9.4
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/DESIGN.md +29 -6
- package/OPERATIONS.md +9 -7
- package/README.md +6 -3
- package/README.zh-CN.md +6 -2
- package/dist/client/assets/AdminUsers-B12FJUMy.js +1 -0
- package/dist/client/assets/CitationLibraryDialog-BTmnCP4V.js +1 -0
- package/dist/client/assets/Dashboard-JA5_M3TK.js +2 -0
- package/dist/client/assets/{GitDialog-CTWAU8q6.js → GitDialog--MoUIW0n.js} +1 -1
- package/dist/client/assets/{HistoryDialog-Mo4p8HEg.js → HistoryDialog-LgKbjsTZ.js} +1 -1
- package/dist/client/assets/LatexEditor-Dj2zIaQm.js +138 -0
- package/dist/client/assets/{PdfPreview-BUpELZd-.js → PdfPreview-C_W7w360.js} +1 -1
- package/dist/client/assets/ProjectIconPickerDialog-GzRuM0oQ.js +1 -0
- package/dist/client/assets/{ProjectNavigationDialogs-CpjkXZTz.js → ProjectNavigationDialogs-BiArHmQ9.js} +1 -1
- package/dist/client/assets/ProjectSettings-DgJohWUv.js +1 -0
- package/dist/client/assets/ProjectWorkspace-C6sYiBF2.js +32 -0
- package/dist/client/assets/{SelectionHistoryDialog-9FWlc3Rb.js → SelectionHistoryDialog-Bpv6nvrC.js} +2 -2
- package/dist/client/assets/SystemMetricsDialog-hJQATmd4.js +1 -0
- package/dist/client/assets/{TagManagementDialog-BHIA0vEA.js → TagManagementDialog-BHC8qULq.js} +1 -1
- package/dist/client/assets/{WordCountDialog-DFIWvLrP.js → WordCountDialog-DZhhO94k.js} +1 -1
- package/dist/client/assets/check-C1db8iNn.js +1 -0
- package/dist/client/assets/{editor-3rgn4IYh.js → editor-B9HNwtsd.js} +1 -1
- package/dist/client/assets/hard-drive-BWZnOgnm.js +1 -0
- package/dist/client/assets/index-BiwJj0Np.css +1 -0
- package/dist/client/assets/{index-CNTGCh7t.js → index-kTamGfTl.js} +1 -1
- package/dist/client/assets/index-p1NHkGGG.js +12 -0
- package/dist/client/assets/{key-round-BjL9H8-H.js → key-round-CoDwyVJc.js} +1 -1
- package/dist/client/assets/{minimize-2-DDCq8c49.js → minimize-2-D100opW2.js} +1 -1
- package/dist/client/assets/{minus-DgKZCT9F.js → minus-DDTYB-Oz.js} +1 -1
- package/dist/client/assets/{plus-CezUzSIZ.js → plus-BVXOlQXP.js} +1 -1
- package/dist/client/assets/{refresh-cw-CDFNL-qu.js → refresh-cw-LtwnqlWh.js} +1 -1
- package/dist/client/assets/{search-CrDJjHoj.js → search-DXo_aAp0.js} +1 -1
- package/dist/client/assets/{sigma-DOcJr7VG.js → sigma-BgvHGPto.js} +1 -1
- package/dist/client/assets/{spellCheck-BgWyesvf.js → spellCheck-BtRppKcC.js} +1 -1
- package/dist/client/assets/{tags-BedQ75lh.js → tags-m9kppdi0.js} +1 -1
- package/dist/client/assets/{type-glZNdzZR.js → type-YO4aprqE.js} +1 -1
- package/dist/client/assets/{user-plus-gQ6q5Nom.js → user-plus-MRmKeDtX.js} +1 -1
- package/dist/client/assets/x-eisRQJv_.js +1 -0
- package/dist/client/index.html +2 -2
- package/dist/client/pdf-download.svg +2 -0
- package/dist/client/pdf-file.svg +19 -0
- package/dist/client/tex-file.svg +7 -0
- package/dist/server/cli.js +5 -2
- package/dist/server/collaboration.js +6 -19
- package/dist/server/db.js +91 -10
- package/dist/server/latexDocumentGraph.js +127 -16
- package/dist/server/projectOutline.js +103 -91
- package/dist/server/projectReferences.js +1 -1
- package/dist/server/routes/comments.js +10 -4
- package/dist/server/routes/projectFiles.js +6 -1
- package/dist/server/routes/projectShared.js +33 -12
- package/dist/shared/collaborationProtocol.js +31 -0
- package/dist/shared/latexDependencies.js +83 -23
- package/dist/shared/writingChecks.js +2 -2
- package/package.json +4 -3
- package/dist/client/assets/AdminUsers-Buf5ZclR.js +0 -1
- package/dist/client/assets/CitationLibraryDialog-DrKbIlD-.js +0 -1
- package/dist/client/assets/Dashboard-DLk2j3oq.js +0 -2
- package/dist/client/assets/LatexEditor-CwvakYva.js +0 -135
- package/dist/client/assets/ProjectIconPickerDialog-BiWSwDdt.js +0 -1
- package/dist/client/assets/ProjectSettings-DoxitHbX.js +0 -1
- package/dist/client/assets/ProjectWorkspace-iNqXZlsj.js +0 -29
- package/dist/client/assets/SystemMetricsDialog-24kifZO6.js +0 -1
- package/dist/client/assets/check-C3GZh6a1.js +0 -1
- package/dist/client/assets/chevron-left-l1oupvGO.js +0 -1
- package/dist/client/assets/hard-drive-B-NSDVVA.js +0 -1
- package/dist/client/assets/index-CrlfruX_.css +0 -1
- package/dist/client/assets/index-DIM_KiG2.js +0 -12
- package/dist/client/assets/x-CSzFikn2.js +0 -1
|
@@ -1,45 +1,55 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
import { maskLatexComments } from "../shared/latexLiterals.js";
|
|
3
|
+
import { findLatexSourceIncludes } from "../shared/latexDependencies.js";
|
|
4
|
+
import { forEachLatexCommand, readLatexMandatoryArguments } from "../shared/latexScanner.js";
|
|
5
|
+
import { resolveLatexInclude, toLatexIncludeDirective } from "./latexDocumentGraph.js";
|
|
6
|
+
import { listProjectFiles, listProjectFilesAsync, resolveSourcePath, safeRelativePath } from "./files.js";
|
|
7
|
+
const levels = new Map([
|
|
8
|
+
["part", 0],
|
|
9
|
+
["chapter", 0],
|
|
10
|
+
["section", 1],
|
|
11
|
+
["subsection", 2],
|
|
12
|
+
["subsubsection", 3],
|
|
13
|
+
["paragraph", 4]
|
|
14
|
+
]);
|
|
6
15
|
export function buildProjectOutline(config, projectId, mainFileInput) {
|
|
7
16
|
const mainFile = safeRelativePath(mainFileInput);
|
|
17
|
+
return buildProjectOutlineFromFiles(config, projectId, mainFile, sourceFilePaths(listProjectFiles(config, projectId)));
|
|
18
|
+
}
|
|
19
|
+
function buildProjectOutlineFromFiles(config, projectId, mainFile, availableFiles) {
|
|
8
20
|
const result = [];
|
|
9
21
|
const visited = new Set();
|
|
10
|
-
const visit = (filePath) => {
|
|
22
|
+
const visit = (filePath, importBase) => {
|
|
11
23
|
if (visited.has(filePath) || visited.size >= 200)
|
|
12
24
|
return;
|
|
13
25
|
visited.add(filePath);
|
|
14
26
|
const absolute = resolveSourcePath(config, projectId, filePath);
|
|
15
27
|
if (!fs.existsSync(absolute) || !fs.statSync(absolute).isFile() || fs.statSync(absolute).size > 3 * 1024 * 1024)
|
|
16
28
|
return;
|
|
17
|
-
const content =
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const start = (command.index ?? 0) + command[0].length;
|
|
22
|
-
const argument = balancedArgument(content, start);
|
|
23
|
-
if (!argument)
|
|
24
|
-
continue;
|
|
25
|
-
if (command[1]) {
|
|
26
|
-
result.push({ path: filePath, line: lineAtOffset(lineStarts, command.index ?? 0), level: levels[command[1]], title: cleanTitle(argument.value) });
|
|
29
|
+
const content = fs.readFileSync(absolute, "utf8");
|
|
30
|
+
for (const event of scanOutlineSource(content)) {
|
|
31
|
+
if (event.type === "heading") {
|
|
32
|
+
result.push({ path: filePath, line: event.line, level: event.level, title: event.title });
|
|
27
33
|
continue;
|
|
28
34
|
}
|
|
29
|
-
const included =
|
|
35
|
+
const included = resolveLatexInclude(event.directive, importBase, availableFiles);
|
|
30
36
|
if (included)
|
|
31
|
-
visit(included);
|
|
37
|
+
visit(included.path, included.importBase);
|
|
32
38
|
}
|
|
33
39
|
};
|
|
34
|
-
visit(mainFile);
|
|
40
|
+
visit(mainFile, "");
|
|
35
41
|
return result;
|
|
36
42
|
}
|
|
37
43
|
/** Async outline builder used by the HTTP path so large projects do not block the event loop. */
|
|
38
44
|
export async function buildProjectOutlineAsync(config, projectId, mainFileInput) {
|
|
39
45
|
const mainFile = safeRelativePath(mainFileInput);
|
|
46
|
+
const availableFiles = sourceFilePaths(await listProjectFilesAsync(config, projectId));
|
|
47
|
+
return buildProjectOutlineAsyncFromFiles(config, projectId, mainFile, availableFiles);
|
|
48
|
+
}
|
|
49
|
+
async function buildProjectOutlineAsyncFromFiles(config, projectId, mainFile, availableFiles) {
|
|
40
50
|
const result = [];
|
|
41
51
|
const visited = new Set();
|
|
42
|
-
const visit = async (filePath) => {
|
|
52
|
+
const visit = async (filePath, importBase) => {
|
|
43
53
|
if (visited.has(filePath) || visited.size >= 200)
|
|
44
54
|
return;
|
|
45
55
|
visited.add(filePath);
|
|
@@ -54,28 +64,22 @@ export async function buildProjectOutlineAsync(config, projectId, mainFileInput)
|
|
|
54
64
|
return;
|
|
55
65
|
let content;
|
|
56
66
|
try {
|
|
57
|
-
content =
|
|
67
|
+
content = await fs.promises.readFile(resolveSourcePath(config, projectId, filePath), "utf8");
|
|
58
68
|
}
|
|
59
69
|
catch {
|
|
60
70
|
return;
|
|
61
71
|
}
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const start = (command.index ?? 0) + command[0].length;
|
|
66
|
-
const argument = balancedArgument(content, start);
|
|
67
|
-
if (!argument)
|
|
68
|
-
continue;
|
|
69
|
-
if (command[1]) {
|
|
70
|
-
result.push({ path: filePath, line: lineAtOffset(lineStarts, command.index ?? 0), level: levels[command[1]], title: cleanTitle(argument.value) });
|
|
72
|
+
for (const event of scanOutlineSource(content)) {
|
|
73
|
+
if (event.type === "heading") {
|
|
74
|
+
result.push({ path: filePath, line: event.line, level: event.level, title: event.title });
|
|
71
75
|
continue;
|
|
72
76
|
}
|
|
73
|
-
const included =
|
|
77
|
+
const included = resolveLatexInclude(event.directive, importBase, availableFiles);
|
|
74
78
|
if (included)
|
|
75
|
-
await visit(included);
|
|
79
|
+
await visit(included.path, included.importBase);
|
|
76
80
|
}
|
|
77
81
|
};
|
|
78
|
-
await visit(mainFile);
|
|
82
|
+
await visit(mainFile, "");
|
|
79
83
|
return result;
|
|
80
84
|
}
|
|
81
85
|
/**
|
|
@@ -119,7 +123,7 @@ export class ProjectOutlineService {
|
|
|
119
123
|
cached.touched = Date.now();
|
|
120
124
|
return cached.outline;
|
|
121
125
|
}
|
|
122
|
-
const outline = await
|
|
126
|
+
const outline = await buildProjectOutlineAsyncFromFiles(this.config, projectId, mainFile, sourceFilePaths(entries));
|
|
123
127
|
this.cache.set(key, { signature, outline, touched: Date.now() });
|
|
124
128
|
if (this.cache.size > 64) {
|
|
125
129
|
const oldest = [...this.cache.entries()].sort((left, right) => left[1].touched - right[1].touched)[0];
|
|
@@ -129,65 +133,51 @@ export class ProjectOutlineService {
|
|
|
129
133
|
return outline;
|
|
130
134
|
}
|
|
131
135
|
}
|
|
132
|
-
function
|
|
133
|
-
|
|
134
|
-
if (!raw || /[\\#]/.test(raw))
|
|
135
|
-
return null;
|
|
136
|
-
const withExtension = path.posix.extname(raw) ? raw : `${raw}.tex`;
|
|
137
|
-
const candidates = [withExtension, path.posix.join(path.posix.dirname(currentFile), withExtension)];
|
|
138
|
-
for (const candidate of [...new Set(candidates)]) {
|
|
139
|
-
try {
|
|
140
|
-
const safe = safeRelativePath(candidate);
|
|
141
|
-
if (fs.existsSync(resolveSourcePath(config, projectId, safe)))
|
|
142
|
-
return safe;
|
|
143
|
-
}
|
|
144
|
-
catch { /* Ignore includes outside the project. */ }
|
|
145
|
-
}
|
|
146
|
-
return null;
|
|
136
|
+
function sourceFilePaths(entries) {
|
|
137
|
+
return new Set(entries.filter((entry) => entry.type === "file").map((entry) => entry.path));
|
|
147
138
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Read one source file into ordered outline events. The shared scanner keeps
|
|
141
|
+
* offsets from the original source while skipping comments and literal TeX
|
|
142
|
+
* forms, so the synchronous and asynchronous builders cannot drift in their
|
|
143
|
+
* interpretation of headings and arguments.
|
|
144
|
+
*/
|
|
145
|
+
function scanOutlineSource(source) {
|
|
146
|
+
const lineStarts = sourceLineStarts(source);
|
|
147
|
+
const events = [];
|
|
148
|
+
forEachLatexCommand(source, (command) => {
|
|
149
|
+
const level = levels.get(command.name);
|
|
150
|
+
if (level === undefined)
|
|
151
|
+
return;
|
|
152
|
+
const argument = readLatexMandatoryArguments(source, command.to, 1)[0];
|
|
153
|
+
if (!argument)
|
|
154
|
+
return;
|
|
155
|
+
const value = source.slice(argument.contentFrom, argument.contentTo);
|
|
156
|
+
events.push({
|
|
157
|
+
type: "heading",
|
|
158
|
+
from: command.from,
|
|
159
|
+
line: lineAtOffset(lineStarts, command.from),
|
|
160
|
+
level,
|
|
161
|
+
title: cleanTitle(maskLatexComments(value))
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
for (const reference of findLatexSourceIncludes(source)) {
|
|
165
|
+
const directive = toLatexIncludeDirective(reference);
|
|
166
|
+
if (!isStaticOutlineInclude(directive))
|
|
167
|
+
continue;
|
|
168
|
+
events.push({
|
|
169
|
+
type: "include",
|
|
170
|
+
from: reference.from,
|
|
171
|
+
directive
|
|
172
|
+
});
|
|
174
173
|
}
|
|
175
|
-
return
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
return line;
|
|
184
|
-
}).join("");
|
|
185
|
-
}
|
|
186
|
-
function escapedAt(source, index) {
|
|
187
|
-
let slashes = 0;
|
|
188
|
-
for (let cursor = index - 1; cursor >= 0 && source[cursor] === "\\"; cursor -= 1)
|
|
189
|
-
slashes += 1;
|
|
190
|
-
return slashes % 2 === 1;
|
|
174
|
+
return events.sort((left, right) => {
|
|
175
|
+
if (left.from !== right.from)
|
|
176
|
+
return left.from - right.from;
|
|
177
|
+
if (left.type === right.type)
|
|
178
|
+
return 0;
|
|
179
|
+
return left.type === "heading" ? -1 : 1;
|
|
180
|
+
});
|
|
191
181
|
}
|
|
192
182
|
function sourceLineStarts(source) {
|
|
193
183
|
const starts = [0];
|
|
@@ -209,5 +199,27 @@ function lineAtOffset(starts, offset) {
|
|
|
209
199
|
return Math.max(1, low);
|
|
210
200
|
}
|
|
211
201
|
function cleanTitle(value) {
|
|
212
|
-
|
|
202
|
+
const cleaned = value
|
|
203
|
+
.replace(/\\(?:texorpdfstring|MakeUppercase)\s*\{([^{}]*)\}(?:\{[^{}]*\})?/g, "$1")
|
|
204
|
+
.replace(/\\[a-zA-Z@]+\*?/g, "")
|
|
205
|
+
.replace(/[{}]/g, "");
|
|
206
|
+
return compactOutlineWhitespace(cleaned) || compactOutlineWhitespace(value);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* `maskLatexComments` intentionally keeps source offsets intact, so a long
|
|
210
|
+
* comment becomes a long run of spaces. Outline titles are display-only: keep
|
|
211
|
+
* their original line breaks but collapse that offset-preserving padding.
|
|
212
|
+
*/
|
|
213
|
+
function compactOutlineWhitespace(value) {
|
|
214
|
+
return value.replace(/[^\S\r\n]{2,}/g, " ").trim();
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* TeX paths containing a command or parameter marker need macro expansion.
|
|
218
|
+
* Do not guess their target while building an outline. The generic document
|
|
219
|
+
* graph deliberately supports backslash-separated paths for other consumers,
|
|
220
|
+
* while the legacy outline already skipped these dynamic forms.
|
|
221
|
+
*/
|
|
222
|
+
function isStaticOutlineInclude(directive) {
|
|
223
|
+
return !/[\\#]/.test(directive.path)
|
|
224
|
+
&& (directive.directory === undefined || !/[\\#]/.test(directive.directory));
|
|
213
225
|
}
|
|
@@ -95,7 +95,7 @@ function citationLookupOrder(sourceDirectives, documentSources, bibPaths, source
|
|
|
95
95
|
if (!directives)
|
|
96
96
|
continue;
|
|
97
97
|
for (const bibliography of directives.bibliographies) {
|
|
98
|
-
const resolved = resolveProjectReferencePath(bibliography, sourcePath, ".bib", bibPathSet,
|
|
98
|
+
const resolved = resolveProjectReferencePath(bibliography, sourcePath, ".bib", bibPathSet, "project-root-then-source-file");
|
|
99
99
|
if (resolved)
|
|
100
100
|
declaredBibliographies.push(resolved);
|
|
101
101
|
}
|
|
@@ -6,7 +6,7 @@ import { resolveSourcePath, safeRelativePath } from "../files.js";
|
|
|
6
6
|
import { apiError, httpError } from "../http.js";
|
|
7
7
|
import { accessibleProject } from "../projects.js";
|
|
8
8
|
import { commentMentionForUser, createCommentMentions, listCommentMentions, markAllCommentMentionsRead, markCommentMentionRead, markCommentMentionsResolved, mentionableUsersForProject } from "../commentMentions.js";
|
|
9
|
-
import { commentsForFile, now, repliesForComment, text } from "./projectShared.js";
|
|
9
|
+
import { commentsForFile, commentsForProject, now, repliesForComment, text } from "./projectShared.js";
|
|
10
10
|
/** Register source-anchored comments and reply routes. */
|
|
11
11
|
export function registerCommentRoutes(app, context) {
|
|
12
12
|
const { config, db, collaboration, projectMutations } = context;
|
|
@@ -75,12 +75,18 @@ export function registerCommentRoutes(app, context) {
|
|
|
75
75
|
if (!user)
|
|
76
76
|
return;
|
|
77
77
|
const { id } = request.params;
|
|
78
|
-
const { path: filePath } = request.query;
|
|
78
|
+
const { path: filePath, scope } = request.query;
|
|
79
79
|
if (!accessibleProject(db, id, user))
|
|
80
80
|
return apiError(reply, 404, "PROJECT_NOT_FOUND");
|
|
81
|
-
|
|
81
|
+
if (scope !== undefined && scope !== "project")
|
|
82
|
+
return apiError(reply, 400, "REQUEST_INVALID");
|
|
83
|
+
if (scope === "project" && filePath !== undefined)
|
|
84
|
+
return apiError(reply, 400, "REQUEST_INVALID");
|
|
85
|
+
const load = scope === "project"
|
|
86
|
+
? () => commentsForProject(db, config, id)
|
|
87
|
+
: () => commentsForFile(db, config, id, safeRelativePath(filePath ?? ""));
|
|
82
88
|
return {
|
|
83
|
-
comments: await projectMutations.runConsistentRead(id,
|
|
89
|
+
comments: await projectMutations.runConsistentRead(id, load, {
|
|
84
90
|
preflight: () => {
|
|
85
91
|
if (!accessibleProject(db, id, user))
|
|
86
92
|
throw httpError(404, "PROJECT_NOT_FOUND");
|
|
@@ -263,7 +263,12 @@ export function registerProjectFileRoutes(app, context) {
|
|
|
263
263
|
throw httpError(409, "PATH_EXISTS", { path: destination });
|
|
264
264
|
}
|
|
265
265
|
};
|
|
266
|
-
|
|
266
|
+
// Moving/renaming is collaboration-aware: movePath() remaps the live Yjs
|
|
267
|
+
// texts and publishes a files event. It therefore needs the ordinary
|
|
268
|
+
// durable write lock, not maintenance mode. Maintenance resets the Yjs
|
|
269
|
+
// epoch and makes every connected browser reload after an otherwise safe
|
|
270
|
+
// drag-and-drop move.
|
|
271
|
+
return await projectMutations.runWrite(id, () => {
|
|
267
272
|
fs.renameSync(sourceAbsolute, destinationAbsolute);
|
|
268
273
|
let hasCommentUpdates = false;
|
|
269
274
|
db.exec("BEGIN IMMEDIATE");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import { reanchorFileComments, offsetToLine } from "../anchors.js";
|
|
3
|
-
import { maxCollaborativeFileBytes } from "../collaboration.js";
|
|
3
|
+
import { isCollaborativeTextFile, maxCollaborativeFileBytes } from "../collaboration.js";
|
|
4
4
|
import { listProjectFiles, resolveSourcePath } from "../files.js";
|
|
5
5
|
import { httpError, ValidationError } from "../http.js";
|
|
6
6
|
import { accessibleProject, canEdit } from "../projects.js";
|
|
@@ -154,8 +154,7 @@ export function requireActualProjectOwner(db, projectId, user) {
|
|
|
154
154
|
return project;
|
|
155
155
|
}
|
|
156
156
|
export function projectTextSnapshot(config, projectId) {
|
|
157
|
-
|
|
158
|
-
return new Map(listProjectFiles(config, projectId).filter((entry) => entry.type === "file" && versionedText(entry.path)).map((entry) => {
|
|
157
|
+
return new Map(listProjectFiles(config, projectId).filter((entry) => entry.type === "file" && isCollaborativeTextFile(entry.path)).map((entry) => {
|
|
159
158
|
const absolute = resolveSourcePath(config, projectId, entry.path);
|
|
160
159
|
return [entry.path, fs.statSync(absolute).size <= maxCollaborativeFileBytes(config) ? fs.readFileSync(absolute, "utf8") : ""];
|
|
161
160
|
}));
|
|
@@ -189,21 +188,34 @@ export function repliesForComment(db, commentId) {
|
|
|
189
188
|
editedAt: reply.edited_at
|
|
190
189
|
}));
|
|
191
190
|
}
|
|
192
|
-
|
|
193
|
-
const
|
|
194
|
-
const
|
|
191
|
+
function commentsForScope(db, config, projectId, filePath) {
|
|
192
|
+
const hasFileScope = typeof filePath === "string";
|
|
193
|
+
const fileClause = hasFileScope ? " AND c.file_path = ?" : "";
|
|
194
|
+
// File-scoped reads retain chronological order for backwards compatibility.
|
|
195
|
+
// A project review is grouped by file and source position so next/previous
|
|
196
|
+
// follows the manuscript rather than arbitrary database insertion order.
|
|
197
|
+
const commentOrder = hasFileScope
|
|
198
|
+
? "c.created_at"
|
|
199
|
+
: "c.file_path COLLATE NOCASE, c.start_line, c.created_at";
|
|
195
200
|
const rows = db.prepare(`SELECT c.*, u.username AS author_username, u.display_name AS author_display_name FROM comments c
|
|
196
|
-
LEFT JOIN users u ON u.id = c.author_id WHERE c.project_id =
|
|
197
|
-
.all(projectId, filePath);
|
|
201
|
+
LEFT JOIN users u ON u.id = c.author_id WHERE c.project_id = ?${fileClause} ORDER BY ${commentOrder}`)
|
|
202
|
+
.all(projectId, ...(hasFileScope ? [filePath] : []));
|
|
198
203
|
if (!rows.length)
|
|
199
204
|
return [];
|
|
205
|
+
const sourceByPath = new Map();
|
|
206
|
+
for (const row of rows) {
|
|
207
|
+
if (sourceByPath.has(row.file_path))
|
|
208
|
+
continue;
|
|
209
|
+
const absolute = resolveSourcePath(config, projectId, row.file_path);
|
|
210
|
+
sourceByPath.set(row.file_path, fs.existsSync(absolute) ? fs.readFileSync(absolute, "utf8") : "");
|
|
211
|
+
}
|
|
200
212
|
const replies = db.prepare(`SELECT reply.*, user.username AS author_username, user.display_name AS author_display_name
|
|
201
213
|
FROM comment_replies reply
|
|
202
214
|
JOIN comments c ON c.id = reply.comment_id
|
|
203
215
|
LEFT JOIN users user ON user.id = reply.author_id
|
|
204
|
-
WHERE c.project_id =
|
|
216
|
+
WHERE c.project_id = ?${fileClause}
|
|
205
217
|
ORDER BY reply.created_at`)
|
|
206
|
-
.all(projectId, filePath);
|
|
218
|
+
.all(projectId, ...(hasFileScope ? [filePath] : []));
|
|
207
219
|
const replyMap = new Map();
|
|
208
220
|
for (const reply of replies) {
|
|
209
221
|
const list = replyMap.get(reply.comment_id) ?? [];
|
|
@@ -221,14 +233,15 @@ export function commentsForFile(db, config, projectId, filePath) {
|
|
|
221
233
|
}
|
|
222
234
|
return rows.map((comment) => ({
|
|
223
235
|
id: comment.id,
|
|
236
|
+
filePath: comment.file_path,
|
|
224
237
|
authorId: comment.author_id,
|
|
225
238
|
authorUsername: comment.author_username,
|
|
226
239
|
authorDisplayName: comment.author_display_name,
|
|
227
240
|
selectedText: comment.selected_text,
|
|
228
241
|
startOffset: comment.start_offset,
|
|
229
242
|
endOffset: comment.end_offset,
|
|
230
|
-
startLine: offsetToLine(
|
|
231
|
-
endLine: offsetToLine(
|
|
243
|
+
startLine: offsetToLine(sourceByPath.get(comment.file_path) ?? "", comment.start_offset),
|
|
244
|
+
endLine: offsetToLine(sourceByPath.get(comment.file_path) ?? "", comment.end_offset),
|
|
232
245
|
content: comment.content,
|
|
233
246
|
resolved: Boolean(comment.resolved),
|
|
234
247
|
orphaned: Boolean(comment.orphaned),
|
|
@@ -238,3 +251,11 @@ export function commentsForFile(db, config, projectId, filePath) {
|
|
|
238
251
|
replies: replyMap.get(comment.id) ?? []
|
|
239
252
|
}));
|
|
240
253
|
}
|
|
254
|
+
/** Comments anchored in one source file, used for editor decorations. */
|
|
255
|
+
export function commentsForFile(db, config, projectId, filePath) {
|
|
256
|
+
return commentsForScope(db, config, projectId, filePath);
|
|
257
|
+
}
|
|
258
|
+
/** All project comments, grouped in manuscript order for the review drawer. */
|
|
259
|
+
export function commentsForProject(db, config, projectId) {
|
|
260
|
+
return commentsForScope(db, config, projectId);
|
|
261
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-level constants and payload shapes shared by the browser and server.
|
|
3
|
+
*
|
|
4
|
+
* These values are part of the persisted collaboration contract. Do not
|
|
5
|
+
* renumber a message or change a payload shape in place: make an intentional
|
|
6
|
+
* protocol-version migration instead.
|
|
7
|
+
*/
|
|
8
|
+
export const CollaborationMessageType = {
|
|
9
|
+
Sync: 0,
|
|
10
|
+
Awareness: 1,
|
|
11
|
+
QueryAwareness: 3,
|
|
12
|
+
Flush: 4,
|
|
13
|
+
Protocol: 5,
|
|
14
|
+
Maintenance: 6,
|
|
15
|
+
Permission: 7,
|
|
16
|
+
// A small, non-Yjs handshake message for ephemeral compile metadata.
|
|
17
|
+
// Source text still uses the normal Yjs sync protocol.
|
|
18
|
+
CompileStates: 8,
|
|
19
|
+
// Ephemeral per-file formatter leases, intentionally outside Yjs state.
|
|
20
|
+
FormatLease: 9
|
|
21
|
+
};
|
|
22
|
+
/** Increment only for an incompatible collaboration wire change. */
|
|
23
|
+
export const COLLABORATION_PROTOCOL_VERSION = 3;
|
|
24
|
+
const compileStatuses = ["queued", "running", "succeeded", "failed", "cleaned"];
|
|
25
|
+
const cleanModes = ["cache", "artifacts"];
|
|
26
|
+
export function isCollaborationCompileStatus(value) {
|
|
27
|
+
return typeof value === "string" && compileStatuses.includes(value);
|
|
28
|
+
}
|
|
29
|
+
export function isCollaborationCleanMode(value) {
|
|
30
|
+
return typeof value === "string" && cleanModes.includes(value);
|
|
31
|
+
}
|
|
@@ -1,23 +1,76 @@
|
|
|
1
1
|
/** Lightweight extraction of source and bibliography dependencies from TeX. */
|
|
2
|
-
import { forEachLatexCommand, readLatexMandatoryArguments } from "./latexScanner.js";
|
|
2
|
+
import { forEachLatexCommand, isLatexCommentStart, readLatexMandatoryArguments, skipLatexComment, skipLatexTrivia } from "./latexScanner.js";
|
|
3
3
|
function pathRanges(source, argument, splitOnComma) {
|
|
4
4
|
const paths = [];
|
|
5
5
|
let segmentStart = argument.contentFrom;
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
let index = argument.contentFrom;
|
|
7
|
+
while (index <= argument.contentTo) {
|
|
8
|
+
if (index < argument.contentTo && isLatexCommentStart(source, index)) {
|
|
9
|
+
index = skipLatexComment(source, index);
|
|
10
|
+
if (source[index] === "\n")
|
|
11
|
+
index += 1;
|
|
8
12
|
continue;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
if (
|
|
16
|
-
paths.push(
|
|
13
|
+
}
|
|
14
|
+
if (index !== argument.contentTo && (!splitOnComma || source[index] !== ",")) {
|
|
15
|
+
index += 1;
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
const reference = pathReferenceFromRange(source, segmentStart, index);
|
|
19
|
+
if (reference)
|
|
20
|
+
paths.push(reference);
|
|
17
21
|
segmentStart = index + 1;
|
|
22
|
+
index += 1;
|
|
18
23
|
}
|
|
19
24
|
return paths;
|
|
20
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Turn a raw argument segment into a project path while preserving the source
|
|
28
|
+
* range that produced it. Comments disappear in TeX before filename handling,
|
|
29
|
+
* so a comment between path fragments must not become part of the path.
|
|
30
|
+
*/
|
|
31
|
+
function pathReferenceFromRange(source, start, end) {
|
|
32
|
+
const characters = [];
|
|
33
|
+
let index = start;
|
|
34
|
+
while (index < end) {
|
|
35
|
+
if (isLatexCommentStart(source, index)) {
|
|
36
|
+
index = skipLatexComment(source, index);
|
|
37
|
+
if (source[index] === "\n")
|
|
38
|
+
index += 1;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
characters.push({ character: source[index], index });
|
|
42
|
+
index += 1;
|
|
43
|
+
}
|
|
44
|
+
let first = 0;
|
|
45
|
+
let last = characters.length;
|
|
46
|
+
while (first < last && /\s/.test(characters[first].character))
|
|
47
|
+
first += 1;
|
|
48
|
+
while (last > first && /\s/.test(characters[last - 1].character))
|
|
49
|
+
last -= 1;
|
|
50
|
+
if (first === last)
|
|
51
|
+
return null;
|
|
52
|
+
return {
|
|
53
|
+
path: characters.slice(first, last).map((entry) => entry.character).join(""),
|
|
54
|
+
command: "",
|
|
55
|
+
from: characters[first].index,
|
|
56
|
+
to: characters[last - 1].index + 1
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/** Read TeX's common `\\input filename` form without guessing macro expansion. */
|
|
60
|
+
function readUnbracedInputPath(source, start) {
|
|
61
|
+
const from = skipLatexTrivia(source, start);
|
|
62
|
+
if (from >= source.length || /[\\{}\[\]#~]/.test(source[from]))
|
|
63
|
+
return null;
|
|
64
|
+
let to = from;
|
|
65
|
+
while (to < source.length && !/\s/.test(source[to]) && !isLatexCommentStart(source, to)) {
|
|
66
|
+
if (/[\\{}\[\]#~]/.test(source[to]))
|
|
67
|
+
break;
|
|
68
|
+
to += 1;
|
|
69
|
+
}
|
|
70
|
+
if (to === from)
|
|
71
|
+
return null;
|
|
72
|
+
return { path: source.slice(from, to), command: "", from, to };
|
|
73
|
+
}
|
|
21
74
|
/**
|
|
22
75
|
* Find source files pulled into a LaTeX document. Paths intentionally remain
|
|
23
76
|
* raw: project-level code is responsible for resolving them safely.
|
|
@@ -28,27 +81,34 @@ export function findLatexSourceIncludes(source) {
|
|
|
28
81
|
const name = command.name.toLowerCase();
|
|
29
82
|
if (name === "input" || name === "include" || name === "subfile") {
|
|
30
83
|
const argument = readLatexMandatoryArguments(source, command.to, 1)[0];
|
|
31
|
-
if (
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
84
|
+
if (argument) {
|
|
85
|
+
for (const include of pathRanges(source, argument, false))
|
|
86
|
+
includes.push({ ...include, command: command.name });
|
|
87
|
+
}
|
|
88
|
+
else if (name === "input") {
|
|
89
|
+
const include = readUnbracedInputPath(source, command.to);
|
|
90
|
+
if (include)
|
|
91
|
+
includes.push({ ...include, command: command.name });
|
|
92
|
+
}
|
|
35
93
|
return;
|
|
36
94
|
}
|
|
37
|
-
if (name !== "import" && name !== "subimport" && name !== "includefrom" && name !== "inputfrom"
|
|
95
|
+
if (name !== "import" && name !== "subimport" && name !== "includefrom" && name !== "inputfrom"
|
|
96
|
+
&& name !== "subinputfrom" && name !== "subincludefrom")
|
|
38
97
|
return;
|
|
39
98
|
const argumentsFound = readLatexMandatoryArguments(source, command.to, 2);
|
|
40
99
|
if (argumentsFound.length !== 2)
|
|
41
100
|
return;
|
|
42
|
-
const directory = source
|
|
43
|
-
const file = source
|
|
44
|
-
if (!
|
|
101
|
+
const directory = pathRanges(source, argumentsFound[0], false)[0]?.path ?? "";
|
|
102
|
+
const file = pathRanges(source, argumentsFound[1], false)[0];
|
|
103
|
+
if (!file)
|
|
45
104
|
return;
|
|
46
|
-
const separator = directory.endsWith("/") ? "" : "
|
|
105
|
+
const separator = directory && !directory.endsWith("/") ? "/" : "";
|
|
47
106
|
includes.push({
|
|
48
|
-
path: directory + separator + file,
|
|
107
|
+
path: directory + separator + file.path,
|
|
49
108
|
command: command.name,
|
|
50
|
-
from:
|
|
51
|
-
to:
|
|
109
|
+
from: file.from,
|
|
110
|
+
to: file.to,
|
|
111
|
+
directory
|
|
52
112
|
});
|
|
53
113
|
});
|
|
54
114
|
return includes;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** Bibliographies are structured
|
|
1
|
+
/** Bibliographies and BibTeX style programs are structured data/code, not prose. */
|
|
2
2
|
export function supportsWritingChecks(path) {
|
|
3
|
-
return !/\.bib$/i.test(path.trim());
|
|
3
|
+
return !/\.(?:bib|bst)$/i.test(path.trim());
|
|
4
4
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "texlite",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.4",
|
|
4
4
|
"description": "A lightweight local-first collaborative LaTeX workspace",
|
|
5
5
|
"license": "AGPL-3.0-only",
|
|
6
6
|
"repository": {
|
|
@@ -41,8 +41,9 @@
|
|
|
41
41
|
},
|
|
42
42
|
"type": "module",
|
|
43
43
|
"scripts": {
|
|
44
|
-
"dev": "npm run generate:lucide-icons &&
|
|
45
|
-
"dev:
|
|
44
|
+
"dev": "npm run generate:lucide-icons && node scripts/run-dev.mjs server",
|
|
45
|
+
"dev:init": "node scripts/run-dev.mjs init",
|
|
46
|
+
"dev:web": "npm run generate:bibtex-parser && node scripts/run-dev.mjs web",
|
|
46
47
|
"build": "npm run build:web && npm run build:server",
|
|
47
48
|
"build:web": "npm run generate:bibtex-parser && vite build",
|
|
48
49
|
"build:server": "tsc -p tsconfig.server.json && npm run generate:lucide-icons",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{u as Y,r as n,j as e}from"./react-BVUjbTTn.js";import{c as h,M as D,d as _,C as ee,g as u,h as m}from"./index-DIM_KiG2.js";import{U as se,T as R,X as O}from"./x-CSzFikn2.js";import{U as $}from"./user-plus-gQ6q5Nom.js";import{S as ae}from"./Dashboard-DLk2j3oq.js";import{K as B}from"./key-round-BjL9H8-H.js";import"./hard-drive-B-NSDVVA.js";import"./sigma-DOcJr7VG.js";import"./tags-BedQ75lh.js";import"./chevron-left-l1oupvGO.js";const re=h("Dices",[["rect",{width:"12",height:"12",x:"2",y:"10",rx:"2",ry:"2",key:"6agr2n"}],["path",{d:"m17.92 14 3.5-3.5a2.24 2.24 0 0 0 0-3l-5-4.92a2.24 2.24 0 0 0-3 0L10 6",key:"1o487t"}],["path",{d:"M6 18h.01",key:"uhywen"}],["path",{d:"M10 14h.01",key:"ssrbsk"}],["path",{d:"M15 6h.01",key:"cblpky"}],["path",{d:"M18 9h.01",key:"2061c0"}]]);const ne=h("FolderCheck",[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",key:"1kt360"}],["path",{d:"m9 13 2 2 4-4",key:"6343dt"}]]);const te=h("FolderX",[["path",{d:"M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z",key:"1kt360"}],["path",{d:"m9.5 10.5 5 5",key:"ra9qjz"}],["path",{d:"m14.5 10.5-5 5",key:"l2rkpq"}]]);const ie=h("ShieldOff",[["path",{d:"m2 2 20 20",key:"1ooewy"}],["path",{d:"M5 5a1 1 0 0 0-1 1v7c0 5 3.5 7.5 7.67 8.94a1 1 0 0 0 .67.01c2.35-.82 4.48-1.97 5.9-3.71",key:"1jlk70"}],["path",{d:"M9.309 3.652A12.252 12.252 0 0 0 11.24 2.28a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1v7a9.784 9.784 0 0 1-.08 1.264",key:"18rp1v"}]]);const oe=h("UserCheck",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}],["polyline",{points:"16 11 18 13 22 9",key:"1pwet4"}]]);const le=h("UserX",[["path",{d:"M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2",key:"1yyitq"}],["circle",{cx:"9",cy:"7",r:"4",key:"nufk8"}],["line",{x1:"17",x2:"22",y1:"8",y2:"13",key:"3nzzx3"}],["line",{x1:"22",x2:"17",y1:"8",y2:"13",key:"1swrse"}]]);function de(p=10){const t="ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%",a=crypto.getRandomValues(new Uint8Array(p));return Array.from(a,S=>t[S%t.length]).join("")}function Ce({currentUser:p,minPasswordLength:t}){const{t:a}=Y(),[S,J]=n.useState([]),[E,d]=n.useState(""),[V,C]=n.useState(!1),[r,o]=n.useState({username:"",displayName:"",password:"",role:"user",canCreateProjects:!1}),[k,g]=n.useState(null),[j,P]=n.useState(""),[x,w]=n.useState(null),[z,N]=n.useState(!1),[i,F]=n.useState(!1),[U,y]=n.useState(""),[l,f]=n.useState(null),[A,H]=n.useState(!1),[X,c]=n.useState(""),b=()=>u("/api/admin/users").then(({users:s})=>J(s)).catch(s=>d(m(s)));n.useEffect(()=>{b()},[]);const q=async()=>{if(r.password.length<t)return d(a("auth.passwordMinimum",{count:t}));try{await u("/api/admin/users",{method:"POST",body:JSON.stringify({username:r.username,displayName:r.displayName||r.username,password:r.password,role:r.role,canCreateProjects:r.canCreateProjects})}),C(!1),o({username:"",displayName:"",password:"",role:"user",canCreateProjects:!1}),await b()}catch(s){d(m(s))}},M=async(s,v)=>{try{await u(`/api/admin/users/${s.id}`,{method:"PATCH",body:JSON.stringify(v)}),await b()}catch(T){d(m(T))}},K=async s=>{if(!s.disabled){f({target:s,setting:"disable"}),c("");return}await M(s,{disabled:!1})},Z=async s=>{if(s.role==="admin"){f({target:s,setting:"demote"}),c("");return}await M(s,{role:"admin"})},G=async s=>{if(s.canCreateProjects){f({target:s,setting:"denyProjectCreation"}),c("");return}await M(s,{canCreateProjects:!0})},I=async()=>{if(!l)return;const{target:s,setting:v}=l,T=v==="disable"?{disabled:!0}:v==="demote"?{role:"user"}:{canCreateProjects:!1};H(!0),c("");try{await u(`/api/admin/users/${s.id}`,{method:"PATCH",body:JSON.stringify(T)}),await b(),f(null)}catch(W){c(m(W))}finally{H(!1)}},L=async()=>{if(!(!k||!j)){if(j.length<t)return d(a("auth.passwordMinimum",{count:t}));try{await u(`/api/admin/users/${k.id}`,{method:"PATCH",body:JSON.stringify({password:j})}),g(null),P("")}catch(s){d(m(s))}}},Q=async()=>{if(!(!x||i)){F(!0),y("");try{await u(`/api/admin/users/${x.id}`,{method:"DELETE",body:JSON.stringify({deleteProjects:z})}),w(null),N(!1),await b()}catch(s){y(m(s))}finally{F(!1)}}};return e.jsxs("main",{className:"dashboard",children:[e.jsxs("div",{className:"section-title",children:[e.jsxs("div",{children:[e.jsxs("h1",{children:[e.jsx(se,{"aria-hidden":!0,size:25}),a("users.manage")]}),e.jsx("p",{className:"muted",children:a("users.onlyAdmin")})]}),e.jsxs("button",{className:"primary icon-button",onClick:()=>C(!0),children:[e.jsx($,{"aria-hidden":!0,size:15}),a("users.add")]})]}),E&&e.jsx("p",{className:"error",children:E}),e.jsx("div",{className:"table-card",children:e.jsxs("table",{children:[e.jsx("thead",{children:e.jsxs("tr",{children:[e.jsx("th",{children:a("common.user")}),e.jsx("th",{children:a("users.role")}),e.jsx("th",{children:a("users.createProjects")}),e.jsx("th",{children:a("users.ownedProjects")}),e.jsx("th",{children:a("users.status")}),e.jsx("th",{children:a("users.actions")})]})}),e.jsx("tbody",{children:S.map(s=>e.jsxs("tr",{children:[e.jsxs("td",{children:[e.jsx("strong",{children:s.displayName}),e.jsxs("small",{children:["@",s.username]})]}),e.jsx("td",{children:s.role==="admin"?a("common.admin"):a("common.user")}),e.jsx("td",{children:s.canCreateProjects?a("users.allow"):a("users.deny")}),e.jsx("td",{children:s.ownedProjects}),e.jsx("td",{children:s.disabled?a("common.disabled"):a("common.normal")}),e.jsxs("td",{children:[e.jsxs("button",{className:"icon-button",disabled:s.id===p.id,onClick:()=>K(s),children:[s.disabled?e.jsx(oe,{"aria-hidden":!0,size:13}):e.jsx(le,{"aria-hidden":!0,size:13}),s.disabled?a("users.enable"):a("users.disable")]}),e.jsxs("button",{className:"icon-button",disabled:s.id===p.id,onClick:()=>Z(s),children:[s.role==="admin"?e.jsx(ie,{"aria-hidden":!0,size:13}):e.jsx(ae,{"aria-hidden":!0,size:13}),s.role==="admin"?a("users.demote"):a("users.promote")]}),e.jsxs("button",{className:"icon-button",disabled:s.role==="admin",onClick:()=>G(s),children:[s.canCreateProjects?e.jsx(te,{"aria-hidden":!0,size:13}):e.jsx(ne,{"aria-hidden":!0,size:13}),s.canCreateProjects?a("users.denyCreate"):a("users.allowCreate")]}),e.jsxs("button",{className:"icon-button",onClick:()=>{g(s),P("")},children:[e.jsx(B,{"aria-hidden":!0,size:13}),a("users.resetPassword")]}),e.jsxs("button",{className:"icon-button danger-text",disabled:s.id===p.id,onClick:()=>{w(s),N(!1),y("")},children:[e.jsx(R,{"aria-hidden":!0,size:13}),a("common.delete")]})]})]},s.id))})]})}),e.jsx(D,{open:V,title:a("users.add"),description:a("users.addDescription"),onOpenChange:C,footer:e.jsxs(e.Fragment,{children:[e.jsxs("button",{className:"icon-button",onClick:()=>C(!1),children:[e.jsx(O,{"aria-hidden":!0,size:14}),a("common.cancel")]}),e.jsxs("button",{className:"primary icon-button",disabled:r.password.length<t,onClick:()=>{q()},children:[e.jsx($,{"aria-hidden":!0,size:14}),a("users.createUser")]})]}),children:e.jsxs("div",{className:"form-stack",children:[e.jsxs("label",{className:"form-field",children:[a("auth.username"),e.jsx("input",{value:r.username,onChange:s=>o({...r,username:s.target.value})})]}),e.jsxs("label",{className:"form-field",children:[a("users.displayName"),e.jsx("input",{value:r.displayName,onChange:s=>o({...r,displayName:s.target.value})})]}),e.jsxs("label",{className:"form-field",children:[a("users.initialPassword"),e.jsxs("span",{className:"password-generator",children:[e.jsx("input",{minLength:t,autoComplete:"new-password",value:r.password,onChange:s=>o({...r,password:s.target.value})}),e.jsxs("button",{type:"button",title:a("users.generatePassword"),onClick:()=>o({...r,password:de()}),children:[e.jsx(re,{size:15}),a("users.randomPassword")]})]}),e.jsx("small",{className:"field-hint",children:a("auth.passwordMinimum",{count:t})})]}),e.jsxs("label",{className:"form-field",children:[a("users.role"),e.jsxs("select",{value:r.role,onChange:s=>o({...r,role:s.target.value}),children:[e.jsx("option",{value:"user",children:a("common.user")}),e.jsx("option",{value:"admin",children:a("common.admin")})]})]}),e.jsxs("label",{className:"checkbox-field",children:[e.jsx("input",{type:"checkbox",checked:r.canCreateProjects||r.role==="admin",disabled:r.role==="admin",onChange:s=>o({...r,canCreateProjects:s.target.checked})})," ",a("users.allowCreate")]})]})}),e.jsx(D,{open:!!k,title:a("users.resetPassword"),description:a("users.resetDescription",{username:k?.username??""}),onOpenChange:s=>{s||g(null)},footer:e.jsxs(e.Fragment,{children:[e.jsxs("button",{className:"icon-button",onClick:()=>g(null),children:[e.jsx(O,{"aria-hidden":!0,size:14}),a("common.cancel")]}),e.jsxs("button",{className:"primary icon-button",disabled:j.length<t,onClick:()=>{L()},children:[e.jsx(B,{"aria-hidden":!0,size:14}),a("users.reset")]})]}),children:e.jsxs("label",{className:"form-field",children:[a("auth.newPassword"),e.jsx("input",{autoFocus:!0,type:"password",minLength:t,autoComplete:"new-password",value:j,onChange:s=>P(s.target.value)}),e.jsx("small",{className:"field-hint",children:a("auth.passwordMinimum",{count:t})})]})}),e.jsx(D,{open:!!x,title:a("users.deleteTitle"),description:a("users.deleteDescription",{username:x?.username??""}),onOpenChange:s=>{!s&&!i&&(w(null),y(""))},footer:e.jsxs(e.Fragment,{children:[e.jsxs("button",{className:"icon-button",disabled:i,onClick:()=>{w(null),y("")},children:[e.jsx(O,{"aria-hidden":!0,size:14}),a("common.cancel")]}),e.jsxs("button",{className:"danger icon-button",disabled:i,"aria-busy":i,onClick:()=>{Q()},children:[i?e.jsx(_,{className:"spin","aria-hidden":!0,size:14}):e.jsx(R,{"aria-hidden":!0,size:14}),a(i?"common.loading":"users.deleteTitle")]})]}),children:e.jsxs(e.Fragment,{children:[U&&e.jsx("p",{className:"error dialog-error",children:U}),e.jsxs("fieldset",{className:"choice-group",disabled:i,children:[e.jsx("legend",{children:a("users.ownedChoice",{count:x?.ownedProjects??0})}),e.jsxs("label",{children:[e.jsx("input",{type:"radio",checked:!z,onChange:()=>N(!1)})," ",a("users.transferProjects")]}),e.jsxs("label",{children:[e.jsx("input",{type:"radio",checked:z,onChange:()=>N(!0)})," ",a("users.deleteProjects")]})]})]})}),e.jsx(ee,{open:!!l,title:a(`userConfirmations.${l?.setting??"disable"}Title`),description:a(`userConfirmations.${l?.setting??"disable"}Description`,{username:l?.target.username??""}),confirmLabel:a(`userConfirmations.${l?.setting??"disable"}Confirm`),danger:!0,busy:A,error:X,onCancel:()=>{A||(f(null),c(""))},onConfirm:()=>{I()}})]})}export{Ce as AdminUsers};
|