From f03dc2c3a68824f11cb72d3344f58d882b34de48 Mon Sep 17 00:00:00 2001 From: jochen Date: Sat, 18 May 2024 12:36:43 +0200 Subject: [PATCH] Fixed admin client --- admin-client/src/components/breadcrumb.js | 23 ++++++ admin-client/src/components/breadcrumb.scss | 16 ++++ admin-client/src/components/header.js | 8 +- admin-client/src/index.js | 78 ++++++++++-------- admin-client/src/layout.js | 63 ++++++++++---- ...nce-selector-page.js => instances-page.js} | 45 ++++++---- ...selector-page.scss => instances-page.scss} | 28 +++++++ .../src/pages/manage-instance-page.js | 82 ------------------- admin-client/src/pages/portfolio-manager.js | 3 +- .../services/instances/instances-context.js | 28 +++---- server/src/api/private/albums.js | 15 ++-- server/src/api/private/instances.js | 11 ++- server/src/services/albums.js | 2 +- server/src/services/instances.js | 8 ++ 14 files changed, 225 insertions(+), 185 deletions(-) create mode 100644 admin-client/src/components/breadcrumb.js create mode 100644 admin-client/src/components/breadcrumb.scss rename admin-client/src/pages/{instance-selector-page.js => instances-page.js} (86%) rename admin-client/src/pages/{instance-selector-page.scss => instances-page.scss} (56%) delete mode 100644 admin-client/src/pages/manage-instance-page.js diff --git a/admin-client/src/components/breadcrumb.js b/admin-client/src/components/breadcrumb.js new file mode 100644 index 0000000..3c6264c --- /dev/null +++ b/admin-client/src/components/breadcrumb.js @@ -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 <> + + ; +} \ No newline at end of file diff --git a/admin-client/src/components/breadcrumb.scss b/admin-client/src/components/breadcrumb.scss new file mode 100644 index 0000000..ceced2b --- /dev/null +++ b/admin-client/src/components/breadcrumb.scss @@ -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; + } +} \ No newline at end of file diff --git a/admin-client/src/components/header.js b/admin-client/src/components/header.js index 017cbf5..d175d50 100644 --- a/admin-client/src/components/header.js +++ b/admin-client/src/components/header.js @@ -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
- {selectedInstance && } {userData &&
diff --git a/admin-client/src/index.js b/admin-client/src/index.js index aa45c4e..8810770 100644 --- a/admin-client/src/index.js +++ b/admin-client/src/index.js @@ -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( + } handle={{ + crumb: () => + }}> + }/> + }/> + {routeData?.params?.instanceId} + }}> + } /> + } handle={{ + crumb: (routeData) => albums + }}> + } /> + } handle={{ + crumb: (routeData) => {routeData.params.albumId} + }}/> + + }/> + + }/> + + ) +); +const root = ReactDOM.createRoot(document.getElementById('root')); root.render( - - - - - - - }/> - }/> - - {/*}/>*/} - } /> - } /> - }/> - }/> - }/> - - }/> - - - - - - + + + + + -); - -function ScrollToTop({children}) { - const location = useLocation(); - useLayoutEffect(() => { - document.documentElement.scrollTo(0, 0); - }, [location.pathname]); - return children -} +); \ No newline at end of file diff --git a/admin-client/src/layout.js b/admin-client/src/layout.js index c2571ee..1dd4ab8 100644 --- a/admin-client/src/layout.js +++ b/admin-client/src/layout.js @@ -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 -
-
-
- {userData ? <> - {(isLoading || instancesIsLoading) && } - {!isLoading && !instancesIsLoading && children} - - : + 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("/"); } -
-
+ } else { + setSelectedInstance(null); + } + } + }, [userData, isLoading, userManager, navigate, signIn, setSelectedInstance, selectedInstance, pathname, instances]); + + return + +
+
+
+ + {userData ? <> + {(isLoading || instancesIsLoading || navigation.state === 'loading') && } + {!isLoading && !instancesIsLoading && } + + : + } +
+
+ +} + + +function ScrollToTop({children}) { + const location = useLocation(); + useLayoutEffect(() => { + document.documentElement.scrollTo(0, 0); + }, [location.pathname]); + return children } \ No newline at end of file diff --git a/admin-client/src/pages/instance-selector-page.js b/admin-client/src/pages/instances-page.js similarity index 86% rename from admin-client/src/pages/instance-selector-page.js rename to admin-client/src/pages/instances-page.js index dca233b..d592df3 100644 --- a/admin-client/src/pages/instance-selector-page.js +++ b/admin-client/src/pages/instances-page.js @@ -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
+
+
+
Sites
+
+
{instanceApiError &&
{instanceApiError}
}
{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 - ? + {instance.coverPhoto.name}/ +
+ : } @@ -162,12 +171,12 @@ export function InstanceSelectorPage() { ) }}/>
- {instance.urls.map(url =>
- handleDeleteUrl(e, idx, url)}> - - - {url} -
)} + {instance.urls.map(url =>
+ handleDeleteUrl(e, idx, url)}> + + + {url} +
)}
diff --git a/admin-client/src/pages/instance-selector-page.scss b/admin-client/src/pages/instances-page.scss similarity index 56% rename from admin-client/src/pages/instance-selector-page.scss rename to admin-client/src/pages/instances-page.scss index 59c95d9..d2a686d 100644 --- a/admin-client/src/pages/instance-selector-page.scss +++ b/admin-client/src/pages/instances-page.scss @@ -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; diff --git a/admin-client/src/pages/manage-instance-page.js b/admin-client/src/pages/manage-instance-page.js deleted file mode 100644 index 28c1fca..0000000 --- a/admin-client/src/pages/manage-instance-page.js +++ /dev/null @@ -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 <> -
- {apiError &&
{apiError}
} - {selectedInstance && <> - {selectedInstance.photo && -
- {{selectedInstance.photo.name}/} -
- {selectedInstance.photo.fileName} -
-
- } -
-
- -
-
- -
-
-
- -
-
-
- } -
- ; -} \ No newline at end of file diff --git a/admin-client/src/pages/portfolio-manager.js b/admin-client/src/pages/portfolio-manager.js index ca5a2b3..aeca1fd 100644 --- a/admin-client/src/pages/portfolio-manager.js +++ b/admin-client/src/pages/portfolio-manager.js @@ -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() {
} {!apiError && albums?.length === 0 &&
Click here to create your first album!
} +
} diff --git a/admin-client/src/services/instances/instances-context.js b/admin-client/src/services/instances/instances-context.js index e1af6c7..ef96d2b 100644 --- a/admin-client/src/services/instances/instances-context.js +++ b/admin-client/src/services/instances/instances-context.js @@ -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, diff --git a/server/src/api/private/albums.js b/server/src/api/private/albums.js index 51dee17..196372e 100644 --- a/server/src/api/private/albums.js +++ b/server/src/api/private/albums.js @@ -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) }; } diff --git a/server/src/api/private/instances.js b/server/src/api/private/instances.js index 3c756d1..9077030 100644 --- a/server/src/api/private/instances.js +++ b/server/src/api/private/instances.js @@ -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 */ diff --git a/server/src/services/albums.js b/server/src/services/albums.js index f02cdd2..c1b156b 100644 --- a/server/src/services/albums.js +++ b/server/src/services/albums.js @@ -132,7 +132,7 @@ class AlbumsService { album, albumToMove ]); - return await this.getAlbums(); + return await this.getAlbums(instanceId); } /** diff --git a/server/src/services/instances.js b/server/src/services/instances.js index e69d3ee..414a3da 100644 --- a/server/src/services/instances.js +++ b/server/src/services/instances.js @@ -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