@fiduswriter/editor 0.1.75 → 0.1.76

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.
@@ -0,0 +1,151 @@
1
+ import {Dialog, addAlert} from "fwtoolkit"
2
+
3
+ import type {EditorDocumentApi} from "../../types.js"
4
+
5
+ interface RequestableRight {
6
+ value: string
7
+ label: string
8
+ }
9
+
10
+ const REQUESTABLE_RIGHTS: RequestableRight[] = [
11
+ {value: "read", label: gettext("Read")},
12
+ {value: "comment", label: gettext("Comment")},
13
+ {value: "review", label: gettext("Review")},
14
+ {value: "write-tracked", label: gettext("Write tracked")},
15
+ {value: "write", label: gettext("Write")}
16
+ ]
17
+
18
+ const RIGHT_ORDER = REQUESTABLE_RIGHTS.map(right => right.value)
19
+
20
+ /**
21
+ * Dialog for requesting access to a document.
22
+ *
23
+ * For users who already have some access, only rights that are higher than
24
+ * the current level are offered. Token users and users without any known
25
+ * access can choose from all requestable rights.
26
+ */
27
+ export class RequestAccessDialog {
28
+ documentId: number
29
+ currentRights: string
30
+ documentApi: EditorDocumentApi
31
+ isTokenUser: boolean
32
+
33
+ constructor(
34
+ documentId: number,
35
+ currentRights = "",
36
+ documentApi: EditorDocumentApi,
37
+ isTokenUser = false
38
+ ) {
39
+ this.documentId = documentId
40
+ this.currentRights = currentRights
41
+ this.documentApi = documentApi
42
+ this.isTokenUser = isTokenUser
43
+ }
44
+
45
+ open(): void {
46
+ const rightsOptions = this.getRightsOptions()
47
+ if (!rightsOptions.length) {
48
+ addAlert(
49
+ "error",
50
+ gettext("You already have the highest possible access level.")
51
+ )
52
+ return
53
+ }
54
+ const defaultRights =
55
+ rightsOptions.find(right => right.value === "write")?.value ||
56
+ rightsOptions[rightsOptions.length - 1].value
57
+ const bodyText = this.currentRights
58
+ ? gettext(
59
+ "Select a higher access level to request from the document owner."
60
+ )
61
+ : gettext(
62
+ "You do not have access to this document. Select the access level you would like to request from the document owner."
63
+ )
64
+ const dialog = new Dialog({
65
+ title: gettext("Request Access"),
66
+ id: "request-access-dialog",
67
+ width: 500,
68
+ body: `<p>${bodyText}</p>
69
+ <table class="fw-dialog-table">
70
+ <tbody>
71
+ <tr>
72
+ <th><label for="request-access-rights">${gettext(
73
+ "Access level"
74
+ )}</label></th>
75
+ <td class="entry-field">
76
+ <select id="request-access-rights" class="fw-button fw-light fw-large">
77
+ ${rightsOptions
78
+ .map(
79
+ right =>
80
+ `<option value="${right.value}"${right.value === defaultRights ? " selected" : ""}>${right.label}</option>`
81
+ )
82
+ .join("")}
83
+ </select>
84
+ <div class="fw-select-arrow fa-solid fa-caret-down"></div>
85
+ </td>
86
+ </tr>
87
+ </tbody>
88
+ </table>`,
89
+ buttons: [
90
+ {
91
+ text: gettext("Request"),
92
+ classes: "fw-dark",
93
+ click: () => {
94
+ const rights = (
95
+ dialog.dialogEl.querySelector(
96
+ "#request-access-rights"
97
+ ) as HTMLSelectElement
98
+ ).value
99
+ this.documentApi
100
+ .requestAccess({
101
+ document_id: this.documentId,
102
+ rights
103
+ })
104
+ .then(({json}: {json: unknown}) => {
105
+ const data = json as {
106
+ success?: boolean
107
+ error?: string
108
+ }
109
+ if (data.success) {
110
+ addAlert(
111
+ "success",
112
+ gettext(
113
+ "Your access request has been sent to the document owner."
114
+ )
115
+ )
116
+ dialog.close()
117
+ } else {
118
+ addAlert(
119
+ "error",
120
+ data.error ||
121
+ gettext(
122
+ "Could not send access request."
123
+ )
124
+ )
125
+ }
126
+ })
127
+ .catch(() => {
128
+ addAlert(
129
+ "error",
130
+ gettext("Could not send access request.")
131
+ )
132
+ })
133
+ }
134
+ },
135
+ {type: "cancel"}
136
+ ]
137
+ })
138
+ dialog.open()
139
+ }
140
+
141
+ getRightsOptions(): RequestableRight[] {
142
+ if (this.isTokenUser || !this.currentRights) {
143
+ return REQUESTABLE_RIGHTS
144
+ }
145
+ const currentIndex = RIGHT_ORDER.indexOf(this.currentRights)
146
+ if (currentIndex < 0) {
147
+ return REQUESTABLE_RIGHTS
148
+ }
149
+ return REQUESTABLE_RIGHTS.filter((_, index) => index > currentIndex)
150
+ }
151
+ }
package/src/index.ts CHANGED
@@ -41,6 +41,7 @@ import {ModCollab} from "./collab/index.js"
41
41
  import {ModComments} from "./comments/index.js"
