From 1e0e9473d24a0e5762a9862251d293bf7f4cc0c1 Mon Sep 17 00:00:00 2001
From: yu-i-i
Date: Sat, 27 Jun 2026 03:44:53 +0200
Subject: [PATCH] Zotero: allow collaborators to refresh files imported from
Zotero; show importer's name in the header
---
.../web/frontend/extracted-translations.json | 4 +-
.../features/file-view/types/binary-file.ts | 7 ++
services/web/locales/de.json | 6 +-
services/web/locales/en.json | 4 +-
services/web/locales/ru.json | 4 +-
.../zotero/app/src/ZoteroLinkedFileAgent.mjs | 70 +++++++++++--------
.../js/components/tpr-file-view-info.tsx | 40 ++++++-----
.../tpr-file-view-not-original-importer.tsx | 48 +------------
.../tpr-file-view-refresh-button.tsx | 36 +++-------
.../tpr-file-view-refresh-error.tsx | 34 ++++-----
10 files changed, 103 insertions(+), 150 deletions(-)
diff --git a/services/web/frontend/extracted-translations.json b/services/web/frontend/extracted-translations.json
index 024cbe70b4..2e75820177 100644
--- a/services/web/frontend/extracted-translations.json
+++ b/services/web/frontend/extracted-translations.json
@@ -992,6 +992,7 @@
"imported_from_papers_at_date": "",
"imported_from_the_output_of_another_project_at_date": "",
"imported_from_zotero_at_date": "",
+ "imported_from_zotero_at_date_by": "",
"importing": "",
"importing_and_merging_changes_in_github": "",
"inactive_projects": "",
@@ -2672,13 +2673,12 @@
"zotero_dynamic_sync_description": "",
"zotero_groups_loading_error": "",
"zotero_groups_relink": "",
- "zotero_imported_by_collaborator": "",
- "zotero_imported_by_unknown": "",
"zotero_integration": "",
"zotero_is_premium": "",
"zotero_reference_loading_error": "",
"zotero_reference_loading_error_expired": "",
"zotero_reference_loading_error_forbidden": "",
+ "zotero_reference_loading_error_not_linked": "",
"zotero_sync_description": "",
"zotero_upgrade_prompt_content": "",
"zotero_upgrade_prompt_title": ""
diff --git a/services/web/frontend/js/features/file-view/types/binary-file.ts b/services/web/frontend/js/features/file-view/types/binary-file.ts
index 4a4104615c..6e709d0932 100644
--- a/services/web/frontend/js/features/file-view/types/binary-file.ts
+++ b/services/web/frontend/js/features/file-view/types/binary-file.ts
@@ -15,6 +15,13 @@ export type LinkedFileData = {
source_project_id: string
source_output_file_path: string
}
+ zotero: {
+ provider: 'zotero'
+ zoteroGroupId?: string
+ importedAt: Date | string
+ importedByUserId?: string
+ importedByName?: string
+ }
}
export type BinaryFile =
diff --git a/services/web/locales/de.json b/services/web/locales/de.json
index ad112779d1..f93a555071 100644
--- a/services/web/locales/de.json
+++ b/services/web/locales/de.json
@@ -780,7 +780,8 @@
"imported_from_external_provider_at_date": "Importiert aus <0>__shortenedUrlHTML__0> am __formattedDate__ __relativeDate__",
"imported_from_mendeley_at_date": "Importiert von Mendeley am __formattedDate__ __relativeDate__",
"imported_from_the_output_of_another_project_at_date": "Importiert aus der Ausgabe von <0>einem anderen Projekt0>: __sourceOutputFilePathHTML__, am __formattedDate__ __relativeDate__",
- "imported_from_zotero_at_date": "Importiert von Zotero at __formattedDate__ __relativeDate__",
+ "imported_from_zotero_at_date": "Importiert aus Zotero am __formattedDate__ __relativeDate__",
+ "imported_from_zotero_at_date_by": "Importiert aus Zotero am __formattedDate__ __relativeDate__ von __importedByName__",
"importing": "Importieren",
"importing_and_merging_changes_in_github": "Änderungen werden in GitHub importiert und zusammengeführt.",
"in_order_to_have_a_secure_account_make_sure_your_password": "Um dein Konto abzusichern, stelle sicher, dass dein Passwort",
@@ -1726,12 +1727,11 @@
"zotero_account_linked_successfully": "Zotero-Konto erfolgreich verknüpft.",
"zotero_groups_loading_error": "Beim Laden von Gruppen von Zotero ist ein Fehler aufgetreten",
"zotero_groups_relink": "Beim Zugriff auf die Zotero-Daten ist ein Fehler aufgetreten. Dies wurde wahrscheinlich durch fehlende Berechtigungen verursacht. Bitte verknüpfe dein Konto neu und versuche es erneut.",
- "zotero_imported_by_collaborator": "Diese Datei wurde aus Zotero von deinem Mitautor importiert. Du kannst sie nicht aktualisieren.",
- "zotero_imported_by_unknown": "Diese Datei wurde aus Zotero von einem unbekannten Benutzer importiert. Du kannst sie nicht aktualisieren.",
"zotero_integration": "Zotero-Integration",
"zotero_is_premium": "Zotero-Integration ist eine Premiumfunktion",
"zotero_reference_loading_error": "Fehler, Referenzen konnten nicht von Mendeley geladen werden",
"zotero_reference_loading_error_expired": "Zotero-Token abgelaufen, bitte verknüpfe dein Konto neu",
"zotero_reference_loading_error_forbidden": "Referenzen konnten nicht von Zotero geladen werden. Bitte verlinke dein Zotero-Konto erneut und versuche es nochmal",
+ "zotero_reference_loading_error_not_linked": "Aktualisierung fehlgeschlagen: Das zum Importieren der Datei verwendete Konto ist nicht mehr mit Zotero verknüpft oder wurde entfernt.",
"zotero_sync_description": "Mit der Zotero-Integration kannst du deine Referenzen von Zotero in deine __appName__ Projekte importieren."
}
diff --git a/services/web/locales/en.json b/services/web/locales/en.json
index 822e69a4f1..68b23b19ab 100644
--- a/services/web/locales/en.json
+++ b/services/web/locales/en.json
@@ -1262,6 +1262,7 @@
"imported_from_papers_at_date": "Imported from Papers at __formattedDate__ __relativeDate__",
"imported_from_the_output_of_another_project_at_date": "Imported from the output of <0>Another project0>: __sourceOutputFilePathHTML__, at __formattedDate__ __relativeDate__",
"imported_from_zotero_at_date": "Imported from Zotero at __formattedDate__ __relativeDate__",
+ "imported_from_zotero_at_date_by": "Imported from Zotero at __formattedDate__ __relativeDate__ by __importedByName__",
"importing": "Importing",
"importing_and_merging_changes_in_github": "Importing and merging changes in GitHub",
"in_order_to_have_a_secure_account_make_sure_your_password": "To help keep your account secure, make sure your new password:",
@@ -3337,13 +3338,12 @@
"zotero_dynamic_sync_description": "With the Zotero integration, you can import your references into __appName__. You can either import all your references at once or dynamically search your Zotero library directly from __appName__.",
"zotero_groups_loading_error": "There was an error loading groups from Zotero",
"zotero_groups_relink": "There was an error accessing your Zotero data. This was likely caused by lack of permissions. Please re-link your account and try again.",
- "zotero_imported_by_collaborator": "This file was imported from Zotero by your collaborator. You cannot refresh it.",
- "zotero_imported_by_unknown": "This file was imported from Zotero by an unknown user. You cannot refresh it.",
"zotero_integration": "Zotero integration",
"zotero_is_premium": "Zotero integration is a premium feature",
"zotero_reference_loading_error": "Error, could not load references from Zotero",
"zotero_reference_loading_error_expired": "Zotero token expired, please re-link your account",
"zotero_reference_loading_error_forbidden": "Could not load references from Zotero, please re-link your account and try again",
+ "zotero_reference_loading_error_not_linked": "Refresh failed: the account used to import the file is no longer linked to Zotero or has been removed.",
"zotero_sync_description": "With the Zotero integration you can import your references from Zotero into your __appName__ projects.",
"zotero_upgrade_prompt_content": "Link your Zotero account to search and add your references from Zotero directly in your project—they’ll automatically be added to your .bib file. Or import them as a file into your __appName__ projects.",
"zotero_upgrade_prompt_title": "Cite from Zotero"
diff --git a/services/web/locales/ru.json b/services/web/locales/ru.json
index dc06014d11..ed6c6eb5f1 100644
--- a/services/web/locales/ru.json
+++ b/services/web/locales/ru.json
@@ -235,6 +235,7 @@
"import_from_github": "Импорт с GitHub",
"import_to_sharelatex": "Импортировать в __appName__",
"imported_from_zotero_at_date": "Импортировано из Zotero в __formattedDate__ __relativeDate__",
+ "imported_from_zotero_at_date_by": "Импортировано из Zotero в __formattedDate__ __relativeDate__ пользователем __importedByName__",
"importing": "Импорт",
"importing_and_merging_changes_in_github": "Импорт и слияние изменений в GitHub",
"inactive_projects": "Неактивные проекты",
@@ -595,10 +596,9 @@
"zh-CN": "Китайский",
"zotero_account_linked_successfully": "Ваш аккаунт Zotero успешно подключён.",
"zotero_groups_loading_error": "При загрузке групп из Zotero произошла ошибка",
- "zotero_imported_by_collaborator": "Этот файл был импортирован из Zotero вашим соавтором. Вы не можете обновить файл.",
- "zotero_imported_by_unknown": "Этот файл был импортирован из Zotero неизвестным пользователем. Вы не можете обновить файл.",
"zotero_reference_loading_error": "Ошибка: не удалось загрузить ссылки из Zotero",
"zotero_reference_loading_error_expired": "Срок действия токена Zotero истёк, пожалуйста, повторно подключите аккаунт",
"zotero_reference_loading_error_forbidden": "Не удалось загрузить ссылки из Zotero, пожалуйста, повторно подключите аккаунт и попробуйте снова",
+ "zotero_reference_loading_error_not_linked": "Не удалось обновить файл: учётная запись, использованная для импорта файла, больше не связана с Zotero или была удалена.",
"zotero_sync_description": "Используя интеграцию с Zotero вы можете импортировать свои ссылки из Zotero в проекты __appName__."
}
diff --git a/services/web/modules/zotero/app/src/ZoteroLinkedFileAgent.mjs b/services/web/modules/zotero/app/src/ZoteroLinkedFileAgent.mjs
index cf5e0364a6..72bfdfba0a 100644
--- a/services/web/modules/zotero/app/src/ZoteroLinkedFileAgent.mjs
+++ b/services/web/modules/zotero/app/src/ZoteroLinkedFileAgent.mjs
@@ -1,23 +1,27 @@
import logger from '@overleaf/logger'
import { callbackify } from '@overleaf/promise-utils'
+import UserGetter from '../../../../app/src/Features/User/UserGetter.mjs'
import LinkedFilesHandler from '../../../../app/src/Features/LinkedFiles/LinkedFilesHandler.mjs'
+import LinkedFilesErrors from '../../../../app/src/Features/LinkedFiles/LinkedFilesErrors.mjs'
import ZoteroApiClient from './ZoteroApiClient.mjs'
import { ZoteroForbiddenError, ZoteroAccountNotLinkedError } from './ZoteroApiClient.mjs'
-import LinkedFilesErrors from '../../../../app/src/Features/LinkedFiles/LinkedFilesErrors.mjs'
-import { Project } from '../../../../app/src/models/Project.mjs'
const {
- FeatureNotAvailableError,
AccessDeniedError,
RemoteServiceError,
- NotOriginalImporterError,
} = LinkedFilesErrors
/**
* Create a linked .bib file from Zotero (either My Library or a Group Library).
*
* linkedFileData shape:
- * { provider: 'zotero', zoteroGroupId?: string, importedAt: string }
+ * {
+ * provider: 'zotero'
+ * zoteroGroupId?: string
+ * importedAt: Date | string
+ * importedByUserId?: string
+ * importedByName?: string
+ * }
*
* - If zoteroGroupId is present, export that group's library.
* - Otherwise, export the user's personal library ("My Library").
@@ -29,14 +33,15 @@ async function createLinkedFile(
parentFolderId,
userId
) {
- logger.info(
+ logger.debug(
{ projectId, userId, groupId: linkedFileData.zoteroGroupId },
'creating Zotero linked file'
)
linkedFileData.importedByUserId = userId
+ linkedFileData.importedByName = await _getUserName(userId)
- const bibtex = await _getBibtex(userId, linkedFileData)
+ const bibtex = await _getBibtex(linkedFileData)
const file = await LinkedFilesHandler.promises.importContent(
projectId,
@@ -59,29 +64,12 @@ async function refreshLinkedFile(
parentFolderId,
userId
) {
- logger.info(
+ logger.debug(
{ projectId, userId, groupId: linkedFileData.zoteroGroupId },
'refreshing Zotero linked file'
)
- if (linkedFileData.importedByUserId) {
- if (linkedFileData.importedByUserId !== userId) {
- throw new NotOriginalImporterError(
- 'Only the user who created the Zotero-linked file can refresh this file'
- )
- }
- } else {
- // No owner metadata (legacy files). Only project owner may refresh.
- const project = await Project.findById(projectId, 'owner_ref').lean()
- if (!project || String(project.owner_ref) !== String(userId)) {
- throw new NotOriginalImporterError(
- 'Only the user who created the Zotero-linked file can refresh this file'
- )
- }
- linkedFileData.importedByUserId = userId
- }
-
- const bibtex = await _getBibtex(userId, linkedFileData)
+ const bibtex = await _getBibtex(linkedFileData)
const file = await LinkedFilesHandler.promises.importContent(
projectId,
@@ -94,7 +82,8 @@ async function refreshLinkedFile(
return file._id
}
-async function _getBibtex(userId, linkedFileData) {
+async function _getBibtex(linkedFileData) {
+ const userId = linkedFileData.importedByUserId
try {
if (linkedFileData.zoteroGroupId) {
return await ZoteroApiClient.getGroupLibraryBibtex(
@@ -118,14 +107,35 @@ async function _getBibtex(userId, linkedFileData) {
function _sanitizeData(data) {
return {
provider: 'zotero',
- zoteroGroupId: data.zoteroGroupId || undefined,
+ ...(data.zoteroGroupId && {
+ zoteroGroupId: data.zoteroGroupId,
+ }),
importedAt: data.importedAt,
- importedByUserId: data.importedByUserId || undefined,
+ ...(data.importedByUserId && {
+ importedByUserId: data.importedByUserId,
+ }),
+ importedByName: data.importedByName || 'Unknown',
}
}
+async function _getUserName(userId) {
+ let user = null
+ try {
+ user = await UserGetter.promises.getUser(userId, {'email': 1, 'first_name': 1, 'last_name': 1})
+ }
+ catch (err) {
+ logger.error({ userId, err }, 'failed to get user info')
+ }
+ if (!user) return 'Unknown'
+
+ const { email, first_name, last_name } = user
+ const name = (first_name || last_name) ?
+ [first_name, last_name].filter(n => n != null).join(' ') : email
+ return name || 'Unknown'
+}
+
export default {
createLinkedFile: callbackify(createLinkedFile),
refreshLinkedFile: callbackify(refreshLinkedFile),
- promises: { createLinkedFile, refreshLinkedFile },
+ promises: { createLinkedFile, refreshLinkedFile }
}
diff --git a/services/web/modules/zotero/frontend/js/components/tpr-file-view-info.tsx b/services/web/modules/zotero/frontend/js/components/tpr-file-view-info.tsx
index 995e128885..1bc362e1d5 100644
--- a/services/web/modules/zotero/frontend/js/components/tpr-file-view-info.tsx
+++ b/services/web/modules/zotero/frontend/js/components/tpr-file-view-info.tsx
@@ -1,10 +1,12 @@
import { useTranslation } from 'react-i18next'
+import getMeta from '@/utils/meta'
import { formatTime, relativeDate } from '@/features/utils/format-date'
import { LinkedFileIcon } from '@/features/file-view/components/file-view-icons'
-import type { BinaryFile } from '@/features/file-view/types/binary-file'
+import { hasProvider } from '@/features/file-view/types/binary-file'
+import type { LinkedFile, LinkedFileData } from '@/features/file-view/types/binary-file'
type TPRFileViewInfoProps = {
- file: BinaryFile
+ file: LinkedFile
}
/**
@@ -15,31 +17,31 @@ type TPRFileViewInfoProps = {
export function TPRFileViewInfo({ file }: TPRFileViewInfoProps) {
const { t } = useTranslation()
- if (file.linkedFileData?.provider !== 'zotero') {
- return null
- }
+ if (!hasProvider(file, 'zotero')) return null
const importedAt = (file.linkedFileData as any)?.importedAt || file.created
const formattedDate = formatTime(importedAt)
const relative = relativeDate(importedAt)
- const groupId = (file.linkedFileData as any)?.zoteroGroupId
+ const importedByUserId = (file.linkedFileData as any)?.importedByUserId
+ const importedByName = (file.linkedFileData as any)?.importedByName || 'Unknown'
return (
-
-
-
-
- {t('imported_from_zotero_at_date', {
- formattedDate,
- relativeDate: relative,
- })}
-
- {groupId && (
- (Group: {groupId})
- )}
-
+
+
+ {(importedByUserId === getMeta('ol-user_id')) ? (
+ t('imported_from_zotero_at_date', {
+ formattedDate,
+ relativeDate: relative,
+ })
+ ) : (
+ t('imported_from_zotero_at_date_by', {
+ formattedDate,
+ relativeDate: relative,
+ importedByName,
+ })
+ )}
)
}
diff --git a/services/web/modules/zotero/frontend/js/components/tpr-file-view-not-original-importer.tsx b/services/web/modules/zotero/frontend/js/components/tpr-file-view-not-original-importer.tsx
index fa571440e2..103b605e32 100644
--- a/services/web/modules/zotero/frontend/js/components/tpr-file-view-not-original-importer.tsx
+++ b/services/web/modules/zotero/frontend/js/components/tpr-file-view-not-original-importer.tsx
@@ -1,55 +1,13 @@
-import { useTranslation } from 'react-i18next'
import type { BinaryFile } from '@/features/file-view/types/binary-file'
-import OLNotification from '@/shared/components/ol/ol-notification'
-import getMeta from '@/utils/meta'
+import type { LinkedFile, LinkedFileData } from '@/features/file-view/types/binary-file'
-type TPRFileViewNotOriginalImporterProps = {
- file: BinaryFile
+type TPRFileViewInfoProps = {
+ file: LinkedFile
}
-/**
- * Shows a warning if the current user is not the original importer of this Zotero file.
- * Registered via overleafModuleImports.tprFileViewNotOriginalImporter.
- */
export function TPRFileViewNotOriginalImporter({
file,
}: TPRFileViewNotOriginalImporterProps) {
- const provider = file.linkedFileData?.provider
- const { t } = useTranslation()
-
- if (provider !== 'zotero') {
- return null
- }
-
- const currentUserId =
- (getMeta('ol-user') as any)?._id || (getMeta('ol-user_id') as string)
- const importedByUserId = (file.linkedFileData as any)?.importedByUserId
-
- if (provider !== 'zotero') {
- return null
- }
-
- if (!importedByUserId) {
- return (
-
- )
- }
-// TODO: fetch the collaborator's name and show it
- if (currentUserId && importedByUserId !== currentUserId) {
- return (
-
- )
- }
return null
}
diff --git a/services/web/modules/zotero/frontend/js/components/tpr-file-view-refresh-button.tsx b/services/web/modules/zotero/frontend/js/components/tpr-file-view-refresh-button.tsx
index c20e88306a..55363997bb 100644
--- a/services/web/modules/zotero/frontend/js/components/tpr-file-view-refresh-button.tsx
+++ b/services/web/modules/zotero/frontend/js/components/tpr-file-view-refresh-button.tsx
@@ -1,10 +1,10 @@
import { useTranslation } from 'react-i18next'
import OLButton from '@/shared/components/ol/ol-button'
-import type { BinaryFile } from '@/features/file-view/types/binary-file'
-import getMeta from '@/utils/meta'
+import type { LinkedFile, LinkedFileData } from '@/features/file-view/types/binary-file'
+import { hasProvider } from '@/features/file-view/types/binary-file'
type TPRFileViewRefreshButtonProps = {
- file: BinaryFile
+ file: LinkedFile
refreshFile: (isTPR: boolean | null) => void
refreshing: boolean
}
@@ -20,35 +20,17 @@ export function TPRFileViewRefreshButton({
refreshing,
}: TPRFileViewRefreshButtonProps) {
const { t } = useTranslation()
- const provider = (file.linkedFileData as Record)?.provider
- const currentUserId =
- (getMeta('ol-user') as any)?._id || (getMeta('ol-user_id') as string)
const importedByUserId = (file.linkedFileData as any)?.importedByUserId
- const isOriginalImporter =
- provider === 'zotero' &&
- currentUserId &&
- importedByUserId === currentUserId
+ const zoteroIsProvider = hasProvider(file, 'zotero')
+ const disabled = (zoteroIsProvider && !importedByUserId) ? true : false
- if (provider !== 'zotero' || isOriginalImporter) {
- // Zotero or default refresh for originator only
- return (
- refreshFile(provider === 'zotero' ? true : null)}
- disabled={refreshing}
- isLoading={refreshing}
- loadingLabel={t('refreshing')}
- >
- {t('refresh')}
-
- )
- }
-
- // collaborator of Zotero file should not refresh
return (
refreshFile(zoteroIsProvider ? true : null)}
+ disabled={refreshing || disabled}
+ isLoading={refreshing}
+ loadingLabel={t('refreshing')}
>
{t('refresh')}
diff --git a/services/web/modules/zotero/frontend/js/components/tpr-file-view-refresh-error.tsx b/services/web/modules/zotero/frontend/js/components/tpr-file-view-refresh-error.tsx
index f35196923e..ca95ce8d90 100644
--- a/services/web/modules/zotero/frontend/js/components/tpr-file-view-refresh-error.tsx
+++ b/services/web/modules/zotero/frontend/js/components/tpr-file-view-refresh-error.tsx
@@ -1,9 +1,10 @@
import { useTranslation } from 'react-i18next'
import OLNotification from '@/shared/components/ol/ol-notification'
-import type { BinaryFile } from '@/features/file-view/types/binary-file'
+import type { LinkedFile, LinkedFileData } from '@/features/file-view/types/binary-file'
+import { hasProvider } from '@/features/file-view/types/binary-file'
type TPRFileViewRefreshErrorProps = {
- file: BinaryFile
+ file: LinkedFile
refreshError: string
}
@@ -17,25 +18,18 @@ export function TPRFileViewRefreshError({
}: TPRFileViewRefreshErrorProps) {
const { t } = useTranslation()
- if (file.linkedFileData?.provider !== 'zotero') {
- // Not a Zotero file — fall back to default error display
- return (
-
- )
- }
-
let message = refreshError
- if (refreshError === 'forbidden' || refreshError?.includes('403')) {
- message = t('zotero_reference_loading_error_forbidden')
- } else if (
- refreshError === 'expired' ||
- refreshError?.includes('token expired')
- ) {
- message = t('zotero_reference_loading_error_expired')
- } else if (!message) {
- message = t('zotero_reference_loading_error')
+
+ if (hasProvider(file, 'zotero')) {
+ if (!refreshError) {
+ message = t('zotero_reference_loading_error')
+ } else if (refreshError?.includes('not linked')) {
+ message = t('zotero_reference_loading_error_not_linked')
+ } else if (refreshError === 'forbidden' || refreshError?.includes('403')) {
+ message = t('zotero_reference_loading_error_forbidden')
+ } else if (refreshError === 'expired' || refreshError?.includes('token expired')) {
+ message = t('zotero_reference_loading_error_expired')
+ }
}
return (