Skip to content

Commit 9bc7d67

Browse files
Pollepsjoepio
authored andcommitted
Fix sidebar item doesn't open when navigating to it's sub resource
1 parent bfef840 commit 9bc7d67

File tree

7 files changed

+107
-68
lines changed

7 files changed

+107
-68
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ This changelog covers all three packages, as they are (for now) updated as a who
66

77
### @tomic/lib
88

9+
- Add `store.getResourceAncestry` method, which returns the ancestry of a resource, including the resource itself.
10+
- Add `resource.title` property, which returns the name of a resource, or the first property that is can be used to name the resource.
11+
912
#### Breaking changes
1013

1114
- `buildSearchSubject` now takes a serverURL instead of the store.

data-browser/src/components/Details/index.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,16 @@ export function Details({
2424
const [isOpen, setIsOpen] = React.useState(initialState);
2525

2626
useEffect(() => {
27+
console.log('opening details');
2728
setIsOpen(open);
2829
}, [open]);
2930

3031
const toggleOpen = useCallback(() => {
31-
onStateToggle?.(!isOpen);
32-
setIsOpen(p => !p);
32+
setIsOpen(p => {
33+
onStateToggle?.(!p);
34+
35+
return !p;
36+
});
3337
}, []);
3438

3539
return (

data-browser/src/components/Dropdown/index.tsx

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { Button } from '../Button';
66
import { DropdownTriggerRenderFunction } from './DropdownTrigger';
77
import { shortcuts } from '../HotKeyWrapper';
88
import { Shortcut } from '../Shortcut';
9+
import { transition } from '../../helpers/transition';
910

1011
export const DIVIDER = 'divider' as const;
1112

@@ -96,13 +97,17 @@ export function DropdownMenu({
9697
const dropdownRef = useRef<HTMLDivElement>(null);
9798
const triggerRef = useRef<HTMLButtonElement>(null);
9899
const [isActive, setIsActive] = useState(false);
100+
const [visible, setVisible] = useState(false);
99101

100102
const handleClose = useCallback(() => {
101-
setIsActive(false);
103+
setVisible(false);
102104
// Whenever the menu closes, assume that the next one will be opened with mouse
103105
setUseKeys(false);
104106
// Always reset to the top item on close
105-
setSelectedIndex(0);
107+
setTimeout(() => {
108+
setIsActive(false);
109+
setSelectedIndex(0);
110+
}, 100);
106111
}, []);
107112

108113
useClickAwayListener([triggerRef, dropdownRef], handleClose, isActive, [
@@ -126,27 +131,31 @@ export function DropdownMenu({
126131
return;
127132
}
128133

129-
const triggerRect = triggerRef.current!.getBoundingClientRect();
130-
const menuRect = dropdownRef.current!.getBoundingClientRect();
131-
const topPos = triggerRect.y - menuRect.height;
134+
setIsActive(true);
132135

133-
// If the top is outside of the screen, render it below
134-
if (topPos < 0) {
135-
setY(triggerRect.y + triggerRect.height / 2);
136-
} else {
137-
setY(topPos + triggerRect.height / 2);
138-
}
136+
requestAnimationFrame(() => {
137+
const triggerRect = triggerRef.current!.getBoundingClientRect();
138+
const menuRect = dropdownRef.current!.getBoundingClientRect();
139+
const topPos = triggerRect.y - menuRect.height;
139140

140-
const leftPos = triggerRect.x - menuRect.width;
141+
// If the top is outside of the screen, render it below
142+
if (topPos < 0) {
143+
setY(triggerRect.y + triggerRect.height / 2);
144+
} else {
145+
setY(topPos + triggerRect.height / 2);
146+
}
141147

142-
// If the left is outside of the screen, render it to the right
143-
if (leftPos < 0) {
144-
setX(triggerRect.x);
145-
} else {
146-
setX(triggerRect.x - menuRect.width + triggerRect.width);
147-
}
148+
const leftPos = triggerRect.x - menuRect.width;
148149

149-
setIsActive(true);
150+
// If the left is outside of the screen, render it to the right
151+
if (leftPos < 0) {
152+
setX(triggerRect.x);
153+
} else {
154+
setX(triggerRect.x - menuRect.width + triggerRect.width);
155+
}
156+
157+
setVisible(true);
158+
});
150159
}, [isActive]);
151160

152161
const handleTriggerClick = useCallback(() => {
@@ -219,7 +228,7 @@ export function DropdownMenu({
219228
isActive={isActive}
220229
menuId={menuId}
221230
/>
222-
<Menu ref={dropdownRef} isActive={isActive} x={x} y={y} id={menuId}>
231+
<Menu ref={dropdownRef} visible={visible} x={x} y={y} id={menuId}>
223232
{isActive &&
224233
normalizedItems.map((props, i) => {
225234
if (!isItem(props)) {
@@ -252,12 +261,6 @@ export function DropdownMenu({
252261
);
253262
}
254263

255-
interface MenuProps {
256-
isActive: boolean;
257-
x: number;
258-
y: number;
259-
}
260-
261264
export interface MenuItemSidebarProps extends MenuItemMinimial {
262265
handleClickItem?: () => unknown;
263266
}
@@ -343,6 +346,12 @@ const ItemDivider = styled.div`
343346
border-bottom: 1px solid ${p => p.theme.colors.bg2};
344347
`;
345348

349+
interface MenuProps {
350+
visible: boolean;
351+
x: number;
352+
y: number;
353+
}
354+
346355
const Menu = styled.div<MenuProps>`
347356
font-size: ${p => p.theme.fontSizeBody}rem;
348357
overflow: hidden;
@@ -358,6 +367,7 @@ const Menu = styled.div<MenuProps>`
358367
left: ${p => p.x}px;
359368
width: auto;
360369
box-shadow: ${p => p.theme.boxShadowSoft};
361-
opacity: ${p => (p.isActive ? 1 : 0)};
362-
visibility: ${p => (p.isActive ? 'visible' : 'hidden')};
370+
opacity: ${p => (p.visible ? 1 : 0)};
371+
372+
transition: ${() => transition('opacity')};
363373
`;

data-browser/src/components/SideBar/ResourceSideBar/ResourceSideBar.tsx

Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,4 @@
1-
import React, {
2-
useCallback,
3-
useDeferredValue,
4-
useEffect,
5-
useMemo,
6-
useRef,
7-
useState,
8-
} from 'react';
1+
import React, { useEffect, useMemo, useRef, useState } from 'react';
92
import { useString, useResource, useTitle, urls, useArray } from '@tomic/react';
103
import { useCurrentSubject } from '../../../helpers/useCurrentSubject';
114
import { SideBarItem } from '../SideBarItem';
@@ -19,30 +12,25 @@ import { getIconForClass } from '../../../views/FolderPage/iconMap';
1912

2013
interface ResourceSideBarProps {
2114
subject: string;
15+
ancestry: string[];
2216
/** When a SideBar item is clicked, we should close the SideBar (on mobile devices) */
2317
handleClose?: () => unknown;
24-
/**
25-
* Is called when any of the subResources is the CurrentURL. This is used to
26-
* recursively open the sidebar menus when the user opens a resource.
27-
*/
28-
onOpen?: (open: boolean) => void;
2918
}
3019

3120
/** Renders a Resource as a nav item for in the sidebar. */
3221
export function ResourceSideBar({
3322
subject,
23+
ancestry,
3424
handleClose,
35-
onOpen,
3625
}: ResourceSideBarProps): JSX.Element {
3726
const spanRef = useRef<HTMLSpanElement>(null);
3827
const resource = useResource(subject, { allowIncomplete: true });
3928
const [currentUrl] = useCurrentSubject();
40-
const deferredUrl = useDeferredValue(currentUrl);
4129

4230
const [title] = useTitle(resource);
4331
const [description] = useString(resource, urls.properties.description);
4432

45-
const active = deferredUrl === subject;
33+
const active = currentUrl === subject;
4634
const [open, setOpen] = useState(active);
4735

4836
const [subResources] = useArray(resource, urls.properties.subResources);
@@ -51,23 +39,11 @@ export function ResourceSideBar({
5139
const [classType] = useString(resource, urls.properties.isA);
5240
const Icon = getIconForClass(classType!);
5341

54-
const handleDetailsToggle = useCallback((state: boolean) => {
55-
setOpen(state);
56-
}, []);
57-
58-
const setAndPropagateOpen = useCallback(
59-
(state: boolean) => {
60-
setOpen(state);
61-
onOpen?.(state);
62-
},
63-
[onOpen],
64-
);
65-
6642
useEffect(() => {
67-
if (active || open) {
68-
onOpen?.(true);
43+
if (ancestry.includes(subject) && ancestry[0] !== subject) {
44+
setOpen(true);
6945
}
70-
}, [active, open]);
46+
}, [ancestry]);
7147

7248
const TitleComp = useMemo(
7349
() => (
@@ -125,17 +101,13 @@ export function ResourceSideBar({
125101
initialState={open}
126102
open={open}
127103
disabled={!hasSubResources}
128-
onStateToggle={handleDetailsToggle}
104+
onStateToggle={setOpen}
129105
data-test='resource-sidebar'
130106
title={TitleComp}
131107
>
132108
{hasSubResources &&
133109
subResources.map(child => (
134-
<ResourceSideBar
135-
subject={child}
136-
onOpen={setAndPropagateOpen}
137-
key={child}
138-
/>
110+
<ResourceSideBar subject={child} key={child} ancestry={ancestry} />
139111
))}
140112
</Details>
141113
);

data-browser/src/components/SideBar/SideBarDrive.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import {
33
useArray,
44
useCanWrite,
55
useResource,
6+
useStore,
67
useTitle,
78
} from '@tomic/react';
8-
import React from 'react';
9+
import React, { useEffect, useState } from 'react';
910
import { FaPlus } from 'react-icons/fa';
1011
import { useNavigate } from 'react-router-dom';
1112
import styled from 'styled-components';
@@ -21,6 +22,7 @@ import { ErrorLook } from '../ErrorLook';
2122
import { DriveSwitcher } from './DriveSwitcher';
2223
import { IconButton } from '../IconButton/IconButton';
2324
import { Row } from '../Row';
25+
import { useCurrentSubject } from '../../helpers/useCurrentSubject';
2426

2527
interface SideBarDriveProps {
2628
/** Closes the sidebar on small screen devices */
@@ -31,12 +33,22 @@ interface SideBarDriveProps {
3133
export function SideBarDrive({
3234
handleClickItem,
3335
}: SideBarDriveProps): JSX.Element {
36+
const store = useStore();
3437
const { drive, agent } = useSettings();
3538
const driveResource = useResource(drive);
3639
const [subResources] = useArray(driveResource, urls.properties.subResources);
3740
const [title] = useTitle(driveResource);
3841
const navigate = useNavigate();
3942
const [angentCanWrite] = useCanWrite(driveResource);
43+
const [currentSubject] = useCurrentSubject();
44+
const currentResource = useResource(currentSubject);
45+
const [ancestry, setAncestry] = useState<string[]>([]);
46+
47+
useEffect(() => {
48+
store.getResourceAncestry(currentResource).then(result => {
49+
setAncestry(result);
50+
});
51+
}, [store, currentResource]);
4052

4153
return (
4254
<>
@@ -74,6 +86,7 @@ export function SideBarDrive({
7486
<ResourceSideBar
7587
key={child}
7688
subject={child}
89+
ancestry={ancestry}
7790
handleClose={handleClickItem}
7891
/>
7992
);

lib/src/resource.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,15 @@ export class Resource {
6262
this.commitBuilder = new CommitBuilder(subject);
6363
}
6464

65+
public get title() {
66+
return (
67+
this.get(properties.name) ??
68+
this.get(properties.shortname) ??
69+
this.get(properties.file.filename) ??
70+
this.subject
71+
);
72+
}
73+
6574
/** Checks if the agent has write rights by traversing the graph. Recursive function. */
6675
public async canWrite(
6776
store: Store,

lib/src/store.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,34 @@ export class Store {
691691
return this.client.postCommit(commit, endpoint);
692692
}
693693

694+
/**
695+
* Returns the ancestry of a resource, starting with the resource itself.
696+
*/
697+
public async getResourceAncestry(resource: Resource): Promise<string[]> {
698+
const ancestry: string[] = [resource.getSubject()];
699+
700+
let lastAncestor: string = resource.get(urls.properties.parent) as string;
701+
lastAncestor && ancestry.push(lastAncestor);
702+
703+
while (lastAncestor) {
704+
const lastResource = await this.getResourceAsync(lastAncestor);
705+
706+
if (lastResource) {
707+
lastAncestor = lastResource.get(urls.properties.parent) as string;
708+
709+
if (ancestry.includes(lastAncestor)) {
710+
throw new Error(
711+
`Resource ${resource.getSubject()} ancestry is cyclical. ${lastAncestor} is already in the ancestry}`,
712+
);
713+
}
714+
715+
ancestry.push(lastAncestor);
716+
}
717+
}
718+
719+
return ancestry;
720+
}
721+
694722
private randomPart(): string {
695723
return Math.random().toString(36).substring(2);
696724
}

0 commit comments

Comments
 (0)