Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
# Production-ready Dockerfile for Next.js application
FROM node:18-alpine AS base
FROM node:20-alpine AS base

# Install dependencies only when needed
FROM base AS deps
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Copy package files
Expand All @@ -12,6 +13,7 @@ RUN npm install --production=false --legacy-peer-deps

# Build the source code
FROM base AS builder
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
Expand Down
9 changes: 9 additions & 0 deletions src/app/components/InspectorSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ const InspectorSidebar = ({
if (items.length > 0) {
setItem(items[0])
setIsOpen(true)
} else {
// Item was deleted — clear inspector
setItem(null)
setFocusedItem(undefined)
}
} else {
setItem(null)
Expand Down Expand Up @@ -172,6 +176,11 @@ const InspectorSidebar = ({
flattenedEntries.push([key, value]),
)

// Inject folder name for molecules
if ((item as ExtendedFolder).dtype === 'molecule') {
flattenedEntries.push(['name', item.name])
}

// Add extended_metadata fields if they exist
if (
metadata.extended_metadata &&
Expand Down
57 changes: 55 additions & 2 deletions src/app/components/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ import { ExportFilesText } from './workspace/ExportFilesText'
import { Toolbar } from './workspace/Toolbar'
import { UploadFilesText } from './workspace/UploadFilesText'
import { UploadedFiles } from './workspace/UploadedFiles'
import RenamePopup from './tree-view/RenamePopup'
import DeletePopup from './tree-view/DeletePopup'

type Database = {
assignedFiles: ExtendedFile[]
Expand Down Expand Up @@ -106,6 +108,18 @@ const Workspace = ({
ExtendedFile | ExtendedFolder
>()

const [renamingItem, setRenamingItem] = useState<{
item: ExtendedFile | ExtendedFolder
x: number
y: number
} | null>(null)

const [deletingItem, setDeletingItem] = useState<{
item: ExtendedFile | ExtendedFolder
x: number
y: number
} | null>(null)

if (!db)
return (
<div className="flex h-full w-full items-center justify-center">
Expand Down Expand Up @@ -223,6 +237,24 @@ const Workspace = ({
const assignmentTreeContextMenuClose = () =>
setAssignmentTreeContextMenu(initialContextMenu)

const fetchItem = async (fullPath: string) => {
const file = await filesDB.files.get({ fullPath })
const folder = await filesDB.folders.get({ fullPath })
return file || folder
}

const handleRenameClick = async (fullPath: string, x: number, y: number) => {
const item = await fetchItem(fullPath)
if (!item) return
setRenamingItem({ item, x, y })
}

const handleDeleteClick = async (fullPath: string, x: number, y: number) => {
const item = await fetchItem(fullPath)
if (!item) return
setDeletingItem({ item, x, y })
}

return (
<Fragment>
<Toolbar
Expand Down Expand Up @@ -271,7 +303,7 @@ const Workspace = ({
{children}
</div>
)}
renderItem={createRenderItem(tree)}
renderItem={createRenderItem(tree, setExpandedItems)}
rootItem={inputTreeRoot}
treeId="inputTree"
treeLabel="Input Tree"
Expand All @@ -295,7 +327,10 @@ const Workspace = ({
{children}
</div>
)}
renderItem={createRenderItem(tree)}
renderItem={createRenderItem(tree, setExpandedItems, {
onRenameClick: handleRenameClick,
onDeleteClick: handleDeleteClick,
})}
rootItem={assignmentTreeRoot}
treeId="assignmentTree"
treeLabel="Assignment Tree"
Expand All @@ -321,6 +356,24 @@ const Workspace = ({
y={assignmentTreeContextMenu.y}
/>
)}
{renamingItem && (
<RenamePopup
item={renamingItem.item}
tree={tree}
x={renamingItem.x}
y={renamingItem.y}
onClose={() => setRenamingItem(null)}
/>
)}
{deletingItem && (
<DeletePopup
item={deletingItem.item}
tree={tree}
x={deletingItem.x}
y={deletingItem.y}
onClose={() => setDeletingItem(null)}
/>
)}
</div>
{children}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const ClickOnAnalysesContextMenu = ({
targetItem={targetItem}
tree={tree}
hideDeleteOption={true}
hideRenameOption={true}
/>
</>
)
Expand Down
14 changes: 9 additions & 5 deletions src/app/components/context-menu/DefaultContextMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,22 @@ const DefaultContextMenu = ({
targetItem,
tree,
hideDeleteOption = false,
hideRenameOption = false,
}: {
closeContextMenu: () => void
targetItem: ExtendedFile | ExtendedFolder
tree: Record<string, FileNode>
hideDeleteOption?: boolean
hideRenameOption?: boolean
}) => (
<>
<RenameContextMenuItem
close={closeContextMenu}
item={targetItem}
tree={tree}
/>
{!hideRenameOption && (
<RenameContextMenuItem
close={closeContextMenu}
item={targetItem}
tree={tree}
/>
)}
<ContextMenuDivider />
{!hideDeleteOption && (
<DeleteContextMenuItem
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import { FileNode } from '@/helper/types'
import { useOnClickOutside } from '@/hooks/useOnClickOutside'
import { FC, useRef, useState } from 'react'
import { FaDeleteLeft } from 'react-icons/fa6'

import deleteFile from '../deleteFile'
import deleteFolder from '../deleteFolder'
import DeleteConfirm from '../../shared/DeleteConfirm'

interface DeleteProps {
className?: string
Expand All @@ -21,25 +19,10 @@ const DeleteContextMenuItem: FC<DeleteProps> = ({
tree,
}) => {
const popupRef = useRef<HTMLDivElement>(null)

const [showConfirmation, setShowConfirmation] = useState(false)

useOnClickOutside(popupRef, () => setShowConfirmation(false))

const handleDelete = (e: React.FormEvent) => {
e.preventDefault()

if (!item.isFolder) deleteFile(item as ExtendedFile)
else deleteFolder(item as ExtendedFolder, tree)

close()
}

const handleCancel = (e: { stopPropagation: () => void }) => {
e.stopPropagation()
setShowConfirmation(false)
}

return (
<li
className={`${className} ${showConfirmation && 'bg-gray-300'} relative`}
Expand All @@ -54,29 +37,12 @@ const DeleteContextMenuItem: FC<DeleteProps> = ({
className="absolute left-full top-[-5px] z-10 ml-2 origin-left-center animate-emerge-from-lamp rounded-lg border border-gray-300 bg-white p-1 shadow-lg"
ref={popupRef}
>
<div className="flex flex-col space-y-1">
<span className="w-auto min-w-[150px] max-w-[300px] truncate whitespace-nowrap rounded-sm p-1 text-center shadow">
Delete{' '}
<span className="underline" title={item.name}>
{item.name}
</span>
?
</span>
<div className="flex justify-end space-x-2">
<button
className="flex-1 rounded bg-blue-500 px-3 py-1 text-white hover:bg-blue-700"
onClick={handleDelete}
>
Confirm
</button>
<button
className="flex-1 rounded bg-red-500 px-3 py-1 text-white hover:bg-red-700"
onClick={handleCancel}
>
Cancel
</button>
</div>
</div>
<DeleteConfirm
item={item}
tree={tree}
onDone={close}
onCancel={() => setShowConfirmation(false)}
/>
</div>
)}
</li>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@ import { FileNode } from '@/helper/types'
import { useOnClickOutside } from '@/hooks/useOnClickOutside'
import { FC, useRef, useState } from 'react'
import { FaEdit } from 'react-icons/fa'

import renameFile from '../renameFile'
import renameFolder from '../renameFolder'
import RenameForm from '../../shared/RenameForm'

interface RenameProps {
className?: string
Expand All @@ -21,45 +19,10 @@ const RenameContextMenuItem: FC<RenameProps> = ({
tree,
}) => {
const popupRef = useRef(null)

const [newName, setNewName] = useState(item.name)
const [showInput, setShowInput] = useState(false)
const [newFullPathAvailable, setNewFullPathAvailable] = useState(false)

useOnClickOutside(popupRef, () => showInput && setShowInput(false))

const validateNewName = (userInput: string) => {
setNewName(userInput)
const parentPath =
item.fullPath.split('/').slice(0, -1).join('/') || 'inputTreeRoot'

const validInput = userInput.trim().length > 0 && !userInput.includes('/')

const newPath =
parentPath === 'inputTreeRoot' ? userInput : parentPath + '/' + userInput

const nameAvailable = !tree[parentPath].children.includes(newPath)

setNewFullPathAvailable(validInput && nameAvailable)
}

const handleCancel = (e: { stopPropagation: () => void }) => {
e.stopPropagation()
setShowInput(false)
setNewName(item.name)
}

const handleRename = (e: React.FormEvent) => {
e.preventDefault()

tree[item.fullPath].data = newName

if (!item.isFolder) renameFile(item as ExtendedFile, newName)
else renameFolder(item as ExtendedFolder, tree, newName)

close()
}

return (
<li
className={`${className} ${showInput && 'bg-gray-300'} relative`}
Expand All @@ -74,33 +37,12 @@ const RenameContextMenuItem: FC<RenameProps> = ({
className="absolute left-full top-[-5px] z-10 ml-2 origin-left-center animate-emerge-from-lamp rounded-lg border border-gray-300 bg-white p-1 shadow-lg"
ref={popupRef}
>
<div className="flex flex-col space-y-1">
<input
className="rounded px-3 py-1 shadow focus:outline-none"
onChange={(e) => validateNewName(e.target.value)}
placeholder="Enter new folder name"
value={newName}
/>
<div className="flex justify-end space-x-2">
<button
className={`${
!newFullPathAvailable
? 'bg-gray-200 hover:bg-gray-200'
: 'hover:bg-blue-700'
} flex-1 rounded bg-blue-500 px-3 py-1 text-white`}
disabled={!newFullPathAvailable}
onClick={handleRename}
>
Rename
</button>
<button
className="flex-1 rounded bg-red-500 px-3 py-1 text-white hover:bg-red-700"
onClick={handleCancel}
>
Cancel
</button>
</div>
</div>
<RenameForm
item={item}
tree={tree}
onDone={close}
onCancel={() => setShowInput(false)}
/>
</div>
)}
</li>
Expand Down
5 changes: 4 additions & 1 deletion src/app/components/hooks/useMetadataHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ export const useMetadataHandlers = ({

// Only update tree/state for the currently selected item
if (fullPath === item.fullPath) {
if (key === 'name') {
if (
key === 'name' &&
(item as ExtendedFolder).dtype !== 'molecule'
) {
renameFolder(item as ExtendedFolder, tree, newValue as string)
}

Expand Down
Loading