Skip to content

Commit

Permalink
Workspace Tree Fixes & Improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
R1c4rdCo5t4 committed May 19, 2024
1 parent 2481d21 commit 776a8cc
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 42 deletions.
48 changes: 44 additions & 4 deletions code/client/src/ui/components/sidebar/Sidebar.scss
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@

ul {
margin: 0;
padding: 0;
width: 90%;
width: 100%;
}

li {
Expand Down Expand Up @@ -64,8 +63,39 @@
}

.workspace-tree {
li {
ul {
padding: 0;
li {
padding: 1px;
}
}

.popup {
position: fixed;
background: black;
color: white;
border-radius: 15px;

margin-left: 20px;
display: flex;
flex-direction: column;
justify-content: start;
align-items: start;

button {
display: flex;
flex-direction: row;
justify-content: start;
align-items: center;
gap: 10px;
width: 100%;
border-radius: 15px;
}

button:hover {
color: white;
background-color: rgb(255, 255, 255, 0.1);
}
}
}

Expand All @@ -80,6 +110,8 @@
justify-content: space-between;
align-items: center;
transition: 0.1s;
width: 90%;
border-radius: 5px;

div {
display: flex;
Expand Down Expand Up @@ -110,15 +142,23 @@
color: gray;
}

.resource-header > div {
white-space: nowrap;
overflow-x: hidden;
text-overflow: ellipsis;
max-width: max-content;
}

.resource-children {
padding-left: 10px;
}

a {
.resource-name {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 2px;
}

button {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { ResourceType } from '@notespace/shared/src/workspace/types/resource';
import { BsFileEarmarkPlusFill } from 'react-icons/bs';
import { MdCreateNewFolder } from 'react-icons/md';

type CreateResourcePopupProps = {
position: { top: number; left: number };
onCreate: (type: ResourceType) => void;
};

function CreateResourcePopup({ position, onCreate }: CreateResourcePopupProps) {
return (
<div
className="popup"
style={{
top: position.top,
left: position.left,
}}
>
<button onClick={() => onCreate(ResourceType.FOLDER)}>
<MdCreateNewFolder />
Folder
</button>
<button onClick={() => onCreate(ResourceType.DOCUMENT)}>
<BsFileEarmarkPlusFill />
Document
</button>
</div>
);
}

export default CreateResourcePopup;
65 changes: 46 additions & 19 deletions code/client/src/ui/components/sidebar/components/ResourceView.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,75 @@
import { Link } from 'react-router-dom';
import { ResourceType } from '@notespace/shared/src/workspace/types/resource';
import { IoDocumentText } from 'react-icons/io5';
import { FaFolder } from 'react-icons/fa6';
import { FaFile, FaFolder } from 'react-icons/fa6';
import { TreeNode, WorkspaceTreeNode } from '@domain/workspaces/tree/types';
import { useState } from 'react';
import { RiArrowDownSFill, RiArrowRightSFill } from 'react-icons/ri';
import { FaPlusSquare } from 'react-icons/fa';
import { DragEvent, MouseEvent } from 'react';

type ResourceViewProps = {
workspace: string;
resource: WorkspaceTreeNode;
onCreateResource: (parent?: string) => void;
onCreateNew?: (parent: string, position: { top: number; left: number }) => void;
onDrag?: (e: DragEvent<HTMLDivElement>) => void;
onDrop?: (e: DragEvent<HTMLDivElement>) => void;
children?: TreeNode[];
};

type ResourceComponentProps = ResourceViewProps;

const ResourceComponents = {
[ResourceType.DOCUMENT]: ({ workspace, resource }: ResourceViewProps) => (
<Link to={`/workspaces/${workspace}/${resource.id}`}>
<IoDocumentText />
{resource.name || 'Untitled'}
</Link>
),
[ResourceType.FOLDER]: ({ resource }: ResourceViewProps) => (
<div>
<FaFolder />
{resource.name}
</div>
),
[ResourceType.DOCUMENT]: (props: ResourceComponentProps) => {
const { resource, workspace, ...rest } = props;
return (
<div {...rest}>
<Link to={`/workspaces/${workspace}/${resource.id}`} className="resource-name">
<FaFile />
{resource.name || 'Untitled'}
</Link>
</div>
);
},
[ResourceType.FOLDER]: (props: ResourceComponentProps) => {
const { resource, ...rest } = props;
return (
<div {...rest} className="resource-name">
<FaFolder />
{resource.name}
</div>
);
},
};

function ResourceView({ resource, workspace, children, onCreateResource }: ResourceViewProps) {
function ResourceView({ resource, workspace, children, onCreateNew, onDrag, onDrop }: ResourceViewProps) {
const [isOpen, setIsOpen] = useState(true);
const ResourceComponent = ResourceComponents[resource.type];

const handleToggle = () => {
setIsOpen(!isOpen);
};

const handleCreateNew = (e: MouseEvent<HTMLButtonElement, MouseEvent>) => {
const { top, left } = e.currentTarget.getBoundingClientRect();
onCreateNew!(resource.id, { top, left });
};

return (
<div className="resource">
<div className="resource-header">
<div>
<button onClick={handleToggle}>{isOpen ? <RiArrowDownSFill /> : <RiArrowRightSFill />}</button>
<ResourceComponent workspace={workspace} resource={resource} onCreateResource={onCreateResource} />
<ResourceComponent
id={resource.id}
workspace={workspace}
resource={resource}
draggable={true}
onDragOver={(e: DragEvent) => e.preventDefault()}
onDragStart={onDrag}
onDrop={onDrop}
/>
</div>
<button onClick={() => onCreateResource(resource.id)}>
<button onClick={handleCreateNew}>
<FaPlusSquare />
</button>
</div>
Expand All @@ -56,7 +81,9 @@ function ResourceView({ resource, workspace, children, onCreateResource }: Resou
workspace={workspace}
resource={child.node}
children={child.children}
onCreateResource={onCreateResource}
onCreateNew={onCreateNew}
onDrag={onDrag}
onDrop={onDrop}
/>
))}
</div>
Expand Down
74 changes: 59 additions & 15 deletions code/client/src/ui/components/sidebar/components/WorkspaceTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { getTree } from '@domain/workspaces/tree/utils';
import { WorkspaceTreeNodes } from '@domain/workspaces/tree/types';
import { ResourceOperationsType } from '@ui/contexts/workspace/WorkspaceContext';
import { ResourceType } from '@notespace/shared/src/workspace/types/resource';
import { DragEvent, useEffect, useState } from 'react';
import CreateResourcePopup from '@ui/components/sidebar/components/CreateResourcePopup';

type WorkspaceTreeProps = {
workspace: WorkspaceMetaData;
Expand All @@ -12,25 +14,67 @@ type WorkspaceTreeProps = {
};

function WorkspaceTree({ workspace, nodes, operations }: WorkspaceTreeProps) {
if (!nodes) return null;
const [dragId, setDragId] = useState<string | null>(null);
const [parent, setParent] = useState<string | null>(null);
const [popupOpen, setPopupOpen] = useState(false);
const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 });

async function onCreateResource(parent?: string) {
await operations.createResource('Untitled', ResourceType.DOCUMENT, parent);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const popupElement = document.querySelector('.popup');
if (popupElement && !popupElement.contains(event.target as Node)) {
setPopupOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);

async function onCreateResource(parent: string, type: ResourceType) {
setPopupOpen(false);
setParent(null);
await operations.createResource('Untitled', type, parent);
}

function onCreateNew(parent: string, position: { top: number; left: number }) {
setParent(parent || null);
setPopupOpen(true);
setPopupPosition(position);
}

async function onDrag(e: DragEvent<HTMLDivElement>) {
setDragId(e.currentTarget.id);
}

async function onDrop(e: DragEvent<HTMLDivElement>) {
if (!dragId) return;
const parentId = e.currentTarget.id || 'root';
await operations.moveResource(dragId, parentId);
}

return (
<ul className="workspace-tree">
{getTree(nodes).children.map(node => (
<li key={node.node.id}>
<ResourceView
workspace={workspace.id}
resource={node.node}
children={node.children}
onCreateResource={onCreateResource}
/>
</li>
))}
</ul>
<div className="workspace-tree">
<ul>
{nodes &&
getTree(nodes).children.map(node => (
<li key={node.node.id}>
<ResourceView
workspace={workspace.id}
resource={node.node}
children={node.children}
onCreateNew={onCreateNew}
onDrag={onDrag}
onDrop={onDrop}
/>
</li>
))}
</ul>
{popupOpen && (
<CreateResourcePopup position={popupPosition} onCreate={type => onCreateResource(parent || 'root', type)} />
)}
</div>
);
}
export default WorkspaceTree;
1 change: 1 addition & 0 deletions code/client/src/ui/contexts/workspace/WorkspaceContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type ResourceOperationsType = {
createResource: (name: string, type: ResourceType, parent?: string) => Promise<void>;
deleteResource: (id: string) => Promise<void>;
updateResource: (id: string, newProps: Partial<WorkspaceResource>) => Promise<void>;
moveResource: (id: string, parent: string) => Promise<void>;
};

export type WorkspaceContextType = {
Expand Down
6 changes: 6 additions & 0 deletions code/client/src/ui/contexts/workspace/useResources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,17 @@ function useResources() {
if (!resource.id) throw new Error('Resource id is required');
setResources(resources.map(res => (res.id === resource.id ? { ...res, ...resource } : res)));
if (resource.name) tree.updateNode(resource.id, resource.name);
if (resource.parent) tree.moveNode(resource.id, resource.parent);
}

async function updateResource(id: string, newProps: Partial<WorkspaceResource>) {
await service.updateResource(id, newProps);
}

async function moveResource(id: string, parent: string) {
await service.updateResource(id, { parent });
}

useSocketListeners(socket, {
createdResource: onCreateResource,
deletedResource: onDeleteResource,
Expand All @@ -63,6 +68,7 @@ function useResources() {
createResource,
deleteResource,
updateResource,
moveResource,
},
};
}
Expand Down
2 changes: 1 addition & 1 deletion code/client/src/ui/pages/workspace/Workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ function Workspace() {

return (
<div className="workspace">
<h2>Workspace {workspace?.name}</h2>
<h2>{workspace?.name}</h2>
<WorkspaceHeader
onCreateNew={async () => operations?.createResource('Untitled', ResourceType.DOCUMENT).catch(publishError)}
></WorkspaceHeader>
Expand Down
6 changes: 3 additions & 3 deletions code/client/src/ui/pages/workspace/components/FileView.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { IoDocumentText } from 'react-icons/io5';
import { Link, useParams } from 'react-router-dom';
import { FaFile } from 'react-icons/fa6';
import FileContextMenu from '@ui/pages/workspace/components/FileContextMenu';
import { DocumentResourceMetadata } from '@notespace/shared/src/workspace/types/resource';
import useEditing from '@ui/hooks/useEditing';
import { DocumentResourceMetadata } from '@notespace/shared/src/workspace/types/resource';

type DocumentPreviewProps = {
document: DocumentResourceMetadata;
Expand All @@ -17,7 +17,7 @@ function FileView({ document, onDelete, onRename, onDuplicate }: DocumentPreview
const DocumentComponent = (
<li>
<div>
<IoDocumentText />
<FaFile />
{component}
</div>
</li>
Expand Down
6 changes: 6 additions & 0 deletions code/server/src/ts/databases/memory/Memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ export function createResource(wid: string, name: string, type: ResourceType, pa
export function updateResource(id: string, newProps: Partial<WorkspaceResource>) {
const resource = getResource(id);
Object.assign(resource, newProps);
if (newProps.parent) {
const prevParent = getResource(resource.parent);
prevParent.children = prevParent.children.filter(childId => childId !== id);
const newParent = getResource(newProps.parent);
newParent.children.push(id);
}
}

export function deleteResource(id: string) {
Expand Down

0 comments on commit 776a8cc

Please sign in to comment.