42
42
  import {ModDB} from "./databases/index.js"
43
43
  import {ModDocumentTemplate} from "./document_template/index.js"
44
+ import {RequestAccessDialog} from "./documents/access_rights/request_access_dialog.js"
44
45
  import {E2EESnapshotManager} from "./e2ee/snapshot-manager.js"
45
46
  import {ExportFidusFile} from "./exporter/native/file.js"
46
47
  import {ModFootnotes} from "./footnotes/index.js"
@@ -374,47 +375,68 @@ export class Editor {
374
375
  token: this.docInfo.token
375
376
  })
376
377
  : Promise.resolve({json: {ws_base: ""}})
377
- const stylesPromise = documentApi.getDocumentStyles(
378
- stylesPayload as {id: number; token?: string}
379
- )
380
- const docDataPromise = documentApi.getDocumentData(
381
- stylesPayload as {id: number; token?: string}
382
- )
378
+ const stylesPromise = documentApi
379
+ .getDocumentStyles(
380
+ stylesPayload as {id: number; token?: string}
381
+ )
382
+ .catch(() => {
383
+ return {json: {}}
384
+ })
385
+ const docDataPromise = documentApi
386
+ .getDocumentData(
387
+ stylesPayload as {id: number; token?: string}
388
+ )
389
+ .catch(error => {
390
+ // Only show "Invalid Share Link" for token validation errors.
391
+ // For authenticated users without access, offer to request access.
392
+ // This is also shown when the document does not exist so that we
393
+ // do not reveal whether a document with the given ID exists.
394
+ if (error.message === "Invalid or expired share link") {
395
+ deactivateWait()
396
+ const errorDialog = new Dialog({
397
+ title: gettext("Invalid Share Link"),
398
+ id: "invalid_share_link_dialog",
399
+ body: gettext(
400
+ "This share link has expired or is invalid. Please ask the document owner for a new link."
401
+ ),
402
+ buttons: [
403
+ {
404
+ text: gettext("OK"),
405
+ classes: "fw-dark",
406
+ click: () => {
407
+ window.location.href = "/"
408
+ }
409
+ }
410
+ ],
411
+ canClose: false
412
+ })
413
+ errorDialog.open()
414
+ return Promise.reject(false)
415
+ } else if (
416
+ error.status === 401 &&
417
+ this.user.is_authenticated
418
+ ) {
419
+ deactivateWait()
420
+ const requestAccessDialog = new RequestAccessDialog(
421
+ this.docInfo.id as number,
422
+ "",
423
+ documentApi,
424
+ Boolean(this.docInfo.token)
425
+ )
426
+ requestAccessDialog.open()
427
+ return Promise.reject(false)
428
+ } else {
429
+ deactivateWait()
430
+ console.error("Editor initialization failed:", error)
431
+ }
432
+ return Promise.reject(error)
433
+ })
383
434
  return Promise.all([
384
435
  wsBasePromise,
385
436
  stylesPromise,
386
437
  docDataPromise
387
438
  ])
388
439
  })
