mirror of
https://github.com/pds-nest/nest.git
synced 2024-11-22 04:54:18 +00:00
✨ Add initial support for sharing a repository
This commit is contained in:
parent
6048de040f
commit
1928435f6f
8 changed files with 157 additions and 29 deletions
|
@ -11,12 +11,6 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.Button[disabled] {
|
|
||||||
opacity: 0.5;
|
|
||||||
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Button:focus-visible {
|
.Button:focus-visible {
|
||||||
outline: 4px solid var(--outline);
|
outline: 4px solid var(--outline);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,11 +4,18 @@ import classNames from "classnames"
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||||
|
|
||||||
|
|
||||||
export default function SummaryLeft({ icon, title, subtitle, className, onClick, ...props }) {
|
export default function SummaryLeft({ icon, title, subtitle, className, onClick, disabled, ...props }) {
|
||||||
|
const _onClick = disabled ? null : onClick
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames(Style.SummaryLeft, onClick ? "Clickable" : null, className)}
|
className={classNames(
|
||||||
onClick={onClick}
|
Style.SummaryLeft,
|
||||||
|
onClick ? "Clickable" : null,
|
||||||
|
(onClick && disabled) ? "Disabled" : null,
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
onClick={_onClick}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<div className={Style.IconContainer}>
|
<div className={Style.IconContainer}>
|
||||||
|
|
|
@ -9,13 +9,15 @@ import ContextLanguage from "../../contexts/ContextLanguage"
|
||||||
* A {@link BoxFullScrollable} rendering an array of users as {@link SummaryUser}s.
|
* A {@link BoxFullScrollable} rendering an array of users as {@link SummaryUser}s.
|
||||||
*
|
*
|
||||||
* @param users - Array of users to render.
|
* @param users - Array of users to render.
|
||||||
|
* @param shareWithUser - Async function to share a repository with an user, to be passed to {@link SummaryUser}.
|
||||||
|
* @param unshareWithUser - Async function to unshare a repository with an user, to be passed to {@link SummaryUser}.
|
||||||
* @param destroyUser - Async function to destroy an user, to be passed to {@link SummaryUser}.
|
* @param destroyUser - Async function to destroy an user, to be passed to {@link SummaryUser}.
|
||||||
* @param running - Whether another request is currently running.
|
* @param running - Whether another request is currently running.
|
||||||
* @param props - Additional props to pass to the box.
|
* @param props - Additional props to pass to the box.
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export default function BoxUserList({ users, destroyUser, running, ...props }) {
|
export default function BoxUserList({ users, shareWithUser, unshareWithUser, destroyUser, running, ...props }) {
|
||||||
const { strings } = useContext(ContextLanguage)
|
const { strings } = useContext(ContextLanguage)
|
||||||
|
|
||||||
let contents
|
let contents
|
||||||
|
@ -23,8 +25,17 @@ export default function BoxUserList({ users, destroyUser, running, ...props }) {
|
||||||
contents = <Loading/>
|
contents = <Loading/>
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
contents = users.map(user =>
|
contents = users.map(user => (
|
||||||
<SummaryUser key={user["email"]} destroyUser={destroyUser} running={running} user={user}/>)
|
<SummaryUser
|
||||||
|
key={user["email"]}
|
||||||
|
shareWithUser={shareWithUser}
|
||||||
|
unshareWithUser={unshareWithUser}
|
||||||
|
destroyUser={destroyUser}
|
||||||
|
running={running}
|
||||||
|
user={user}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -35,6 +35,7 @@ export default function SummaryRepository(
|
||||||
title={repo.name}
|
title={repo.name}
|
||||||
subtitle={repo.owner ? repo.owner.username : null}
|
subtitle={repo.owner ? repo.owner.username : null}
|
||||||
onClick={view}
|
onClick={view}
|
||||||
|
disabled={running}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SummaryLabels
|
<SummaryLabels
|
||||||
|
|
|
@ -1,24 +1,27 @@
|
||||||
import React, { useContext } from "react"
|
import React, { useContext } from "react"
|
||||||
import { faStar, faTrash, faUser } from "@fortawesome/free-solid-svg-icons"
|
import { faShare, faStar, faTrash, faUser } from "@fortawesome/free-solid-svg-icons"
|
||||||
import ContextLanguage from "../../contexts/ContextLanguage"
|
import ContextLanguage from "../../contexts/ContextLanguage"
|
||||||
import SummaryBase from "../base/summary/SummaryBase"
|
import SummaryBase from "../base/summary/SummaryBase"
|
||||||
import SummaryLeft from "../base/summary/SummaryLeft"
|
import SummaryLeft from "../base/summary/SummaryLeft"
|
||||||
import SummaryLabels from "../base/summary/SummaryLabels"
|
import SummaryLabels from "../base/summary/SummaryLabels"
|
||||||
import SummaryButton from "../base/summary/SummaryButton"
|
import SummaryButton from "../base/summary/SummaryButton"
|
||||||
import SummaryRight from "../base/summary/SummaryRight"
|
import SummaryRight from "../base/summary/SummaryRight"
|
||||||
|
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link SummaryBase} representing a N.E.S.T. user.
|
* A {@link SummaryBase} representing a N.E.S.T. user.
|
||||||
*
|
*
|
||||||
* @param user - The user to represent.
|
* @param user - The user to represent.
|
||||||
|
* @param shareWithUser - Async function to share a repository with an user, to be passed to {@link SummaryUser}.
|
||||||
|
* @param unshareWithUser - Async function to unshare a repository with an user, to be passed to {@link SummaryUser}.
|
||||||
* @param destroyUser - Async function <string> to destroy an user from the frontend.
|
* @param destroyUser - Async function <string> to destroy an user from the frontend.
|
||||||
* @param running - Whether another request is already running.
|
* @param running - Whether another request is already running.
|
||||||
* @param props - Additional props to pass to the summary.
|
* @param props - Additional props to pass to the summary.
|
||||||
* @returns {JSX.Element}
|
* @returns {JSX.Element}
|
||||||
* @constructor
|
* @constructor
|
||||||
*/
|
*/
|
||||||
export default function SummaryUser({ user, destroyUser, running, ...props }) {
|
export default function SummaryUser({ user, shareWithUser, unshareWithUser, destroyUser, running, ...props }) {
|
||||||
const { strings } = useContext(ContextLanguage)
|
const { strings } = useContext(ContextLanguage)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -27,23 +30,52 @@ export default function SummaryUser({ user, destroyUser, running, ...props }) {
|
||||||
icon={user.isAdmin ? faStar : faUser}
|
icon={user.isAdmin ? faStar : faUser}
|
||||||
title={user.username}
|
title={user.username}
|
||||||
subtitle={user.email}
|
subtitle={user.email}
|
||||||
|
disabled={running}
|
||||||
/>
|
/>
|
||||||
<SummaryLabels
|
<SummaryLabels
|
||||||
upperLabel={strings.type}
|
upperLabel={strings.type}
|
||||||
upperValue={user.isAdmin ? strings.admin : strings.user}
|
upperValue={user.isAdmin ? strings.admin : strings.user}
|
||||||
/>
|
/>
|
||||||
<SummaryButton
|
{shareWithUser ?
|
||||||
color={"Red"}
|
<SummaryButton
|
||||||
icon={faTrash}
|
color={"Green"}
|
||||||
onClick={async event => {
|
icon={faShare}
|
||||||
event.stopPropagation()
|
onClick={async event => {
|
||||||
// TODO: Errors are not caught here. Where should they be displayed?
|
event.stopPropagation()
|
||||||
await destroyUser(user["email"])
|
await shareWithUser(user)
|
||||||
}}
|
}}
|
||||||
disabled={running}
|
disabled={running}
|
||||||
>
|
>
|
||||||
{strings.delete}
|
{strings.share}
|
||||||
</SummaryButton>
|
</SummaryButton>
|
||||||
|
: null}
|
||||||
|
{unshareWithUser ?
|
||||||
|
<SummaryButton
|
||||||
|
color={"Red"}
|
||||||
|
icon={<FontAwesomeIcon icon={faShare} flip={"horizontal"}/>}
|
||||||
|
onClick={async event => {
|
||||||
|
event.stopPropagation()
|
||||||
|
await unshareWithUser(user)
|
||||||
|
}}
|
||||||
|
disabled={running}
|
||||||
|
>
|
||||||
|
{strings.unshare}
|
||||||
|
</SummaryButton>
|
||||||
|
: null}
|
||||||
|
{destroyUser ?
|
||||||
|
<SummaryButton
|
||||||
|
color={"Red"}
|
||||||
|
icon={faTrash}
|
||||||
|
onClick={async event => {
|
||||||
|
event.stopPropagation()
|
||||||
|
// TODO: Errors are not caught here. Where should they be displayed?
|
||||||
|
await destroyUser(user["email"])
|
||||||
|
}}
|
||||||
|
disabled={running}
|
||||||
|
>
|
||||||
|
{strings.delete}
|
||||||
|
</SummaryButton>
|
||||||
|
: null}
|
||||||
<SummaryRight/>
|
<SummaryRight/>
|
||||||
</SummaryBase>
|
</SummaryBase>
|
||||||
)
|
)
|
||||||
|
|
|
@ -24,8 +24,9 @@ h1, h2, h3, h4, h5, h6 {
|
||||||
font-family: var(--font-title);
|
font-family: var(--font-title);
|
||||||
}
|
}
|
||||||
|
|
||||||
*[disabled] {
|
*[disabled], .Disabled {
|
||||||
cursor: not-allowed;
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|
|
@ -1,18 +1,76 @@
|
||||||
import React, { useContext } from "react"
|
import React, { useCallback, useContext } from "react"
|
||||||
import classNames from "classnames"
|
import classNames from "classnames"
|
||||||
import BoxHeader from "../components/base/BoxHeader"
|
import BoxHeader from "../components/base/BoxHeader"
|
||||||
import ContextLanguage from "../contexts/ContextLanguage"
|
import ContextLanguage from "../contexts/ContextLanguage"
|
||||||
import Style from "./PageShare.module.css"
|
import Style from "./PageShare.module.css"
|
||||||
|
import BoxUserList from "../components/interactive/BoxUserList"
|
||||||
|
import useBackendViewset from "../hooks/useBackendViewset"
|
||||||
|
import { useParams } from "react-router"
|
||||||
|
|
||||||
|
|
||||||
export default function PageShare({ className, ...props }) {
|
export default function PageShare({ className, ...props }) {
|
||||||
const { strings } = useContext(ContextLanguage)
|
const { strings } = useContext(ContextLanguage)
|
||||||
|
const { id } = useParams()
|
||||||
|
const {resources: users} = useBackendViewset(
|
||||||
|
"/api/v1/users/",
|
||||||
|
"email",
|
||||||
|
{
|
||||||
|
list: true,
|
||||||
|
create: false,
|
||||||
|
retrieve: false,
|
||||||
|
edit: false,
|
||||||
|
destroy: false,
|
||||||
|
command: false,
|
||||||
|
action: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const {resources: authorizations, createResource: createAuthorization, destroyResource: destroyAuthorization} = useBackendViewset(
|
||||||
|
`/api/v1/repositories/${id}/authorizations/`,
|
||||||
|
"email",
|
||||||
|
{
|
||||||
|
list: true,
|
||||||
|
create: true,
|
||||||
|
retrieve: false,
|
||||||
|
edit: false,
|
||||||
|
destroy: true,
|
||||||
|
command: false,
|
||||||
|
action: false,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const shareWith = useCallback(
|
||||||
|
user => {
|
||||||
|
console.info("Authorizing ", user, " ...")
|
||||||
|
createAuthorization({rid: id, email: user.email})
|
||||||
|
},
|
||||||
|
[createAuthorization, id]
|
||||||
|
)
|
||||||
|
|
||||||
|
const unshareWith = useCallback(
|
||||||
|
user => {
|
||||||
|
console.info("Deauthorizing ", user, " ...")
|
||||||
|
destroyAuthorization(user.email)
|
||||||
|
},
|
||||||
|
[destroyAuthorization, id]
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(Style.PageShare, className)} {...props}>
|
<div className={classNames(Style.PageShare, className)} {...props}>
|
||||||
<BoxHeader className={Style.Header}>
|
<BoxHeader className={Style.Header}>
|
||||||
{strings.repoShare}
|
{strings.repoShare}
|
||||||
</BoxHeader>
|
</BoxHeader>
|
||||||
|
<BoxUserList
|
||||||
|
className={Style.UserList}
|
||||||
|
users={users.filter(user => !authorizations.map(a => a.email).includes(user.email))}
|
||||||
|
shareWithUser={shareWith}
|
||||||
|
header={strings.availableUsers}
|
||||||
|
/>
|
||||||
|
<BoxUserList
|
||||||
|
className={Style.SharingWith}
|
||||||
|
users={users.filter(user => authorizations.map(a => a.email).includes(user.email))}
|
||||||
|
unshareWithUser={unshareWith}
|
||||||
|
header={strings.sharingWith}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,27 @@
|
||||||
.PageShare {
|
.PageShare {
|
||||||
|
display: grid;
|
||||||
|
|
||||||
|
grid-template-areas:
|
||||||
|
"a"
|
||||||
|
"b"
|
||||||
|
"c";
|
||||||
|
grid-template-rows: auto 1fr 1fr;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-gap: 10px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.Header {
|
||||||
|
grid-area: a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.UserList {
|
||||||
|
grid-area: b;
|
||||||
|
}
|
||||||
|
|
||||||
|
.SharingWith {
|
||||||
|
grid-area: c;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue