placeholder utill we loaded up
This commit is contained in:
@@ -80,7 +80,17 @@ const Reactions: React.FC<ReactionsProps> = ({ issueId, commentId }) => {
|
|||||||
handleReactionClick(emojiData.emoji);
|
handleReactionClick(emojiData.emoji);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading || !myUserId) return null;
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div style={styles.container}>
|
||||||
|
<div style={{ ...styles.skeleton, width: '50px' }}></div>
|
||||||
|
<div style={{ ...styles.skeleton, width: '45px' }}></div>
|
||||||
|
<div style={styles.addButtonSkeleton}></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!myUserId) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.container}>
|
<div style={styles.container}>
|
||||||
@@ -109,7 +119,7 @@ const Reactions: React.FC<ReactionsProps> = ({ issueId, commentId }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const styles: { [key: string]: React.CSSProperties } = {
|
const styles: { [key: string]: React.CSSProperties } = {
|
||||||
container: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '6px', marginTop: '8px', marginBottom: '8px', paddingLeft: '10px', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', fontSize: '13px' },
|
container: { display: 'flex', alignItems: 'center', flexWrap: 'wrap', gap: '6px', marginTop: '8px', marginBottom: '8px', paddingLeft: '10px', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif', fontSize: '13px', minHeight: '36px' },
|
||||||
badge: { display: 'flex', alignItems: 'center', padding: '2px 8px', border: '1px solid #dcdcdc', borderRadius: '12px', cursor: 'pointer', transition: 'background-color 0.2s', backgroundColor: '#f7f7f7' },
|
badge: { display: 'flex', alignItems: 'center', padding: '2px 8px', border: '1px solid #dcdcdc', borderRadius: '12px', cursor: 'pointer', transition: 'background-color 0.2s', backgroundColor: '#f7f7f7' },
|
||||||
badgeSelected: { backgroundColor: '#e6f2ff', borderColor: '#99ccff' },
|
badgeSelected: { backgroundColor: '#e6f2ff', borderColor: '#99ccff' },
|
||||||
emoji: { marginRight: '4px', fontSize: '15px', lineHeight: '1' },
|
emoji: { marginRight: '4px', fontSize: '15px', lineHeight: '1' },
|
||||||
@@ -117,6 +127,8 @@ const styles: { [key: string]: React.CSSProperties } = {
|
|||||||
countSelected: { color: '#005cc5' },
|
countSelected: { color: '#005cc5' },
|
||||||
addButton: { display: 'flex', alignItems: 'center', padding: '2px 8px', border: '1px dashed #ccc', borderRadius: '12px', backgroundColor: 'transparent', cursor: 'pointer', color: '#555', fontFamily: 'inherit', fontSize: '13px' },
|
addButton: { display: 'flex', alignItems: 'center', padding: '2px 8px', border: '1px dashed #ccc', borderRadius: '12px', backgroundColor: 'transparent', cursor: 'pointer', color: '#555', fontFamily: 'inherit', fontSize: '13px' },
|
||||||
pickerWrapper: { position: 'absolute', bottom: '100%', left: 0, marginBottom: '10px', zIndex: 1000 },
|
pickerWrapper: { position: 'absolute', bottom: '100%', left: 0, marginBottom: '10px', zIndex: 1000 },
|
||||||
|
skeleton: { height: '26px', backgroundColor: '#eef0f2', borderRadius: '12px', animation: 'reactions-pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite' },
|
||||||
|
addButtonSkeleton: { height: '26px', width: '42px', backgroundColor: 'transparent', border: '1px dashed #e0e0e0', borderRadius: '12px' },
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Reactions;
|
export default Reactions;
|
||||||
|
|||||||
+53
-32
@@ -2,40 +2,61 @@ import React from 'react';
|
|||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import Reactions from './components/Reactions';
|
import Reactions from './components/Reactions';
|
||||||
|
|
||||||
console.log('Redmine Reactions Extension Loaded!');
|
// --- Подготовка ---
|
||||||
|
// Создаем элемент <style>, но пока не вставляем его
|
||||||
|
const styleSheet = document.createElement("style");
|
||||||
|
styleSheet.innerText = `
|
||||||
|
@keyframes reactions-pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.5; }
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
// Флаг, чтобы убедиться, что стили вставляются только один раз
|
||||||
|
let stylesInjected = false;
|
||||||
|
|
||||||
// Функция для извлечения ID задачи из URL
|
function processCommentContainer(container: HTMLElement) {
|
||||||
function getIssueId(): number | null {
|
if (container.dataset.reactionsInitialized) return;
|
||||||
const match = window.location.pathname.match(/\/issues\/(\d+)/);
|
container.dataset.reactionsInitialized = 'true';
|
||||||
return match && match[1] ? parseInt(match[1], 10) : null;
|
|
||||||
|
const issueIdMatch = window.location.pathname.match(/\/issues\/(\d+)/);
|
||||||
|
if (!issueIdMatch) return;
|
||||||
|
const issueId = parseInt(issueIdMatch[1], 10);
|
||||||
|
|
||||||
|
const noteElement = container.querySelector<HTMLElement>('div[id^="note-"]');
|
||||||
|
if (!noteElement) return;
|
||||||
|
const commentId = noteElement.id;
|
||||||
|
|
||||||
|
const reactionRootEl = document.createElement('div');
|
||||||
|
reactionRootEl.style.minHeight = '36px'; // Резервируем место
|
||||||
|
container.appendChild(reactionRootEl);
|
||||||
|
|
||||||
|
const root = ReactDOM.createRoot(reactionRootEl);
|
||||||
|
root.render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<Reactions issueId={issueId} commentId={commentId} />
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Находим все блоки с комментариями на странице
|
const observer = new MutationObserver((mutations) => {
|
||||||
const commentContainers = document.querySelectorAll('div.journal.has-notes');
|
if (!stylesInjected && document.head) {
|
||||||
|
document.head.appendChild(styleSheet);
|
||||||
|
stylesInjected = true;
|
||||||
|
}
|
||||||
|
|
||||||
const issueId = getIssueId();
|
for (const mutation of mutations) {
|
||||||
|
for (const node of mutation.addedNodes) {
|
||||||
|
if (node instanceof HTMLElement) {
|
||||||
|
if (node.matches('div.journal.has-notes')) {
|
||||||
|
processCommentContainer(node);
|
||||||
|
}
|
||||||
|
node.querySelectorAll<HTMLElement>('div.journal.has-notes').forEach(processCommentContainer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if (issueId) {
|
observer.observe(document, {
|
||||||
commentContainers.forEach(container => {
|
childList: true,
|
||||||
// Находим вложенный div с ID комментария
|
subtree: true,
|
||||||
const noteElement = container.querySelector<HTMLDivElement>('div[id^="note-"]');
|
});
|
||||||
if (!noteElement) return;
|
|
||||||
|
|
||||||
const commentId = noteElement.id;
|
|
||||||
|
|
||||||
// Создаем div, в который будем рендерить наш React-компонент
|
|
||||||
const reactionRootEl = document.createElement('div');
|
|
||||||
reactionRootEl.className = 'reactions-app-root';
|
|
||||||
|
|
||||||
// Вставляем наш div в конец контейнера, как вы и определили
|
|
||||||
container.appendChild(reactionRootEl);
|
|
||||||
|
|
||||||
// Создаем React-root и рендерим компонент
|
|
||||||
const root = ReactDOM.createRoot(reactionRootEl);
|
|
||||||
root.render(
|
|
||||||
<React.StrictMode>
|
|
||||||
<Reactions issueId={issueId} commentId={commentId} />
|
|
||||||
</React.StrictMode>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user