File manager refactor PoC

This commit is contained in:
Michał Kurczewski
2025-11-07 16:57:25 +01:00
parent 12754a493c
commit 6da12a03dc
12 changed files with 435 additions and 147 deletions

View File

@@ -13,7 +13,7 @@ export const GetEntitiesConfigResponseValidator = z.object({
fields: z.record(
z.string(),
z.object({
type: z.enum(["id", "object", "string", "array", "boolean"]),
type: z.enum(["id", "object", "string", "number", "array", "boolean"]),
})
),
})

View File

@@ -67,21 +67,11 @@ export const useApiDeviceRouter = (device?: Device) => {
path={`${ApiDevicePaths.Index}/${DeviceManageFileFeature.Internal}`}
>
<Route
path={DeviceManageFileFeature.Internal}
element={
<DeviceManageFilesScreen
feature={DeviceManageFileFeature.Internal}
/>
}
/>
<Route
path={DeviceManageFileFeature.External}
element={
<DeviceManageFilesScreen
feature={DeviceManageFileFeature.External}
/>
}
/>
path={`:feature/:category?`}
element={<DeviceManageFilesScreen />}
>
<Route index element={<DeviceManageFilesScreen.List />} />
</Route>
</Route>
<Route path={`${ApiDevicePaths.Index}/mc-contacts`}>
<Route path={"mc-contacts"} element={<McContactsScreen />} />

View File

@@ -87,37 +87,35 @@ export const McContactsScreen: FunctionComponent = () => {
)
}, [contacts])
if (!feature || !contacts) {
return (
<LoaderWrapper
key="loader"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25 }}
>
<Typography.H3 message={messages.loaderTitle.id} />
<ProgressBar value={progress} indeterminate={progress === 0} />
</LoaderWrapper>
)
}
const headerTitle =
contacts.length > 0
feature && contacts && contacts.length > 0
? `${feature.title} (${contacts.length})`
: feature.title
: feature?.title || "All contacts"
return (
<>
<DashboardHeaderTitle title={headerTitle} />
<Content
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25 }}
>
<Contacts contacts={sortedContacts} onDelete={handleDelete} />
</Content>
{!feature || !contacts ? (
<LoaderWrapper
key="loader"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25 }}
>
<Typography.H3 message={messages.loaderTitle.id} />
<ProgressBar value={progress} indeterminate={progress === 0} />
</LoaderWrapper>
) : (
<Content
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25 }}
>
<Contacts contacts={sortedContacts} onDelete={handleDelete} />
</Content>
)}
</>
)
}

View File

