1
Fork 0
mirror of https://github.com/Steffo99/sophon.git synced 2024-12-22 14:54:22 +00:00

💥 Implement group creation, deletion and editing

This commit is contained in:
Steffo 2021-10-14 03:43:05 +02:00
parent 6f3e2fc9e9
commit 1f1b0c6e41
11 changed files with 315 additions and 49 deletions

View file

@ -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(
() => <>
<SophonDescriptionBox/>
<Chapter>
<SophonDescriptionBox/>
</Chapter>
<InstanceProvider>
<InstanceRouter
unselectedRoute={() => <>
<InstanceFormBox/>
</>}
selectedRoute={() => <>
<InstanceDescriptionBox/>
<Chapter>
<InstanceDescriptionBox/>
</Chapter>
<AuthorizationProvider>
<CacheProvider>
<AuthorizationRouter
unselectedRoute={() => <>
<AuthorizationStepPage/>
</>}
unselectedRoute={AuthorizationStepPage}
selectedRoute={() => <>
<GroupRouter
unselectedRoute={({viewSet}) => <>
<GroupListBox viewSet={viewSet}/>
<GroupCreateBox viewSet={viewSet}/>
</>}
unselectedRoute={GroupStepPage}
selectedRoute={({selection}) => <>
<Chapter>
<ResourceDescriptionBox resource={selection} icon={faUsers}/>
<GroupMembersBox resource={selection}/>
</Chapter>
<ProjectRouter
groupPk={selection.value.slug}
unselectedRoute={({viewSet}) => <>
<GroupCreateBox resource={selection}/>
<ProjectListBox viewSet={viewSet}/>
</>}
selectedRoute={({selection}) => <>
<ResourceDescriptionBox resource={selection} icon={faProjectDiagram}/>
<NotebookRouter
projectPk={selection.value.slug}
unselectedRoute={({viewSet}) => <>
<NotebookListBox viewSet={viewSet}/>
</>}
selectedRoute={(props) => <>
<DebugBox {...props}/>
</>}
/>
</>}
/>
<GroupProvider resource={selection}>
<Chapter>
<ResourceDescriptionBox resource={selection} icon={faUsers}/>
<GroupMembersBox/>
</Chapter>
<ProjectRouter
groupPk={selection.value.slug}
unselectedRoute={({viewSet}) => <>
<GroupCreateBox resource={selection}/>
<ProjectListBox viewSet={viewSet}/>
<ProjectCreateBox viewSet={viewSet}/>
</>}
selectedRoute={({selection}) => <>
<ResourceDescriptionBox resource={selection} icon={faProjectDiagram}/>
<ProjectCreateBox resource={selection}/>
<NotebookRouter
projectPk={selection.value.slug}
unselectedRoute={({viewSet}) => <>
<NotebookListBox viewSet={viewSet}/>
</>}
selectedRoute={(props) => <>
<DebugBox {...props}/>
</>}
/>
</>}
/>
</GroupProvider>
</>}
/>
</>}

View file

@ -43,7 +43,7 @@ export function GroupCreateBox({viewSet, resource}: GroupCreateBoxProps): JSX.El
const description =
useFormState<string>(
resource?.value.description ?? "",
Validators.notZeroLength,
Validators.alwaysValid,
)
const members =

View file

@ -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<SophonResearchGroup>
}
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 (
<Box>
<Heading level={3}>
<FontAwesomeIcon icon={faUsersCog}/> Members of <Idiomatic>{resource.value.name}</Idiomatic>
<FontAwesomeIcon icon={faUsersCog}/> Members of <Idiomatic>{group.value.name}</Idiomatic>
</Heading>
<ListUnordered>
{users}

View file

@ -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<any>) => JSX.Element | null,
selectedRoute: (props: Dict<any>) => JSX.Element | null,
unselectedRoute: (props: any) => JSX.Element | null,
selectedRoute: (props: any) => JSX.Element | null,
}

View file

@ -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<SophonResearchGroup>
}
export function GroupStepPage({viewSet}: GroupStepPageProps): JSX.Element {
return (
<>
<GroupListBox viewSet={viewSet}/>
<GroupCreateBox viewSet={viewSet}/>
</>
)
}

View file

@ -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<SophonResearchProject>,
resource?: ManagedResource<SophonResearchProject>,
}
export function ProjectCreateBox({viewSet, resource}: ProjectCreateBoxProps): JSX.Element | null {
const authorization = useAuthorizationContext()
const group = useGroupContext()
const name =
useFormState<string>(
resource?.value.name ?? "",
Validators.notZeroLength,
)
const description =
useFormState<string>(
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 (
<Box>
<Details>
<Details.Summary>
{resource ? <>Edit <I>{resource.value.name}</I></> : "Create a new research project"}
</Details.Summary>
<Details.Content>
<Form>
<Form.Field
label={"Name"}
required={true}
{...name}
/>
<Form.Field
label={"Slug"}
required={true}
disabled={true}
value={slug}
validity={slug.length > 0 ? true : undefined}
/>
<Form.Area
label={"Description"}
{...description}
/>
<Form.Select
label={"Visibility"}
options={{
"": undefined,
"🔒 Private": "PRIVATE",
"🎓 Internal": "INTERNAL",
"🌍 Public": "PUBLIC",
}}
{...visibility}
/>
<Form.Row>
<Form.Button
type={"button"}
onClick={applyChanges}
disabled={!canApply}
builtinColor={hasError ? "red" : undefined}
>
{resource ? "Edit" : "Create"}
</Form.Button>
</Form.Row>
</Form>
</Details.Content>
</Details>
</Box>
)
}

View file

@ -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<SophonResearchProject>,
}
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 (
<SafetyButton timeout={3} onClick={() => resource.destroy()}>
<FontAwesomeIcon icon={faTrash} pulse={resource.busy}/>&nbsp;Delete
</SafetyButton>
)
}

View file

@ -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
</ResourcePanel.Text>
<ResourcePanel.Buttons>
<ProjectDeleteButton resource={resource}/>
</ResourcePanel.Buttons>
</ResourcePanel>
)

View file

@ -16,7 +16,7 @@ export function ProjectRouter({groupPk, ...props}: ProjectRouterProps): JSX.Elem
return (
<ViewSetRouter
{...props}
viewSet={useManagedViewSet<SophonResearchProject>(`/api/projects/by-group/${groupPk}`, "slug")}
viewSet={useManagedViewSet<SophonResearchProject>(`/api/projects/by-group/${groupPk}/`, "slug")}
pathSegment={"researchProject"}
pkKey={"slug"}
/>

View file

@ -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<ManagedResource<SophonResearchGroup> | undefined>(undefined)
const GroupContext = groupContext
/**
* Hook to access the {@link groupContext}.
*/
export function useGroupContext(): ManagedResource<SophonResearchGroup> | undefined {
return React.useContext(groupContext)
}
export function GroupProvider({resource, children}: WithResource<SophonResearchGroup> & WithChildren): JSX.Element {
return <GroupContext.Provider value={resource} children={children}/>
}

View file

@ -1,4 +1,5 @@
import * as React from "react"
import {ManagedResource, ManagedViewSet} from "../hooks/useManagedViewSet"
/**
@ -15,3 +16,19 @@ export interface Dict<T> {
export interface WithChildren {
children?: React.ReactNode,
}
/**
* Props including the selection key.
*/
export interface WithResource<T> {
resource: ManagedResource<T>,
}
/**
* Props including the viewset key.
*/
export interface WithViewSet<T> {
viewSet: ManagedViewSet<T>,
}