Skip to content

Commit ee8feed

Browse files
committed
Added Dynamic Edit Form and Update API
1 parent a36e914 commit ee8feed

File tree

14 files changed

+155
-36
lines changed

14 files changed

+155
-36
lines changed

Backend/EcommerceInventory/EcommerceInventory/Helpers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from rest_framework.views import exception_handler
44
from rest_framework.exceptions import AuthenticationFailed,NotAuthenticated,PermissionDenied
55
from rest_framework.pagination import PageNumberPagination
6+
from django.forms.models import model_to_dict
67

78
def getDynamicFormModels():
89
return {
@@ -66,6 +67,7 @@ def getDynamicFormFields(model_instance,domain_user_id):
6667

6768
fielddata['options']=[{'id':option[0],'value':option[1]} for option in options]
6869
fielddata['type']='select'
70+
fielddata['default']=model_to_dict(model_instance)[field.name] if field.name in model_to_dict(model_instance) else ''
6971
fields[fielddata['type']].append(fielddata)
7072
return fields
7173

Binary file not shown.
Binary file not shown.

Backend/EcommerceInventory/EcommerceInventory/templates/index.html

Whitespace-only changes.

Backend/EcommerceInventory/EcommerceInventory/urls.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
path('admin/', admin.site.urls),
3030
path('api/auth/', include('UserServices.urls')),
3131
path('api/getForm/<str:modelName>/',DynamicFormController.as_view(),name='dynamicForm'),
32+
path('api/getForm/<str:modelName>/<str:id>/',DynamicFormController.as_view(),name='dynamicForm'),
3233
path('api/superAdminForm/<str:modelName>/',SuperAdminDynamicFormController.as_view(),name='superadmindynamicForm'),
3334
path('api/getMenus/',ModuleView.as_view(),name='sidebarmenu'),
3435
path('api/products/',include('ProductServices.urls')),

Backend/EcommerceInventory/UserServices/Controller/DynamicFormController.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class DynamicFormController(APIView):
1212
authentication_classes = [JWTAuthentication]
1313
permission_classes = [IsAuthenticated]
1414

15-
def post(self,request,modelName):
15+
def post(self,request,modelName,id=None):
1616
#Checking if Model Exist in Our Dynamic Form Models
1717
if modelName not in getDynamicFormModels():
1818
return renderResponse(data='Model Not Exist',message='Model Not Exist',status=404)
@@ -71,7 +71,16 @@ def post(self,request,modelName):
7171
fieldsdata['domain_user_id']=request.user.domain_user_id
7272
fieldsdata['added_by_user_id']=Users.objects.get(id=request.user.id)
7373

74-
model_instace=model_class.objects.create(**fieldsdata)
74+
if id:
75+
model_instace=model_class.objects.filter(id=id,domain_user_id=request.user.domain_user_id)
76+
if not model_instace.exists():
77+
return renderResponse(data='Model Item Not Found',message='Model Item Not Found',status=404)
78+
model_instace=model_instace.first()
79+
for key,value in fieldsdata.items():
80+
setattr(model_instace,key,value)
81+
model_instace.save()
82+
else:
83+
model_instace=model_class.objects.create(**fieldsdata)
7584

7685
#Serializing Data
7786
serialized_data=serialize('json',[model_instace])
@@ -83,7 +92,7 @@ def post(self,request,modelName):
8392
#Returning the Response
8493
return renderResponse(data=response_json,message='Data saved successfully')
8594

86-
def get(self,request,modelName):
95+
def get(self,request,modelName,id=None):
8796
if modelName not in getDynamicFormModels():
8897
return renderResponse(data='Model Not Found',message='Model Not Found',status=404)
8998

@@ -93,7 +102,14 @@ def get(self,request,modelName):
93102
if model_class is None:
94103
return renderResponse(data='Model Not Found',message='Model Not Found',status=404)
95104

105+
if id:
106+
model_instance=model_class.objects.filter(id=id,domain_user_id=request.user.domain_user_id)
107+
if model_instance.exists():
108+
model_instance=model_instance.first()
109+
else:
110+
return renderResponse(data='Model Item Not Found',message='Model Item Not Found',status=404)
111+
else:
112+
model_instance = model_class()
96113

97-
model_instance = model_class()
98114
fields=getDynamicFormFields(model_instance,request.user.domain_user_id)
99115
return renderResponse(data=fields,message='Form fields fetched successfully')

Frontend/ecommerce_inventory/src/App.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ function App() {
3333
element:<Layout sidebarList={items}/>,
3434
children:[
3535
{path:"/",element:<ProtectedRoute element={<Home/>}/>},
36-
{path:"/form/:formName",element:<ProtectedRoute element={<DynamicForm/>}/>},
36+
{path:"/home",element:<ProtectedRoute element={<Home/>}/>},
37+
{path:"/form/:formName/:id?",element:<ProtectedRoute element={<DynamicForm/>}/>},
3738
{path:"/manage/category",element:<ProtectedRoute element={<ManageCategories/>}/>}
3839
]},
3940
]

Frontend/ecommerce_inventory/src/components/FileInputComponents.js

Lines changed: 72 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,29 @@ import DescriptionIcon from '@mui/icons-material/Description';
66
import { Delete } from '@mui/icons-material';
77
import Button from '@mui/material/Button';
88
import { toast } from 'react-toastify';
9+
import { checkIsJson, getFileMimeTypeFromFileName, getFileNameFromUrl } from '../utils/Helper';
910

1011
const FileInputComponent = ({field}) => {
11-
const {register,formState:{errors},watch,setValue} = useFormContext();
12+
const {register,formState:{errors},watch,setValue,resetField} = useFormContext();
1213
const {callApi,loading}=useApi();
1314
const [selectedFiles,setSelectedFiles]=useState([]);
1415
const [filePreviews,setFilePreviews]=useState([]);
1516
const [fileUploaded,setFileUploaded]=useState(false);
17+
const [oldFiles,setOldFiles]=useState((checkIsJson(field.default) && Array.isArray(JSON.parse(field.default)))?JSON.parse(field.default):[])
18+
const [oldFilePreviews,setOldFilePreviews]=useState([]);
19+
const [newFilesUrl,setNewFilesUrl]=useState([])
20+
21+
useEffect(()=>{
22+
if(oldFiles.length>0){
23+
const preview=oldFiles.map((file,index)=>({
24+
url:new URL(file),
25+
name:getFileNameFromUrl(file),
26+
type:getFileMimeTypeFromFileName(getFileNameFromUrl(file)).split('/')[0]
27+
}))
28+
29+
setOldFilePreviews(preview);
30+
}
31+
},[oldFiles])
1632

1733
const handleDeleteImage=(index)=>{
1834
// Delete File Actual
@@ -28,6 +44,33 @@ const FileInputComponent = ({field}) => {
2844
setFileUploaded(false);
2945
}
3046

47+
const handleDeleteImageOld=(index)=>{
48+
// Delete File Actual
49+
const updatedFiles=[...oldFiles]
50+
updatedFiles.splice(index,1);
51+
setOldFiles(updatedFiles);
52+
53+
//Delete File preview
54+
const updatedPreviews=[...oldFilePreviews];
55+
updatedPreviews.splice(index,1);
56+
setOldFilePreviews(updatedPreviews);
57+
58+
}
59+
60+
useEffect(()=>{
61+
buildFileUrls();
62+
},[oldFiles,newFilesUrl])
63+
64+
const buildFileUrls=()=>{
65+
const finalUrl=[...oldFiles,...newFilesUrl];
66+
if(finalUrl.length>0){
67+
setValue(field.name,JSON.stringify(finalUrl));
68+
}
69+
else{
70+
resetField(field.name);
71+
}
72+
}
73+
3174
const uploadFiles=async()=>{
3275
try{
3376
const formData=new FormData();
@@ -36,7 +79,7 @@ const FileInputComponent = ({field}) => {
3679
})
3780

3881
const response=await callApi({url:'uploads/',method:'post',body:formData,header:{'Content-Type':'multipart/form-data'}});
39-
setValue(field.name,JSON.stringify(response?.data?.urls));
82+
setNewFilesUrl(response?.data?.urls);
4083
toast.success(response?.data?.message);
4184
setFileUploaded(true);
4285
}
@@ -52,9 +95,9 @@ const FileInputComponent = ({field}) => {
5295
}
5396

5497
useEffect(()=>{
55-
if(!selectedFiles.length && watch(field.name)){
98+
if(!selectedFiles.length && watch(field.name) && Array.from(watch(field.name)).filter((item)=>item instanceof File).length>0){
5699
console.log(watch(field.name));
57-
const fileArray=Array.from(watch(field.name)) || [];
100+
const fileArray=Array.from(watch(field.name)).filter((item)=>item instanceof File) || [];
58101
setSelectedFiles(fileArray);
59102
const preview=fileArray.map((file,index)=>({
60103
url:URL.createObjectURL(file),
@@ -65,6 +108,7 @@ const FileInputComponent = ({field}) => {
65108
setFilePreviews(preview);
66109
setFileUploaded(false);
67110
}
111+
buildFileUrls();
68112
},[watch(field.name)]);
69113

70114
return (
@@ -75,7 +119,7 @@ const FileInputComponent = ({field}) => {
75119
{
76120
file.type==='image'?<img src={file.url} alt={file.name} style={{width:'60px',height:'60px'}} />:<DescriptionIcon sx={{width:'60px',height:'60px'}} />
77121
}
78-
<Typography variant='body1' p={1}>{file.name}</Typography>
122+
<Typography variant='body1' p={1} sx={{width:'230px',wordWrap:'break-word'}}>{file.name}</Typography>
79123
<IconButton onClick={()=>handleDeleteImage(index)} sx={{color:'red'}}>
80124
<Delete/>
81125
</IconButton>
@@ -88,11 +132,6 @@ const FileInputComponent = ({field}) => {
88132
<Box component={"div"} className='fileInput' mt={1}>
89133
<input type='file' multiple {...register(field.name,{required:field.required})} />
90134
</Box>
91-
{
92-
!!errors[field.name] && <Alert variant="outlined" severity='error'>
93-
This Field is Required
94-
</Alert>
95-
}
96135
</Box>
97136
}
98137
{
@@ -104,6 +143,29 @@ const FileInputComponent = ({field}) => {
104143
</Box>
105144
)
106145
}
146+
{
147+
oldFilePreviews.length>0 && <Typography mt={2} variant='h6'>Modify Old Files</Typography>
148+
}
149+
{
150+
oldFilePreviews.length>0 && oldFilePreviews.map((file,index)=>(
151+
<Box key={index} sx={{display:'flex',alignItems:'center',mb:2}}>
152+
{
153+
file.type==='image'?<img src={file.url} alt={file.name} style={{width:'60px',height:'60px'}} />:<DescriptionIcon sx={{width:'60px',height:'60px'}} />
154+
}
155+
<Typography variant='body1' p={1} sx={{width:'230px',wordWrap:'break-word'}}>{file.name}</Typography>
156+
<IconButton onClick={()=>handleDeleteImageOld(index)} sx={{color:'red'}}>
157+
<Delete/>
158+
</IconButton>
159+
</Box>
160+
))
161+
}
162+
{
163+
!!errors[field.name] && <Alert variant="outlined" severity='error' sx={{marginTop:'10px'}}>
164+
This Field is Required and Upload the Files if Already Selected
165+
</Alert>
166+
}
167+
168+
107169
</>
108170
)
109171
}

Frontend/ecommerce_inventory/src/components/StepSelectComponents.js

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,11 @@ const StepSelectComponents = ({formConfig,fieldType}) => {
1717
<Box>
1818
{selectFields.map((field,index)=>(
1919
<FormControl fullWidth margin="normal" key={field.name}>
20-
{/* <InputLabel>{field.label}</InputLabel>
21-
<Select
22-
{...register(field.name,{required:field.required})}
23-
defaultValue={field.default}
24-
error={!!errors[field.name]}
25-
label={field.label}>
26-
{
27-
field.options.map((option,index)=>
28-
<MenuItem key={option.id} value={option.id}>{option.value}</MenuItem>
29-
)
30-
}
31-
</Select> */}
3220
<Autocomplete
3321
{...register(field.name,{required:field.required})}
3422
options={field.options}
3523
getOptionLabel={(option)=>option.value}
36-
defaultValue={field.options.find(option=>option.id===watch(field.name)) || null}
24+
defaultValue={field.options.find(option=>option.id===watch(field.name)) || field.options.find(option=>option.id===field.default) || null}
3725
onChange={(event,newValue)=>{
3826
setValue(field.name,newValue?newValue.id:'')
3927
}}

Frontend/ecommerce_inventory/src/pages/DynamicForm.js

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,24 @@ import { FormProvider, get } from 'react-hook-form';
1212
import { useForm } from 'react-hook-form';
1313
import { toast } from 'react-toastify';
1414
import { getFormTypes } from '../utils/Helper';
15-
15+
import { useNavigate } from 'react-router-dom';
1616
const DynamicForm=()=>{
1717
const stepItems=getFormTypes();
18-
const {formName}=useParams();
18+
const {formName,id}=useParams();
1919
const {error,loading,callApi}=useApi();
2020
const [formConfig,setFormConfig]=useState(null);
2121
const [currentStep,setCurrentStep]=useState(0);
2222
const methods=useForm();
2323
const [steps,setSteps]=useState(stepItems)
24+
const navigate=useNavigate();
2425

2526
useEffect(()=>{
2627
fetchForm();
2728
},[formName])
2829

2930
const fetchForm=async()=>{
30-
const response=await callApi({url:`getForm/${formName}/`});
31+
const PID=id?`${id}/`:'';
32+
const response=await callApi({url:`getForm/${formName}/${PID}`});
3133
let stepFilter=stepItems.filter(step=>response.data.data[step.fieldType] && response.data.data[step.fieldType].length>0);
3234
setSteps(stepFilter);
3335
setFormConfig(response.data);
@@ -40,10 +42,25 @@ const DynamicForm=()=>{
4042

4143
const onSubmit=async(data)=>{
4244
try{
43-
const response=await callApi({url:`getForm/${formName}/`,method:'post',body:data});
45+
let isError=false;
46+
const currentStepFields=getCurrentStepFields();
47+
const errors=validateCurrentStepFields(currentStepFields);
48+
if(errors.length>0){
49+
errors.forEach(error=>{
50+
methods.setError(error.name,{type:'manual',message:`${error.label} is Required`})
51+
isError=true;
52+
})
53+
}
54+
if(isError){
55+
return;
56+
}
57+
58+
const PID=id?`${id}/`:'';
59+
const response=await callApi({url:`getForm/${formName}/${PID}`,method:'post',body:data});
4460
toast.success(response.data.message);
4561
setCurrentStep(0);
4662
methods.reset();
63+
navigate(`/manage/${formName}`)
4764
}
4865
catch(err){
4966
console.log(err);
@@ -79,9 +96,9 @@ const DynamicForm=()=>{
7996

8097
return (
8198
<Container>
82-
<Typography variant="h6" gutterBottom>Add {formName.toUpperCase()}</Typography>
99+
<Typography variant="h6" gutterBottom>{id?'EDIT':'ADD'} {formName.toUpperCase()}</Typography>
83100
<Divider sx={{margingTop:'15px',marginBottom:'15px'}}/>
84-
<Stepper activeStep={currentStep} alternativeLabel>
101+
<Stepper activeStep={currentStep} sx={{overflowX:'auto'}} alternativeLabel>
85102
{steps.map((step,index)=>(
86103
<Step key={index} onClick={()=>goToStep(index)}>
87104
<StepLabel>{step.label}</StepLabel>

Frontend/ecommerce_inventory/src/pages/category/ManageCategories.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ const ManageCategories = () => {
5959
}
6060
const onEditClick=(params)=>{
6161
console.log(params);
62+
navigate(`/form/category/${params.row.id}`)
6263
}
6364
const onAddClick=(params)=>{
6465
console.log(params);

Frontend/ecommerce_inventory/src/redux/reducer/sidebardata.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const fetchSidebar=createAsyncThunk('data/fetchSidebar',async()=>{
66
const response=await axios.get(`${config.API_URL}getMenus/`);
77
const sidebarData=response.data.data;
88
const setActiveAndExpanded=(item)=>{
9-
if(item.module_url && window.location.pathname===item.module_url){
9+
if(item.module_url && window.location.pathname.indexOf(item.module_url)!==-1){
1010
item.active=true;
1111
item.expanded=true;
1212
return true;
@@ -63,7 +63,7 @@ const sidebarSlice=createSlice({
6363
item.expanded=false;
6464
item.submenus.forEach(submenu=>{
6565
submenu.active=false;
66-
if(submenu.module_url && window.location.pathname===submenu.module_url){
66+
if(submenu.module_url && window.location.pathname.indexOf(item.module_url)!==-1){
6767
submenu.active=true;
6868
item.active=true;
6969
item.expanded=true;

Frontend/ecommerce_inventory/src/utils/Helper.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,35 @@ export const getFormTypes=()=>{
9494
{component:StepJsonComponents,label:"Additional Details",fieldType:'json'},
9595
{component:StepFileComponents,label:"Documents & Files",fieldType:'file'},
9696
]
97+
}
98+
99+
100+
export const getFileNameFromUrl=(url)=>{
101+
const parseUrl=new URL(url);
102+
const pathname=parseUrl.pathname;
103+
const filename=pathname.substring(pathname.lastIndexOf("/")+1);
104+
return filename;
105+
}
106+
107+
export const getFileMimeTypeFromFileName=(filename)=>{
108+
const extension=filename.split(".").pop().toLowerCase();
109+
const mimeTypes={
110+
"txt":"text/plain",
111+
"html":"text/html",
112+
"htm":"text/html",
113+
"css":"text/css",
114+
"js":"application/javascript",
115+
"jpg":"image/jpg",
116+
"png":"image/png",
117+
"jpeg":"image/jpeg",
118+
"mp3":"audio/mpeg",
119+
}
120+
121+
if(extension in mimeTypes){
122+
return mimeTypes[extension];
123+
}
124+
else{
125+
return "other/other";
126+
}
127+
97128
}

0 commit comments

Comments
 (0)