@@ -8,29 +8,36 @@ import { DashboardHeaderTitle } from "app-routing/feature"
import { ApiDevice } from "devices/api-device/models"
import { AppResultFactory } from "app-utils/models"
import {
openFileDialog,
useActiveDeviceQuery,
useManageFilesSelection,
} from "devices/common/feature"
import {
FileTransferResult,
ManageFiles,
ManageFiles2,
manageFilesMessages,
ManageFilesViewProps,
} from "devices/common/ui"
import { useApiDeviceDeleteEntitiesMutation } from "devices/api-device/feature"
import { DeviceManageFilesTableSection } from "./device-manage-files-table-section"
import { deviceManageFilesMessages } from "./device-manage-files.messages"
import { useDeviceManageFiles } from "./use-device-manage-files"
import { OTHER_FILES_LABEL_TEXTS } from "./device-manage-files.config"
import {
DeviceManageFileFeature,
DeviceManageFileFeatureId,
} from "./device-manage-files.types"
import { ProgressBar, Typography } from "app-theme/ui"
import styled from "styled-components"
import { motion } from "motion/react"
import { Navigate, useParams } from "react-router"
import { List } from "./list"
import { OTHER_FILES_LABEL_TEXTS } from "./device-manage-files.config"
export const DeviceManageFilesScreen: FunctionComponent<{
feature: DeviceManageFileFeatureId
}> = ({ feature }) => {
export const DeviceManageFilesScreen: FunctionComponent & {
List: typeof List
} = () => {
const { feature, category } = useParams<{
feature: string
category?: string
}>()
const { data: device } = useActiveDeviceQuery<ApiDevice>()
const {
isLoading,
@@ -42,73 +49,137 @@ export const DeviceManageFilesScreen: FunctionComponent<{
otherSpaceBytes,
refetch,
progress,
} = useDeviceManageFiles(feature, device)
} = useDeviceManageFiles(feature as DeviceManageFileFeatureId, device)
const { mutateAsync: deleteFilesMutate } =
useApiDeviceDeleteEntitiesMutation(device)
console.log({
segments,
categories,
categoryFileMap,
freeSpaceBytes,
usedSpaceBytes,
otherSpaceBytes,
})
const { activeCategoryId, setActiveCategoryId, activeFileMap } =
useManageFilesSelection({ categories, categoryFileMap })
// const { mutateAsync: deleteFilesMutate } =
// useApiDeviceDeleteEntitiesMutation(device)
// const { activeCategoryId, setActiveCategoryId, activeFileMap } =
// useManageFilesSelection({ categories, categoryFileMap })
const summaryHeader =
feature === DeviceManageFileFeature.Internal
? deviceManageFilesMessages.internalSummaryHeader
: deviceManageFilesMessages.externalSummaryHeader
const addFileButtonText =
activeCategoryId === "applicationFiles"
? deviceManageFilesMessages.addAppFileButtonText
: manageFilesMessages.addFileButtonText
// const addFileButtonText =
// activeCategoryId === "applicationFiles"
// ? deviceManageFilesMessages.addAppFileButtonText
// : manageFilesMessages.addFileButtonText
const messages = {
...deviceManageFilesMessages,
summaryHeader,
addFileButtonText,
// addFileButtonText,
}
const transferFile: ManageFilesViewProps["transferFile"] = async (
_params
): Promise<FileTransferResult> => {
// TODO: Implement file transfer logic here
return AppResultFactory.success<FileTransferResult>()
}
const deleteFiles: ManageFilesViewProps["deleteFiles"] = async (
ids: string[]
): Promise<{ failedIds: string[] }> => {
const { failedIds = [] } = await deleteFilesMutate({
entityType: activeCategoryId,
ids,
})
return { failedIds }
}
// const transferFile: ManageFilesViewProps["transferFile"] = async (
// _params
// ): Promise<FileTransferResult> => {
// // TODO: Implement file transfer logic here
// return AppResultFactory.success<FileTransferResult>()
// }
//
// const deleteFiles: ManageFilesViewProps["deleteFiles"] = async (
// ids: string[]
// ): Promise<{ failedIds: string[] }> => {
// const { failedIds = [] } = await deleteFilesMutate({
// entityType: activeCategoryId,
// ids,
// })
// return { failedIds }
// }
console.log({ categories })
return (
<>
<DashboardHeaderTitle title={"Manage Files"} />
<ManageFiles
activeCategoryId={activeCategoryId}
activeFileMap={activeFileMap}
onActiveCategoryChange={setActiveCategoryId}
segments={segments}
categories={categories}
freeSpaceBytes={freeSpaceBytes}
usedSpaceBytes={usedSpaceBytes}
otherSpaceBytes={otherSpaceBytes}
deleteFiles={deleteFiles}
onDeleteSuccess={refetch}
isLoading={isLoading}
otherFiles={OTHER_FILES_LABEL_TEXTS}
openFileDialog={openFileDialog}
transferFile={transferFile}
messages={messages}
onTransferSuccess={refetch}
progress={progress}
>
{(props) => (
<DeviceManageFilesTableSection fileMap={activeFileMap} {...props} />
)}
</ManageFiles>
{!feature || isLoading ? (
<LoaderWrapper
key="loader"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25 }}
>
<Typography.H3 message={messages.loadStateText.id} />
<ProgressBar value={progress} indeterminate={progress === 0} />
</LoaderWrapper>
) : (
<>
{!category && categories && <Navigate to={categories[0].id} />}
<Content
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.25 }}
>
<ManageFiles2
segments={segments}
categories={categories}
freeSpaceBytes={freeSpaceBytes}
usedSpaceBytes={usedSpaceBytes}
otherSpaceBytes={otherSpaceBytes}
otherFiles={OTHER_FILES_LABEL_TEXTS}
messages={messages}
/>
{/*<ManageFiles*/}
{/* activeCategoryId={activeCategoryId}*/}
{/* activeFileMap={activeFileMap}*/}
{/* onActiveCategoryChange={setActiveCategoryId}*/}
{/* segments={segments}*/}
{/* categories={categories}*/}
{/* freeSpaceBytes={freeSpaceBytes}*/}
{/* usedSpaceBytes={usedSpaceBytes}*/}
{/* otherSpaceBytes={otherSpaceBytes}*/}
{/* deleteFiles={deleteFiles}*/}
{/* onDeleteSuccess={refetch}*/}
{/* otherFiles={OTHER_FILES_LABEL_TEXTS}*/}
{/* openFileDialog={openFileDialog}*/}
{/* transferFile={transferFile}*/}
{/* messages={messages}*/}
{/* onTransferSuccess={refetch}*/}
{/*>*/}
{/* {(props) => (*/}
{/* <DeviceManageFilesTableSection*/}
{/* fileMap={activeFileMap}*/}
{/* {...props}*/}
{/* />*/}
{/* )}*/}
{/*</ManageFiles>*/}
</Content>
</>
)}
</>
)
}
DeviceManageFilesScreen.List = List
const LoaderWrapper = styled(motion.div)`
display: flex;
flex-direction: column;
gap: 2.4rem;
width: 100%;
height: 100%;
justify-content: center;
align-items: center;
`
const Content = styled(motion.div)`
width: 100%;
height: 100%;
background-color: ${({ theme }) => theme.app.color.white};
display: flex;
flex-direction: row;
overflow-x: hidden;
`

