diff --git a/frontend/pages/data-card/[dataCardId].tsx b/frontend/pages/data-card/[dataCardId].tsx index e3029c994..ce695da70 100644 --- a/frontend/pages/data-card/[dataCardId].tsx +++ b/frontend/pages/data-card/[dataCardId].tsx @@ -3,13 +3,13 @@ import { useGetCurrentUser } from 'actions/user' import { useRouter } from 'next/router' import { useMemo } from 'react' import Loading from 'src/common/Loading' -import PageWithTabs from 'src/common/PageWithTabs' +import PageWithTabs, { PageTab } from 'src/common/PageWithTabs' import Title from 'src/common/Title' import Overview from 'src/entry/overview/Overview' import Settings from 'src/entry/settings/Settings' import MultipleErrorWrapper from 'src/errors/MultipleErrorWrapper' import { EntryKind } from 'types/types' -import { getCurrentUserRoles } from 'utils/roles' +import { getCurrentUserRoles, getRequiredRolesText, hasRole } from 'utils/roles' export default function DataCard() { const router = useRouter() @@ -23,7 +23,12 @@ export default function DataCard() { const currentUserRoles = useMemo(() => getCurrentUserRoles(dataCard, currentUser), [dataCard, currentUser]) - const tabs = useMemo( + const [isReadOnly, requiredRolesText] = useMemo(() => { + const validRoles = ['owner'] + return [!hasRole(currentUserRoles, validRoles), getRequiredRolesText(currentUserRoles, validRoles)] + }, [currentUserRoles]) + + const tabs: PageTab[] = useMemo( () => dataCard ? [ @@ -35,11 +40,13 @@ export default function DataCard() { { title: 'Settings', path: 'settings', + disabled: isReadOnly, + disabledText: requiredRolesText, view: , }, ] : [], - [currentUserRoles, dataCard], + [currentUserRoles, dataCard, isReadOnly, requiredRolesText], ) const error = MultipleErrorWrapper(`Unable to load data card page`, { diff --git a/frontend/pages/model/[modelId].tsx b/frontend/pages/model/[modelId].tsx index 5477684de..94cb785a4 100644 --- a/frontend/pages/model/[modelId].tsx +++ b/frontend/pages/model/[modelId].tsx @@ -4,7 +4,7 @@ import { useGetCurrentUser } from 'actions/user' import { useRouter } from 'next/router' import { useMemo } from 'react' import Loading from 'src/common/Loading' -import PageWithTabs from 'src/common/PageWithTabs' +import PageWithTabs, { PageTab } from 'src/common/PageWithTabs' import Title from 'src/common/Title' import AccessRequests from 'src/entry/model/AccessRequests' import InferenceServices from 'src/entry/model/InferenceServices' @@ -14,7 +14,7 @@ import Overview from 'src/entry/overview/Overview' import Settings from 'src/entry/settings/Settings' import MultipleErrorWrapper from 'src/errors/MultipleErrorWrapper' import { EntryKind } from 'types/types' -import { getCurrentUserRoles } from 'utils/roles' +import { getCurrentUserRoles, getRequiredRolesText, hasRole } from 'utils/roles' export default function Model() { const router = useRouter() @@ -25,7 +25,12 @@ export default function Model() { const currentUserRoles = useMemo(() => getCurrentUserRoles(model, currentUser), [model, currentUser]) - const tabs = useMemo( + const [isReadOnly, requiredRolesText] = useMemo(() => { + const validRoles = ['owner'] + return [!hasRole(currentUserRoles, validRoles), getRequiredRolesText(currentUserRoles, validRoles)] + }, [currentUserRoles]) + + const tabs: PageTab[] = useMemo( () => model && uiConfig ? [ @@ -81,11 +86,13 @@ export default function Model() { { title: 'Settings', path: 'settings', + disabled: isReadOnly, + disabledText: requiredRolesText, view: , }, ] : [], - [model, uiConfig, currentUserRoles], + [model, uiConfig, currentUserRoles, isReadOnly, requiredRolesText], ) function requestAccess() { diff --git a/frontend/src/entry/EntityIcon.tsx b/frontend/src/entry/EntityIcon.tsx new file mode 100644 index 000000000..cc5f6d118 --- /dev/null +++ b/frontend/src/entry/EntityIcon.tsx @@ -0,0 +1,13 @@ +import GroupsIcon from '@mui/icons-material/Groups' +import PersonIcon from '@mui/icons-material/Person' +import { useMemo } from 'react' +import { CollaboratorEntry } from 'types/types' + +type EntityIconProps = { + entryCollaborator: CollaboratorEntry +} + +export default function EntityIcon({ entryCollaborator }: EntityIconProps) { + const isUser = useMemo(() => entryCollaborator.entity.startsWith('user:'), [entryCollaborator]) + return isUser ? : +} diff --git a/frontend/src/entry/EntityNameDisplay.tsx b/frontend/src/entry/EntityNameDisplay.tsx new file mode 100644 index 000000000..59c1a462e --- /dev/null +++ b/frontend/src/entry/EntityNameDisplay.tsx @@ -0,0 +1,20 @@ +import { Typography } from '@mui/material' +import { useMemo } from 'react' +import UserDisplay from 'src/common/UserDisplay' +import { CollaboratorEntry, EntityKind } from 'types/types' + +type EntityNameDisplayProps = { + entryCollaborator: CollaboratorEntry +} + +export default function EntityNameDisplay({ entryCollaborator }: EntityNameDisplayProps) { + const [entryCollaboratorKind, entryCollaboratorName] = useMemo( + () => entryCollaborator.entity.split(':'), + [entryCollaborator], + ) + return entryCollaboratorKind === EntityKind.USER || entryCollaboratorKind === EntityKind.GROUP ? ( + + ) : ( + {entryCollaboratorName} + ) +} diff --git a/frontend/src/entry/EntryDescriptionInput.tsx b/frontend/src/entry/EntryDescriptionInput.tsx index 027578c9c..a6edab017 100644 --- a/frontend/src/entry/EntryDescriptionInput.tsx +++ b/frontend/src/entry/EntryDescriptionInput.tsx @@ -1,4 +1,4 @@ -import { TextField, Tooltip } from '@mui/material' +import { TextField } from '@mui/material' import { ChangeEvent } from 'react' import LabelledInput from 'src/common/LabelledInput' @@ -7,42 +7,23 @@ const htmlId = 'entry-description-input' type EntryDescriptionInputProps = { value: string onChange: (value: string) => void -} & ( - | { - isReadOnly: boolean - requiredRolesText: string - } - | { - isReadOnly?: never - requiredRolesText?: never - } -) +} -export default function EntryDescriptionInput({ - value, - onChange, - isReadOnly = false, - requiredRolesText = '', -}: EntryDescriptionInputProps) { +export default function EntryDescriptionInput({ value, onChange }: EntryDescriptionInputProps) { const handleChange = (event: ChangeEvent) => { onChange(event.target.value) } return ( - - - - - + ) } diff --git a/frontend/src/entry/EntryNameInput.tsx b/frontend/src/entry/EntryNameInput.tsx index 7ab873b60..79b4f2cb2 100644 --- a/frontend/src/entry/EntryNameInput.tsx +++ b/frontend/src/entry/EntryNameInput.tsx @@ -1,4 +1,4 @@ -import { TextField, Tooltip } from '@mui/material' +import { TextField } from '@mui/material' import { ChangeEvent } from 'react' import LabelledInput from 'src/common/LabelledInput' import { EntryKindKeys } from 'types/types' @@ -11,46 +11,25 @@ type EntryNameInputProps = { kind: EntryKindKeys onChange: (value: string) => void autoFocus?: boolean -} & ( - | { - isReadOnly: boolean - requiredRolesText: string - } - | { - isReadOnly?: never - requiredRolesText?: never - } -) +} -export default function EntryNameInput({ - value, - kind, - onChange, - autoFocus = false, - isReadOnly = false, - requiredRolesText = '', -}: EntryNameInputProps) { +export default function EntryNameInput({ value, kind, onChange, autoFocus = false }: EntryNameInputProps) { const handleChange = (event: ChangeEvent) => { onChange(event.target.value) } return ( - - - - - + ) } diff --git a/frontend/src/entry/model/mirroredModels/ExportModelAgreement.tsx b/frontend/src/entry/model/mirroredModels/ExportModelAgreement.tsx index 112bd73e9..d5277012a 100644 --- a/frontend/src/entry/model/mirroredModels/ExportModelAgreement.tsx +++ b/frontend/src/entry/model/mirroredModels/ExportModelAgreement.tsx @@ -1,5 +1,5 @@ import { LoadingButton } from '@mui/lab' -import { Box, Card, Checkbox, Container, FormControlLabel, Stack, Tooltip, Typography } from '@mui/material' +import { Box, Card, Checkbox, Container, FormControlLabel, Stack, Typography } from '@mui/material' import { postModelExportToS3 } from 'actions/model' import { ChangeEvent, FormEvent, useState } from 'react' import ModelExportAgreementText from 'src/entry/model/mirroredModels/ModelExportAgreementText' @@ -7,13 +7,11 @@ import useNotification from 'src/hooks/useNotification' import MessageAlert from 'src/MessageAlert' import { getErrorMessage } from 'utils/fetcher' -type ExportModelProps = { +type ExportModelAgreementProps = { modelId: string - isReadOnly: boolean - requiredRolesText: string } -export default function ExportModel({ modelId, isReadOnly, requiredRolesText }: ExportModelProps) { +export default function ExportModelAgreement({ modelId }: ExportModelAgreementProps) { const [checked, setChecked] = useState(false) const [errorMessage, setErrorMessage] = useState('') const [loading, setLoading] = useState(false) @@ -55,19 +53,13 @@ export default function ExportModel({ modelId, isReadOnly, requiredRolesText }: - - } - label='I agree to the terms and conditions of this model export agreement' - /> - - - - - Submit - - - + } + label='I agree to the terms and conditions of this model export agreement' + /> + + Submit + diff --git a/frontend/src/entry/model/mirroredModels/ExportSettings.tsx b/frontend/src/entry/model/mirroredModels/ExportSettings.tsx index 8e27098d0..62f2839fc 100644 --- a/frontend/src/entry/model/mirroredModels/ExportSettings.tsx +++ b/frontend/src/entry/model/mirroredModels/ExportSettings.tsx @@ -1,15 +1,6 @@ import ExpandMoreIcon from '@mui/icons-material/ExpandMore' import { LoadingButton } from '@mui/lab' -import { - Accordion, - AccordionDetails, - AccordionSummary, - Box, - Stack, - TextField, - Tooltip, - Typography, -} from '@mui/material' +import { Accordion, AccordionDetails, AccordionSummary, Box, Stack, TextField, Typography } from '@mui/material' import { patchModel } from 'actions/model' import { ChangeEvent, FormEvent, useState } from 'react' import LabelledInput from 'src/common/LabelledInput' @@ -21,11 +12,9 @@ import { getErrorMessage } from 'utils/fetcher' type ExportSettingsProps = { model: EntryInterface - isReadOnly: boolean - requiredRolesText: string } -export default function ExportSettings({ model, isReadOnly, requiredRolesText }: ExportSettingsProps) { +export default function ExportSettings({ model }: ExportSettingsProps) { const sendNotification = useNotification() const [destinationModelId, setDestinationModelId] = useState(model.settings.mirror?.destinationModelId || '') const [loading, setLoading] = useState(false) @@ -66,7 +55,7 @@ export default function ExportSettings({ model, isReadOnly, requiredRolesText }: return ( <> - + }> @@ -77,17 +66,12 @@ export default function ExportSettings({ model, isReadOnly, requiredRolesText }: - - - - - + {/*TODO - Add the ability to filter releases needed for export (This functionality is not available on the backend) */} - - - - Save - - - + + Save + diff --git a/frontend/src/entry/model/settings/AccessRequestSettings.tsx b/frontend/src/entry/model/settings/AccessRequestSettings.tsx index 2d4550f7f..a68372048 100644 --- a/frontend/src/entry/model/settings/AccessRequestSettings.tsx +++ b/frontend/src/entry/model/settings/AccessRequestSettings.tsx @@ -1,5 +1,5 @@ import { LoadingButton } from '@mui/lab' -import { Checkbox, Divider, FormControlLabel, Stack, Tooltip, Typography } from '@mui/material' +import { Checkbox, Divider, FormControlLabel, Stack, Typography } from '@mui/material' import { patchModel } from 'actions/model' import { useState } from 'react' import useNotification from 'src/hooks/useNotification' @@ -9,11 +9,9 @@ import { getErrorMessage } from 'utils/fetcher' type AccessRequestSettingsProps = { model: EntryInterface - isReadOnly: boolean - requiredRolesText: string } -export default function AccessRequestSettings({ model, isReadOnly, requiredRolesText }: AccessRequestSettingsProps) { +export default function AccessRequestSettings({ model }: AccessRequestSettingsProps) { const [allowUngoverned, setAllowUngoverned] = useState(model.settings.ungovernedAccess) const [loading, setLoading] = useState(false) const [errorMessage, setErrorMessage] = useState('') @@ -48,36 +46,26 @@ export default function AccessRequestSettings({ model, isReadOnly, requiredRoles Manage access requests -
- - setAllowUngoverned(event.target.checked)} - checked={allowUngoverned} - disabled={isReadOnly} - size='small' - /> - } + setAllowUngoverned(event.target.checked)} + checked={allowUngoverned} + size='small' /> - -
+ } + />
- - - - Save - - - + + Save +
diff --git a/frontend/src/entry/model/settings/TemplateSettings.tsx b/frontend/src/entry/model/settings/TemplateSettings.tsx index 851ec634e..6a35b5f3c 100644 --- a/frontend/src/entry/model/settings/TemplateSettings.tsx +++ b/frontend/src/entry/model/settings/TemplateSettings.tsx @@ -1,5 +1,5 @@ import { LoadingButton } from '@mui/lab' -import { Checkbox, Divider, FormControlLabel, Stack, Tooltip, Typography } from '@mui/material' +import { Checkbox, Divider, FormControlLabel, Stack, Typography } from '@mui/material' import { patchModel } from 'actions/model' import { useState } from 'react' import useNotification from 'src/hooks/useNotification' @@ -9,11 +9,9 @@ import { getErrorMessage } from 'utils/fetcher' type TemplateSettingsProps = { model: EntryInterface - isReadOnly: boolean - requiredRolesText: string } -export default function TemplateSettings({ model, isReadOnly, requiredRolesText }: TemplateSettingsProps) { +export default function TemplateSettings({ model }: TemplateSettingsProps) { const [allowTemplating, setAllowTemplating] = useState(model.settings.allowTemplating) const [loading, setLoading] = useState(false) const [errorMessage, setErrorMessage] = useState('') @@ -48,35 +46,27 @@ export default function TemplateSettings({ model, isReadOnly, requiredRolesText Manage Templating
- - setAllowTemplating(event.target.checked)} - checked={allowTemplating} - disabled={isReadOnly} - size='small' - /> - } - /> - + setAllowTemplating(event.target.checked)} + checked={allowTemplating} + size='small' + /> + } + />
- - - - Save - - - + + Save +
diff --git a/frontend/src/entry/overview/EntryRoleList.tsx b/frontend/src/entry/overview/EntryRoleList.tsx new file mode 100644 index 000000000..9489b2c5c --- /dev/null +++ b/frontend/src/entry/overview/EntryRoleList.tsx @@ -0,0 +1,42 @@ +import { Grid, Stack } from '@mui/material' +import { Fragment, useMemo } from 'react' +import EntityIcon from 'src/entry/EntityIcon' +import EntityNameDisplay from 'src/entry/EntityNameDisplay' +import EntryRolesChipSet from 'src/entry/overview/EntryRolesChipSet' +import { EntryInterface } from 'types/types' + +type EntryRoleListProps = { + entry: EntryInterface +} + +export default function EntryRoleList({ entry }: EntryRoleListProps) { + const rows = useMemo( + () => + entry.collaborators.map((collaborator) => ( + + + + + + + + + + + + )), + [entry.collaborators], + ) + + return ( + + + Entity + + + Roles + + {rows} + + ) +} diff --git a/frontend/src/entry/overview/EntryRolesChipSet.tsx b/frontend/src/entry/overview/EntryRolesChipSet.tsx new file mode 100644 index 000000000..0bac129ce --- /dev/null +++ b/frontend/src/entry/overview/EntryRolesChipSet.tsx @@ -0,0 +1,16 @@ +import { Chip } from '@mui/material' +import { useMemo } from 'react' +import { CollaboratorEntry } from 'types/types' + +type EntryRolesChipSetProps = { + entryCollaborator: CollaboratorEntry +} + +export default function EntryRolesChipSet({ entryCollaborator }: EntryRolesChipSetProps) { + const roleChips = useMemo( + () => entryCollaborator.roles.map((role) => ), + [entryCollaborator.roles], + ) + + return roleChips.length ? roleChips : +} diff --git a/frontend/src/entry/overview/EntryRolesDialog.tsx b/frontend/src/entry/overview/EntryRolesDialog.tsx new file mode 100644 index 000000000..e94e63390 --- /dev/null +++ b/frontend/src/entry/overview/EntryRolesDialog.tsx @@ -0,0 +1,22 @@ +import { DialogContent, DialogTitle } from '@mui/material' +import Dialog from '@mui/material/Dialog' +import { Transition } from 'src/common/Transition' +import EntryRoleList from 'src/entry/overview/EntryRoleList' +import { EntryInterface } from 'types/types' + +type EntryRolesDialogProps = { + entry: EntryInterface + open: boolean + onClose: () => void +} + +export default function EntryRolesDialog({ entry, open, onClose }: EntryRolesDialogProps) { + return ( + + Roles + + + + + ) +} diff --git a/frontend/src/entry/overview/ExportEntryCardDialog.tsx b/frontend/src/entry/overview/ExportEntryCardDialog.tsx index 8292acbae..c8179d497 100644 --- a/frontend/src/entry/overview/ExportEntryCardDialog.tsx +++ b/frontend/src/entry/overview/ExportEntryCardDialog.tsx @@ -7,6 +7,7 @@ import Image from 'next/image' import logo from 'public/horizontal-dark.png' import { useMemo, useRef } from 'react' import { useReactToPrint } from 'react-to-print' +import { Transition } from 'src/common/Transition' import { ArrayFieldTemplate, DescriptionFieldTemplate, ObjectFieldTemplate } from 'src/Form/FormTemplates' import { EntryInterface, SplitSchemaNoRender } from 'types/types' import { widgets } from 'utils/formUtils' @@ -60,7 +61,7 @@ export default function ExportEntryCardDialog({ entry, splitSchema, open, setOpe }, [splitSchema.steps]) return ( - setOpen(false)} fullWidth maxWidth='md'> + setOpen(false)} fullWidth maxWidth='md' TransitionComponent={Transition}> }> diff --git a/frontend/src/entry/overview/FormEditPage.tsx b/frontend/src/entry/overview/FormEditPage.tsx index 2f83b59ec..14cb04ad9 100644 --- a/frontend/src/entry/overview/FormEditPage.tsx +++ b/frontend/src/entry/overview/FormEditPage.tsx @@ -8,6 +8,7 @@ import Loading from 'src/common/Loading' import TextInputDialog from 'src/common/TextInputDialog' import UnsavedChangesContext from 'src/contexts/unsavedChangesContext' import EntryCardHistoryDialog from 'src/entry/overview/EntryCardHistoryDialog' +import EntryRolesDialog from 'src/entry/overview/EntryRolesDialog' import ExportEntryCardDialog from 'src/entry/overview/ExportEntryCardDialog' import SaveAndCancelButtons from 'src/entry/overview/SaveAndCancelFormButtons' import JsonSchemaForm from 'src/Form/JsonSchemaForm' @@ -16,34 +17,29 @@ import MessageAlert from 'src/MessageAlert' import { EntryCardKindLabel, EntryInterface, SplitSchemaNoRender } from 'types/types' import { getStepsData, getStepsFromSchema } from 'utils/formUtils' import { getRequiredRolesText, hasRole } from 'utils/roles' - type FormEditPageProps = { entry: EntryInterface readOnly?: boolean currentUserRoles: string[] } - export default function FormEditPage({ entry, currentUserRoles, readOnly = false }: FormEditPageProps) { const [isEdit, setIsEdit] = useState(false) const [splitSchema, setSplitSchema] = useState({ reference: '', steps: [] }) const [errorMessage, setErrorMessage] = useState('') - const { schema, isSchemaLoading, isSchemaError } = useGetSchema(entry.card.schemaId) const { isModelError: isEntryError, mutateModel: mutateEntry } = useGetModel(entry.id, entry.kind) const { mutateModelCardRevisions: mutateEntryCardRevisions } = useGetModelCardRevisions(entry.id) + const [rolesDialogOpen, setRolesDialogOpen] = useState(false) const [historyDialogOpen, setHistoryDialogOpen] = useState(false) const [exportDialogOpen, setExportDialogOpen] = useState(false) const [jsonUploadDialogOpen, setJsonUploadDialogOpen] = useState(false) const [loading, setLoading] = useState(false) - const sendNotification = useNotification() const { setUnsavedChanges } = useContext(UnsavedChangesContext) - const [canEdit, requiredRolesText] = useMemo(() => { const validRoles = ['owner', 'contributor'] return [hasRole(currentUserRoles, validRoles), getRequiredRolesText(currentUserRoles, validRoles)] }, [currentUserRoles]) - async function onSubmit() { if (schema) { setErrorMessage('') @@ -59,7 +55,6 @@ export default function FormEditPage({ entry, currentUserRoles, readOnly = false setLoading(false) } } - function onCancel() { if (schema) { mutateEntry() @@ -71,23 +66,18 @@ export default function FormEditPage({ entry, currentUserRoles, readOnly = false setIsEdit(false) } } - useEffect(() => { if (!entry || !schema) return const metadata = entry.card.metadata const steps = getStepsFromSchema(schema, {}, ['properties.contacts'], metadata) - for (const step of steps) { step.steps = steps } - setSplitSchema({ reference: schema.id, steps }) }, [schema, entry]) - useEffect(() => { setUnsavedChanges(isEdit) }, [isEdit, setUnsavedChanges]) - function handleJsonFormOnSubmit(formData: string) { setJsonUploadDialogOpen(false) try { @@ -106,15 +96,12 @@ export default function FormEditPage({ entry, currentUserRoles, readOnly = false }) } } - if (isSchemaError) { return } - if (isEntryError) { return } - return ( <> {isSchemaLoading && } @@ -141,6 +128,9 @@ export default function FormEditPage({ entry, currentUserRoles, readOnly = false + @@ -183,6 +173,7 @@ export default function FormEditPage({ entry, currentUserRoles, readOnly = false )} + setRolesDialogOpen(false)} /> setJsonUploadDialogOpen(false)} diff --git a/frontend/src/entry/settings/DangerZone.tsx b/frontend/src/entry/settings/DangerZone.tsx index d68595c0f..908538c48 100644 --- a/frontend/src/entry/settings/DangerZone.tsx +++ b/frontend/src/entry/settings/DangerZone.tsx @@ -1,16 +1,14 @@ import { LoadingButton } from '@mui/lab' -import { Stack, Tooltip, Typography } from '@mui/material' +import { Stack, Typography } from '@mui/material' import { useState } from 'react' import { EntryInterface } from 'types/types' import { toTitleCase } from 'utils/stringUtils' type DangerZoneProps = { entry: EntryInterface - isReadOnly: boolean - requiredRolesText: string } -export default function DangerZone({ entry, isReadOnly, requiredRolesText }: DangerZoneProps) { +export default function DangerZone({ entry }: DangerZoneProps) { const [loading, setLoading] = useState(false) const handleDeleteEntry = () => { @@ -24,20 +22,10 @@ export default function DangerZone({ entry, isReadOnly, requiredRolesText }: Dan Danger Zone! - - - {/* TODO - Set disabled to disabled={isReadOnly} when re-enabling delete functionality */} - - {`Delete ${toTitleCase(entry.kind)}`} - - - + {/* TODO - Remove disabled prop when reenabling delete functionality */} + + {`Delete ${toTitleCase(entry.kind)}`} + ) } diff --git a/frontend/src/entry/settings/EntityItem.tsx b/frontend/src/entry/settings/EntityItem.tsx index c698e6620..388b41f12 100644 --- a/frontend/src/entry/settings/EntityItem.tsx +++ b/frontend/src/entry/settings/EntityItem.tsx @@ -1,21 +1,10 @@ import ClearIcon from '@mui/icons-material/Clear' -import GroupsIcon from '@mui/icons-material/Groups' -import PersonIcon from '@mui/icons-material/Person' -import { - Autocomplete, - Chip, - IconButton, - Stack, - TableCell, - TableRow, - TextField, - Tooltip, - Typography, -} from '@mui/material' +import { Autocomplete, Chip, IconButton, Stack, TableCell, TableRow, TextField, Tooltip } from '@mui/material' import _ from 'lodash-es' import { SyntheticEvent, useMemo } from 'react' -import UserDisplay from 'src/common/UserDisplay' -import { CollaboratorEntry, EntityKind, Role } from 'types/types' +import EntityIcon from 'src/entry/EntityIcon' +import EntityNameDisplay from 'src/entry/EntityNameDisplay' +import { CollaboratorEntry, Role } from 'types/types' import { toSentenceCase } from 'utils/stringUtils' type EntityItemProps = { @@ -24,19 +13,9 @@ type EntityItemProps = { onAccessListChange: (value: CollaboratorEntry[]) => void entryKind: string entryRoles: Role[] - isReadOnly: boolean - requiredRolesText: string } -export default function EntityItem({ - entity, - accessList, - onAccessListChange, - entryKind, - entryRoles, - isReadOnly, - requiredRolesText, -}: EntityItemProps) { +export default function EntityItem({ entity, accessList, onAccessListChange, entryKind, entryRoles }: EntityItemProps) { const entryRoleOptions = useMemo(() => entryRoles.map((role) => role.id), [entryRoles]) function onRoleChange(_event: SyntheticEvent, newValues: string[]) { @@ -61,69 +40,41 @@ export default function EntityItem({ - - + + {entryRoles.length > 0 && ( - - getRole(role).name} - onChange={onRoleChange} - renderInput={(params) => } - renderTags={(value, getTagProps) => - value.map((option, index) => ( - - )) - } - /> - + getRole(role).name} + onChange={onRoleChange} + renderInput={(params) => } + renderTags={(value, getTagProps) => + value.map((option, index) => ( + + )) + } + /> )} - - - - - - + + + + ) } - -type EntityIconProps = { - entity: CollaboratorEntry -} - -function EntityIcon({ entity }: EntityIconProps) { - const isUser = useMemo(() => entity.entity.startsWith('user:'), [entity]) - return isUser ? : -} - -type EntityNameDisplayProps = { - entity: CollaboratorEntry -} - -function EntityNameDisplay({ entity }: EntityNameDisplayProps) { - const [entityKind, entityName] = useMemo(() => entity.entity.split(':'), [entity]) - return entityKind === EntityKind.USER || entityKind === EntityKind.GROUP ? ( - - ) : ( - {entityName} - ) -} diff --git a/frontend/src/entry/settings/EntryAccessInput.tsx b/frontend/src/entry/settings/EntryAccessInput.tsx index 292c8c964..1e3c5f412 100644 --- a/frontend/src/entry/settings/EntryAccessInput.tsx +++ b/frontend/src/entry/settings/EntryAccessInput.tsx @@ -1,14 +1,4 @@ -import { - Autocomplete, - Stack, - Table, - TableBody, - TableCell, - TableHead, - TableRow, - TextField, - Tooltip, -} from '@mui/material' +import { Autocomplete, Stack, Table, TableBody, TableCell, TableHead, TableRow, TextField } from '@mui/material' import { useListUsers } from 'actions/user' import { debounce } from 'lodash-es' import { SyntheticEvent, useCallback, useEffect, useMemo, useState } from 'react' @@ -33,14 +23,7 @@ type EntryAccessInputProps = { } ) -export default function EntryAccessInput({ - value, - onUpdate, - entryKind, - entryRoles, - isReadOnly = false, - requiredRolesText = '', -}: EntryAccessInputProps) { +export default function EntryAccessInput({ value, onUpdate, entryKind, entryRoles }: EntryAccessInputProps) { const [open, setOpen] = useState(false) const [accessList, setAccessList] = useState(value) const [userListQuery, setUserListQuery] = useState('') @@ -57,11 +40,9 @@ export default function EntryAccessInput({ onAccessListChange={setAccessList} entryRoles={entryRoles} entryKind={entryKind} - isReadOnly={isReadOnly} - requiredRolesText={requiredRolesText} /> )), - [accessList, entryKind, entryRoles, isReadOnly, requiredRolesText], + [accessList, entryKind, entryRoles], ) useEffect(() => { @@ -106,35 +87,32 @@ export default function EntryAccessInput({ return ( - - setOpen(true)} - onClose={() => setOpen(false)} - size='small' - noOptionsText={noOptionsText} - onInputChange={debounceOnInputChange} - groupBy={(option) => option.kind.toUpperCase()} - getOptionLabel={(option) => option.id} - isOptionEqualToValue={(option, value) => option.id === value.id} - onChange={onUserChange} - options={users} - filterOptions={(options) => - options.filter( - (option) => !accessList.find((collaborator) => collaborator.entity === `${option.kind}:${option.id}`), - ) - } - loading={isUsersLoading && userListQuery.length >= 3} - renderInput={(params) => ( - - )} - /> - + setOpen(true)} + onClose={() => setOpen(false)} + size='small' + noOptionsText={noOptionsText} + onInputChange={debounceOnInputChange} + groupBy={(option) => option.kind.toUpperCase()} + getOptionLabel={(option) => option.id} + isOptionEqualToValue={(option, value) => option.id === value.id} + onChange={onUserChange} + options={users} + filterOptions={(options) => + options.filter( + (option) => !accessList.find((collaborator) => collaborator.entity === `${option.kind}:${option.id}`), + ) + } + loading={isUsersLoading && userListQuery.length >= 3} + renderInput={(params) => ( + + )} + /> diff --git a/frontend/src/entry/settings/EntryAccessTab.tsx b/frontend/src/entry/settings/EntryAccessTab.tsx index ef782eb04..c5c978e19 100644 --- a/frontend/src/entry/settings/EntryAccessTab.tsx +++ b/frontend/src/entry/settings/EntryAccessTab.tsx @@ -1,5 +1,5 @@ import { LoadingButton } from '@mui/lab' -import { Stack, Tooltip, Typography } from '@mui/material' +import { Stack, Typography } from '@mui/material' import { patchModel, useGetModel, useGetModelRoles } from 'actions/model' import { useState } from 'react' import HelpDialog from 'src/common/HelpDialog' @@ -14,11 +14,9 @@ import { toSentenceCase, toTitleCase } from 'utils/stringUtils' type EntryAccessTabProps = { entry: EntryInterface - isReadOnly: boolean - requiredRolesText: string } -export default function EntryAccessTab({ entry, isReadOnly, requiredRolesText }: EntryAccessTabProps) { +export default function EntryAccessTab({ entry }: EntryAccessTabProps) { const [loading, setLoading] = useState(false) const [errorMessage, setErrorMessage] = useState('') const [accessList, setAccessList] = useState(entry.collaborators) @@ -70,24 +68,10 @@ export default function EntryAccessTab({ entry, isReadOnly, requiredRolesText }: onUpdate={(val) => setAccessList(val)} entryKind={entry.kind} entryRoles={entryRoles} - isReadOnly={isReadOnly} - requiredRolesText={requiredRolesText} /> -
- - - - Save - - - -
+ + Save + ) diff --git a/frontend/src/entry/settings/EntryDetails.tsx b/frontend/src/entry/settings/EntryDetails.tsx index c458b80b3..9774418a1 100644 --- a/frontend/src/entry/settings/EntryDetails.tsx +++ b/frontend/src/entry/settings/EntryDetails.tsx @@ -15,11 +15,9 @@ import { toSentenceCase, toTitleCase } from 'utils/stringUtils' type EntryDetailsProps = { entry: EntryInterface - isReadOnly: boolean - requiredRolesText: string } -export default function EntryDetails({ entry, isReadOnly, requiredRolesText }: EntryDetailsProps) { +export default function EntryDetails({ entry }: EntryDetailsProps) { const [team, setTeam] = useState() const [name, setName] = useState(entry.name) const [description, setDescription] = useState(entry.description) @@ -37,13 +35,11 @@ export default function EntryDetails({ entry, isReadOnly, requiredRolesText }: E const isFormValid = useMemo(() => team && name && description, [team, name, description]) const saveButtonTooltip = useMemo(() => { - if (isReadOnly) { - return requiredRolesText - } else if (!isFormValid) { + if (!isFormValid) { return 'Please make sure all required fields are filled out' } return '' - }, [isFormValid, isReadOnly, requiredRolesText]) + }, [isFormValid]) async function onSubmit(event: FormEvent) { event.preventDefault() @@ -111,58 +107,39 @@ export default function EntryDetails({ entry, isReadOnly, requiredRolesText }: E setTeam(value)} /> - setName(value)} - /> + setName(value)} /> - setDescription(value)} - /> + setDescription(value)} /> <> Access control - - setVisibility(e.target.value as UpdateEntryForm['visibility'])} - > - } - label={publicLabel()} - data-test='publicButtonSelector' - /> - } - label={privateLabel()} - data-test='privateButtonSelector' - /> - - + setVisibility(e.target.value as UpdateEntryForm['visibility'])} + > + } + label={publicLabel()} + data-test='publicButtonSelector' + /> + } + label={privateLabel()} + data-test='privateButtonSelector' + /> +
- + Save diff --git a/frontend/src/entry/settings/Settings.tsx b/frontend/src/entry/settings/Settings.tsx index 1504acb53..087a4a8cc 100644 --- a/frontend/src/entry/settings/Settings.tsx +++ b/frontend/src/entry/settings/Settings.tsx @@ -1,7 +1,7 @@ import { Container, Divider, List, Stack } from '@mui/material' import { useGetUiConfig } from 'actions/uiConfig' import { useRouter } from 'next/router' -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useState } from 'react' import Loading from 'src/common/Loading' import SimpleListItemButton from 'src/common/SimpleListItemButton' import ExportSettings from 'src/entry/model/mirroredModels/ExportSettings' @@ -12,7 +12,7 @@ import EntryAccessTab from 'src/entry/settings/EntryAccessTab' import EntryDetails from 'src/entry/settings/EntryDetails' import MessageAlert from 'src/MessageAlert' import { EntryInterface, EntryKind, UiConfig } from 'types/types' -import { getRequiredRolesText, hasRole } from 'utils/roles' +import { hasRole } from 'utils/roles' import { toTitleCase } from 'utils/stringUtils' export const SettingsCategory = { @@ -64,10 +64,15 @@ export default function Settings({ entry, currentUserRoles }: SettingsProps) { const [selectedCategory, setSelectedCategory] = useState(SettingsCategory.DETAILS) - const [isReadOnly, requiredRolesText] = useMemo(() => { + useEffect(() => { const validRoles = ['owner'] - return [!hasRole(currentUserRoles, validRoles), getRequiredRolesText(currentUserRoles, validRoles)] - }, [currentUserRoles]) + if (!hasRole(currentUserRoles, validRoles)) { + const { category: _category, ...filteredQuery } = router.query + router.replace({ + query: { ...filteredQuery, tab: 'overview' }, + }) + } + }, [currentUserRoles, router]) useEffect(() => { if (isSettingsCategory(category, entry, uiConfig)) { @@ -146,24 +151,12 @@ export default function Settings({ entry, currentUserRoles }: SettingsProps) { )} - {selectedCategory === SettingsCategory.DETAILS && ( - - )} - {selectedCategory === SettingsCategory.PERMISSIONS && ( - - )} - {selectedCategory === SettingsCategory.ACCESS_REQUESTS && ( - - )} - {selectedCategory === SettingsCategory.TEMPLATING && ( - - )} - {selectedCategory === SettingsCategory.MIRRORED_MODELS && ( - - )} - {selectedCategory === SettingsCategory.DANGER && ( - - )} + {selectedCategory === SettingsCategory.DETAILS && } + {selectedCategory === SettingsCategory.PERMISSIONS && } + {selectedCategory === SettingsCategory.ACCESS_REQUESTS && } + {selectedCategory === SettingsCategory.TEMPLATING && } + {selectedCategory === SettingsCategory.MIRRORED_MODELS && } + {selectedCategory === SettingsCategory.DANGER && } )