diff --git a/.playwright-mcp/console-2026-03-16T09-27-00-418Z.log b/.playwright-mcp/console-2026-03-16T09-27-00-418Z.log new file mode 100644 index 0000000..f7b0012 --- /dev/null +++ b/.playwright-mcp/console-2026-03-16T09-27-00-418Z.log @@ -0,0 +1,28 @@ +[ 3110815ms] [ERROR] %o + +%s + +%s + ReferenceError: Upload is not defined + at ArtefactDetailVersionsTab (http://localhost:5173/src/components/ArtefactDetailVersionsTab.jsx?t=1773656331074:286:42) + at Object.react_stack_bottom_frame (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:18509:20) + at renderWithHooks (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:5654:24) + at updateFunctionComponent (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:7475:21) + at beginWork (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:8525:20) + at runWithFiberInDEV (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:997:72) + at performUnitOfWork (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12561:98) + at workLoopSync (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12424:43) + at renderRootSync (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12408:13) + at performWorkOnRoot (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:11827:37) The above error occurred in the component. React will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary. @ http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:7000 +[ 3110816ms] [ERROR] ErrorBoundary caught: ReferenceError: Upload is not defined + at ArtefactDetailVersionsTab (http://localhost:5173/src/components/ArtefactDetailVersionsTab.jsx?t=1773656331074:286:42) + at Object.react_stack_bottom_frame (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:18509:20) + at renderWithHooks (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:5654:24) + at updateFunctionComponent (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:7475:21) + at beginWork (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:8525:20) + at runWithFiberInDEV (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:997:72) + at performUnitOfWork (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12561:98) + at workLoopSync (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12424:43) + at renderRootSync (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:12408:13) + at performWorkOnRoot (http://localhost:5173/node_modules/.vite/deps/react-dom_client.js?v=50a373cd:11827:37) {componentStack: + at ArtefactDetailVersionsTab (http://localhos…vite/deps/react-router-dom.js?v=50a373cd:10250:3)} @ http://localhost:5173/src/components/ErrorBoundary.jsx:12 diff --git a/approver-dropdown-test.png b/approver-dropdown-test.png new file mode 100644 index 0000000..601716e Binary files /dev/null and b/approver-dropdown-test.png differ diff --git a/campaign-select-test.png b/campaign-select-test.png new file mode 100644 index 0000000..ce6c877 Binary files /dev/null and b/campaign-select-test.png differ diff --git a/client/src/App.jsx b/client/src/App.jsx index 7032e01..af9cba2 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -290,7 +290,7 @@ function AppContent() { {showTutorial && } -
Loading...
}> +
}> : } /> : } /> diff --git a/client/src/components/ApproverMultiSelect.jsx b/client/src/components/ApproverMultiSelect.jsx index 6f9758b..51151b5 100644 --- a/client/src/components/ApproverMultiSelect.jsx +++ b/client/src/components/ApproverMultiSelect.jsx @@ -1,30 +1,51 @@ -import { useState, useRef, useEffect } from 'react' +import { useState, useRef, useEffect, useCallback } from 'react' +import { createPortal } from 'react-dom' import { Check, ChevronDown, X } from 'lucide-react' export default function ApproverMultiSelect({ users = [], selected = [], onChange }) { const [open, setOpen] = useState(false) - const [dropUp, setDropUp] = useState(false) - const wrapperRef = useRef(null) + const triggerRef = useRef(null) + const dropdownRef = useRef(null) + const [pos, setPos] = useState({ top: 0, left: 0, width: 0 }) + + const updatePosition = useCallback(() => { + if (!triggerRef.current) return + const rect = triggerRef.current.getBoundingClientRect() + const spaceBelow = window.innerHeight - rect.bottom + const dropdownHeight = Math.min(users.length * 40 + 8, 220) + const showAbove = spaceBelow < dropdownHeight && rect.top > dropdownHeight + + setPos({ + top: showAbove ? rect.top - dropdownHeight - 4 : rect.bottom + 4, + left: rect.left, + width: rect.width, + }) + }, [users.length]) - // Close dropdown when clicking outside useEffect(() => { if (!open) return const handleClick = (e) => { - if (wrapperRef.current && !wrapperRef.current.contains(e.target)) { - setOpen(false) - } + if (triggerRef.current?.contains(e.target)) return + if (dropdownRef.current?.contains(e.target)) return + setOpen(false) } - document.addEventListener('mousedown', handleClick) - return () => document.removeEventListener('mousedown', handleClick) - }, [open]) + const handleEsc = (e) => { if (e.key === 'Escape') setOpen(false) } + const handleScroll = () => updatePosition() - // Detect if dropdown should open upward - useEffect(() => { - if (!open || !wrapperRef.current) return - const rect = wrapperRef.current.getBoundingClientRect() - const spaceBelow = window.innerHeight - rect.bottom - setDropUp(spaceBelow < 220) - }, [open]) + document.addEventListener('mousedown', handleClick) + document.addEventListener('keydown', handleEsc) + window.addEventListener('scroll', handleScroll, true) + return () => { + document.removeEventListener('mousedown', handleClick) + document.removeEventListener('keydown', handleEsc) + window.removeEventListener('scroll', handleScroll, true) + } + }, [open, updatePosition]) + + const handleOpen = () => { + updatePosition() + setOpen(!open) + } const toggle = (userId) => { const id = String(userId) @@ -39,9 +60,10 @@ export default function ApproverMultiSelect({ users = [], selected = [], onChang const selectedUsers = selected.map(id => users.find(u => String(u._id || u.id || u.Id) === String(id))).filter(Boolean) return ( -
+ <>
setOpen(!open)} + ref={triggerRef} + onClick={handleOpen} className={`w-full min-h-[38px] px-3 py-1.5 text-sm border rounded-lg bg-surface cursor-pointer flex items-center flex-wrap gap-1.5 transition-colors ${ open ? 'border-brand-primary ring-2 ring-brand-primary/20' : 'border-border' }`} @@ -58,7 +80,7 @@ export default function ApproverMultiSelect({ users = [], selected = [], onChang @@ -66,8 +88,13 @@ export default function ApproverMultiSelect({ users = [], selected = [], onChang ))}
- {open && ( -
+ + {open && createPortal( +
{users.map(u => { const uid = String(u._id || u.id || u.Id) const isSelected = selected.includes(uid) @@ -76,7 +103,7 @@ export default function ApproverMultiSelect({ users = [], selected = [], onChang key={uid} type="button" onClick={() => toggle(uid)} - className={`w-full text-start px-3 py-2 text-sm hover:bg-surface-secondary flex items-center justify-between ${ + className={`w-full text-start px-3 py-2 text-sm hover:bg-surface-secondary flex items-center justify-between transition-colors ${ isSelected ? 'text-brand-primary font-medium' : 'text-text-primary' }`} > @@ -88,8 +115,9 @@ export default function ApproverMultiSelect({ users = [], selected = [], onChang {users.length === 0 && (
No users available
)} -
+
, + document.body )} -
+ ) } diff --git a/client/src/components/ArtefactDetailPanel.jsx b/client/src/components/ArtefactDetailPanel.jsx index cf134b6..994590e 100644 --- a/client/src/components/ArtefactDetailPanel.jsx +++ b/client/src/components/ArtefactDetailPanel.jsx @@ -7,6 +7,7 @@ import Modal from './Modal' import TabbedModal from './TabbedModal' import { useToast } from './ToastContainer' import ApproverMultiSelect from './ApproverMultiSelect' +import PortalSelect from './PortalSelect' import { ArtefactDetailVersionsTab } from './ArtefactDetailVersionsTab' const STATUS_COLORS = { @@ -380,38 +381,6 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel /> - {/* Project & Campaign dropdowns */} -
-
-

{t('artefacts.project')}

- -
-
-

{t('artefacts.campaign')}

- -
-
- )} @@ -500,21 +469,16 @@ export default function ArtefactDetailPanel({ artefact, onClose, onUpdate, onDel {['draft', 'revision_requested', 'rejected'].includes(artefact.status) && (

{t('artefacts.reviewer')}

- + />
)} diff --git a/client/src/components/ArtefactDetailVersionsTab.jsx b/client/src/components/ArtefactDetailVersionsTab.jsx index 9c81ee0..301e1ce 100644 --- a/client/src/components/ArtefactDetailVersionsTab.jsx +++ b/client/src/components/ArtefactDetailVersionsTab.jsx @@ -1,5 +1,7 @@ import { useState } from 'react' -import { Plus, Upload, Trash2, Globe, Image as ImageIcon } from 'lucide-react' +import { Plus, Trash2, Globe, Image as ImageIcon } from 'lucide-react' +import PortalSelect from './PortalSelect' +import UploadZone from './UploadZone' import { useLanguage } from '../i18n/LanguageContext' import Modal from './Modal' import ArtefactVersionTimeline from './ArtefactVersionTimeline' @@ -172,29 +174,17 @@ export function ArtefactDetailVersionsTab({ {/* DESIGN TYPE: Image gallery */} {artefact.type === 'design' && (
-
-

{t('artefacts.imagesLabel')}

- -
+

{t('artefacts.imagesLabel')}

- {versionData.attachments && versionData.attachments.length > 0 ? ( -
+ {versionData.attachments && versionData.attachments.length > 0 && ( +
{versionData.attachments.map(att => (
{att.original_name}
- ) : ( -
- -

{t('artefacts.noImages')}

-
)} + 0} + />
)} @@ -256,30 +250,14 @@ export function ArtefactDetailVersionsTab({ )} {/* Drag-and-drop / click-to-upload zone */} -