View File

@@ -0,0 +1,41 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/
import { FunctionComponent } from "react"
import { useParams } from "react-router"
import { useApiEntitiesDataQuery } from "devices/api-device/feature"
import { useActiveDeviceQuery } from "devices/common/feature"
import { ApiDevice } from "devices/api-device/models"
import { ManageFiles2 } from "devices/common/ui"
export const List: FunctionComponent = () => {
const { category } = useParams()
const { data: device } = useActiveDeviceQuery<ApiDevice>()
const { data: entities = [] } = useApiEntitiesDataQuery(category, device)
const files = entities.map((entity) => {
return {
id: entity.id as string,
name: entity.fileName as string,
size: entity.fileSize as number,
type: entity.extension as string,
}
})
return (
<ManageFiles2.List
files={files}
onDelete={() => {
//
}}
onAdd={() => {
//
}}
onExport={() => {
//
}}
/>
)
}

View File

@@ -14,6 +14,7 @@ export * from "./lib/device-troubleshooting/device-troubleshooting"
export * from "./lib/welcome-screen/welcome-screen"
export * from "./lib/views/manage-files/manage-files.messages"
export * from "./lib/views/manage-files/manage-files"
export * from "./lib/views/manage-files/manage-files-2"
export * from "./lib/views/manage-files/manage-files.types"
export * from "./lib/views/overview/overview"
export * from "./lib/views/overview/overview-status-item"

View File

@@ -0,0 +1,23 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/
import { FunctionComponent, PropsWithChildren } from "react"
import { FormProvider, useForm } from "react-hook-form"
export interface FormValues {
selectedFiles: Record<string, boolean>
activeFileId?: string
}
export const Form: FunctionComponent<PropsWithChildren> = ({ children }) => {
const form = useForm<FormValues>({
defaultValues: {
selectedFiles: {},
activeFileId: undefined,
},
})
return <FormProvider {...form}>{children}</FormProvider>
}

View File

@@ -0,0 +1,109 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/
import { FunctionComponent, useCallback } from "react"
import styled from "styled-components"
import { Form, FormValues } from "./manage-files-2-form"
import { useFormContext } from "react-hook-form"
import { Checkbox, TableNew, Tooltip } from "app-theme/ui"
import { Contact } from "devices/common/models"
import {
ColumnCheckbox,
ColumnMorePhones,
ColumnName,
ColumnPhone,
} from "../contacts/columns"
import { CheckboxSize } from "app-theme/models"
interface File {
id: string
name: string
size: number
type: string
}
interface Props {
files: File[]
onAdd: (file: File) => void
onDelete?: (fileId: string) => void
onSelect?: (fileId: string) => void
onExport?: (fileId: string) => void
}
export const ManageFiles2List: FunctionComponent<Props> = (props) => {
return (
<Form>
<ManageFiles2ListInner {...props} />
</Form>
)
}
const ManageFiles2ListInner: FunctionComponent<Props> = ({
files,
onAdd,
onDelete,
onSelect,
onExport,
}) => {
const { watch, register } = useFormContext<FormValues>()
const activeFileId = watch("activeFileId")
const rowRenderer = useCallback(
(file: File) => {
const onClick = onSelect
? () => {
onSelect(file.id)
}
: undefined
return (
<TableNew.Row
key={file.id}
onClick={onClick}
rowSelectorCheckboxDataAttr={"data-row-checkbox"}
active={activeFileId === file.id}
>
<div style={{ width: 80 }}>
<Tooltip placement={"bottom-right"} offset={{ x: 24, y: 5 }}>
<Tooltip.Anchor>
<CustomCheckbox
data-row-checkbox
key={file.id}
size={CheckboxSize.Small}
{...register(`selectedFiles.${file.id}`)}
/>
</Tooltip.Anchor>
<Tooltip.Content>Select</Tooltip.Content>
</Tooltip>
</div>
<div style={{ flex: 1 }}>{file.name}</div>
<div style={{ width: 120 }}>{file.type}</div>
<div style={{ width: 120 }}>{file.size} bytes</div>
</TableNew.Row>
)
},
[activeFileId, onSelect, register]
)
return (
<Wrapper>
<TableNew itemIdField={"id"} items={files} rowRenderer={rowRenderer} />
</Wrapper>
)
}
const Wrapper = styled.div`
flex: 1;
display: flex;
flex-direction: column;
`
const CustomCheckbox = styled(Checkbox)`
width: 3.6rem;
height: 3.6rem;
display: flex;
align-items: center;
justify-content: center;
`

