diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index bd293f3..6a5584b 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -9,9 +9,9 @@ import {SophonFooter} from "./components/elements/SophonFooter"
import {ErrorCatcherBox} from "./components/errors/ErrorCatcherBox"
import {ResourceDescriptionBox} from "./components/generic/ResourceDescriptionBox"
import {GroupCreateBox} from "./components/group/GroupCreateBox"
-import {GroupListBox} from "./components/group/GroupListBox"
import {GroupMembersBox} from "./components/group/GroupMembersBox"
import {GroupRouter} from "./components/group/GroupRouter"
+import {GroupStepPage} from "./components/group/GroupStepPage"
import {SophonDescriptionBox} from "./components/informative/SophonDescriptionBox"
import {InstanceDescriptionBox} from "./components/instance/InstanceDescriptionBox"
import {InstanceFormBox} from "./components/instance/InstanceFormBox"
@@ -19,12 +19,14 @@ import {InstanceRouter} from "./components/instance/InstanceRouter"
import {NotebookListBox} from "./components/notebook/NotebookListBox"
import {NotebookRouter} from "./components/notebook/NotebookRouter"
import {DebugBox} from "./components/placeholder/DebugBox"
+import {ProjectCreateBox} from "./components/project/ProjectCreateBox"
import {ProjectListBox} from "./components/project/ProjectListBox"
import {ProjectRouter} from "./components/project/ProjectRouter"
import {ThemedBluelib} from "./components/theme/ThemedBluelib"
import {ThemedTitle} from "./components/theme/ThemedTitle"
import {AuthorizationProvider} from "./contexts/authorization"
import {CacheProvider} from "./contexts/cache"
+import {GroupProvider} from "./contexts/group"
import {InstanceProvider} from "./contexts/instance"
import {ThemeProvider} from "./contexts/theme"
@@ -32,50 +34,53 @@ import {ThemeProvider} from "./contexts/theme"
function App({..._}: RouteComponentProps) {
return React.useMemo(
() => <>
-
+
+
+
<>
>}
selectedRoute={() => <>
-
+
+
+
<>
-
- >}
+ unselectedRoute={AuthorizationStepPage}
selectedRoute={() => <>
<>
-
-
- >}
+ unselectedRoute={GroupStepPage}
selectedRoute={({selection}) => <>
-
-
-
-
- <>
-
-
- >}
- selectedRoute={({selection}) => <>
-
- <>
-
- >}
- selectedRoute={(props) => <>
-
- >}
- />
- >}
- />
+
+
+
+
+
+ <>
+
+
+
+ >}
+ selectedRoute={({selection}) => <>
+
+
+ <>
+
+ >}
+ selectedRoute={(props) => <>
+
+ >}
+ />
+ >}
+ />
+
>}
/>
>}
diff --git a/frontend/src/components/group/GroupCreateBox.tsx b/frontend/src/components/group/GroupCreateBox.tsx
index 9698734..52cbfed 100644
--- a/frontend/src/components/group/GroupCreateBox.tsx
+++ b/frontend/src/components/group/GroupCreateBox.tsx
@@ -43,7 +43,7 @@ export function GroupCreateBox({viewSet, resource}: GroupCreateBoxProps): JSX.El
const description =
useFormState(
resource?.value.description ?? "",
- Validators.notZeroLength,
+ Validators.alwaysValid,
)
const members =
diff --git a/frontend/src/components/group/GroupMembersBox.tsx b/frontend/src/components/group/GroupMembersBox.tsx
index c264a11..e147f5b 100644
--- a/frontend/src/components/group/GroupMembersBox.tsx
+++ b/frontend/src/components/group/GroupMembersBox.tsx
@@ -4,18 +4,13 @@ import {Box, Heading, Idiomatic, ListUnordered, UAnnotation} from "@steffo/bluel
import * as React from "react"
import {useAuthorizationContext} from "../../contexts/authorization"
import {useCacheContext} from "../../contexts/cache"
-import {ManagedResource} from "../../hooks/useManagedViewSet"
-import {SophonResearchGroup} from "../../types/SophonTypes"
+import {useGroupContext} from "../../contexts/group"
-export interface GroupMembersBoxProps {
- resource: ManagedResource
-}
-
-
-export function GroupMembersBox({resource}: GroupMembersBoxProps): JSX.Element | null {
+export function GroupMembersBox(): JSX.Element | null {
const authorization = useAuthorizationContext()
const cache = useCacheContext()
+ const group = useGroupContext()
if(!cache) {
return null
@@ -23,8 +18,11 @@ export function GroupMembersBox({resource}: GroupMembersBoxProps): JSX.Element |
if(!cache.users) {
return null
}
+ if(!group) {
+ return null
+ }
- const trueMembers = [...new Set([resource.value.owner, ...resource.value.members])]
+ const trueMembers = [...new Set([group.value.owner, ...group.value.members])]
const users = trueMembers.map((id, index) => {
const user = cache.getUserById(id)
@@ -45,7 +43,7 @@ export function GroupMembersBox({resource}: GroupMembersBoxProps): JSX.Element |
return (
- Members of {resource.value.name}
+ Members of {group.value.name}
{users}
diff --git a/frontend/src/components/group/GroupRouter.tsx b/frontend/src/components/group/GroupRouter.tsx
index 700c73d..05b1907 100644
--- a/frontend/src/components/group/GroupRouter.tsx
+++ b/frontend/src/components/group/GroupRouter.tsx
@@ -1,13 +1,12 @@
import * as React from "react"
import {useManagedViewSet} from "../../hooks/useManagedViewSet"
-import {Dict} from "../../types/ExtraTypes"
import {SophonResearchGroup} from "../../types/SophonTypes"
import {ViewSetRouter} from "../routing/ViewSetRouter"
export interface GroupRouterProps {
- unselectedRoute: (props: Dict) => JSX.Element | null,
- selectedRoute: (props: Dict) => JSX.Element | null,
+ unselectedRoute: (props: any) => JSX.Element | null,
+ selectedRoute: (props: any) => JSX.Element | null,
}
diff --git a/frontend/src/components/group/GroupStepPage.tsx b/frontend/src/components/group/GroupStepPage.tsx
new file mode 100644
index 0000000..51ee885
--- /dev/null
+++ b/frontend/src/components/group/GroupStepPage.tsx
@@ -0,0 +1,20 @@
+import * as React from "react"
+import {ManagedViewSet} from "../../hooks/useManagedViewSet"
+import {SophonResearchGroup} from "../../types/SophonTypes"
+import {GroupCreateBox} from "./GroupCreateBox"
+import {GroupListBox} from "./GroupListBox"
+
+
+export interface GroupStepPageProps {
+ viewSet: ManagedViewSet
+}
+
+
+export function GroupStepPage({viewSet}: GroupStepPageProps): JSX.Element {
+ return (
+ <>
+
+
+ >
+ )
+}
diff --git a/frontend/src/components/project/ProjectCreateBox.tsx b/frontend/src/components/project/ProjectCreateBox.tsx
new file mode 100644
index 0000000..004743f
--- /dev/null
+++ b/frontend/src/components/project/ProjectCreateBox.tsx
@@ -0,0 +1,165 @@
+import {Box, Details, Form, Idiomatic as I, useFormState} from "@steffo/bluelib-react"
+import * as React from "react"
+import {useAuthorizationContext} from "../../contexts/authorization"
+import {useGroupContext} from "../../contexts/group"
+import {ManagedResource, ManagedViewSet} from "../../hooks/useManagedViewSet"
+import {SophonResearchProject} from "../../types/SophonTypes"
+import {Validators} from "../../utils/Validators"
+
+
+export interface ProjectCreateBoxProps {
+ viewSet?: ManagedViewSet,
+ resource?: ManagedResource,
+}
+
+
+export function ProjectCreateBox({viewSet, resource}: ProjectCreateBoxProps): JSX.Element | null {
+ const authorization = useAuthorizationContext()
+ const group = useGroupContext()
+
+ const name =
+ useFormState(
+ resource?.value.name ?? "",
+ Validators.notZeroLength,
+ )
+
+ const description =
+ useFormState(
+ resource?.value.description ?? "",
+ Validators.alwaysValid,
+ )
+
+ const visibility =
+ useFormState<"PUBLIC" | "INTERNAL" | "PRIVATE" | undefined>(
+ resource?.value.visibility ?? undefined,
+ Validators.notEmpty,
+ )
+
+ const slug =
+ React.useMemo(
+ () => resource ? resource.value.slug : name.value.replaceAll(/[^A-Za-z0-9-]/g, "-").toLowerCase(),
+ [resource, name],
+ )
+
+ const canAdministrate =
+ React.useMemo(
+ () => {
+ if(resource) {
+ if(!authorization) {
+ return false
+ }
+ if(!group) {
+ return false
+ }
+ if(!authorization.state.user) {
+ return false
+ }
+ if(!(
+ group.value.members.includes(authorization.state.user.id) || group.value.owner === authorization.state.user.id
+ )) {
+ return false
+ }
+ return true
+ }
+ else {
+ return true
+ }
+ },
+ [authorization, resource],
+ )
+
+ const applyChanges =
+ React.useCallback(
+ async () => {
+ if(resource) {
+ await resource.update({
+ name: name.value,
+ slug: slug,
+ description: description.value,
+ visibility: visibility.value,
+ group: group!.value.slug,
+ })
+ }
+ else {
+ await viewSet!.create({
+ name: name.value,
+ slug: slug,
+ description: description.value,
+ visibility: visibility.value,
+ group: group!.value.slug,
+ })
+ }
+ },
+ [viewSet, resource, name, slug, description, visibility, group],
+ )
+
+ const canApply =
+ React.useMemo(
+ () => name.validity === true && visibility.validity === true && Boolean(authorization?.state.user?.username) && group,
+ [name, visibility, authorization],
+ )
+
+ const hasError =
+ React.useMemo(
+ () => viewSet?.operationError || resource?.error,
+ [viewSet, resource],
+ )
+
+ if(!authorization?.state.token ||
+ !(
+ viewSet || resource
+ ) ||
+ !canAdministrate) {
+ return null
+ }
+
+ return (
+
+
+
+ {resource ? <>Edit {resource.value.name}> : "Create a new research project"}
+
+
+
+ 0 ? true : undefined}
+ />
+
+
+
+
+ {resource ? "Edit" : "Create"}
+
+
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/project/ProjectDeleteButton.tsx b/frontend/src/components/project/ProjectDeleteButton.tsx
new file mode 100644
index 0000000..9ac309b
--- /dev/null
+++ b/frontend/src/components/project/ProjectDeleteButton.tsx
@@ -0,0 +1,40 @@
+import {faTrash} from "@fortawesome/free-solid-svg-icons"
+import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
+import * as React from "react"
+import {useAuthorizationContext} from "../../contexts/authorization"
+import {useGroupContext} from "../../contexts/group"
+import {ManagedResource} from "../../hooks/useManagedViewSet"
+import {SophonResearchProject} from "../../types/SophonTypes"
+import {SafetyButton} from "../elements/SafetyButton"
+
+
+export interface ProjectDeleteButtonProps {
+ resource: ManagedResource,
+}
+
+
+export function ProjectDeleteButton({resource}: ProjectDeleteButtonProps): JSX.Element | null {
+ const authorization = useAuthorizationContext()
+ const group = useGroupContext()
+
+ if(!authorization) {
+ return null
+ }
+ if(!group) {
+ return null
+ }
+ if(!authorization.state.user) {
+ return null
+ }
+ if(!(
+ group.value.members.includes(authorization.state.user.id) || group.value.owner === authorization.state.user.id
+ )) {
+ return null
+ }
+
+ return (
+ resource.destroy()}>
+ Delete
+
+ )
+}
diff --git a/frontend/src/components/project/ProjectResourcePanel.tsx b/frontend/src/components/project/ProjectResourcePanel.tsx
index b77a6ff..7047da4 100644
--- a/frontend/src/components/project/ProjectResourcePanel.tsx
+++ b/frontend/src/components/project/ProjectResourcePanel.tsx
@@ -6,6 +6,7 @@ import {ManagedResource} from "../../hooks/useManagedViewSet"
import {SophonResearchProject} from "../../types/SophonTypes"
import {Link} from "../elements/Link"
import {ResourcePanel} from "../elements/ResourcePanel"
+import {ProjectDeleteButton} from "./ProjectDeleteButton"
export interface ProjectResourcePanelProps {
@@ -34,7 +35,7 @@ export function ProjectResourcePanel({resource}: ProjectResourcePanelProps): JSX
-
+
)
diff --git a/frontend/src/components/project/ProjectRouter.tsx b/frontend/src/components/project/ProjectRouter.tsx
index b5f45ba..72e28cd 100644
--- a/frontend/src/components/project/ProjectRouter.tsx
+++ b/frontend/src/components/project/ProjectRouter.tsx
@@ -16,7 +16,7 @@ export function ProjectRouter({groupPk, ...props}: ProjectRouterProps): JSX.Elem
return (
(`/api/projects/by-group/${groupPk}`, "slug")}
+ viewSet={useManagedViewSet(`/api/projects/by-group/${groupPk}/`, "slug")}
pathSegment={"researchProject"}
pkKey={"slug"}
/>
diff --git a/frontend/src/contexts/group.tsx b/frontend/src/contexts/group.tsx
new file mode 100644
index 0000000..85b25a6
--- /dev/null
+++ b/frontend/src/contexts/group.tsx
@@ -0,0 +1,21 @@
+import * as React from "react"
+import {ManagedResource} from "../hooks/useManagedViewSet"
+import {WithChildren, WithResource} from "../types/ExtraTypes"
+import {SophonResearchGroup} from "../types/SophonTypes"
+
+
+const groupContext = React.createContext | undefined>(undefined)
+const GroupContext = groupContext
+
+
+/**
+ * Hook to access the {@link groupContext}.
+ */
+export function useGroupContext(): ManagedResource | undefined {
+ return React.useContext(groupContext)
+}
+
+
+export function GroupProvider({resource, children}: WithResource & WithChildren): JSX.Element {
+ return
+}
diff --git a/frontend/src/types/ExtraTypes.ts b/frontend/src/types/ExtraTypes.ts
index 7e8ebfe..e2810b7 100644
--- a/frontend/src/types/ExtraTypes.ts
+++ b/frontend/src/types/ExtraTypes.ts
@@ -1,4 +1,5 @@
import * as React from "react"
+import {ManagedResource, ManagedViewSet} from "../hooks/useManagedViewSet"
/**
@@ -15,3 +16,19 @@ export interface Dict {
export interface WithChildren {
children?: React.ReactNode,
}
+
+
+/**
+ * Props including the selection key.
+ */
+export interface WithResource {
+ resource: ManagedResource,
+}
+
+
+/**
+ * Props including the viewset key.
+ */
+export interface WithViewSet {
+ viewSet: ManagedViewSet,
+}
\ No newline at end of file