| @@ -0,0 +1,23 @@ | |||
| import {useMatches} from "react-router-dom"; | |||
| import "./breadcrumb.scss"; | |||
| export function Breadcrumb() { | |||
| let matches = useMatches(); | |||
| let crumbs = matches | |||
| // first get rid of any matches that don't have handle and crumb | |||
| .filter((match) => Boolean(match.handle?.crumb)) | |||
| // now map them into an array of elements, passing the loader | |||
| // data to each one | |||
| .map((match) => match.handle.crumb(match)); | |||
| return <> | |||
| <div id="breadcrumb"> | |||
| {crumbs.map((crumb, index) => <span key={index}> | |||
| <span className="crumb">{crumb}</span> | |||
| {index > 0 && index < crumbs.length - 1 | |||
| ? <span className="separator">/</span> | |||
| : <></>} | |||
| </span>)} | |||
| </div> | |||
| </>; | |||
| } | |||
| @@ -0,0 +1,16 @@ | |||
| @import "../variables"; | |||
| #breadcrumb { | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: flex-start; | |||
| a { | |||
| color: $accentColor; | |||
| } | |||
| span.crumb { | |||
| display: inline-block; | |||
| margin: 0 5px; | |||
| } | |||
| } | |||
| @@ -2,16 +2,15 @@ import {useOutsideAlerter} from "../hooks/outside-click-alerter"; | |||
| import * as React from "react"; | |||
| import {useNavigate} from "react-router-dom"; | |||
| import {useRef, useState} from "react"; | |||
| import {Edit, Person} from '@mui/icons-material'; | |||
| import {Person} from '@mui/icons-material'; | |||
| import {Logo} from "./logo"; | |||
| import {Button, IconButton} from "@mui/material"; | |||
| import "./header.scss"; | |||
| import {useAuth} from "../services/authentication/use-auth"; | |||
| import {useInstances} from "../services/instances/use-instances"; | |||
| import {Breadcrumb} from "./breadcrumb"; | |||
| export default function Header() { | |||
| const {selectedInstance} = useInstances(); | |||
| const navigate = useNavigate(); | |||
| const {userData, userManager} = useAuth(); | |||
| const [showUserDropout, setShowUserDropout] = useState(false); | |||
| @@ -32,9 +31,6 @@ export default function Header() { | |||
| return <header id="admin-header" ref={wrapperRef}> | |||
| <div id="logo" onClick={() => navigate("/")}><Logo/></div> | |||
| {selectedInstance && <Button variant="contained" onClick={() => navigate("/")} color="warning"> | |||
| <Edit /> {selectedInstance.title} | |||
| </Button>} | |||
| {userData && | |||
| <div id="user-icon"> | |||
| <IconButton aria-label="person" onClick={handleUserIconClicked}> | |||
| @@ -1,7 +1,13 @@ | |||
| import React, {useLayoutEffect} from 'react'; | |||
| import React from 'react'; | |||
| import ReactDOM from 'react-dom/client'; | |||
| import './index.css'; | |||
| import {BrowserRouter, Navigate, Route, Routes, useLocation} from "react-router-dom"; | |||
| import { | |||
| createBrowserRouter, | |||
| createRoutesFromElements, Link, | |||
| Navigate, Outlet, | |||
| Route, | |||
| RouterProvider, | |||
| } from "react-router-dom"; | |||
| import '@fontsource/roboto/300.css'; | |||
| import '@fontsource/roboto/400.css'; | |||
| @@ -11,45 +17,45 @@ import '@fontsource/roboto/700.css'; | |||
| import Keycloak from "./services/authentication/keycloak"; | |||
| import PortfolioManager from "./pages/portfolio-manager"; | |||
| import AlbumManager from "./pages/album-manager"; | |||
| import {InstanceSelectorPage} from "./pages/instance-selector-page"; | |||
| import {InstancesPage} from "./pages/instances-page"; | |||
| import {NoAccessPage} from "./pages/no-access"; | |||
| import {InstancesProvider} from "./services/instances/instances-context"; | |||
| import {AdminLayout} from "./layout"; | |||
| import {Home} from "@mui/icons-material"; | |||
| const root = ReactDOM.createRoot(document.getElementById('root')); | |||
| const router = createBrowserRouter( | |||
| createRoutesFromElements( | |||
| <Route path="/" element={<AdminLayout/>} handle={{ | |||
| crumb: () => <Link to="/"><Home /></Link> | |||
| }}> | |||
| <Route index element={<InstancesPage/>}/> | |||
| <Route path="/no-access" element={<NoAccessPage/>}/> | |||
| <Route path="/:instanceId" handle={{ | |||
| crumb: (routeData) => <span>{routeData?.params?.instanceId}</span> | |||
| }}> | |||
| <Route index path="" element={<Navigate to="albums"/>} /> | |||
| <Route path="albums/*" element={<Outlet />} handle={{ | |||
| crumb: (routeData) => <Link to={`/${routeData?.params?.instanceId}/albums`}>albums</Link> | |||
| }}> | |||
| <Route index element={<PortfolioManager/>} /> | |||
| <Route path=":albumId" element={<AlbumManager/>} handle={{ | |||
| crumb: (routeData) => <Link to={`/${routeData?.params?.instanceId}/albums/${routeData.params.albumId}`}>{routeData.params.albumId}</Link> | |||
| }}/> | |||
| </Route> | |||
| <Route path="signin-keycloak" element={<Navigate to="/"/>}/> | |||
| </Route> | |||
| <Route path="*" element={<Navigate to={"/no-access"}/>}/> | |||
| </Route> | |||
| ) | |||
| ); | |||
| const root = ReactDOM.createRoot(document.getElementById('root')); | |||
| root.render( | |||
| <React.StrictMode> | |||
| <BrowserRouter> | |||
| <ScrollToTop> | |||
| <Keycloak> | |||
| <InstancesProvider> | |||
| <AdminLayout> | |||
| <Routes> | |||
| <Route index path="/" element={<InstanceSelectorPage/>}/> | |||
| <Route path="/no-access" element={<NoAccessPage/>}/> | |||
| <Route path="/:instanceId"> | |||
| {/*<Route index element={<ManageInstancePage/>}/>*/} | |||
| <Route index path="" element={<Navigate to="albums" />} /> | |||
| <Route path="albums" element={<PortfolioManager/>} /> | |||
| <Route path="albums/:albumId" element={<AlbumManager/>}/> | |||
| <Route path="signin-keycloak" element={<Navigate to="/"/>}/> | |||
| <Route path="signin-keycloak" element={<Navigate to="/"/>}/> | |||
| </Route> | |||
| <Route path="*" element={<Navigate to={"/no-access"}/>}/> | |||
| </Routes> | |||
| </AdminLayout> | |||
| </InstancesProvider> | |||
| </Keycloak> | |||
| </ScrollToTop> | |||
| </BrowserRouter> | |||
| <Keycloak> | |||
| <InstancesProvider> | |||
| <RouterProvider router={router} /> | |||
| </InstancesProvider> | |||
| </Keycloak> | |||
| </React.StrictMode> | |||
| ); | |||
| function ScrollToTop({children}) { | |||
| const location = useLocation(); | |||
| useLayoutEffect(() => { | |||
| document.documentElement.scrollTo(0, 0); | |||
| }, [location.pathname]); | |||
| return children | |||
| } | |||
| ); | |||
| @@ -1,7 +1,7 @@ | |||
| import * as React from "react"; | |||
| import Header from "./components/header"; | |||
| import {useNavigate} from "react-router-dom"; | |||
| import {useEffect} from "react"; | |||
| import {Outlet, useLocation, useNavigate, useNavigation} from "react-router-dom"; | |||
| import {useEffect, useLayoutEffect} from "react"; | |||
| import "./admin.scss"; | |||
| import Loader from "./components/Loader"; | |||
| @@ -10,6 +10,7 @@ import {createTheme} from '@mui/material/styles'; | |||
| import {ThemeProvider} from "@mui/material"; | |||
| import {useAuth} from "./services/authentication/use-auth"; | |||
| import {useInstances} from "./services/instances/use-instances"; | |||
| import {Breadcrumb} from "./components/breadcrumb"; | |||
| const theme = createTheme({ | |||
| palette: { | |||
| @@ -20,10 +21,12 @@ const theme = createTheme({ | |||
| }); | |||
| export function AdminLayout({children}) { | |||
| export function AdminLayout() { | |||
| const navigation = useNavigation(); | |||
| const {pathname} = useLocation(); | |||
| const navigate = useNavigate(); | |||
| const {isLoading, userData, userManager, signIn} = useAuth(); | |||
| const {isLoading: instancesIsLoading} = useInstances(); | |||
| const {isLoading: instancesIsLoading, setSelectedInstance, selectedInstance, instances} = useInstances(); | |||
| useEffect(() => { | |||
| async function handleLogin() { | |||
| @@ -50,19 +53,47 @@ export function AdminLayout({children}) { | |||
| silentLogin(); | |||
| } | |||
| } | |||
| }, [userData, isLoading, userManager, navigate, signIn]); | |||
| return <ThemeProvider theme={theme}> | |||
| <Header/> | |||
| <div id="admin-page"> | |||
| <main id="page"> | |||
| {userData ? <> | |||
| {(isLoading || instancesIsLoading) && <Loader/>} | |||
| {!isLoading && !instancesIsLoading && children} | |||
| </> | |||
| : <Loader/> | |||
| if (instances && !selectedInstance && setSelectedInstance) { | |||
| const possibleSelectedInstance = pathname.split('/')[1]; | |||
| if (possibleSelectedInstance) { | |||
| const selectedInstance = instances?.find(inst => inst.instanceId === possibleSelectedInstance); | |||
| if (selectedInstance) { | |||
| console.log("We found an instance", selectedInstance); | |||
| setSelectedInstance(selectedInstance); | |||
| } else { | |||
| console.log("We could not detect an instance", possibleSelectedInstance); | |||
| navigate("/"); | |||
| } | |||
| </main> | |||
| </div> | |||
| } else { | |||
| setSelectedInstance(null); | |||
| } | |||
| } | |||
| }, [userData, isLoading, userManager, navigate, signIn, setSelectedInstance, selectedInstance, pathname, instances]); | |||
| return <ThemeProvider theme={theme}> | |||
| <ScrollToTop> | |||
| <Header/> | |||
| <div id="admin-page"> | |||
| <main id="page"> | |||
| <Breadcrumb /> | |||
| {userData ? <> | |||
| {(isLoading || instancesIsLoading || navigation.state === 'loading') && <Loader/>} | |||
| {!isLoading && !instancesIsLoading && <Outlet/>} | |||
| </> | |||
| : <Loader/> | |||
| } | |||
| </main> | |||
| </div> | |||
| </ScrollToTop> | |||
| </ThemeProvider> | |||
| } | |||
| function ScrollToTop({children}) { | |||
| const location = useLocation(); | |||
| useLayoutEffect(() => { | |||
| document.documentElement.scrollTo(0, 0); | |||
| }, [location.pathname]); | |||
| return children | |||
| } | |||
| @@ -1,5 +1,5 @@ | |||
| import * as React from "react"; | |||
| import {useRef, useState} from "react"; | |||
| import {useEffect, useRef, useState} from "react"; | |||
| import {NavLink, useNavigate} from "react-router-dom"; | |||
| import {Button, IconButton, InputAdornment, TextField} from "@mui/material"; | |||
| import {useAuth} from "../services/authentication/use-auth"; | |||
| @@ -7,13 +7,13 @@ import {useInstances} from "../services/instances/use-instances"; | |||
| import {Instance} from "../models/instance"; | |||
| import { | |||
| AddAPhoto, AddCircle, | |||
| ArrowRightAltOutlined, RemoveCircle | |||
| ArrowRightAltOutlined, Edit, RemoveCircle | |||
| } from "@mui/icons-material"; | |||
| import "./instance-selector-page.scss"; | |||
| import "./instances-page.scss"; | |||
| import DragAndDrop from "../components/drag-and-drop/drag-and-drop"; | |||
| export function InstanceSelectorPage() { | |||
| export function InstancesPage() { | |||
| const navigate = useNavigate(); | |||
| const { | |||
| instances, | |||
| @@ -23,14 +23,14 @@ export function InstanceSelectorPage() { | |||
| uploadCoverPhoto, | |||
| instanceApiError | |||
| } = useInstances(); | |||
| const [instancesToUpdate, setInstancesToUpdate] = useState(instances?.map(inst => new Instance(inst)) || []); | |||
| const [instancesToUpdate, setInstancesToUpdate] = useState(null); | |||
| const {userData} = useAuth(); | |||
| const [newInstanceTitle, setNewInstanceTitle] = useState(""); | |||
| // useEffect(() => { | |||
| // setInstancesToUpdate(instances); | |||
| // }, [instances]); | |||
| useEffect(() => { | |||
| setInstancesToUpdate(instances?.map(inst => new Instance(inst)) || []); | |||
| }, [instances]); | |||
| let onNewInstanceTitleChanged = (e) => { | |||
| setNewInstanceTitle(e.target.value); | |||
| @@ -59,14 +59,15 @@ export function InstanceSelectorPage() { | |||
| } | |||
| const instanceHasChanges = (idx) => { | |||
| return (instancesToUpdate[idx].title || "") !== (instances[idx].title || "") || | |||
| (instancesToUpdate[idx].subtitle || "") !== (instances[idx].subtitle || "") || | |||
| JSON.stringify(instancesToUpdate[idx].urls) !== JSON.stringify(instances[idx].urls); | |||
| return (instancesToUpdate[idx].title || "") !== (instances[idx]?.title || "") || | |||
| (instancesToUpdate[idx].subtitle || "") !== (instances[idx]?.subtitle || "") || | |||
| JSON.stringify(instancesToUpdate[idx].urls) !== JSON.stringify(instances[idx]?.urls); | |||
| } | |||
| const handleCreateInstance = async () => { | |||
| await createInstance(new Instance({ | |||
| title: newInstanceTitle | |||
| })); | |||
| setNewInstanceTitle(""); | |||
| } | |||
| const handleSaveInstance = async (idx) => { | |||
| if (instanceHasChanges(idx)) { | |||
| @@ -93,6 +94,11 @@ export function InstanceSelectorPage() { | |||
| } | |||
| return <div id="instance-selector-page"> | |||
| <div id="page-title"> | |||
| <div id="left-part"> | |||
| <div className="title">Sites</div> | |||
| </div> | |||
| </div> | |||
| {instanceApiError && <div className="error-box">{instanceApiError}</div>} | |||
| <div> | |||
| {userData?.profile?.resource_access?.photos?.roles?.indexOf("admin") >= 0 && <> | |||
| @@ -122,8 +128,11 @@ export function InstanceSelectorPage() { | |||
| onClick={(e) => fileInputRefs.current[idx].click(e)} | |||
| isLoading={isUploading[idx]}> | |||
| {!isUploading[idx] && instance.coverPhoto | |||
| ? <img src={`${instance.coverPhoto.url}?width=450&height=450&fit=cover`} | |||
| ? <> | |||
| <img src={`${instance.coverPhoto.url}?width=450&height=450&fit=cover`} | |||
| alt={instance.coverPhoto.name}/> | |||
| <div className="btn-edit-photo"><Edit /></div> | |||
| </> | |||
| : <AddAPhoto className="no-photos"/> | |||
| } | |||
| </DragAndDrop> | |||
| @@ -162,12 +171,12 @@ export function InstanceSelectorPage() { | |||
| ) | |||
| }}/> | |||
| <div className="instance-links"> | |||
| {instance.urls.map(url => <div className="instance-link"> | |||
| <IconButton color="error" onClick={(e) => handleDeleteUrl(e, idx, url)}> | |||
| <RemoveCircle /> | |||
| </IconButton> | |||
| <NavLink to={url}>{url}</NavLink> | |||
| </div>)} | |||
| {instance.urls.map(url => <div className="instance-link" key={url}> | |||
| <IconButton color="error" onClick={(e) => handleDeleteUrl(e, idx, url)}> | |||
| <RemoveCircle/> | |||
| </IconButton> | |||
| <NavLink to={url}>{url}</NavLink> | |||
| </div>)} | |||
| </div> | |||
| </div> | |||
| <div className="input-buttons"> | |||
| @@ -9,6 +9,32 @@ | |||
| > .instance { | |||
| .cover-photo { | |||
| img { | |||
| position: relative; | |||
| } | |||
| .btn-edit-photo { | |||
| display: none; | |||
| } | |||
| &:hover { | |||
| .btn-edit-photo { | |||
| position: absolute; | |||
| display: flex; | |||
| align-items: center; | |||
| justify-content: center; | |||
| left: 0; | |||
| top: 0; | |||
| width: 100%; | |||
| height: 100%; | |||
| background-color: rgba(255, 255, 255, 0.2); | |||
| svg { | |||
| color: white; | |||
| font-size: 48px; | |||
| } | |||
| } | |||
| } | |||
| } | |||
| .title { | |||
| display: flex; | |||
| @@ -28,8 +54,10 @@ | |||
| .instance-links { | |||
| margin-top: 10px; | |||
| } | |||
| div.instance-link { | |||
| padding: 2px 0; | |||
| a { | |||
| color: $accentColor; | |||
| text-decoration: none; | |||
| @@ -1,82 +0,0 @@ | |||
| import {Button, TextField} from "@mui/material"; | |||
| import * as React from "react"; | |||
| import {useState} from "react"; | |||
| import useInstancesApi from "../services/instances/instances-api"; | |||
| import {useInstances} from "../services/instances/use-instances"; | |||
| export function ManageInstancePage() { | |||
| const {selectedInstance, setSelectedInstance} = useInstances(); | |||
| const {updateInstance} = useInstancesApi(); | |||
| let [instanceToUpdate, setInstanceToUpdate] = useState({}); | |||
| const [apiError, setApiError] = useState(null); | |||
| const handleTitleChange = (e) => { | |||
| instanceToUpdate.title = e.target.value; | |||
| setInstanceToUpdate(instanceToUpdate); | |||
| } | |||
| const handleSubtitleChange = (e) => { | |||
| instanceToUpdate.subtitle = e.target.value; | |||
| setInstanceToUpdate(instanceToUpdate); | |||
| } | |||
| const instanceHasChanges = () => { | |||
| return (instanceToUpdate.title || "") !== (selectedInstance.title || "") || | |||
| (instanceToUpdate.subtitle || "") !== (selectedInstance.subtitle || ""); | |||
| } | |||
| const handleSaveInstance = async () => { | |||
| try { | |||
| setApiError(null); | |||
| if (instanceHasChanges()) { | |||
| const updatedInstance = await updateInstance( | |||
| instanceToUpdate | |||
| ); | |||
| setSelectedInstance(updatedInstance); | |||
| setInstanceToUpdate(updatedInstance); | |||
| } | |||
| } catch (err) { | |||
| console.error(err); | |||
| setApiError("An error occurred trying to save the instance, contact the system administrator"); | |||
| } | |||
| } | |||
| return <> | |||
| <div className="card-layout photo"> | |||
| {apiError && <div className="error-box">{apiError}</div>} | |||
| {selectedInstance && <> | |||
| {selectedInstance.photo && | |||
| <div className="cover-photo"> | |||
| {<img src={`${selectedInstance.photo.url}?width=450&height=450&fit=cover`} | |||
| alt={selectedInstance.photo.name}/>} | |||
| <div className="file-name"> | |||
| {selectedInstance.photo.fileName} | |||
| </div> | |||
| </div> | |||
| } | |||
| <div className="card-details"> | |||
| <div className="title input-element"> | |||
| <TextField fullWidth={true} variant="standard" | |||
| value={selectedInstance.title} | |||
| onChange={handleTitleChange} | |||
| /> | |||
| </div> | |||
| <div className="description input-element"> | |||
| <TextField multiline={true} fullWidth={true} rows={8} | |||
| onChange={handleSubtitleChange} | |||
| value={selectedInstance.subtitle || ""} | |||
| /> | |||
| </div> | |||
| <div className="input-buttons"> | |||
| <div className="left-buttons"> | |||
| <Button className="photo-save" variant="contained" color="info" | |||
| disabled={!instanceHasChanges()} | |||
| onClick={handleSaveInstance}> | |||
| Save | |||
| </Button> | |||
| </div> | |||
| </div> | |||
| </div> | |||
| </>} | |||
| </div> | |||
| </>; | |||
| } | |||
| @@ -7,7 +7,7 @@ import { | |||
| MoveDown, | |||
| MoveUp | |||
| } from "@mui/icons-material"; | |||
| import {Link, useNavigate} from "react-router-dom"; | |||
| import {Link, Outlet, useNavigate} from "react-router-dom"; | |||
| import Loader from "../components/Loader"; | |||
| import {Button, TextField} from "@mui/material"; | |||
| @@ -204,6 +204,7 @@ function PortfolioManager() { | |||
| </div>} | |||
| {!apiError && albums?.length === 0 && <div><Link to="new" state={{album: new Album()}}>Click here</Link> to create your first album!</div>} | |||
| <Loader loading={isLoading} /> | |||
| <Outlet /> | |||
| </div> | |||
| } | |||
| @@ -1,7 +1,6 @@ | |||
| import React, {useEffect, useState} from "react"; | |||
| import useInstancesApi from "./instances-api"; | |||
| import {useAuth} from "../authentication/use-auth"; | |||
| import {useLocation, useNavigate} from "react-router-dom"; | |||
| import {Instance} from "../../models/instance"; | |||
| export const InstancesContext = React.createContext( | |||
| @@ -9,13 +8,12 @@ export const InstancesContext = React.createContext( | |||
| ); | |||
| export const InstancesProvider = ({children}) => { | |||
| const navigate = useNavigate(); | |||
| const {pathname} = useLocation(); | |||
| // const {pathname} = useLocation(); | |||
| const {userData} = useAuth(); | |||
| const {fetchInstances, createInstance, deleteInstance, updateInstance, uploadCoverPhoto, instanceApiError} = useInstancesApi(); | |||
| const [isLoading, setIsLoading] = useState(true); | |||
| const [instances, setInstances] = useState(undefined); | |||
| const [instances, setInstances] = useState(/** @type {Instance[]} **/undefined); | |||
| const [selectedInstance, setSelectedInstance] = useState(null); | |||
| useEffect(() => { | |||
| @@ -26,22 +24,10 @@ export const InstancesProvider = ({children}) => { | |||
| const instances = await fetchInstances(); | |||
| setInstances(instances); | |||
| setIsLoading(false); | |||
| } else { | |||
| const possibleSelectedInstance = pathname.split('/')[1]; | |||
| if(possibleSelectedInstance){ | |||
| const selectedInstance = instances?.find(inst => inst.instanceId === possibleSelectedInstance); | |||
| if (selectedInstance) { | |||
| setSelectedInstance(selectedInstance); | |||
| } else { | |||
| navigate("/"); | |||
| } | |||
| } else { | |||
| setSelectedInstance(null); | |||
| } | |||
| } | |||
| } | |||
| })(); | |||
| }, [navigate, pathname, userData, instances, fetchInstances]); | |||
| }, [userData, instances, fetchInstances]); | |||
| const handleCreateInstance = async (instance) => { | |||
| const newInstance = await createInstance(instance); | |||
| @@ -54,6 +40,12 @@ export const InstancesProvider = ({children}) => { | |||
| setInstances([...instances]); | |||
| return updatedInstance; | |||
| } | |||
| const handleDeleteInstance = async (instanceId) => { | |||
| await deleteInstance(instanceId); | |||
| const idxToRemove = instances.findIndex(i => i.instanceId === instanceId); | |||
| instances.splice(idxToRemove, 1) | |||
| setInstances([...instances]); | |||
| } | |||
| const handleUpdateCoverPhoto = async (instanceId, formData) => { | |||
| const updatedInstance = await uploadCoverPhoto(instanceId, formData); | |||
| const idx = instances.findIndex(i => i.instanceId === updatedInstance.instanceId); | |||
| @@ -68,7 +60,7 @@ export const InstancesProvider = ({children}) => { | |||
| selectedInstance, | |||
| setSelectedInstance, | |||
| createInstance: handleCreateInstance, | |||
| deleteInstance, | |||
| deleteInstance: handleDeleteInstance, | |||
| updateInstance: handleUpdateInstance, | |||
| uploadCoverPhoto: handleUpdateCoverPhoto, | |||
| instanceApiError, | |||
| @@ -2,7 +2,6 @@ import {ApiRouter, AuthorizeOptions, HttpDelete, HttpGet, HttpPatch, HttpPost, H | |||
| import {BadRequestError, ValidationError} from "../../models/errors/index.js"; | |||
| import {albums} from "../../services/albums.js"; | |||
| import multer from "@koa/multer"; | |||
| import {instances} from "../../services/instances.js"; | |||
| const upload = multer(); | |||
| class AlbumsApi extends ApiRouter { | |||
| @@ -40,7 +39,8 @@ class AlbumsApi extends ApiRouter { | |||
| /** | |||
| * @param {KoaRequestContext} ctx | |||
| */ | |||
| async create({parameters: {instanceId}, body}) { | |||
| async create(ctx) { | |||
| const {parameters: {instanceId}, body} = ctx; | |||
| return { | |||
| data: await albums.createAlbum(instanceId, body) | |||
| }; | |||
| @@ -49,7 +49,8 @@ class AlbumsApi extends ApiRouter { | |||
| /** | |||
| * @param {KoaRequestContext} ctx | |||
| */ | |||
| async update({parameters: {instanceId, albumId}, body: {name, description, sort}}) { | |||
| async update(ctx) { | |||
| let {parameters: {instanceId, albumId}, body: {name, description, sort}} = ctx; | |||
| sort = sort || "auto"; | |||
| if (!["manual", "auto"].includes(sort)) { | |||
| throw new ValidationError([{ | |||
| @@ -65,7 +66,8 @@ class AlbumsApi extends ApiRouter { | |||
| /** | |||
| * @param {KoaRequestContext} ctx | |||
| */ | |||
| async patch({parameters: {instanceId, albumId}, body}) { | |||
| async patch(ctx) { | |||
| const {parameters: {instanceId, albumId}, body} = ctx; | |||
| return { | |||
| data: await albums.patchAlbum(instanceId, albumId, body) | |||
| }; | |||
| @@ -122,9 +124,10 @@ class AlbumsApi extends ApiRouter { | |||
| /** | |||
| * @param {KoaRequestContext} ctx | |||
| */ | |||
| async patchPhoto({parameters: {instanceId, albumId}, body}) { | |||
| async patchPhoto(ctx) { | |||
| const {parameters: {instanceId, albumId, photoId}, body} = ctx; | |||
| return { | |||
| data: await albums.patchPhoto(instanceId, albumId, parameters.photoId, body) | |||
| data: await albums.patchPhoto(instanceId, albumId, photoId, body) | |||
| }; | |||
| } | |||
| @@ -1,4 +1,4 @@ | |||
| import {ApiRouter, AuthorizeOptions, HttpGet, HttpPatch, HttpPost, HttpPut} from "../api-routing/index.js"; | |||
| import {ApiRouter, AuthorizeOptions, HttpDelete, HttpGet, HttpPatch, HttpPost, HttpPut} from "../api-routing/index.js"; | |||
| import {instances} from "../../services/instances.js"; | |||
| import {BadRequestError} from "../../models/errors/index.js"; | |||
| import {albums} from "../../services/albums.js"; | |||
| @@ -14,6 +14,7 @@ class InstancesApi extends ApiRouter { | |||
| })); | |||
| this.registerEndpoint(new HttpGet("/:instanceId", this.fetchInstance)); | |||
| this.registerEndpoint(new HttpPut("/:instanceId", this.updateInstance)); | |||
| this.registerEndpoint(new HttpDelete("/:instanceId", this.deleteInstance)); | |||
| this.registerEndpoint(new HttpPost("/:instanceId/upload-cover-photo", this.uploadPhoto, { | |||
| middlewares: [upload.array('cover-photo')] | |||
| } | |||
| @@ -51,6 +52,14 @@ class InstancesApi extends ApiRouter { | |||
| }; | |||
| } | |||
| /** | |||
| * @param {KoaRequestContext} ctx | |||
| */ | |||
| async deleteInstance({parameters, body}) { | |||
| await instances.deleteInstance(parameters.instanceId); | |||
| return null; | |||
| } | |||
| /** | |||
| * @param {KoaRequestContext} ctx | |||
| */ | |||
| @@ -132,7 +132,7 @@ class AlbumsService { | |||
| album, | |||
| albumToMove | |||
| ]); | |||
| return await this.getAlbums(); | |||
| return await this.getAlbums(instanceId); | |||
| } | |||
| /** | |||
| @@ -47,6 +47,14 @@ class InstancesService { | |||
| throw new ApiError(err); | |||
| } | |||
| } | |||
| async deleteInstance(instanceId) { | |||
| try { | |||
| await db.instances.deleteOne({ instanceId }); | |||
| console.log("Instance deleted", instanceId) | |||
| } catch(err) { | |||
| throw new ApiError(err); | |||
| } | |||
| } | |||
| /** | |||
| * @param {Instance} instance | |||