View File

@@ -0,0 +1,70 @@
/**
* Copyright (c) Mudita sp. z o.o. All rights reserved.
* For licensing, see https://github.com/mudita/mudita-center/blob/master/LICENSE.md
*/
import { ComponentProps, FunctionComponent } from "react"
import { Outlet } from "react-router"
import styled from "styled-components"
import { ManageFilesStorageSummary } from "./manage-files-content/manage-files-storage-summary"
import { ManageFilesCategoryList } from "./manage-files-content/manage-files-category-list"
import { ManageFilesOtherFiles } from "./manage-files-content/manage-files-other-files"
import { ManageFiles2List } from "./manage-files-2-list"
type Props = ComponentProps<typeof ManageFilesStorageSummary> &
ComponentProps<typeof ManageFilesCategoryList> &
ComponentProps<typeof ManageFilesOtherFiles>
export const ManageFiles2: FunctionComponent<Props> & {
List: typeof ManageFiles2List
} = ({
otherFiles,
categories,
segments,
freeSpaceBytes,
usedSpaceBytes,
otherSpaceBytes,
messages,
}) => {
return (
<Wrapper>
<Sidebar>
<ManageFilesStorageSummary
freeSpaceBytes={freeSpaceBytes}
usedSpaceBytes={usedSpaceBytes}
segments={segments}
messages={messages}
/>
<ManageFilesCategoryList categories={categories} />
<ManageFilesOtherFiles
otherFiles={otherFiles}
otherSpaceBytes={otherSpaceBytes}
/>
</Sidebar>
<Content>
<Outlet />
</Content>
</Wrapper>
)
}
ManageFiles2.List = ManageFiles2List
const Wrapper = styled.div`
flex: 1;
display: grid;
grid-template-columns: 31.2rem 1fr;
grid-template-areas: "sidebar content";
`
const Sidebar = styled.div`
grid-area: sidebar;
flex: 1;
border-right: 0.1rem solid ${({ theme }) => theme.app.color.grey4};
`
const Content = styled.div`
grid-area: content;
flex: 1;
overflow-x: hidden;
overflow-y: auto;
`

View File

