Commit bb5e188b authored by Stefan Probst's avatar Stefan Probst
Browse files

fix: fix account screen permissions

parent ffcfdbb0
......@@ -13,6 +13,7 @@ import { RelatedItemsFormSection } from '@/components/item/RelatedItemsFormSecti
import { ThumbnailFormSection } from '@/components/item/ThumbnailFormSection/ThumbnailFormSection'
import { Button } from '@/elements/Button/Button'
import { useToast } from '@/elements/Toast/useToast'
import { useQueryParam } from '@/lib/hooks/useQueryParam'
import { sanitizeFormValues } from '@/lib/sshoc/sanitizeFormValues'
import { useValidateCommonFormFields } from '@/lib/sshoc/validateCommonFormFields'
import { validateDateFormFields } from '@/lib/sshoc/validateDateFormFields'
......@@ -42,6 +43,8 @@ export function ItemForm(props: ItemFormProps<ItemFormValues>): JSX.Element {
const useItemMutation = useUpdateDataset
const isReviewToApprove = useQueryParam('review', false, Boolean)
const toast = useToast()
const router = useRouter()
const auth = useAuth()
......@@ -184,7 +187,10 @@ export function ItemForm(props: ItemFormProps<ItemFormValues>): JSX.Element {
form.change('draft', undefined)
}}
isDisabled={
pristine || invalid || submitting || create.isLoading
(pristine && isReviewToApprove !== true) ||
invalid ||
submitting ||
create.isLoading
}
>
{isAllowedToPublish ? 'Publish' : 'Submit'}
......
......@@ -13,6 +13,7 @@ import { RelatedItemsFormSection } from '@/components/item/RelatedItemsFormSecti
import { ThumbnailFormSection } from '@/components/item/ThumbnailFormSection/ThumbnailFormSection'
import { Button } from '@/elements/Button/Button'
import { useToast } from '@/elements/Toast/useToast'
import { useQueryParam } from '@/lib/hooks/useQueryParam'
import { sanitizeFormValues } from '@/lib/sshoc/sanitizeFormValues'
import { useValidateCommonFormFields } from '@/lib/sshoc/validateCommonFormFields'
import { validateDateFormFields } from '@/lib/sshoc/validateDateFormFields'
......@@ -42,6 +43,8 @@ export function ItemForm(props: ItemFormProps<ItemFormValues>): JSX.Element {
const useItemMutation = useUpdatePublication
const isReviewToApprove = useQueryParam('review', false, Boolean)
const toast = useToast()
const router = useRouter()
const auth = useAuth()
......@@ -184,7 +187,10 @@ export function ItemForm(props: ItemFormProps<ItemFormValues>): JSX.Element {
form.change('draft', undefined)
}}
isDisabled={
pristine || invalid || submitting || create.isLoading
(pristine && isReviewToApprove !== true) ||
invalid ||
submitting ||
create.isLoading
}
>
{isAllowedToPublish ? 'Publish' : 'Submit'}
......
......@@ -12,6 +12,7 @@ import { RelatedItemsFormSection } from '@/components/item/RelatedItemsFormSecti
import { ThumbnailFormSection } from '@/components/item/ThumbnailFormSection/ThumbnailFormSection'
import { Button } from '@/elements/Button/Button'
import { useToast } from '@/elements/Toast/useToast'
import { useQueryParam } from '@/lib/hooks/useQueryParam'
import { sanitizeFormValues } from '@/lib/sshoc/sanitizeFormValues'
import { useValidateCommonFormFields } from '@/lib/sshoc/validateCommonFormFields'
import { useAuth } from '@/modules/auth/AuthContext'
......@@ -40,6 +41,8 @@ export function ItemForm(props: ItemFormProps<ItemFormValues>): JSX.Element {
const useItemMutation = useUpdateTool
const isReviewToApprove = useQueryParam('review', false, Boolean)
const toast = useToast()
const router = useRouter()
const auth = useAuth()
......@@ -180,7 +183,10 @@ export function ItemForm(props: ItemFormProps<ItemFormValues>): JSX.Element {
form.change('draft', undefined)
}}
isDisabled={
pristine || invalid || submitting || create.isLoading
(pristine && isReviewToApprove !== true) ||
invalid ||
submitting ||
create.isLoading
}
>
{isAllowedToPublish ? 'Publish' : 'Submit'}
......
......@@ -12,6 +12,7 @@ import { RelatedItemsFormSection } from '@/components/item/RelatedItemsFormSecti
import { ThumbnailFormSection } from '@/components/item/ThumbnailFormSection/ThumbnailFormSection'
import { Button } from '@/elements/Button/Button'
import { useToast } from '@/elements/Toast/useToast'
import { useQueryParam } from '@/lib/hooks/useQueryParam'
import { sanitizeFormValues } from '@/lib/sshoc/sanitizeFormValues'
import { useValidateCommonFormFields } from '@/lib/sshoc/validateCommonFormFields'
import { useAuth } from '@/modules/auth/AuthContext'
......@@ -40,6 +41,8 @@ export function ItemForm(props: ItemFormProps<ItemFormValues>): JSX.Element {
const useItemMutation = useUpdateTrainingMaterial
const isReviewToApprove = useQueryParam('review', false, Boolean)
const toast = useToast()
const router = useRouter()
const auth = useAuth()
......@@ -180,7 +183,10 @@ export function ItemForm(props: ItemFormProps<ItemFormValues>): JSX.Element {
form.change('draft', undefined)
}}
isDisabled={
pristine || invalid || submitting || create.isLoading
(pristine && isReviewToApprove !== true) ||
invalid ||
submitting ||
create.isLoading
}
>
{isAllowedToPublish ? 'Publish' : 'Submit'}
......
......@@ -26,6 +26,7 @@ import { ThumbnailFormSection } from '@/components/item/ThumbnailFormSection/Thu
import { WorkflowStepsFormSection } from '@/components/item/WorkflowStepsFormSection/WorkflowStepsFormSection'
import { Button } from '@/elements/Button/Button'
import { useToast } from '@/elements/Toast/useToast'
import { useQueryParam } from '@/lib/hooks/useQueryParam'
import { sanitizeFormValues } from '@/lib/sshoc/sanitizeFormValues'
import { useValidateCommonFormFields } from '@/lib/sshoc/validateCommonFormFields'
import { useAuth } from '@/modules/auth/AuthContext'
......@@ -59,6 +60,8 @@ export function ItemForm(props: ItemFormProps<ItemFormValues>): JSX.Element {
const useItemMutation = useUpdateWorkflow
const isReviewToApprove = useQueryParam('review', false, Boolean)
const toast = useToast()
const router = useRouter()
const auth = useAuth()
......@@ -350,7 +353,8 @@ export function ItemForm(props: ItemFormProps<ItemFormValues>): JSX.Element {
function isFormPristine(isCurrentPagePristine: boolean) {
return (
isCurrentPagePristine &&
Object.values(pageStatus).every((pristine) => pristine === true)
Object.values(pageStatus).every((pristine) => pristine === true) &&
isReviewToApprove !== true
)
}
......
......@@ -31,6 +31,8 @@ export interface SelectProps<T> extends AriaSelectProps<T> {
hideSelectionIcon?: boolean
/** @default "default" */
variant?: 'default' | 'search' | 'form'
/** @default "medium" */
size?: 'small' | 'medium'
style?: CSSProperties
}
......@@ -115,28 +117,48 @@ export function Select<T extends object>(props: SelectProps<T>): JSX.Element {
},
}
const sizes = {
small: {
button: 'w-56 text-ui-sm',
value: 'px-3 py-2',
iconContainer: 'px-2',
icon: 'h-2 w-2',
},
medium: {
button: 'w-64 text-ui-base',
value: 'px-4 py-3',
iconContainer: 'px-3.5',
icon: 'h-2.5 w-2.5',
},
}
const variant = variants[props.variant ?? 'default']
const size = sizes[props.size ?? 'medium']
const styles = {
container: 'relative inline-flex',
button: cx(
'w-64 cursor-default',
'font-body font-normal text-ui-base text-gray-800 inline-flex items-center justify-between focus:outline-none',
'cursor-default',
'font-body font-normal text-gray-800 inline-flex items-center justify-between focus:outline-none',
'transition rounded border border-gray-300 hover:text-primary-750',
variant.button,
size.button,
),
value: cx(
'px-4 py-3 flex-1 text-left',
'flex-1 text-left',
isLoadingInitial && 'pr-0',
variant.value,
size.value,
),
iconContainer: cx(
'transition self-stretch inline-flex items-center justify-center px-3.5 border-l',
'transition self-stretch inline-flex items-center justify-center border-l',
variant.iconContainer,
size.iconContainer,
),
icon: cx(
'transition transform h-2.5 w-2.5',
'transition transform',
state.isOpen && 'rotate-180',
variant.icon,
size.icon,
),
spinnerContainer: 'inline-flex items-center justify-center',
spinner: 'w-4 h-4 text-primary-750',
......
......@@ -6,7 +6,7 @@ import ActorsScreen from '@/screens/account/ActorsScreen'
*/
export default function ActorsPage(): JSX.Element {
return (
<ProtectedScreen>
<ProtectedScreen roles={['administrator', 'moderator']}>
<ActorsScreen />
</ProtectedScreen>
)
......
......@@ -6,7 +6,7 @@ import SourcesScreen from '@/screens/account/SourcesScreen'
*/
export default function SourcesPage(): JSX.Element {
return (
<ProtectedScreen>
<ProtectedScreen roles={['administrator']}>
<SourcesScreen />
</ProtectedScreen>
)
......
......@@ -37,11 +37,13 @@ const fields = [
label: 'Sources',
pathname: '/account/sources',
icon: SourcesIcon,
roles: ['administrator'],
},
{
label: 'Actors',
pathname: '/account/actors',
icon: ActorsIcon,
roles: ['administrator', 'moderator'],
},
{
label: 'Users',
......
......@@ -565,6 +565,7 @@ function EditActorButton(props: EditActorButtonProps) {
const editActor = useUpdateActor({
onSuccess() {
toast.success('Successfully updated actor.')
queryClient.invalidateQueries(['searchItems'])
queryClient.invalidateQueries(['getActors'])
queryClient.invalidateQueries(['getActor', { id: props.actor.id }])
},
......@@ -635,6 +636,7 @@ function DeleteActorButton(props: DeleteActorButton) {
const deleteActor = useDeleteActor({
onSuccess() {
toast.success('Successfully deleted actor.')
queryClient.invalidateQueries(['searchItems'])
queryClient.invalidateQueries(['getActors'])
},
onError(error) {
......
......@@ -46,6 +46,7 @@ export default function ContributedItemsScreen(): JSX.Element {
const toast = useToast()
const items = useSearchItems(
{
order: ['modified-on'],
...query,
'd.status': '(approved OR suggested)',
// When a user is logged in as admin, this will not return the user's
......
......@@ -42,7 +42,10 @@ export default function DraftItemsScreen(): JSX.Element {
const handleErrors = useErrorHandlers()
const toast = useToast()
const items = useGetMyDraftItems(
query,
{
order: 'modified-on',
...query,
},
{
enabled: auth.session?.accessToken != null,
keepPreviousData: true,
......
......@@ -44,6 +44,7 @@ export default function ModerateItemsScreen(): JSX.Element {
const toast = useToast()
const items = useSearchItems(
{
order: ['modified-on'],
...query,
// When a user is signed in as admin/moderator, this returns
// ingested/suggested items by *all* users
......@@ -76,8 +77,8 @@ export default function ModerateItemsScreen(): JSX.Element {
{ pathname: '/', label: 'Home' },
{ pathname: '/account', label: 'My account' },
{
pathname: '/account/contributed',
label: 'My contributed items',
pathname: '/account/moderate',
label: 'Items to moderate',
},
]}
/>
......@@ -86,7 +87,7 @@ export default function ModerateItemsScreen(): JSX.Element {
className="px-6 py-12 space-y-12"
style={{ gridColumn: '4 / span 8' }}
>
<Title>My contributed items</Title>
<Title>Items to moderate</Title>
{items.data === undefined ? (
<ProgressSpinner />
) : items.data.items?.length === 0 ? (
......@@ -163,19 +164,21 @@ function ContributedItem(props: ContributedItemProps) {
<span className="text-gray-550">Status:</span>
<span>{item.status}</span>
</div>
<div className="space-x-1.5">
<span className="text-gray-550">Contributior:</span>
<span>
{item.contributors
?.map((contributor) => {
return contributor.actor?.name
})
.join(', ')}
</span>
</div>
{Array.isArray(item.contributors) && item.contributors.length > 0 ? (
<div className="space-x-1.5">
<span className="text-gray-550">Contributors:</span>
<span>
{item.contributors
.map((contributor) => {
return contributor.actor?.name
})
.join(', ')}
</span>
</div>
) : null}
</div>
<div className="text-sm">
<ProtectedView>
<ProtectedView roles={['moderator', 'administrator']}>
<Link
passHref
href={{
......@@ -187,6 +190,9 @@ function ContributedItem(props: ContributedItemProps) {
item.id,
'edit',
].join('/'),
query: {
review: true,
},
}}
>
<Anchor className="cursor-default text-ui-base">Edit</Anchor>
......
......@@ -743,6 +743,7 @@ function EditSourceButton(props: EditSourceButtonProps) {
const editSource = useUpdateSource({
onSuccess() {
toast.success('Successfully updated source.')
queryClient.invalidateQueries(['searchItems'])
queryClient.invalidateQueries(['getSources'])
queryClient.invalidateQueries(['getSource', { id: props.source.id }])
},
......@@ -813,6 +814,7 @@ function DeleteSourceButton(props: DeleteSourceButtonProps) {
const deleteSource = useDeleteSource({
onSuccess() {
toast.success('Successfully deleted source.')
queryClient.invalidateQueries(['searchItems'])
queryClient.invalidateQueries(['getSources'])
},
onError(error) {
......
......@@ -8,7 +8,11 @@ import { Fragment, useEffect, useState } from 'react'
import { QueryClientProvider, useQueryClient } from 'react-query'
import type { GetUsers, UserDto } from '@/api/sshoc'
import { useGetUsers, useUpdateUserStatus } from '@/api/sshoc'
import {
useGetUsers,
useUpdateUserRole,
useUpdateUserStatus,
} from '@/api/sshoc'
import { Button } from '@/elements/Button/Button'
import { Icon } from '@/elements/Icon/Icon'
import { Svg as CloseIcon } from '@/elements/icons/small/cross.svg'
......@@ -179,6 +183,7 @@ function User(props: UserProps) {
</div>
<div className="flex space-x-4 text-sm text-primary-750">
<ProtectedView roles={['administrator']}>
<UserRoleSelect user={user} />
<UserStatusSelect user={user} />
{/* FIXME: Needs endpoint */}
{/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */}
......@@ -611,10 +616,11 @@ function UserStatusSelect(props: UserStatusSelectProps) {
<label className="space-x-1.5 flex items-center">
<span className="text-xs text-gray-550">Status:</span>
<Select
aria-label="Sort order"
aria-label="User status"
items={labeledAllowedStatus}
onSelectionChange={onSubmit}
selectedKey={user.status}
size="small"
>
{(item) => <Select.Item>{item.label}</Select.Item>}
</Select>
......@@ -625,3 +631,73 @@ function UserStatusSelect(props: UserStatusSelectProps) {
function capitalize(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1)
}
type AllowedUserRole = Exclude<UserDto['role'], undefined>
interface UserRoleSelectProps {
user: UserDto
}
/**
* Set user status.
*/
function UserRoleSelect(props: UserRoleSelectProps) {
const { user } = props
const toast = useToast()
const auth = useAuth()
const handleErrors = useErrorHandlers()
const queryClient = useQueryClient()
const updateUserRole = useUpdateUserRole({
onSuccess() {
toast.success('Successfully changed user role')
queryClient.invalidateQueries(['getUsers'])
queryClient.invalidateQueries(['getUser', { id: user.id }])
},
onError(error) {
toast.error('Failed to update user role')
if (error instanceof Error) {
handleErrors(error)
}
},
})
const allowedRoles: Array<AllowedUserRole> = [
'contributor',
'system-contributor',
'moderator',
'system-moderator',
'administrator',
]
const labeledAllowedRoles = allowedRoles.map((id) => ({
id,
label: id.split('-').map(capitalize).join(' '),
}))
function onSubmit(role: Key) {
updateUserRole.mutate([
{
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
id: user.id!,
userRole: role as AllowedUserRole,
},
{ token: auth.session?.accessToken },
])
}
return (
<label className="space-x-1.5 flex items-center">
<span className="text-xs text-gray-550">Role:</span>
<Select
aria-label="User role"
items={labeledAllowedRoles}
onSelectionChange={onSubmit}
selectedKey={user.role}
size="small"
>
{(item) => <Select.Item>{item.label}</Select.Item>}
</Select>
</label>
)
}
......@@ -124,7 +124,7 @@ function ItemVersion(props: ItemVersionProps) {
onSuccess() {
toast.success('Successfully reverted to version.')
queryClient.invalidateQueries({ queryKey: ['itemSearch'] })
queryClient.invalidateQueries({ queryKey: ['searchItems'] })
queryClient.invalidateQueries({ queryKey: [getAllKey] })
queryClient.invalidateQueries({
queryKey: [
......
......@@ -50,7 +50,6 @@ export default function WorkflowDraftEditScreen(): JSX.Element {
className="px-6 py-12 space-y-12"
style={{ gridColumn: '4 / span 8' }}
>
<Title>Edit workflow</Title>
{workflow.data === undefined || id == null ? (
<div className="flex flex-col items-center justify-center">
<ProgressSpinner />
......
......@@ -12,7 +12,6 @@ import { useErrorHandlers } from '@/modules/error/useErrorHandlers'
import ContentColumn from '@/modules/layout/ContentColumn'
import GridLayout from '@/modules/layout/GridLayout'
import Metadata from '@/modules/metadata/Metadata'
import { Title } from '@/modules/ui/typography/Title'
/**
* Edit workflow version screen.
......@@ -51,7 +50,6 @@ export default function WorkflowVersionEditScreen(): JSX.Element {
className="px-6 py-12 space-y-12"
style={{ gridColumn: '4 / span 8' }}
>
<Title>Edit workflow</Title>
{workflow.data === undefined || id == null ? (
<div className="flex flex-col items-center justify-center">
<ProgressSpinner />
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment