diff --git a/src/frontend/apps/impress/src/components/DropdownMenu.tsx b/src/frontend/apps/impress/src/components/DropdownMenu.tsx index 5fc3d64be..5513ccb78 100644 --- a/src/frontend/apps/impress/src/components/DropdownMenu.tsx +++ b/src/frontend/apps/impress/src/components/DropdownMenu.tsx @@ -99,6 +99,9 @@ export const DropdownMenu = ({ $size="xs" $weight="bold" $padding={{ vertical: 'xs', horizontal: 'base' }} + $css={css` + white-space: pre-line; + `} > {topMessage} diff --git a/src/frontend/apps/impress/src/components/quick-search/QuickSearchStyle.tsx b/src/frontend/apps/impress/src/components/quick-search/QuickSearchStyle.tsx index b6fa0ad6a..99bbc57fa 100644 --- a/src/frontend/apps/impress/src/components/quick-search/QuickSearchStyle.tsx +++ b/src/frontend/apps/impress/src/components/quick-search/QuickSearchStyle.tsx @@ -65,9 +65,7 @@ export const QuickSearchStyle = createGlobalStyle` [cmdk-list] { - padding: 0 var(--c--theme--spacings--base) var(--c--theme--spacings--base) - var(--c--theme--spacings--base); - + flex:1; overflow-y: auto; overscroll-behavior: contain; diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx index cae723092..bd152eb7c 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocHeader.tsx @@ -8,6 +8,7 @@ import { LinkReach, Role, currentDocRole, + getDocLinkReach, useIsCollaborativeEditable, useTrans, } from '@/docs/doc-management'; @@ -28,8 +29,8 @@ export const DocHeader = ({ doc }: DocHeaderProps) => { const { t } = useTranslation(); const { transRole } = useTrans(); const { isEditable } = useIsCollaborativeEditable(doc); - const docIsPublic = doc.link_reach === LinkReach.PUBLIC; - const docIsAuth = doc.link_reach === LinkReach.AUTHENTICATED; + const docIsPublic = getDocLinkReach(doc) === LinkReach.PUBLIC; + const docIsAuth = getDocLinkReach(doc) === LinkReach.AUTHENTICATED; return ( <> diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx index 3cdadfabb..3ff83d179 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx @@ -1,7 +1,8 @@ +import { useTreeContext } from '@gouvfr-lasuite/ui-kit'; import { Button, useModal } from '@openfun/cunningham-react'; import { useQueryClient } from '@tanstack/react-query'; import dynamic from 'next/dynamic'; -import { useEffect } from 'react'; +import { useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; @@ -23,7 +24,20 @@ const DocToolBoxLicence = dynamic(() => export const DocToolBox = ({ doc }: DocToolBoxProps) => { const { t } = useTranslation(); - const hasAccesses = doc.nb_accesses_direct > 1 && doc.abilities.accesses_view; + const treeContext = useTreeContext(); + + /** + * Following the change where there is no default owner when adding a sub-page, + * we need to handle both the case where the doc is the root and the case of sub-pages. + */ + const hasAccesses = useMemo(() => { + if (treeContext?.root?.id === doc.id) { + return doc.nb_accesses_direct > 1 && doc.abilities.accesses_view; + } + + return doc.nb_accesses_direct >= 1 && doc.abilities.accesses_view; + }, [doc, treeContext?.root]); + const queryClient = useQueryClient(); const { spacingsTokens } = useCunninghamTheme(); diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocInheritedShareContent.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocInheritedShareContent.tsx new file mode 100644 index 000000000..5d8aef9ab --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocInheritedShareContent.tsx @@ -0,0 +1,206 @@ +import { Button, Modal, ModalSize, useModal } from '@openfun/cunningham-react'; +import { Fragment, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { createGlobalStyle } from 'styled-components'; + +import { Box, StyledLink, Text } from '@/components'; +import { useCunninghamTheme } from '@/cunningham'; + +import { + Access, + RoleImportance, + useDoc, + useDocStore, +} from '../../doc-management'; +import SimpleFileIcon from '../../docs-grid/assets/simple-document.svg'; + +import { DocShareMemberItem } from './DocShareMemberItem'; +const ShareModalStyle = createGlobalStyle` + .c__modal__title { + padding-bottom: 0 !important; + } + .c__modal__scroller { + padding: 15px 15px !important; + } +`; + +type Props = { + rawAccesses: Access[]; +}; + +const getMaxRoleBetweenAccesses = (access1: Access, access2: Access) => { + const role1 = access1.max_role; + const role2 = access2.max_role; + + const roleImportance1 = RoleImportance[role1]; + const roleImportance2 = RoleImportance[role2]; + + return roleImportance1 > roleImportance2 ? role1 : role2; +}; + +export const DocInheritedShareContent = ({ rawAccesses }: Props) => { + const { t } = useTranslation(); + const { spacingsTokens } = useCunninghamTheme(); + const { currentDoc } = useDocStore(); + + const inheritedData = useMemo(() => { + if (!currentDoc || rawAccesses.length === 0) { + return null; + } + + let parentId = null; + let parentPathLength = 0; + const members: Access[] = []; + + // Find the parent document with the longest path that is different from currentDoc + for (const access of rawAccesses) { + const docPath = access.document.path; + + // Skip if it's the current document + if (access.document.id === currentDoc.id) { + continue; + } + + const findIndex = members.findIndex( + (member) => member.user.id === access.user.id, + ); + if (findIndex === -1) { + members.push(access); + } else { + const accessToUpdate = members[findIndex]; + const currentRole = accessToUpdate.max_role; + const maxRole = getMaxRoleBetweenAccesses(accessToUpdate, access); + + if (maxRole !== currentRole) { + members[findIndex] = access; + } + } + + // Check if this document has a longer path than our current candidate + if (docPath && (!parentId || docPath.length > parentPathLength)) { + parentId = access.document.id; + parentPathLength = docPath.length; + } + } + + return { parentId, members }; + }, [currentDoc, rawAccesses]); + + // Check if accesses map is empty + const hasAccesses = rawAccesses.length > 0; + + if (!hasAccesses) { + return null; + } + + return ( + + + + {t('Inherited share')} + + + {inheritedData && ( + + )} + + + ); +}; + +type DocInheritedShareContentItemProps = { + accesses: Access[]; + document_id: string; +}; +export const DocInheritedShareContentItem = ({ + accesses, + document_id, +}: DocInheritedShareContentItemProps) => { + const { t } = useTranslation(); + const { spacingsTokens } = useCunninghamTheme(); + const { data: doc, error, isLoading } = useDoc({ id: document_id }); + const errorCode = error?.status; + + const accessModal = useModal(); + if ((!doc && !isLoading && !error) || (error && errorCode !== 403)) { + return null; + } + + return ( + <> + + + + + {isLoading ? ( + + + + + ) : ( + <> + + + {error && errorCode === 403 + ? t('You do not have permission to view this document') + : (doc?.title ?? t('Untitled document'))} + + + + {t('Members of this page have access')} + + + )} + + + {!isLoading && ( + + )} + + {accessModal.isOpen && ( + + + {t('Access inherited from the parent page')} + + + } + size={ModalSize.MEDIUM} + > + + + {accesses.map((access) => ( + + + + ))} + + + )} + + ); +}; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocRoleDropdown.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocRoleDropdown.tsx index 261eb5f6f..b62db0147 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocRoleDropdown.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocRoleDropdown.tsx @@ -1,3 +1,5 @@ +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; import { DropdownMenu, DropdownMenuOption, Text } from '@/components'; @@ -18,8 +20,38 @@ export const DocRoleDropdown = ({ onSelectRole, rolesAllowed, }: DocRoleDropdownProps) => { + const { t } = useTranslation(); const { transRole, translatedRoles } = useTrans(); + /** + * When there is a higher role, the rolesAllowed are truncated + * We display a message to indicate that there is a higher role + */ + const topMessage = useMemo(() => { + if (!canUpdate || !rolesAllowed || rolesAllowed.length === 0) { + return message; + } + + const allRoles = Object.keys(translatedRoles); + + if (rolesAllowed.length < allRoles.length) { + let result = message ? `${message}\n\n` : ''; + result += t('This user has access inherited from a parent page.'); + return result; + } + + return message; + }, [canUpdate, rolesAllowed, translatedRoles, message, t]); + + const roles: DropdownMenuOption[] = Object.keys(translatedRoles).map( + (key) => { + return { + label: transRole(key as Role), + callback: () => onSelectRole?.(key as Role), + isSelected: currentRole === (key as Role), + }; + }, + ); if (!canUpdate) { return ( @@ -27,21 +59,9 @@ export const DocRoleDropdown = ({ ); } - - const roles: DropdownMenuOption[] = Object.keys(translatedRoles).map( - (key) => { - return { - label: transRole(key as Role), - callback: () => onSelectRole?.(key as Role), - disabled: rolesAllowed && !rolesAllowed.includes(key as Role), - isSelected: currentRole === (key as Role), - }; - }, - ); - return ( { diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx index 6fa843d9e..1307e7436 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx @@ -8,32 +8,31 @@ import { DropdownMenu, DropdownMenuOption, IconOptions, - LoadMoreText, } from '@/components'; import { QuickSearchData, QuickSearchGroup } from '@/components/quick-search'; import { useCunninghamTheme } from '@/cunningham'; import { Access, Doc, KEY_SUB_PAGE, Role } from '@/docs/doc-management/'; import { useResponsiveStore } from '@/stores'; -import { - useDeleteDocAccess, - useDocAccessesInfinite, - useUpdateDocAccess, -} from '../api'; +import { useDeleteDocAccess, useDocAccesses, useUpdateDocAccess } from '../api'; import { useWhoAmI } from '../hooks'; import { DocRoleDropdown } from './DocRoleDropdown'; import { SearchUserRow } from './SearchUserRow'; type Props = { - doc: Doc; + doc?: Doc; access: Access; + isInherited?: boolean; }; - -const DocShareMemberItem = ({ doc, access }: Props) => { +export const DocShareMemberItem = ({ + doc, + access, + isInherited = false, +}: Props) => { const { t } = useTranslation(); const queryClient = useQueryClient(); - const { isLastOwner, isOtherOwner } = useWhoAmI(access); + const { isLastOwner } = useWhoAmI(access); const { toast } = useToastProvider(); const { isDesktop } = useResponsiveStore(); @@ -47,6 +46,9 @@ const DocShareMemberItem = ({ doc, access }: Props) => { const { mutate: updateDocAccess } = useUpdateDocAccess({ onSuccess: () => { + if (!doc) { + return; + } void queryClient.invalidateQueries({ queryKey: [KEY_SUB_PAGE, { id: doc.id }], }); @@ -60,6 +62,9 @@ const DocShareMemberItem = ({ doc, access }: Props) => { const { mutate: removeDocAccess } = useDeleteDocAccess({ onSuccess: () => { + if (!doc) { + return; + } void queryClient.invalidateQueries({ queryKey: [KEY_SUB_PAGE, { id: doc.id }], }); @@ -72,6 +77,9 @@ const DocShareMemberItem = ({ doc, access }: Props) => { }); const onUpdate = (newRole: Role) => { + if (!doc) { + return; + } updateDocAccess({ docId: doc.id, role: newRole, @@ -80,6 +88,9 @@ const DocShareMemberItem = ({ doc, access }: Props) => { }; const onRemove = () => { + if (!doc) { + return; + } removeDocAccess({ accessId: access.id, docId: doc.id }); }; @@ -92,6 +103,10 @@ const DocShareMemberItem = ({ doc, access }: Props) => { }, ]; + const canUpdate = isInherited + ? false + : (doc?.abilities.accesses_manage ?? false); + return ( { right={ - {isDesktop && doc.abilities.accesses_manage && ( + {isDesktop && canUpdate && ( { const { t } = useTranslation(); - const membersQuery = useDocAccessesInfinite({ + const membersQuery = useDocAccesses({ docId: doc.id, }); const membersData: QuickSearchData = useMemo(() => { - const members = - membersQuery.data?.pages.flatMap((page) => page.results) || []; + const members = membersQuery.data || []; - const count = membersQuery.data?.pages[0]?.count ?? 1; + const count = members.length; return { groupName: @@ -153,14 +167,7 @@ export const QuickSearchGroupMember = ({ count: count, }), elements: members, - endActions: membersQuery.hasNextPage - ? [ - { - content: , - onSelect: () => void membersQuery.fetchNextPage(), - }, - ] - : undefined, + endActions: undefined, }; }, [membersQuery, t]); diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx index 19d0fcc6e..29267b582 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx @@ -11,22 +11,26 @@ import { QuickSearchGroup, } from '@/components/quick-search/'; import { User } from '@/features/auth'; -import { Doc } from '@/features/docs'; +import { Access, Doc } from '@/features/docs'; import { useResponsiveStore } from '@/stores'; import { isValidEmail } from '@/utils'; -import { KEY_LIST_USER, useUsers } from '../api'; - import { - ButtonAccessRequest, - QuickSearchGroupAccessRequest, -} from './DocShareAccessRequest'; + KEY_LIST_USER, + useDocAccesses, + useDocInvitationsInfinite, + useUsers, +} from '../api'; +import { Invitation } from '../types'; + +import { DocInheritedShareContent } from './DocInheritedShareContent'; +import { ButtonAccessRequest } from './DocShareAccessRequest'; import { DocShareAddMemberList } from './DocShareAddMemberList'; import { + DocShareInvitationItem, DocShareModalInviteUserRow, - QuickSearchGroupInvitation, } from './DocShareInvitation'; -import { QuickSearchGroupMember } from './DocShareMember'; +import { DocShareMemberItem } from './DocShareMember'; import { DocShareModalFooter } from './DocShareModalFooter'; const ShareModalStyle = createGlobalStyle` @@ -69,6 +73,10 @@ export const DocShareModal = ({ doc, onClose }: Props) => { setInputValue(''); }; + const { data: membersQuery } = useDocAccesses({ + docId: doc.id, + }); + const searchUsersQuery = useUsers( { query: userQuery, docId: doc.id }, { @@ -77,6 +85,23 @@ export const DocShareModal = ({ doc, onClose }: Props) => { }, ); + const membersData: QuickSearchData = useMemo(() => { + const members: Access[] = + membersQuery?.filter((access) => access.document.id === doc.id) ?? []; + + const count = doc.nb_accesses_direct > 1 ? doc.nb_accesses_direct : 1; + + return { + groupName: + count === 1 + ? t('Document owner') + : t('Share with {{count}} users', { + count: count, + }), + elements: members, + }; + }, [membersQuery, doc.id, doc.nb_accesses_direct, t]); + const onFilter = useDebouncedCallback((str: string) => { setUserQuery(str); }, 300); @@ -103,6 +128,18 @@ export const DocShareModal = ({ doc, onClose }: Props) => { setListHeight(height); }; + const inheritedAccesses = useMemo(() => { + return ( + membersQuery?.filter((access) => access.document.id !== doc.id) ?? [] + ); + }, [membersQuery, doc.id]); + + // const rootDoc = treeContext?.root; + const isRootDoc = false; + + const showInheritedShareContent = + inheritedAccesses.length > 0 && showMemberSection && !isRootDoc; + return ( <> { loading={searchUsersQuery.isLoading} placeholder={t('Type a name or email')} > + {inheritedAccesses.length > 0 && + showInheritedShareContent && ( + access.document.id !== doc.id, + ) ?? [] + } + /> + )} {showMemberSection ? ( - <> - - - - + 0} + membersData={membersData} + /> ) : ( } - /> + + } + /> + + ); +}; + +interface QuickSearchMemberSectionProps { + doc: Doc; + membersData: QuickSearchData; + hasInheritedShareContent?: boolean; +} + +const QuickSearchMemberSection = ({ + doc, + membersData, + hasInheritedShareContent = false, +}: QuickSearchMemberSectionProps) => { + const { t } = useTranslation(); + const { data, hasNextPage, fetchNextPage } = useDocInvitationsInfinite({ + docId: doc.id, + }); + + const invitationsData: QuickSearchData = useMemo(() => { + const invitations = data?.pages.flatMap((page) => page.results) || []; + + return { + groupName: t('Pending invitations'), + elements: invitations, + endActions: hasNextPage + ? [ + { + content: , + onSelect: () => void fetchNextPage(), + }, + ] + : undefined, + }; + }, [data?.pages, fetchNextPage, hasNextPage, t]); + + const showSeparator = + invitationsData.elements.length > 0 && membersData.elements.length > 0; + + if ( + invitationsData.elements.length === 0 && + membersData.elements.length === 0 + ) { + return null; + } + + return ( + <> + {hasInheritedShareContent && } + {invitationsData.elements.length > 0 && ( + + ( + + )} + /> + + )} + + {showSeparator && } + + + ( + + )} + /> + + ); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocVisibility.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocVisibility.tsx index 14fa8cc1f..2e03d0654 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocVisibility.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocVisibility.tsx @@ -1,5 +1,9 @@ -import { VariantType, useToastProvider } from '@openfun/cunningham-react'; -import { useState } from 'react'; +import { + Button, + VariantType, + useToastProvider, +} from '@openfun/cunningham-react'; +import { useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; @@ -17,12 +21,17 @@ import { KEY_LIST_DOC, LinkReach, LinkRole, + docLinkIsDesync, + getDocLinkReach, useUpdateDocLink, } from '@/features/docs'; import { useResponsiveStore } from '@/stores'; import { useTranslatedShareSettings } from '../hooks/'; +import Desync from './../assets/desynchro.svg'; +import Undo from './../assets/undo.svg'; + interface DocVisibilityProps { doc: Doc; } @@ -33,11 +42,19 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => { const { isDesktop } = useResponsiveStore(); const { spacingsTokens, colorsTokens } = useCunninghamTheme(); const canManage = doc.abilities.accesses_manage; - const [linkReach, setLinkReach] = useState(doc.link_reach); - const [docLinkRole, setDocLinkRole] = useState(doc.link_role); + const [linkReach, setLinkReach] = useState(getDocLinkReach(doc)); + const [docLinkRole, setDocLinkRole] = useState( + doc.computed_link_role ?? LinkRole.READER, + ); + const { linkModeTranslations, linkReachChoices, linkReachTranslations } = useTranslatedShareSettings(); + const description = + docLinkRole === LinkRole.READER + ? linkReachChoices[linkReach].descriptionReadOnly + : linkReachChoices[linkReach].descriptionEdit; + const api = useUpdateDocLink({ onSuccess: () => { toast( @@ -51,38 +68,94 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => { listInvalideQueries: [KEY_LIST_DOC, KEY_DOC], }); - const updateReach = (link_reach: LinkReach) => { - api.mutate({ id: doc.id, link_reach }); - setLinkReach(link_reach); - }; + const updateReach = useCallback( + (link_reach: LinkReach, link_role?: LinkRole) => { + const params: { + id: string; + link_reach: LinkReach; + link_role?: LinkRole; + } = { + id: doc.id, + link_reach, + }; - const updateLinkRole = (link_role: LinkRole) => { - api.mutate({ id: doc.id, link_role }); - setDocLinkRole(link_role); - }; - - const linkReachOptions: DropdownMenuOption[] = Object.keys( - linkReachTranslations, - ).map((key) => ({ - label: linkReachTranslations[key as LinkReach], - icon: linkReachChoices[key as LinkReach].icon, - callback: () => updateReach(key as LinkReach), - isSelected: linkReach === (key as LinkReach), - })); - - const linkMode: DropdownMenuOption[] = Object.keys(linkModeTranslations).map( - (key) => ({ - label: linkModeTranslations[key as LinkRole], - callback: () => updateLinkRole(key as LinkRole), - isSelected: docLinkRole === (key as LinkRole), - }), + api.mutate(params); + setLinkReach(link_reach); + if (link_role) { + params.link_role = link_role; + setDocLinkRole(link_role); + } + }, + [api, doc.id], ); - const showLinkRoleOptions = doc.link_reach !== LinkReach.RESTRICTED; - const description = - docLinkRole === LinkRole.READER - ? linkReachChoices[linkReach].descriptionReadOnly - : linkReachChoices[linkReach].descriptionEdit; + const updateLinkRole = useCallback( + (link_role: LinkRole) => { + api.mutate({ id: doc.id, link_role }); + setDocLinkRole(link_role); + }, + [api, doc.id], + ); + + const linkReachOptions: DropdownMenuOption[] = useMemo(() => { + return Object.values(LinkReach).map((key) => { + const isDisabled = + doc.abilities.link_select_options[key as LinkReach] === undefined; + + return { + label: linkReachTranslations[key as LinkReach], + callback: () => updateReach(key as LinkReach), + isSelected: linkReach === (key as LinkReach), + disabled: isDisabled, + }; + }); + }, [doc, linkReach, linkReachTranslations, updateReach]); + + const haveDisabledOptions = linkReachOptions.some( + (option) => option.disabled, + ); + + const showLinkRoleOptions = doc.computed_link_reach !== LinkReach.RESTRICTED; + + const linkRoleOptions: DropdownMenuOption[] = useMemo(() => { + const options = doc.abilities.link_select_options[linkReach] ?? []; + return Object.values(LinkRole).map((key) => { + const isDisabled = !options.includes(key); + return { + label: linkModeTranslations[key], + callback: () => updateLinkRole(key), + isSelected: docLinkRole === key, + disabled: isDisabled, + }; + }); + }, [doc, docLinkRole, linkModeTranslations, updateLinkRole, linkReach]); + + const haveDisabledLinkRoleOptions = linkRoleOptions.some( + (option) => option.disabled, + ); + + const undoDesync = () => { + const params: { + id: string; + link_reach: LinkReach; + link_role?: LinkRole; + } = { + id: doc.id, + link_reach: doc.ancestors_link_reach, + }; + if (doc.ancestors_link_role) { + params.link_role = doc.ancestors_link_role; + } + api.mutate(params); + setLinkReach(doc.ancestors_link_reach); + if (doc.ancestors_link_role) { + setDocLinkRole(doc.ancestors_link_role); + } + }; + + const showDesync = useMemo(() => { + return docLinkIsDesync(doc); + }, [doc]); return ( { {t('Link parameters')} + {showDesync && ( + + + + + {t('Sharing rules differ from the parent page')} + + + {doc.abilities.accesses_manage && ( + + )} + + )} { `} disabled={!canManage} showArrow={true} + topMessage={ + haveDisabledOptions + ? t( + 'You cannot restrict access to a subpage relative to its parent page.', + ) + : undefined + } options={linkReachOptions} > @@ -145,7 +257,14 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => { diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx index 8cb305fc3..a49db979e 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx @@ -39,7 +39,6 @@ export const DocSubPageItem = (props: Props) => { const { spacingsTokens } = useCunninghamTheme(); const [isHover, setIsHover] = useState(false); - const spacing = spacingsTokens(); const router = useRouter(); const { togglePanel } = useLeftPanelStore(); @@ -74,8 +73,9 @@ export const DocSubPageItem = (props: Props) => { .then((allChildren) => { node.open(); - router.push(`/docs/${doc.id}`); + router.push(`/docs/${createdDoc.id}`); treeContext?.treeData.setChildren(node.data.value.id, allChildren); + treeContext?.treeData.setSelectedNode(createdDoc); togglePanel(); }) .catch(console.error); @@ -89,6 +89,7 @@ export const DocSubPageItem = (props: Props) => { treeContext?.treeData.addChild(node.data.value.id, newDoc); node.open(); router.push(`/docs/${createdDoc.id}`); + treeContext?.treeData.setSelectedNode(newDoc); togglePanel(); } }; @@ -115,7 +116,7 @@ export const DocSubPageItem = (props: Props) => { data-testid={`doc-sub-page-item-${props.node.data.value.id}`} $width="100%" $direction="row" - $gap={spacing['xs']} + $gap={spacingsTokens['xs']} role="button" tabIndex={0} $align="center" @@ -139,7 +140,7 @@ export const DocSubPageItem = (props: Props) => { {doc.title || untitledDocument} - {doc.nb_accesses_direct > 1 && ( + {doc.nb_accesses_direct >= 1 && ( { const { spacingsTokens } = useCunninghamTheme(); - const spacing = spacingsTokens(); + const treeContext = useTreeContext(); const { currentDoc } = useDocStore(); const router = useRouter(); @@ -134,11 +134,25 @@ export const DocTree = ({ initialTargetId }: DocTreeProps) => { } return ( - - + + { - onCreateSuccess?.(doc); - togglePanel(); - router.push(`/docs/${doc.id}`); - treeContext?.treeData.setSelectedNode(doc); + onSuccess: (newDoc) => { + onCreateSuccess?.(newDoc); }, }); const afterDelete = () => { if (parentId) { treeContext?.treeData.deleteNode(doc.id); - router.push(`/docs/${parentId}`); + void router.push(`/docs/${parentId}`); } else if (doc.id === treeContext?.root?.id && !parentId) { - router.push(`/docs/`); + void router.push(`/docs/`); } else if (treeContext && treeContext.root) { treeContext?.treeData.deleteNode(doc.id); - router.push(`/docs/${treeContext.root.id}`); + void router.push(`/docs/${treeContext.root.id}`); } }; diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocGridContentList.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocGridContentList.tsx index 6d03cac77..ea756924d 100644 --- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocGridContentList.tsx +++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocGridContentList.tsx @@ -152,10 +152,11 @@ export const DraggableDocGridItem = ({ canDrag, updateCanDrop, }: DocGridItemProps) => { - const canDropItem = doc.user_roles.some( - (role) => - role === Role.ADMIN || role === Role.OWNER || role === Role.EDITOR, - ); + const userRole = doc.user_role; + const canDropItem = + userRole === Role.ADMIN || + userRole === Role.OWNER || + userRole === Role.EDITOR; return (