[WIKI-509] feat: comment copy link option (#7385)
* feat: comment copy link option * chore: add translations * chore: update block position * chore: rename use id scroll hook * refactor: setTimeout function * refactor: use-hash-scroll hook
This commit is contained in:
parent
f90e553881
commit
2c70c1aaa8
31 changed files with 694 additions and 250 deletions
|
|
@ -1,3 +1,4 @@
|
|||
export * from "./use-hash-scroll";
|
||||
export * from "./use-local-storage";
|
||||
export * from "./use-outside-click-detector";
|
||||
export * from "./use-platform-os";
|
||||
|
|
|
|||
128
packages/hooks/src/use-hash-scroll.ts
Normal file
128
packages/hooks/src/use-hash-scroll.ts
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
type TArgs = {
|
||||
elementId: string;
|
||||
pathname: string;
|
||||
scrollDelay?: number;
|
||||
};
|
||||
|
||||
type TReturnType = {
|
||||
isHashMatch: boolean;
|
||||
hashIds: string[];
|
||||
scrollToElement: () => boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Custom hook for handling hash-based scrolling to a specific element
|
||||
* Supports multiple IDs in URL hash (comma-separated, space-separated, or other delimiters)
|
||||
*
|
||||
* @param {TArgs} args - The ID of the element to scroll to
|
||||
* @returns {TReturnType} Object containing hash match status and scroll function
|
||||
*/
|
||||
export const useHashScroll = (args: TArgs): TReturnType => {
|
||||
const { elementId, pathname, scrollDelay = 200 } = args;
|
||||
// State to track if the current hash contains the provided element ID
|
||||
const [isHashMatch, setIsHashMatch] = useState(false);
|
||||
// State to track all IDs found in the hash
|
||||
const [hashIds, setHashIds] = useState<string[]>([]);
|
||||
|
||||
/**
|
||||
* Scrolls to the element with the provided ID
|
||||
* @returns {boolean} - Whether the scroll was successful
|
||||
*/
|
||||
const scrollToElement = useCallback((): boolean => {
|
||||
try {
|
||||
const element = document.getElementById(elementId);
|
||||
|
||||
if (element) {
|
||||
setTimeout(() => {
|
||||
element.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}, scrollDelay);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} catch (error) {
|
||||
console.warn("Hash scroll error:", error);
|
||||
return false;
|
||||
}
|
||||
}, [elementId, scrollDelay]);
|
||||
|
||||
/**
|
||||
* Extracts multiple IDs from hash string
|
||||
* Supports various delimiters: comma, space, pipe, semicolon
|
||||
* @param {string} hashString - The hash part of the URL
|
||||
* @returns {string[]} - Array of clean ID strings
|
||||
*/
|
||||
const extractIdsFromHash = (hashString: string | null): string[] => {
|
||||
if (!hashString) return [];
|
||||
|
||||
// Split by common delimiters and clean up
|
||||
return hashString
|
||||
.split(/[,\s|;]+/) // Split by comma, space, pipe, or semicolon
|
||||
.map((id) => id.trim()) // Remove whitespace
|
||||
.filter((id) => id.length > 0); // Remove empty strings
|
||||
};
|
||||
|
||||
/**
|
||||
* Get current hash from window.location
|
||||
* @returns {string | null} - Current hash without the # symbol
|
||||
*/
|
||||
const getCurrentHash = (): string | null => {
|
||||
if (typeof window === "undefined") return null;
|
||||
const hash = window.location.hash;
|
||||
return hash ? hash.slice(1) : null; // Remove the # symbol
|
||||
};
|
||||
|
||||
// Effect to handle hash changes and initial load
|
||||
useEffect(() => {
|
||||
if (!elementId) {
|
||||
setIsHashMatch(false);
|
||||
setHashIds([]);
|
||||
return;
|
||||
}
|
||||
|
||||
const handleHashChange = () => {
|
||||
const hash = getCurrentHash();
|
||||
|
||||
// Extract all IDs from the hash
|
||||
const idsInHash = extractIdsFromHash(hash);
|
||||
setHashIds(idsInHash);
|
||||
|
||||
// Check if provided element ID is present in the hash
|
||||
const hashMatches = idsInHash.includes(elementId);
|
||||
setIsHashMatch(hashMatches);
|
||||
|
||||
// If hash matches, attempt to scroll to the element
|
||||
if (hashMatches) {
|
||||
scrollToElement();
|
||||
}
|
||||
};
|
||||
|
||||
// Handle initial load
|
||||
handleHashChange();
|
||||
|
||||
// Listen for hash changes
|
||||
window.addEventListener("hashchange", handleHashChange);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("hashchange", handleHashChange);
|
||||
};
|
||||
}, [elementId, pathname, scrollToElement]); // Include pathname to handle route changes
|
||||
|
||||
// Return object with hash match status and utility functions
|
||||
return {
|
||||
// Whether the current URL hash contains the provided element ID
|
||||
isHashMatch,
|
||||
|
||||
// Array of all IDs found in the current hash
|
||||
hashIds,
|
||||
|
||||
// Manually trigger scroll to the element
|
||||
scrollToElement,
|
||||
};
|
||||
};
|
||||
|
|
@ -1042,6 +1042,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "Nahrání přílohy se nezdařilo. Zkuste to prosím později."
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "Odkaz na komentář byl zkopírován do schránky",
|
||||
"error": "Chyba při kopírování odkazu na komentář. Zkuste to prosím později."
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -1042,6 +1042,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "Anhang konnte nicht hochgeladen werden. Bitte versuchen Sie es später erneut."
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "Kommentar-Link in die Zwischenablage kopiert",
|
||||
"error": "Fehler beim Kopieren des Kommentar-Links. Bitte versuchen Sie es später erneut."
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -885,6 +885,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "Asset upload failed. Please try again later."
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "Comment link copied to clipboard",
|
||||
"error": "Error copying comment link. Please try again later."
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -1045,6 +1045,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "Error al subir el archivo. Por favor, inténtalo más tarde."
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "Enlace del comentario copiado al portapapeles",
|
||||
"error": "Error al copiar el enlace del comentario. Inténtelo de nuevo más tarde."
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -1043,6 +1043,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "Échec du téléchargement du fichier. Veuillez réessayer plus tard."
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "Lien du commentaire copié dans le presse-papiers",
|
||||
"error": "Erreur lors de la copie du lien du commentaire. Veuillez réessayer plus tard."
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -1042,6 +1042,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "Gagal mengunggah aset. Silakan coba lagi nanti."
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "Tautan komentar berhasil disalin ke clipboard",
|
||||
"error": "Gagal menyalin tautan komentar. Silakan coba lagi nanti."
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -1041,6 +1041,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "Caricamento dell'asset fallito. Per favore, riprova più tardi."
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "Link del commento copiato negli appunti",
|
||||
"error": "Errore durante la copia del link del commento. Riprova più tardi."
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -1043,6 +1043,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "アセットのアップロードに失敗しました。後でもう一度お試しください。"
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "コメントリンクがクリップボードにコピーされました",
|
||||
"error": "コメントリンクのコピーに失敗しました。後でもう一度お試しください。"
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -1044,6 +1044,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "자산 업로드 실패. 나중에 다시 시도해주세요."
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "댓글 링크가 클립보드에 복사되었습니다",
|
||||
"error": "댓글 링크 복사 중 오류가 발생했습니다. 나중에 다시 시도해 주세요."
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -1044,6 +1044,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "Nie udało się przesłać załącznika. Spróbuj później."
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "Link do komentarza skopiowany do schowka",
|
||||
"error": "Błąd podczas kopiowania linka do komentarza. Spróbuj ponownie później."
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -1044,6 +1044,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "Falha ao carregar o recurso. Por favor, tente novamente mais tarde."
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "Link do comentário copiado para a área de transferência",
|
||||
"error": "Erro ao copiar o link do comentário. Tente novamente mais tarde."
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -1042,6 +1042,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "Încărcarea fișierului a eșuat. Te rugăm să încerci mai târziu."
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "Linkul comentariului a fost copiat în clipboard",
|
||||
"error": "Eroare la copierea linkului comentariului. Încercați din nou mai târziu."
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -1044,6 +1044,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "Ошибка загрузки файла. Попробуйте позже."
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "Ссылка на комментарий скопирована в буфер обмена",
|
||||
"error": "Ошибка при копировании ссылки на комментарий. Попробуйте позже."
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -1044,6 +1044,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "Nahratie prílohy zlyhalo. Skúste to prosím neskôr."
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "Odkaz na komentár bol skopírovaný do schránky",
|
||||
"error": "Chyba pri kopírovaní odkazu na komentár. Skúste to prosím neskôr."
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -1045,6 +1045,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "Dosya yüklenemedi. Lütfen daha sonra tekrar deneyin."
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "Yorum bağlantısı panoya kopyalandı",
|
||||
"error": "Yorum bağlantısı kopyalanırken hata oluştu. Lütfen daha sonra tekrar deneyin."
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -1044,6 +1044,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "Не вдалося завантажити вкладення. Спробуйте пізніше."
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "Посилання на коментар скопійовано в буфер обміну",
|
||||
"error": "Помилка при копіюванні посилання на коментар. Спробуйте пізніше."
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -1043,6 +1043,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "Không thể tải lên tài nguyên. Vui lòng thử lại sau."
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "Liên kết bình luận đã được sao chép vào clipboard",
|
||||
"error": "Lỗi khi sao chép liên kết bình luận. Vui lòng thử lại sau."
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -1043,6 +1043,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "资源上传失败。请稍后重试。"
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "评论链接已复制到剪贴板",
|
||||
"error": "复制评论链接时出错。请稍后再试。"
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -1044,6 +1044,10 @@
|
|||
},
|
||||
"upload": {
|
||||
"error": "資產上傳失敗。請稍後再試。"
|
||||
},
|
||||
"copy_link": {
|
||||
"success": "評論連結已複製到剪貼簿",
|
||||
"error": "複製評論連結時出錯。請稍後再試。"
|
||||
}
|
||||
},
|
||||
"empty_state": {
|
||||
|
|
|
|||
|
|
@ -41,12 +41,13 @@ export type TIssueComment = {
|
|||
};
|
||||
|
||||
export type TCommentsOperations = {
|
||||
copyCommentLink: (commentId: string) => void;
|
||||
createComment: (data: Partial<TIssueComment>) => Promise<Partial<TIssueComment> | undefined>;
|
||||
updateComment: (commentId: string, data: Partial<TIssueComment>) => Promise<void>;
|
||||
removeComment: (commentId: string) => Promise<void>;
|
||||
uploadCommentAsset: (blockId: string, file: File, commentId?: string) => Promise<TFileSignedURLResponse>;
|
||||
addCommentReaction: (commentId: string, reactionEmoji: string) => Promise<void>;
|
||||
deleteCommentReaction: (commentId: string, reactionEmoji: string, userReactions: TCommentReaction[]) => Promise<void>;
|
||||
deleteCommentReaction: (commentId: string, reactionEmoji: string) => Promise<void>;
|
||||
react: (commentId: string, reactionEmoji: string, userReactions: string[]) => Promise<void>;
|
||||
reactionIds: (commentId: string) =>
|
||||
| {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue