Skip to content

Commit 71b33b5

Browse files
authored
Merge pull request #144 from AvaCodeSolutions/feat/169/organization-frontend
feat: #169 Organizations frontend
2 parents 7d76801 + 348ce50 commit 71b33b5

4 files changed

Lines changed: 122 additions & 17 deletions

File tree

django_email_learning/platform/api/serializers.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ class OrganizationResponse(BaseModel):
158158
id: int
159159
name: str
160160
logo: Optional[str] = None
161+
logo_path: Optional[str] = None
161162
description: Optional[str] = None
162163
public_url: str
163164

@@ -178,6 +179,7 @@ def from_django_model(
178179
"logo": abs_url_builder(organization.logo.url)
179180
if organization.logo
180181
else None,
182+
"logo_path": organization.logo.name if organization.logo else None,
181183
"description": organization.description,
182184
"public_url": abs_url_builder(url),
183185
}

frontend/platform/organizations/Organizations.jsx

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import Base from "../../src/components/Base";
2-
import { Box, Button, Dialog, Grid, IconButton, TableContainer, Table, TableHead, TableRow,TableBody, TableCell } from "@mui/material";
2+
import { Alert, Box, Button, Dialog, Grid, IconButton, Paper, TableContainer, Table, TableHead, TableRow,TableBody, TableCell, Typography } from "@mui/material";
33
import AddIcon from '@mui/icons-material/Add';
44
import PublicIcon from '@mui/icons-material/Public';
5+
import DeleteIcon from '@mui/icons-material/Delete';
6+
import EditIcon from '@mui/icons-material/Edit';
57
import { useState, useEffect, use } from "react";
68
import { getCookie } from "../../src/utils";
79
import render from "../../src/render";
@@ -32,50 +34,108 @@ function Organizations() {
3234

3335
const apiBaseUrl = localStorage.getItem('apiBaseUrl');
3436

35-
const handleOrganizationCreated = (data) => {
37+
const handleSuccessFormSubmission = (data) => {
3638
console.log('Organization created successfully:', data);
3739
setDialogOpen(false);
3840
setTableUpdates(prev => [...prev, data]);
3941
};
4042

41-
const handleOrganizationCreationFailed = (error) => {
43+
const handleFailedFormSubmission = (error) => {
4244
console.error('Error creating organization:', error);
4345
};
4446

47+
const goToUrl = (url) => {
48+
window.open(url, '_blank');
49+
}
50+
51+
const deleteOrganization = (organizationId) => {
52+
fetch(`${apiBaseUrl}/organizations/${organizationId}/`, {
53+
method: 'DELETE',
54+
headers: {
55+
'Content-Type': 'application/json',
56+
'X-CSRFToken': getCookie('csrftoken'),
57+
},
58+
})
59+
.then(response => {
60+
if (response.ok) {
61+
console.log('Organization deleted successfully');
62+
setTableUpdates(prev => [...prev, { deletedOrganizationId: organizationId }]);
63+
} else {
64+
console.error('Error deleting organization');
65+
}
66+
})
67+
.catch(error => {
68+
console.error('Error deleting organization:', error);
69+
});
70+
}
71+
72+
const deleteConfirmationDialog = (organization) => {
73+
setDialogContent(
74+
<Box sx={{ p: 2 }}>
75+
<Typography variant="h6">Confirm Deletion</Typography>
76+
<Alert severity="warning" variant="outlined" sx={{ mt: 1 }}>Are you sure you want to delete the organization "{organization.name}"? All the courses contents and users under this organization will also be deleted.</Alert>
77+
<Box sx={{ display: 'flex', justifyContent: 'flex-end', mt: 2 }}>
78+
<Button onClick={() => setDialogOpen(false)} sx={{ mr: 1 }}>Cancel</Button>
79+
<Button variant="contained" color="error" onClick={() => {
80+
deleteOrganization(organization.id);
81+
setDialogOpen(false);
82+
}}>Delete</Button>
83+
</Box>
84+
</Box>
85+
);
86+
setDialogOpen(true);
87+
}
88+
4589
return (
4690
<Base breadCrumbList={[{label: 'Organizations', href: '#'}]} showOrganizationSwitcher={false}>
4791
<Grid size={12} py={2} pl={2}>
48-
<Box p={2} sx={{ border: '1px solid', borderColor: 'grey.300', borderRadius: 1, minHeight: 300 }}>
92+
<Box p={2} sx={{ border: '1px solid', borderColor: 'grey.300', borderRadius: 1, minHeight: 300, width: { lg: '80%' } }}>
4993
<Button variant="contained" startIcon={<AddIcon />} sx={{ marginBottom: 2 }} onClick={() => {
5094
setDialogContent(<OrganizationForm
51-
successCallback={handleOrganizationCreated}
52-
failureCallback={handleOrganizationCreationFailed}
95+
successCallback={handleSuccessFormSubmission}
96+
failureCallback={handleFailedFormSubmission}
5397
cancelCallback={() => setDialogOpen(false)}
5498
createMode={true}
5599
/>);
56100
setDialogOpen(true);
57101
}}>Add an Organization</Button>
58102

59-
{ organizations.length > 0 && (<TableContainer sx={{ maxHeight: 440, border: '1px solid', borderColor: 'grey.300', borderRadius: 1 }}>
103+
{ organizations.length > 0 && (<TableContainer component={Paper} sx={{ maxHeight: 440, border: '1px solid', borderColor: 'grey.300', borderRadius: 1 }}>
60104
<Table>
61105
<TableHead>
62106
<TableRow>
63107
<TableCell>Name</TableCell>
64-
<TableCell>Public URL</TableCell>
65-
<TableCell>Actions</TableCell>
108+
<TableCell sx={{ width: '100px' }}>Actions</TableCell>
66109
</TableRow>
67110
</TableHead>
68111
<TableBody>
69112
{ organizations.map((org) => (
70113
<TableRow key={org.id}>
71114
<TableCell>{org.name}</TableCell>
72-
<TableCell><a href={org.public_url}><IconButton><PublicIcon fontSize="small"/></IconButton></a></TableCell>
73-
<TableCell>Actions</TableCell>
115+
<TableCell>
116+
<IconButton onClick={() => goToUrl(org.public_url)}><PublicIcon fontSize="small"/></IconButton>
117+
<IconButton onClick={() => {
118+
setDialogContent(<OrganizationForm
119+
successCallback={handleSuccessFormSubmission}
120+
failureCallback={handleFailedFormSubmission}
121+
cancelCallback={() => setDialogOpen(false)}
122+
createMode={false}
123+
initialName={org.name}
124+
initialDescription={org.description}
125+
initialLogoUrl={org.logo}
126+
organizationId={org.id}
127+
/>);
128+
setDialogOpen(true);
129+
}}><EditIcon fontSize="small"/></IconButton>
130+
<IconButton onClick={() => deleteConfirmationDialog(org)}><DeleteIcon fontSize="small" /></IconButton>
131+
</TableCell>
74132
</TableRow>
75133
))}
76134
</TableBody>
77135
</Table>
78-
</TableContainer>)}
136+
</TableContainer>
137+
138+
)}
79139

80140
</Box>
81141

frontend/platform/organizations/components/OrganizationForm.jsx

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,13 @@ import CloudUploadIcon from '@mui/icons-material/CloudUpload';
55
import { useState, useEffect, use } from "react";
66
import { getCookie } from '../../../src/utils.js';
77

8-
function OrganizationForm({ successCallback, failureCallback, cancelCallback, createMode }) {
9-
const [name, setName] = useState("");
10-
const [description, setDescription] = useState("");
8+
function OrganizationForm({ successCallback, failureCallback, cancelCallback, createMode, initialName, initialDescription, initialLogoUrl, organizationId }) {
9+
const [name, setName] = useState(initialName || "");
10+
const [description, setDescription] = useState(initialDescription || "");
1111
const [nameHelperText, setNameHelperText] = useState("");
1212
const [descriptionHelperText, setDescriptionHelperText] = useState("");
1313
const [logoFile, setLogoFile] = useState(null);
14-
const [logoUrl, setLogoUrl] = useState(null);
14+
const [logoUrl, setLogoUrl] = useState(initialLogoUrl || null);
1515
const [logoServerPath, setLogoServerPath] = useState(null);
1616

1717
const apiBaseUrl = localStorage.getItem('apiBaseUrl');
@@ -22,6 +22,46 @@ function OrganizationForm({ successCallback, failureCallback, cancelCallback, cr
2222
setLogoFile(null);
2323
}
2424

25+
const handleUpdate = () => (event) => {
26+
event.preventDefault();
27+
28+
let payload = {
29+
name: name,
30+
description: description,
31+
};
32+
33+
if (logoServerPath) {
34+
payload.logo = logoServerPath;
35+
}
36+
37+
if (!logoServerPath && !logoUrl) {
38+
payload.remove_logo = true;
39+
}
40+
41+
fetch(`${apiBaseUrl}/organizations/${organizationId}/`, {
42+
method: 'POST',
43+
headers: {
44+
'Content-Type': 'application/json',
45+
'X-CSRFToken': getCookie('csrftoken'),
46+
},
47+
body: JSON.stringify(payload),
48+
})
49+
.then(response => {
50+
if (!response.ok) {
51+
return response.json().then(data => {
52+
throw data;
53+
});
54+
}
55+
return response.json();
56+
})
57+
.then(data => {
58+
successCallback(data);
59+
})
60+
.catch(error => {
61+
failureCallback(error);
62+
});
63+
}
64+
2565
const handleCreate = () => (event) => {
2666
event.preventDefault();
2767
fetch(`${apiBaseUrl}/organizations/`, {
@@ -117,7 +157,7 @@ function OrganizationForm({ successCallback, failureCallback, cancelCallback, cr
117157
)}
118158
<DialogActions>
119159
<Button onClick={cancelCallback}>Cancel</Button>
120-
<Button type="submit" color="primary" onClick={handleCreate()}>
160+
<Button variant='contained' type="submit" color="primary" onClick={createMode? handleCreate() : handleUpdate() }>
121161
{createMode ? 'Create' : 'Update'} Organization
122162
</Button>
123163
</DialogActions>

frontend/src/components/MenuBar.jsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ function MenuBar({activeOrganizationId, changeOrganizationCallback, showOrganiza
6060
const logoVerticalUrl = theme.palette.mode === 'light' ? logoVerticalLightUrl : logoVerticalDarkUrl;
6161

6262
useEffect(() => {
63+
if (!showOrganizationSwitcher) {
64+
return;
65+
}
6366
fetch(apiBaseUrl + '/organizations/', {
6467
method: 'GET',
6568
credentials: 'include',

0 commit comments

Comments
 (0)