@@ -5,53 +5,49 @@
import { FunctionComponent } from "react"
import styled from "styled-components"
import { noop } from "lodash"
import { formatMessage } from "app-localize/utils"
import { Icon, ListItem, Marker, Typography } from "app-theme/ui"
import { IconSize } from "app-theme/models"
import { FileManagerFileCategory } from "../manage-files.types"
import { manageFilesMessages } from "../manage-files.messages"
import { NavLink } from "react-router"
export interface ManageFilesCategoryListProps {
categories: FileManagerFileCategory[]
activeCategoryId: string
onCategoryClick?: (categoryId: string) => void
}
export const ManageFilesCategoryList: FunctionComponent<
ManageFilesCategoryListProps
> = ({ categories, activeCategoryId, onCategoryClick = noop }) => {
> = ({ categories }) => {
return (
<Wrapper>
{categories.map((category) => (
<CategoryListItem
key={category.id}
active={category.id === activeCategoryId}
onClick={() => onCategoryClick(category.id)}
>
<CategoryListItemName>
<CategoryListItemNameIcon
size={IconSize.Big}
type={category.icon}
></CategoryListItemNameIcon>
<CategoryListItemNameText>
{category.label}
</CategoryListItemNameText>
</CategoryListItemName>
<CategoryListItemStorage>
<CategoryListItemStorageText>
{category.size}
</CategoryListItemStorageText>
<CategoryListItemStorageMarker
$color={category.markerColor}
></CategoryListItemStorageMarker>
</CategoryListItemStorage>
<CategoryListItemCountTextWrapper>
{formatMessage(manageFilesMessages.categoryCount, {
count: Number(category.count),
})}
</CategoryListItemCountTextWrapper>
</CategoryListItem>
<Link to={`../${category.id}`} relative={"path"}>
{({ isActive }) => (
<CategoryListItem key={category.id} active={isActive}>
<CategoryListItemName>
<CategoryListItemNameIcon
size={IconSize.Big}
type={category.icon}
/>
<CategoryListItemNameText>
{category.label}
</CategoryListItemNameText>
</CategoryListItemName>
<CategoryListItemStorage>
<CategoryListItemStorageText>
{category.size}
</CategoryListItemStorageText>
<CategoryListItemStorageMarker $color={category.markerColor} />
</CategoryListItemStorage>
<CategoryListItemCountTextWrapper>
{formatMessage(manageFilesMessages.categoryCount, {
count: Number(category.count),
})}
</CategoryListItemCountTextWrapper>
</CategoryListItem>
)}
</Link>
))}
</Wrapper>
)
@@ -62,6 +58,11 @@ const Wrapper = styled.div`
flex-direction: column;
`
const Link = styled(NavLink)`
text-decoration: none;
color: inherit;
`
const CategoryListItem = styled(ListItem)`
padding: 1.2rem 3.2rem 1rem 3.2rem;
display: grid;

View File

@@ -40,7 +40,6 @@ interface Props
> {
selectedFiles: FileManagerFile[]
onAddFileClick?: () => void
opened: boolean
allFilesSelected: boolean
messages: ManageFilesStorageSummaryProps["messages"] &
ManageFilesFileListEmptyProps["messages"] &
@@ -50,7 +49,6 @@ interface Props
export const ManageFilesContent: FunctionComponent<
Props & PropsWithChildren
> = ({
opened,
categories,
segments,
activeCategoryId,
@@ -74,10 +72,6 @@ export const ManageFilesContent: FunctionComponent<
const fileListPanelHeader = `${activeCategory?.label} ${activeCategory?.count ? `(${activeCategory.count})` : ""}`
if (!opened) {
return null
}
return (
<Wrapper>
<CategoriesSidebar>

View File

@@ -57,14 +57,14 @@ export interface ManageFilesViewProps
> {
activeFileMap: FileManagerFileMap
onActiveCategoryChange: (categoryId: string) => void
isLoading: boolean
children: ManageFilesViewChild
messages: ManageFilesViewMessages
deleteFiles: GenericDeleteFlowProps["deleteItemsAction"]
onDeleteSuccess?: VoidFunction
progress?: number
}
export const ManageFiles: FunctionComponent<ManageFilesViewProps> = (props) => {
const {
messages,
@@ -76,7 +76,6 @@ export const ManageFiles: FunctionComponent<ManageFilesViewProps> = (props) => {
transferFile,
onTransferSuccess,
openFileDialog,
isLoading,
categories,
segments,
freeSpaceBytes,
@@ -84,7 +83,6 @@ export const ManageFiles: FunctionComponent<ManageFilesViewProps> = (props) => {
otherSpaceBytes,
otherFiles,
children,
progress,
} = props
const genericDeleteRef = useRef<GenericDeleteFlow>(null)
@@ -139,8 +137,6 @@ export const ManageFiles: FunctionComponent<ManageFilesViewProps> = (props) => {
[activeCategoryId, onActiveCategoryChange]
)
const loadingState = isLoading || activeCategoryId === undefined
const finalizeDeleteSuccess: NonNullable<
GenericDeleteFlowProps["onDeleteSuccess"]
> = useCallback(
@@ -172,13 +168,7 @@ export const ManageFiles: FunctionComponent<ManageFilesViewProps> = (props) => {
return (
<>
<LoadingState
opened={loadingState}
message={manageFilesMessages.loadStateText.id}
progress={progress}
/>
<ManageFilesContent
opened={!loadingState}
segments={segments}
categories={categories}
activeCategoryId={activeCategoryId}