Skip to content

Commit

Permalink
✨ Allow dragging and dropping/uploading file to import into workspace
Browse files Browse the repository at this point in the history
  • Loading branch information
foysalit committed Dec 7, 2024
1 parent 7628338 commit 1690391
Show file tree
Hide file tree
Showing 6 changed files with 276 additions and 83 deletions.
24 changes: 23 additions & 1 deletion components/workspace/ItemCreator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,16 @@ import { PlusIcon, XMarkIcon } from '@heroicons/react/20/solid'
import { toast } from 'react-toastify'
import { buildItemsSummary, groupSubjects } from './utils'
import { getDidFromHandleInBatch } from '@/lib/identity'
import { ArrowPathIcon } from '@heroicons/react/24/solid'
import { ArrowPathIcon, PaperClipIcon } from '@heroicons/react/24/solid'

interface WorkspaceItemCreatorProps {
onFileUploadClick: () => void
onCancel?: () => void
size?: 'sm' | 'lg'
}

const WorkspaceItemCreator: React.FC<WorkspaceItemCreatorProps> = ({
onFileUploadClick,
onCancel,
size = 'lg',
}) => {
Expand All @@ -30,6 +32,12 @@ const WorkspaceItemCreator: React.FC<WorkspaceItemCreatorProps> = ({
try {
const formData = new FormData(event.currentTarget)
const items = formData.get('items') as string

if (!items) {
setIsAdding(false)
return false
}

const isDid = (item) => item.startsWith('did:')
const isAtUri = (item) => item.startsWith('at://')
// if it's not did or at-uri but contains .s it's possibly a handle
Expand Down Expand Up @@ -115,6 +123,20 @@ const WorkspaceItemCreator: React.FC<WorkspaceItemCreatorProps> = ({
<PlusIcon className={size === 'lg' ? 'h-5 w-5' : 'h-3 w-3'} />
)}
</ActionButton>
<ActionButton
type="button"
appearance="outlined"
size={size}
onClick={onFileUploadClick}
disabled={isAdding}
title="Import from csv/json file with DIDs/AT-URIs"
>
{isAdding ? (
<ArrowPathIcon className={size === 'lg' ? 'h-5 w-5' : 'h-3 w-3'} />
) : (
<PaperClipIcon className={size === 'lg' ? 'h-5 w-5' : 'h-3 w-3'} />
)}
</ActionButton>
{!!onCancel && (
<ActionButton
type="button"
Expand Down
204 changes: 124 additions & 80 deletions components/workspace/Panel.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import Dropzone, { DropzoneRef } from 'react-dropzone'
import { ActionPanel } from '@/common/ActionPanel'
import { FullScreenActionPanel } from '@/common/FullScreenActionPanel'
import { LabelChip } from '@/common/labels'
Expand All @@ -18,11 +19,12 @@ import { Dialog } from '@headlessui/react'
import { MagnifyingGlassIcon } from '@heroicons/react/20/solid'
import { CheckCircleIcon } from '@heroicons/react/24/outline'
import Link from 'next/link'
import { FormEvent, useRef, useState } from 'react'
import { createRef, FormEvent, useRef, useState } from 'react'
import { toast } from 'react-toastify'
import { WORKSPACE_FORM_ID } from './constants'
import {
useWorkspaceEmptyMutation,
useWorkspaceImport,
useWorkspaceList,
useWorkspaceRemoveItemsMutation,
} from './hooks'
Expand All @@ -40,11 +42,13 @@ export function WorkspacePanel(props: PropsOf<typeof ActionPanel>) {
const [showActionForm, setShowActionForm] = useState(false)
const removeItemsMutation = useWorkspaceRemoveItemsMutation()
const emptyWorkspaceMutation = useWorkspaceEmptyMutation()
const { importFromFiles } = useWorkspaceImport()
const [modEventType, setModEventType] = useState<string>(
MOD_EVENTS.ACKNOWLEDGE,
)
const [showItemCreator, setShowItemCreator] = useState(false)
const actionSubjects = useActionSubjects()
const dropzoneRef = createRef<DropzoneRef>()

const handleRemoveSelected = () => {
const selectedItems = Array.from(
Expand Down Expand Up @@ -273,86 +277,126 @@ export function WorkspacePanel(props: PropsOf<typeof ActionPanel>) {
onClose={onClose}
{...others}
>
{!workspaceList?.length ? (
<div className="flex flex-col flex-1 h-full item-center justify-center">
<>
<CheckCircleIcon
title="Empty workspace"
className="h-10 w-10 text-green-300 align-text-bottom mx-auto mb-4"
/>
<p className="pb-4 text-center text-gray-400 dark:text-gray-50">
Workspace is empty.
</p>
<WorkspaceItemCreator />
</>
</div>
) : (
<>
{showItemCreator && (
<WorkspaceItemCreator
size="sm"
onCancel={() => setShowItemCreator(false)}
/>
)}
<form ref={formRef} id={WORKSPACE_FORM_ID} onSubmit={onFormSubmit}>
{showActionForm && (
<WorkspacePanelActionForm
{...{
modEventType,
setModEventType,
onCancel: () => setShowActionForm((current) => !current),
}}
/>
)}
{!showItemCreator && (
<div className="mb-2 flex space-x-2">
<WorkspacePanelActions
listData={workspaceListStatuses || {}}
handleRemoveSelected={handleRemoveSelected}
handleEmptyWorkspace={handleEmptyWorkspace}
handleFindCorrelation={handleFindCorrelation}
setShowActionForm={setShowActionForm}
setShowItemCreator={setShowItemCreator}
showActionForm={showActionForm}
workspaceList={workspaceList}
/>
</div>
<Dropzone
accept={{ 'application/json': ['.json'], 'text/csv': ['.csv'] }}
onDrop={importFromFiles}
ref={dropzoneRef}
onDropRejected={(rejections) => {
toast.error(
rejections
.map((r) => r.errors.map((e) => e.message).join(' | '))
.flat()
.join(' | '),
)
}}
noKeyboard
noClick
>
{({ getRootProps, getInputProps }) => (
<div
{...getRootProps()}
className={
!workspaceList?.length
? 'flex flex-col flex-1 h-full item-center justify-center'
: ''
}
>
<input {...getInputProps()} />
{!workspaceList?.length ? (
<>
<>
<CheckCircleIcon
title="Empty workspace"
className="h-10 w-10 text-green-300 align-text-bottom mx-auto mb-4"
/>
<p className="pb-4 text-center text-gray-400 dark:text-gray-50">
Workspace is empty.
</p>
<WorkspaceItemCreator
onFileUploadClick={() => {
dropzoneRef.current?.open()
}}
/>
</>
</>
) : (
<>
{showItemCreator && (
<WorkspaceItemCreator
size="sm"
onFileUploadClick={() => {
dropzoneRef.current?.open()
}}
onCancel={() => setShowItemCreator(false)}
/>
)}
<form
ref={formRef}
id={WORKSPACE_FORM_ID}
onSubmit={onFormSubmit}
>
{showActionForm && (
<WorkspacePanelActionForm
{...{
modEventType,
setModEventType,
onCancel: () =>
setShowActionForm((current) => !current),
}}
/>
)}
{!showItemCreator && (
<div className="mb-2 flex space-x-2">
<WorkspacePanelActions
listData={workspaceListStatuses || {}}
handleRemoveSelected={handleRemoveSelected}
handleEmptyWorkspace={handleEmptyWorkspace}
handleFindCorrelation={handleFindCorrelation}
setShowActionForm={setShowActionForm}
setShowItemCreator={setShowItemCreator}
showActionForm={showActionForm}
workspaceList={workspaceList}
/>
</div>
)}
{/* The inline styling is not ideal but there's no easy way to set calc() values in tailwind */}
{/* We are basically telling the browser to leave 180px at the bottom of the container to make room for navigation arrows and use the remaining vertical space for the main content where scrolling will be allowed if content overflows */}
{/* @ts-ignore */}
<style jsx>{`
.scrollable-container {
height: calc(100vh - 100px);
}
@supports (-webkit-touch-callout: none) {
.scrollable-container {
height: calc(100svh - 100px);
}
}
@media (min-width: 640px) {
.scrollable-container {
height: calc(100vh - 180px);
}
}
`}</style>
<div className="scrollable-container overflow-y-auto">
<WorkspaceList
canExport={
!!role &&
[
ToolsOzoneTeamDefs.ROLEADMIN,
ToolsOzoneTeamDefs.ROLEMODERATOR,
].includes(role)
}
list={workspaceList}
onRemoveItem={handleRemoveItem}
listData={workspaceListStatuses || {}}
/>
</div>
</form>
</>
)}
{/* The inline styling is not ideal but there's no easy way to set calc() values in tailwind */}
{/* We are basically telling the browser to leave 180px at the bottom of the container to make room for navigation arrows and use the remaining vertical space for the main content where scrolling will be allowed if content overflows */}
{/* @ts-ignore */}
<style jsx>{`
.scrollable-container {
height: calc(100vh - 100px);
}
@supports (-webkit-touch-callout: none) {
.scrollable-container {
height: calc(100svh - 100px);
}
}
@media (min-width: 640px) {
.scrollable-container {
height: calc(100vh - 180px);
}
}
`}</style>
<div className="scrollable-container overflow-y-auto">
<WorkspaceList
canExport={
!!role &&
[
ToolsOzoneTeamDefs.ROLEADMIN,
ToolsOzoneTeamDefs.ROLEMODERATOR,
].includes(role)
}
list={workspaceList}
onRemoveItem={handleRemoveItem}
listData={workspaceListStatuses || {}}
/>
</div>
</form>
</>
)}
</div>
)}
</Dropzone>
</FullScreenActionPanel>
)
}
30 changes: 28 additions & 2 deletions components/workspace/hooks.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { createCSV, downloadCSV, escapeCSVValue } from '@/lib/csv'
import {
createCSV,
downloadCSV,
escapeCSVValue,
processFileForWorkspaceImport,
} from '@/lib/csv'
import { getLocalStorageData, setLocalStorageData } from '@/lib/local-storage'
import { buildBlueSkyAppUrl, isNonNullable, pluralize } from '@/lib/util'
import { useServerConfig } from '@/shell/ConfigurationContext'
import {
AppBskyActorProfile,
AtUri,
ComAtprotoAdminDefs,
ComAtprotoLabelDefs,
ComAtprotoRepoStrongRef,
ToolsOzoneModerationDefs,
ToolsOzoneTeamDefs,
Expand Down Expand Up @@ -231,6 +235,28 @@ export const useWorkspaceExport = () => {
}
}

export const useWorkspaceImport = () => {
const { mutateAsync: addToWorkspace } = useWorkspaceAddItemsMutation()

const importFromFiles = async (acceptedFiles: File[]) => {
console.log(acceptedFiles)
if (acceptedFiles.length === 0) return
try {
const results = await Promise.all(
acceptedFiles.map((file) => processFileForWorkspaceImport(file)),
)
const items = results.flat()
addToWorkspace(items)
} catch (error) {
toast.error(
`Failed to import items to workspace. ${(error as Error).message}`,
)
}
}

return { importFromFiles }
}

const getList = (): string[] => {
const list = getLocalStorageData<string>(WORKSPACE_LIST_KEY)
if (!list) return []
Expand Down
Loading

0 comments on commit 1690391

Please sign in to comment.