diff --git a/src/components/form/asset/CreateEditAsset.tsx b/src/components/form/asset/CreateEditAsset.tsx new file mode 100644 index 00000000..2db28e47 --- /dev/null +++ b/src/components/form/asset/CreateEditAsset.tsx @@ -0,0 +1,254 @@ +import { + useDataProvider, +} from '@refinedev/core' +import { Controller } from 'react-hook-form' +import { + Box, + TextField, + Stack, + Typography, + Input, + Button, +} from '@mui/material' +import { LoadingButton } from '@mui/lab' +import FileUploadIcon from '@mui/icons-material/FileUpload' +import { Add, Delete } from '@mui/icons-material' +import Grid from '@mui/material/Grid2' +import { useState } from 'react' + +interface CreateEditAssetProps { + control: any + watch?: any + setValue?: any + setError?: any + register?: any + errors?: any + mode?: 'standalone' | 'step' + fieldPrefix?: string + // Asset-level array management (for multiple assets) + assetIndex?: number + onRemoveAsset?: (index: number) => void + onAddAsset?: (asset: any) => void + canRemoveAsset?: boolean + totalAssets?: number + existingAsset?: any +} + +export const CreateEditAsset: React.FC = ({ + control, + watch, + setValue, + setError, + register, + errors, + mode = 'standalone', + fieldPrefix = '', + assetIndex, + onRemoveAsset, + onAddAsset, + canRemoveAsset = true, + totalAssets = 1, + existingAsset +}) => { + const [isUploadLoading, setIsUploadLoading] = useState(false) + + const getFieldName = (fieldName: string) => { + return mode === 'step' ? `${fieldPrefix}${fieldName}` : fieldName + } + + const dataProvider = useDataProvider() + const provider = dataProvider('dataforge') + + const onChangeHandler = async ( + event: React.ChangeEvent + ) => { + try { + setIsUploadLoading(true) + + const target = event.target + const file: File = (target.files as FileList)[0] + + const formData = new FormData() + formData.append('file', file) + + const asset = await provider + .custom({ + url: 'asset/upload', + method: 'post', + payload: formData, + headers: { + 'Content-Type': file.type, + }, + }) + .then((res) => { + if (res.data) { + return res.data + } else { + throw new Error('Upload failed') + } + }) + .catch((error) => { + setError('file', { message: error.message }) + setIsUploadLoading(false) + throw error + }) + + const { name, size, type, lastModified } = file + const imagePayload = [ + { + name, + size, + type, + lastModified, + url: asset.url, + }, + ] + setValue(getFieldName('file'), imagePayload, { shouldValidate: true }) + setValue(getFieldName('name'), name, { shouldValidate: true }) + setValue(getFieldName('storage_path'), asset.storage_path, { shouldValidate: true }) + setValue(getFieldName('mime_type'), type, { shouldValidate: true }) + setValue(getFieldName('size'), size, { shouldValidate: true }) + setValue(getFieldName('url'), asset.url, { shouldValidate: true }) + setIsUploadLoading(false) + } catch (error) { + console.log(error) + setError(getFieldName('file'), { message: 'Upload failed. Please try again.' }) + setIsUploadLoading(false) + } + } + + //handle create image input + const imageInput = watch ? watch(getFieldName('file')) : null + + //handle creating preview for existing asset + const existingAssetPreview = existingAsset ? { + name: existingAsset.name, + url: existingAsset.url, + mime_type: existingAsset.mime_type, + size: existingAsset.size + } : null + //determine which preview to use, upload or existing asset + const previewAsset = imageInput || (existingAssetPreview ? [existingAssetPreview] : null) + + return ( + + {/* Asset Header with Remove Button */} + {assetIndex !== undefined && ( + + + Asset {assetIndex + 1} + {onRemoveAsset && ( + + )} + + + )} + + {/* Asset Label */} + + ( + + )} + /> + + + + + {/* File Upload Section */} + + + + {previewAsset && ( + + )} + + + + {/* Add Asset Button - Show on last asset only */} + {onAddAsset && assetIndex === totalAssets - 1 && ( + + + + )} + + ) +} \ No newline at end of file diff --git a/src/interfaces/dataforge/IWellInventoryForm.ts b/src/interfaces/dataforge/IWellInventoryForm.ts index 3fb392b9..81345d50 100644 --- a/src/interfaces/dataforge/IWellInventoryForm.ts +++ b/src/interfaces/dataforge/IWellInventoryForm.ts @@ -40,7 +40,11 @@ export interface IWellInventoryForm { assets?: Array<{ label: string name: string + storage_path: string + mime_type: string + size: number + url: string file?: File - mime_type?: string + thing_id?: number | null }> } \ No newline at end of file diff --git a/src/pages/dataforge/asset/create.tsx b/src/pages/dataforge/asset/create.tsx index 0e8a9d47..c72dc4c2 100644 --- a/src/pages/dataforge/asset/create.tsx +++ b/src/pages/dataforge/asset/create.tsx @@ -1,218 +1,33 @@ -import { - HttpError, - useApiUrl, - useCreate, - useDataProvider, -} from '@refinedev/core' -import { Create, useAutocomplete } from '@refinedev/mui' -import Box from '@mui/material/Box' -import TextField from '@mui/material/TextField' -import Autocomplete from '@mui/material/Autocomplete' +import type { HttpError } from '@refinedev/core' +import { Create } from '@refinedev/mui' import { useForm } from '@refinedev/react-hook-form' -import { Controller } from 'react-hook-form' import { Nullable } from '../../../interfaces' import { IAsset } from '@/interfaces/dataforge/IAsset' -import { useCallback, useState } from 'react' -import axios from 'axios' -import { Input, Stack, Typography, Button } from '@mui/material' -// import { LoadingButton } from '@mui/lab' -import FileUploadIcon from '@mui/icons-material/FileUpload' -import { IThing } from '@/interfaces/dataforge/IThing' +import { CreateEditAsset } from '@/components/form/asset/CreateEditAsset' export const AssetCreate: React.FC = () => { const { saveButtonProps, - register, control, - formState: { errors }, + watch, setValue, setError, - watch, + register, + formState: { errors }, } = useForm>() - const [isUploadLoading, setIsUploadLoading] = useState(false) - - const [thingValue, setThingValue] = useState(null) - const { autocompleteProps } = useAutocomplete({ - resource: 'thing', - dataProviderName: 'dataforge', - onSearch: (value) => [ - { - field: 'name', - operator: 'contains', - value, - }, - ], - }) - - const dataProvider = useDataProvider() - const provider = dataProvider('dataforge') - - const onChangeHandler = async ( - event: React.ChangeEvent - ) => { - try { - setIsUploadLoading(true) - - const target = event.target - const file: File = (target.files as FileList)[0] - - const formData = new FormData() - formData.append('file', file) - - const asset = await provider - .custom({ - url: 'asset/upload', - method: 'post', - payload: formData, - headers: { - 'Content-Type': file.type, - }, - }) - .then((res) => { - if (res.data) { - return res.data - } else { - throw new Error('Upload failed') - } - }) - .catch((error) => { - setError('file', { message: error.message }) - setIsUploadLoading(false) - throw error - }) - - const { name, size, type, lastModified } = file - const imagePayload = [ - { - name, - size, - type, - lastModified, - url: asset.url, - }, - ] - setValue('file', imagePayload, { shouldValidate: true }) - setValue('name', name, { shouldValidate: true }) - setValue('storage_path', asset.storage_path, { shouldValidate: true }) - setValue('mime_type', type, { shouldValidate: true }) - setValue('size', size, { shouldValidate: true }) - setValue('url', asset.url, { shouldValidate: true }) - setIsUploadLoading(false) - } catch (error) { - console.log(error) - setError('file', { message: 'Upload failed. Please try again.' }) - setIsUploadLoading(false) - } - } - - const imageInput = watch('file') return ( - - - ( - { - setThingValue(newValue) - field.onChange(newValue?.id || null) - }} - getOptionKey={(option) => option.id} - getOptionLabel={(option) => option.name || ''} - renderInput={(params) => ( - - )} - /> - )} - /> - - {/**/} - - - {imageInput && ( - - )} - + ) } diff --git a/src/pages/dataforge/asset/edit.tsx b/src/pages/dataforge/asset/edit.tsx index 65c2b9c1..f06bad60 100644 --- a/src/pages/dataforge/asset/edit.tsx +++ b/src/pages/dataforge/asset/edit.tsx @@ -7,66 +7,43 @@ import CircularProgress from '@mui/material/CircularProgress' import type { Nullable } from '@/interfaces' import { IAsset } from '@/interfaces/dataforge/IAsset' import { useState } from 'react' +import { CreateEditAsset } from '@/components/form/asset/CreateEditAsset' export const AssetEdit: React.FC = () => { const { saveButtonProps, refineCore: { query: queryResult }, - register, control, + watch, + setValue, + setError, + register, formState: { errors }, } = useForm>() - // const { autocompleteProps } = useAutocomplete({ - // resource: "categories", - // defaultValue: queryResult?.data?.data.category.id, - // }); const { data, isLoading, isError } = useOne({ resource: 'asset', id: queryResult?.data?.data.id, dataProviderName: 'dataforge', queryOptions: { - cacheTime: 10 * 60 * 1000, // 10 minutes - staleTime: 5 * 60 * 1000, // 5 minutes + cacheTime: 0, + staleTime: 0 }, }) const image = data?.data - // const [imgLoading, setImgLoading] = useState(true) return ( - - - - {isLoading && ( - - )} - ) diff --git a/src/pages/dataforge/well-inventory-form/index.tsx b/src/pages/dataforge/well-inventory-form/index.tsx index 6228c9bd..238592e9 100644 --- a/src/pages/dataforge/well-inventory-form/index.tsx +++ b/src/pages/dataforge/well-inventory-form/index.tsx @@ -25,6 +25,7 @@ import { createWellInventoryForm } from '@/pages/dataforge/well-inventory-form/w import { CreateEditLocation } from '@/components/form/location/CreateEditLocation' import { CreateEditWell } from '@/components/form/thing/CreateEditWell' import { CreateEditContact } from '@/components/form/contact/CreateEditContact' +import { CreateEditAsset } from '@/components/form/asset/CreateEditAsset' import { FormReview } from '@/components/form/stepper/FormReview' import { FormStepper } from '@/components/form/stepper/FormStepper' @@ -51,6 +52,8 @@ export const WellInventoryForm: React.FC = () => { handleSubmit, reset, setValue, + setError, + register, watch, trigger, formState: { errors }, @@ -165,8 +168,8 @@ export const WellInventoryForm: React.FC = () => { return case 2: return - case 3: - return + case 3: + return case 4: return default: @@ -365,17 +368,22 @@ const ContactsStep: React.FC<{ //Assets Step #4 ----------------------------------------------- /** - * @TODO this step a placeholder right now - * @refactor + * @TODO Link asset to a well via thing_id in future API changes + * @TODO Create a new component that allows for multi-file selection without immediate upload, and + * change the asset step to use this new component + * @TODO change the well inventory form to only upload new asset on form submit, not on file selection */ const AssetsStep: React.FC<{ control: any watch: any + setValue: any + setError: any + register: any errors: any assetFields: any appendAsset: any removeAsset: any -}> = ({ control, watch, errors, assetFields, appendAsset, removeAsset }) => ( +}> = ({ control, watch, setValue, setError, register, errors, assetFields, appendAsset, removeAsset }) => ( @@ -384,46 +392,61 @@ const AssetsStep: React.FC<{ {assetFields.map((field, index) => ( - - - - - - - - - - + + { + appendAsset({ + ...asset, + thing_id: null, + /** + * @TODO Link asset to a well via thing_id in future API changes + */ + storage_path: '', + mime_type: '', + size: 0, + url: '' + }) + }} + canRemoveAsset={assetFields.length >= 1} + totalAssets={assetFields.length} + /> ))} - - - + {/* Show Add Asset button when no assets exist */} + {assetFields.length === 0 && ( + + + + )} ) @@ -472,8 +495,8 @@ const ReviewStep: React.FC<{ }, { title: `Assets (${formData.assets?.length || 0})`, - items: formData.assets?.map((asset) => ({ - label: `Asset`, + items: formData.assets?.map((asset, index) => ({ + label: `Asset ${index + 1}`, value: `${asset.label || 'Not specified'} - ${asset.name || 'Not specified'}` })) || [] } diff --git a/src/pages/dataforge/well-inventory-form/well_inventory.schema.ts b/src/pages/dataforge/well-inventory-form/well_inventory.schema.ts index fcfef070..576d28d7 100644 --- a/src/pages/dataforge/well-inventory-form/well_inventory.schema.ts +++ b/src/pages/dataforge/well-inventory-form/well_inventory.schema.ts @@ -67,6 +67,28 @@ export const wellInventoryStepSchemas: Yup.ObjectSchema[] = [ Yup.object({ label: Yup.string().required('Asset label is required'), name: Yup.string().required('Asset name is required'), + storage_path: Yup.string().when('file', { + is: (file: any) => file !== null && file !== undefined, + then: (schema) => schema.required('Storage path is required'), + otherwise: (schema) => schema.nullable(), + }), + mime_type: Yup.string().when('file', { + is: (file: any) => file !== null && file !== undefined, + then: (schema) => schema.required('MIME type is required'), + otherwise: (schema) => schema.nullable(), + }), + size: Yup.number().when('file', { + is: (file: any) => file !== null && file !== undefined, + then: (schema) => schema.required('File size is required').min(0, 'File size must be non-negative'), + otherwise: (schema) => schema.nullable(), + }), + url: Yup.string().when('file', { + is: (file: any) => file !== null && file !== undefined, + then: (schema) => schema.required('File URL is required'), + otherwise: (schema) => schema.nullable(), + }), + file: Yup.mixed().nullable(), + thing_id: Yup.number().nullable(), }) ), }), @@ -80,7 +102,7 @@ export const SchemaDefaults: Partial = { name: '', notes: '', point: '', - release_status: 'public', + release_status: '', }, well: { name: '', @@ -93,7 +115,7 @@ export const SchemaDefaults: Partial = { contacts: [ { name: '', - role: 'owner', + role: '', emails: [], phones: [], addresses: [], diff --git a/src/pages/dataforge/well-inventory-form/well_inventory.service.ts b/src/pages/dataforge/well-inventory-form/well_inventory.service.ts index 2b1d17bd..09d9bdcb 100644 --- a/src/pages/dataforge/well-inventory-form/well_inventory.service.ts +++ b/src/pages/dataforge/well-inventory-form/well_inventory.service.ts @@ -68,15 +68,23 @@ export const createWellInventoryForm = async (data: IWellInventoryForm) => { const assetResponses = [] if (data.assets?.length) { for (const asset of data.assets) { - const assetResponse = await dataForgeDataProvider.create({ - resource: 'dataforge.asset', - variables: { - name: asset.name, - label: asset.label, - }, - }) + // Only create assets that have been properly uploaded + if (asset.storage_path && asset.mime_type && asset.size && asset.url) { + const assetResponse = await dataForgeDataProvider.create({ + resource: 'dataforge.asset', + variables: { + name: asset.name, + label: asset.label, + storage_path: asset.storage_path, + mime_type: asset.mime_type, + size: asset.size, + url: asset.url, + thing_id: wellId, + }, + }) - assetResponses.push(assetResponse.data) + assetResponses.push(assetResponse.data) + } } }