389
- .catch(error => {
390
- // Only show "Invalid Share Link" for token validation errors.
391
- // Other errors (REST failures, etc.) should not show this dialog.
392
- if (error.message === "Invalid or expired share link") {
393
- deactivateWait()
394
- const errorDialog = new Dialog({
395
- title: gettext("Invalid Share Link"),
396
- id: "invalid_share_link_dialog",
397
- body: gettext(
398
- "This share link has expired or is invalid. Please ask the document owner for a new link."
399
- ),
400
- buttons: [
401
- {
402
- text: gettext("OK"),
403
- classes: "fw-dark",
404
- click: () => {
405
- window.location.href = "/"
406
- }
407
- }
408
- ],
409
- canClose: false
410
- })
411
- errorDialog.open()
412
- } else {
413
- deactivateWait()
414
- console.error("Editor initialization failed:", error)
415
- }
416
- return Promise.reject(error)
417
- })
418
440
  .then(([wsResult, stylesResult, docResult]) => {
419
441
  let resubScribed = false
420
442
  this.render()
@@ -7,6 +7,7 @@ import {
7
7
  import type {BibDB, ExportDoc, ImageDB} from "@fiduswriter/document"
8
8
  import {CopyrightDialog} from "../../copyright_dialog/index.js"
9
9
  import {DocumentAccessRightsDialog} from "../../documents/access_rights/index.js"
10
+ import {RequestAccessDialog} from "../../documents/access_rights/request_access_dialog.js"
10
11
  import {SaveCopy, SaveRevision} from "../../exporter/native/index.js"
11
12
  import {ExportFidusFile} from "../../exporter/native/file.js"
12
13
  import {LanguageDialog, RevisionDialog} from "../../dialogs/index.js"
@@ -72,6 +73,13 @@ const exportProgress = (doc: {title: string; path?: string}) => {
72
73
  task.update(percentage ?? null, message)
73
74
  }
74
75
 
76
+ const showRequestAccess = (editor: Editor): boolean =>
77
+ editor.user.is_authenticated === true &&
78
+ !editor.docInfo.is_owner &&
79
+ (Boolean(editor.docInfo.token) ||
80
+ (Boolean(editor.docInfo.access_rights) &&
81
+ editor.docInfo.access_rights !== "write"))
82
+
75
83
  const languageItem = (
76
84
  language: string,
77
85
  name: string,
@@ -115,62 +123,26 @@ export const headerbarModel = () => ({
115
123
  content: [
116
124
  {
117
125
  title: (editor: Editor) =>
118
- editor.user.is_authenticated &&
119
- editor.docInfo.token &&
120
- !editor.docInfo.is_owner
126
+ showRequestAccess(editor)
121
127
  ? gettext("Request Access")
122
128
  : gettext("Share"),
123
129
  type: "action",
124
130
  //icon: 'share',
125
131
  tooltip: (editor: Editor) =>
126
- editor.user.is_authenticated &&
127
- editor.docInfo.token &&
128
- !editor.docInfo.is_owner
132
+ showRequestAccess(editor)
129
133
  ? gettext("Request to be added as a collaborator.")
130
134
  : gettext("Share the document with other users."),
131
135
  order: 0,
132
136
  action: (editor: Editor) => {
133
- if (
134
- editor.user.is_authenticated &&
135
- editor.docInfo.token &&
136
- !editor.docInfo.is_owner
137
- ) {
138
- // TokenUser requesting access
139
- editor.app.apiConnectors.document
140
- .requestAccess({
141
- document_id: editor.docInfo.id as number,
142
- rights: "write"
143
- })
144
- .then(({json}: {json: unknown}) => {
145
- const data = json as {
146
- success?: boolean
147
- error?: string
148
- }
149
- if (data.success) {
150
- addAlert(
151
- "success",
152
- gettext(
153
- "Your access request has been sent to the document owner."
154
- )
155
- )
156
- } else {
157
- addAlert(
158
- "error",
159
- data.error ||
160
- gettext(
161
- "Could not send access request."
162
- )
163
- )
164
- }
165
- })
166
- .catch(() => {
167
- addAlert(
168
- "error",
169
- gettext(
170
- "Could not send access request."
171
- )
172
- )
173
- })
137
+ if (showRequestAccess(editor)) {
138
+ // Request higher access rights from the document owner
139
+ const requestAccessDialog = new RequestAccessDialog(
140
+ editor.docInfo.id as number,
141
+ editor.docInfo.access_rights || "",
142
+ editor.app.apiConnectors.document,
143
+ Boolean(editor.docInfo.token)
144
+ )
145
+ requestAccessDialog.open()
174
146
  return
175
147
  }
176
148
  const onShareSuccess = async (
@@ -254,19 +226,40 @@ export const headerbarModel = () => ({
254
226
  editor.e2ee?.password || "",
255
227
  onShareSuccess,
256
228
  editor.app.settings,
229
+ editor.docInfo.is_owner,
257
230
  editor.app.apiConnectors.contacts,
258
231
  editor.app.apiConnectors.document
259
232
  )
260
233
  shareDialog.init()
261
234
  },
235
+ available: (editor: Editor) => {
236
+ if (editor.app.settings.EDITOR_SAVE_MODE === "external") {
237
+ return false
238
+ }
239
+ if (!editor.user.is_authenticated) {
240
+ return true
241
+ }
242
+ if (editor.docInfo.is_owner) {
243
+ return true
244
+ }
245
+ if (editor.docInfo.token) {
246
+ return true
247
+ }
248
+ if (!editor.docInfo.access_rights) {
249
+ return false
250
+ }
251
+ return editor.docInfo.access_rights !== "write"
252
+ },
262
253
  disabled: (editor: Editor) => {
263
254
  return (
264
255
  editor.app.isOffline() ||
265
- !editor.user.is_authenticated
256
+ !editor.user.is_authenticated ||
257
+ !editor.docInfo.owner ||
258
+ (!editor.docInfo.is_owner &&
259
+ !editor.docInfo.token &&
260
+ editor.docInfo.access_rights === "write")
266
261
  )
267
- },
268
- available: (editor: Editor) =>
269
- editor.app.settings.EDITOR_SAVE_MODE !== "external"
262
+ }
270
263
  },
271
264
  {
272
265
  title: (editor: Editor) =>