feat: more changes
This commit is contained in:
parent
eff2044a6b
commit
33f06c1035
@ -4,10 +4,10 @@ import { DragDebugWrapper } from './Styles';
|
|||||||
type DragDebugProps = {
|
type DragDebugProps = {
|
||||||
zone: ImpactZone | null;
|
zone: ImpactZone | null;
|
||||||
depthTarget: number;
|
depthTarget: number;
|
||||||
draggingID: string | null;
|
draggedNodes: Array<string> | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DragDebug: React.FC<DragDebugProps> = ({ zone, depthTarget, draggingID }) => {
|
const DragDebug: React.FC<DragDebugProps> = ({ zone, depthTarget, draggedNodes }) => {
|
||||||
let aboveID = null;
|
let aboveID = null;
|
||||||
let belowID = null;
|
let belowID = null;
|
||||||
if (zone) {
|
if (zone) {
|
||||||
@ -15,7 +15,9 @@ const DragDebug: React.FC<DragDebugProps> = ({ zone, depthTarget, draggingID })
|
|||||||
belowID = zone.below ? zone.below.node.id : null;
|
belowID = zone.below ? zone.below.node.id : null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<DragDebugWrapper>{`aboveID=${aboveID} / belowID=${belowID} / depthTarget=${depthTarget} draggingID=${draggingID}`}</DragDebugWrapper>
|
<DragDebugWrapper>{`aboveID=${aboveID} / belowID=${belowID} / depthTarget=${depthTarget} draggedNodes=${
|
||||||
|
draggedNodes ? draggedNodes.toString() : null
|
||||||
|
}`}</DragDebugWrapper>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -32,7 +32,8 @@ const DragIndicator: React.FC<DragIndicatorProps> = ({ container, zone, depthTar
|
|||||||
}
|
}
|
||||||
let left = 0;
|
let left = 0;
|
||||||
if (container && container.current) {
|
if (container && container.current) {
|
||||||
left = container.current.getBoundingClientRect().left + 25 + depthTarget * 35;
|
left = container.current.getBoundingClientRect().left + (depthTarget - 1) * 35;
|
||||||
|
width = container.current.getBoundingClientRect().width - depthTarget * 35;
|
||||||
}
|
}
|
||||||
return <DragIndicatorBar top={top} left={left} width={width} />;
|
return <DragIndicatorBar top={top} left={left} width={width} />;
|
||||||
};
|
};
|
||||||
|
@ -1,17 +1,42 @@
|
|||||||
import React, { useRef, useCallback, useState, useMemo, useEffect } from 'react';
|
import React, { useRef, useCallback, useState, useMemo, useEffect } from 'react';
|
||||||
import { DotCircle } from 'shared/icons';
|
import { Dot } from 'shared/icons';
|
||||||
import { findNextDraggable, getDimensions, getTargetDepth, getNodeAbove, getBelowParent, findNodeAbove } from './utils';
|
import styled from 'styled-components';
|
||||||
|
import {
|
||||||
|
findNextDraggable,
|
||||||
|
getDimensions,
|
||||||
|
getTargetDepth,
|
||||||
|
getNodeAbove,
|
||||||
|
getBelowParent,
|
||||||
|
findNodeAbove,
|
||||||
|
getNodeOver,
|
||||||
|
getLastChildInBranch,
|
||||||
|
findNodeDepth,
|
||||||
|
} from './utils';
|
||||||
import { useDrag } from './useDrag';
|
import { useDrag } from './useDrag';
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 9px;
|
||||||
|
background: rgba(${p => p.theme.colors.primary});
|
||||||
|
svg {
|
||||||
|
fill: rgba(${p => p.theme.colors.text.primary});
|
||||||
|
stroke: rgba(${p => p.theme.colors.text.primary});
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
type DraggerProps = {
|
type DraggerProps = {
|
||||||
container: React.RefObject<HTMLDivElement>;
|
container: React.RefObject<HTMLDivElement>;
|
||||||
draggingID: string;
|
draggedNodes: { nodes: Array<string>; first?: OutlineNode | null };
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
onDragEnd: (zone: ImpactZone) => void;
|
onDragEnd: (zone: ImpactZone) => void;
|
||||||
initialPos: { x: number; y: number };
|
initialPos: { x: number; y: number };
|
||||||
};
|
};
|
||||||
|
|
||||||
const Dragger: React.FC<DraggerProps> = ({ draggingID, container, onDragEnd, isDragging, initialPos }) => {
|
const Dragger: React.FC<DraggerProps> = ({ draggedNodes, container, onDragEnd, isDragging, initialPos }) => {
|
||||||
const [pos, setPos] = useState<{ x: number; y: number }>(initialPos);
|
const [pos, setPos] = useState<{ x: number; y: number }>(initialPos);
|
||||||
const { outline, impact, setImpact } = useDrag();
|
const { outline, impact, setImpact } = useDrag();
|
||||||
const $handle = useRef<HTMLDivElement>(null);
|
const $handle = useRef<HTMLDivElement>(null);
|
||||||
@ -22,38 +47,9 @@ const Dragger: React.FC<DraggerProps> = ({ draggingID, container, onDragEnd, isD
|
|||||||
e => {
|
e => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const { clientX, clientY, pageX, pageY } = e;
|
const { clientX, clientY, pageX, pageY } = e;
|
||||||
console.log(clientX, clientY);
|
|
||||||
setPos({ x: clientX, y: clientY });
|
setPos({ x: clientX, y: clientY });
|
||||||
let curDepth = 1;
|
const { curDepth, curPosition, curDraggable } = getNodeOver({ x: clientX, y: clientY }, outline.current);
|
||||||
let curDraggables: any;
|
let depthTarget: number = 0;
|
||||||
let prevDraggable: any;
|
|
||||||
let curDraggable: any;
|
|
||||||
let depthTarget = 1;
|
|
||||||
let curPosition: ImpactPosition = 'after';
|
|
||||||
|
|
||||||
// get hovered over node
|
|
||||||
// decide if node is bottom or top
|
|
||||||
// calculate the missing node, if it exists
|
|
||||||
// calculate available depth
|
|
||||||
// calulcate current selected depth
|
|
||||||
|
|
||||||
while (outline.current.nodes.size + 1 > curDepth) {
|
|
||||||
curDraggables = outline.current.nodes.get(curDepth);
|
|
||||||
if (curDraggables) {
|
|
||||||
const nextDraggable = findNextDraggable({ x: clientX, y: clientY }, outline.current, curDepth, draggingID);
|
|
||||||
if (nextDraggable) {
|
|
||||||
prevDraggable = curDraggable;
|
|
||||||
curDraggable = nextDraggable.node;
|
|
||||||
curPosition = nextDraggable.position;
|
|
||||||
if (nextDraggable.found) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
curDepth += 1;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let aboveNode: null | OutlineNode = null;
|
let aboveNode: null | OutlineNode = null;
|
||||||
let belowNode: null | OutlineNode = null;
|
let belowNode: null | OutlineNode = null;
|
||||||
|
|
||||||
@ -66,36 +62,38 @@ const Dragger: React.FC<DraggerProps> = ({ draggingID, container, onDragEnd, isD
|
|||||||
// if belowNode has the depth of 1, then the above element will be a part of a different branch
|
// if belowNode has the depth of 1, then the above element will be a part of a different branch
|
||||||
|
|
||||||
const { relationships, nodes } = outline.current;
|
const { relationships, nodes } = outline.current;
|
||||||
if (belowNode) {
|
if (!belowNode || !aboveNode) {
|
||||||
aboveNode = findNodeAbove(outline.current, curDepth, belowNode);
|
if (belowNode) {
|
||||||
} else if (aboveNode) {
|
aboveNode = findNodeAbove(outline.current, curDepth, belowNode);
|
||||||
let targetBelowNode: RelationshipChild | null = null;
|
} else if (aboveNode) {
|
||||||
const parent = relationships.get(aboveNode.parent);
|
let targetBelowNode: RelationshipChild | null = null;
|
||||||
if (aboveNode.children !== 0) {
|
const parent = relationships.get(aboveNode.parent);
|
||||||
const abr = relationships.get(aboveNode.id);
|
if (aboveNode.children !== 0 && !aboveNode.collapsed) {
|
||||||
if (abr) {
|
const abr = relationships.get(aboveNode.id);
|
||||||
const newTarget = abr.children[0];
|
if (abr) {
|
||||||
if (newTarget) {
|
const newTarget = abr.children[0];
|
||||||
targetBelowNode = newTarget;
|
if (newTarget) {
|
||||||
|
targetBelowNode = newTarget;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (parent) {
|
||||||
|
const aboveNodeIndex = parent.children.findIndex(c => aboveNode && c.id === aboveNode.id);
|
||||||
|
if (aboveNodeIndex !== -1) {
|
||||||
|
if (aboveNodeIndex === parent.children.length - 1) {
|
||||||
|
targetBelowNode = getBelowParent(aboveNode, outline.current);
|
||||||
|
} else {
|
||||||
|
const nextChild = parent.children[aboveNodeIndex + 1];
|
||||||
|
targetBelowNode = nextChild ?? null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (parent) {
|
if (targetBelowNode) {
|
||||||
const aboveNodeIndex = parent.children.findIndex(c => aboveNode && c.id === aboveNode.id);
|
const depthNodes = nodes.get(targetBelowNode.depth);
|
||||||
if (aboveNodeIndex !== -1) {
|
if (depthNodes) {
|
||||||
if (aboveNodeIndex === parent.children.length - 1) {
|
belowNode = depthNodes.get(targetBelowNode.id) ?? null;
|
||||||
targetBelowNode = getBelowParent(aboveNode, outline.current);
|
|
||||||
} else {
|
|
||||||
const nextChild = parent.children[aboveNodeIndex + 1];
|
|
||||||
targetBelowNode = nextChild ?? null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (targetBelowNode) {
|
|
||||||
const depthNodes = nodes.get(targetBelowNode.depth);
|
|
||||||
if (depthNodes) {
|
|
||||||
belowNode = depthNodes.get(targetBelowNode.id) ?? null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// if outside outline, get either first or last item in list based on mouse Y
|
// if outside outline, get either first or last item in list based on mouse Y
|
||||||
@ -111,19 +109,47 @@ const Dragger: React.FC<DraggerProps> = ({ draggingID, container, onDragEnd, isD
|
|||||||
aboveNode = null;
|
aboveNode = null;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// TODO: enhance to actually get last child item, not last top level branch
|
||||||
const rootChildren = outline.current.relationships.get('root');
|
const rootChildren = outline.current.relationships.get('root');
|
||||||
const rootDepth = outline.current.nodes.get(1);
|
const rootDepth = outline.current.nodes.get(1);
|
||||||
if (rootChildren && rootDepth) {
|
if (rootChildren && rootDepth) {
|
||||||
const lastChild = rootChildren.children[rootChildren.children.length - 1];
|
const lastChild = rootChildren.children[rootChildren.children.length - 1];
|
||||||
aboveNode = rootDepth.get(lastChild.id) ?? null;
|
const lastParentNode = rootDepth.get(lastChild.id) ?? null;
|
||||||
|
|
||||||
|
if (lastParentNode) {
|
||||||
|
const lastBranchChild = getLastChildInBranch(outline.current, lastParentNode);
|
||||||
|
if (lastBranchChild) {
|
||||||
|
const lastChildDepth = outline.current.nodes.get(lastBranchChild.depth);
|
||||||
|
if (lastChildDepth) {
|
||||||
|
aboveNode = lastChildDepth.get(lastBranchChild.id) ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (aboveNode && aboveNode.id === draggingID) {
|
if (aboveNode) {
|
||||||
belowNode = aboveNode;
|
const { ancestors } = findNodeDepth(outline.current.published, aboveNode.id);
|
||||||
aboveNode = findNodeAbove(outline.current, aboveNode.depth, aboveNode);
|
for (let i = 0; i < draggedNodes.nodes.length; i++) {
|
||||||
|
const nodeID = draggedNodes.nodes[i];
|
||||||
|
if (ancestors.find(c => c === nodeID)) {
|
||||||
|
if (draggedNodes.first) {
|
||||||
|
belowNode = draggedNodes.first;
|
||||||
|
aboveNode = findNodeAbove(outline.current, aboveNode ? aboveNode.depth : 1, draggedNodes.first);
|
||||||
|
} else {
|
||||||
|
const { depth } = findNodeDepth(outline.current.published, nodeID);
|
||||||
|
const nodeDepth = outline.current.nodes.get(depth);
|
||||||
|
const targetNode = nodeDepth ? nodeDepth.get(nodeID) : null;
|
||||||
|
if (targetNode) {
|
||||||
|
belowNode = targetNode;
|
||||||
|
|
||||||
|
aboveNode = findNodeAbove(outline.current, depth, targetNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// calculate available depths
|
// calculate available depths
|
||||||
@ -132,7 +158,7 @@ const Dragger: React.FC<DraggerProps> = ({ draggingID, container, onDragEnd, isD
|
|||||||
let maxDepth = 2;
|
let maxDepth = 2;
|
||||||
if (aboveNode) {
|
if (aboveNode) {
|
||||||
const aboveParent = relationships.get(aboveNode.parent);
|
const aboveParent = relationships.get(aboveNode.parent);
|
||||||
if (aboveNode.children !== 0) {
|
if (aboveNode.children !== 0 && !aboveNode.collapsed) {
|
||||||
minDepth = aboveNode.depth + 1;
|
minDepth = aboveNode.depth + 1;
|
||||||
maxDepth = aboveNode.depth + 1;
|
maxDepth = aboveNode.depth + 1;
|
||||||
} else if (aboveParent) {
|
} else if (aboveParent) {
|
||||||
@ -205,9 +231,9 @@ const Dragger: React.FC<DraggerProps> = ({ draggingID, container, onDragEnd, isD
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{pos && (
|
{pos && (
|
||||||
<div ref={$handle} style={styles}>
|
<Container ref={$handle} style={styles}>
|
||||||
<DotCircle width={18} height={18} />
|
<Dot width={18} height={18} />
|
||||||
</div>
|
</Container>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -1,16 +1,37 @@
|
|||||||
import React, { useRef, useEffect } from 'react';
|
import React, { useRef, useEffect } from 'react';
|
||||||
import { DotCircle } from 'shared/icons';
|
import { Dot, CaretDown, CaretRight } from 'shared/icons';
|
||||||
|
|
||||||
import { EntryChildren, EntryWrapper, EntryContent, EntryInnerContent, EntryHandle } from './Styles';
|
import { EntryChildren, EntryWrapper, EntryContent, EntryInnerContent, EntryHandle, ExpandButton } from './Styles';
|
||||||
import { useDrag } from './useDrag';
|
import { useDrag } from './useDrag';
|
||||||
|
|
||||||
|
function getCaretPosition(editableDiv: any) {
|
||||||
|
let caretPos = 0;
|
||||||
|
let sel: any = null;
|
||||||
|
let range: any = null;
|
||||||
|
if (window.getSelection) {
|
||||||
|
sel = window.getSelection();
|
||||||
|
if (sel && sel.rangeCount) {
|
||||||
|
range = sel.getRangeAt(0);
|
||||||
|
if (range.commonAncestorContainer.parentNode === editableDiv) {
|
||||||
|
caretPos = range.endOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return caretPos;
|
||||||
|
}
|
||||||
|
|
||||||
type EntryProps = {
|
type EntryProps = {
|
||||||
id: string;
|
id: string;
|
||||||
|
collapsed?: boolean;
|
||||||
|
onToggleCollapse: (id: string, collapsed: boolean) => void;
|
||||||
parentID: string;
|
parentID: string;
|
||||||
onStartDrag: (e: { id: string; clientX: number; clientY: number }) => void;
|
onStartDrag: (e: { id: string; clientX: number; clientY: number }) => void;
|
||||||
|
onStartSelect: (e: { id: string; depth: number }) => void;
|
||||||
isRoot?: boolean;
|
isRoot?: boolean;
|
||||||
draggingID: null | string;
|
selection: null | Array<{ id: string }>;
|
||||||
|
draggedNodes: null | Array<string>;
|
||||||
entries: Array<ItemElement>;
|
entries: Array<ItemElement>;
|
||||||
|
onCancelDrag: () => void;
|
||||||
position: number;
|
position: number;
|
||||||
chain?: Array<string>;
|
chain?: Array<string>;
|
||||||
depth?: number;
|
depth?: number;
|
||||||
@ -20,16 +41,21 @@ const Entry: React.FC<EntryProps> = ({
|
|||||||
id,
|
id,
|
||||||
parentID,
|
parentID,
|
||||||
isRoot = false,
|
isRoot = false,
|
||||||
|
selection,
|
||||||
|
onToggleCollapse,
|
||||||
|
onStartSelect,
|
||||||
position,
|
position,
|
||||||
|
onCancelDrag,
|
||||||
onStartDrag,
|
onStartDrag,
|
||||||
draggingID,
|
collapsed = false,
|
||||||
|
draggedNodes,
|
||||||
entries,
|
entries,
|
||||||
chain = [],
|
chain = [],
|
||||||
depth = 0,
|
depth = 0,
|
||||||
}) => {
|
}) => {
|
||||||
const $entry = useRef<HTMLDivElement>(null);
|
const $entry = useRef<HTMLDivElement>(null);
|
||||||
const $children = useRef<HTMLDivElement>(null);
|
const $children = useRef<HTMLDivElement>(null);
|
||||||
const { setNodeDimensions } = useDrag();
|
const { setNodeDimensions, clearNodeDimensions } = useDrag();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isRoot) return;
|
if (isRoot) return;
|
||||||
if ($entry && $entry.current) {
|
if ($entry && $entry.current) {
|
||||||
@ -38,24 +64,67 @@ const Entry: React.FC<EntryProps> = ({
|
|||||||
children: entries.length !== 0 ? $children : null,
|
children: entries.length !== 0 ? $children : null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return () => {
|
||||||
|
clearNodeDimensions(id);
|
||||||
|
};
|
||||||
}, [position, depth, entries]);
|
}, [position, depth, entries]);
|
||||||
let showHandle = true;
|
let showHandle = true;
|
||||||
if (draggingID && draggingID === id) {
|
if (draggedNodes && draggedNodes.length === 1 && draggedNodes.find(c => c === id)) {
|
||||||
showHandle = false;
|
showHandle = false;
|
||||||
}
|
}
|
||||||
|
let isSelected = false;
|
||||||
|
if (selection && selection.find(c => c.id === id)) {
|
||||||
|
isSelected = true;
|
||||||
|
}
|
||||||
|
let onSaveTimer: any = null;
|
||||||
|
const onSaveTimeout = 300;
|
||||||
return (
|
return (
|
||||||
<EntryWrapper isDragging={!showHandle}>
|
<EntryWrapper isSelected={isSelected} isDragging={!showHandle}>
|
||||||
{!isRoot && (
|
{!isRoot && (
|
||||||
<EntryContent>
|
<EntryContent>
|
||||||
|
{entries.length !== 0 && (
|
||||||
|
<ExpandButton onClick={() => onToggleCollapse(id, !collapsed)}>
|
||||||
|
{collapsed ? <CaretRight width={20} height={20} /> : <CaretDown width={20} height={20} />}
|
||||||
|
</ExpandButton>
|
||||||
|
)}
|
||||||
{showHandle && (
|
{showHandle && (
|
||||||
<EntryHandle onMouseDown={e => onStartDrag({ id, clientX: e.clientX, clientY: e.clientY })}>
|
<EntryHandle
|
||||||
<DotCircle width={18} height={18} />
|
onMouseUp={() => onCancelDrag()}
|
||||||
|
onMouseDown={e => {
|
||||||
|
onStartDrag({ id, clientX: e.clientX, clientY: e.clientY });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Dot width={18} height={18} />
|
||||||
</EntryHandle>
|
</EntryHandle>
|
||||||
)}
|
)}
|
||||||
<EntryInnerContent ref={$entry}>{id.toString()}</EntryInnerContent>
|
<EntryInnerContent
|
||||||
|
onMouseDown={() => {
|
||||||
|
onStartSelect({ id, depth });
|
||||||
|
}}
|
||||||
|
onKeyDown={e => {
|
||||||
|
if (e.key === 'z' && e.ctrlKey) {
|
||||||
|
if ($entry && $entry.current) {
|
||||||
|
console.log(getCaretPosition($entry.current));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
clearTimeout(onSaveTimer);
|
||||||
|
if ($entry && $entry.current) {
|
||||||
|
onSaveTimer = setTimeout(() => {
|
||||||
|
if ($entry && $entry.current) {
|
||||||
|
console.log($entry.current.textContent);
|
||||||
|
}
|
||||||
|
}, onSaveTimeout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
contentEditable
|
||||||
|
ref={$entry}
|
||||||
|
>
|
||||||
|
{`${id.toString()} - ${position}`}
|
||||||
|
</EntryInnerContent>
|
||||||
</EntryContent>
|
</EntryContent>
|
||||||
)}
|
)}
|
||||||
{entries.length !== 0 && (
|
{entries.length !== 0 && !collapsed && (
|
||||||
<EntryChildren ref={$children} isRoot={isRoot}>
|
<EntryChildren ref={$children} isRoot={isRoot}>
|
||||||
{entries
|
{entries
|
||||||
.sort((a, b) => a.position - b.position)
|
.sort((a, b) => a.position - b.position)
|
||||||
@ -65,11 +134,16 @@ const Entry: React.FC<EntryProps> = ({
|
|||||||
key={entry.id}
|
key={entry.id}
|
||||||
position={entry.position}
|
position={entry.position}
|
||||||
depth={depth + 1}
|
depth={depth + 1}
|
||||||
draggingID={draggingID}
|
draggedNodes={draggedNodes}
|
||||||
|
collapsed={entry.collapsed}
|
||||||
id={entry.id}
|
id={entry.id}
|
||||||
|
onStartSelect={onStartSelect}
|
||||||
onStartDrag={onStartDrag}
|
onStartDrag={onStartDrag}
|
||||||
|
onCancelDrag={onCancelDrag}
|
||||||
entries={entry.children ?? []}
|
entries={entry.children ?? []}
|
||||||
chain={[...chain, id]}
|
chain={[...chain, id]}
|
||||||
|
selection={selection}
|
||||||
|
onToggleCollapse={onToggleCollapse}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</EntryChildren>
|
</EntryChildren>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import styled, { css } from 'styled-components';
|
import styled, { css } from 'styled-components';
|
||||||
import { mixin } from 'shared/utils/styles';
|
import { mixin } from 'shared/utils/styles';
|
||||||
|
|
||||||
export const EntryWrapper = styled.div<{ isDragging: boolean }>`
|
export const EntryWrapper = styled.div<{ isDragging: boolean; isSelected: boolean }>`
|
||||||
position: relative;
|
position: relative;
|
||||||
${props =>
|
${props =>
|
||||||
props.isDragging &&
|
props.isDragging &&
|
||||||
@ -11,12 +11,26 @@ export const EntryWrapper = styled.div<{ isDragging: boolean }>`
|
|||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
right: -2px;
|
right: -5px;
|
||||||
left: -2px;
|
left: -5px;
|
||||||
bottom: -2px;
|
bottom: -2px;
|
||||||
background-color: #eceef0;
|
background-color: #eceef0;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
${props =>
|
||||||
|
props.isSelected &&
|
||||||
|
css`
|
||||||
|
&:before {
|
||||||
|
border-radius: 3px;
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: -5px;
|
||||||
|
bottom: -2px;
|
||||||
|
left: -5px;
|
||||||
|
background-color: rgba(${props.theme.colors.primary}, 0.75);
|
||||||
|
}
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const EntryChildren = styled.div<{ isRoot: boolean }>`
|
export const EntryChildren = styled.div<{ isRoot: boolean }>`
|
||||||
@ -26,7 +40,7 @@ export const EntryChildren = styled.div<{ isRoot: boolean }>`
|
|||||||
css`
|
css`
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
padding-left: 25px;
|
padding-left: 25px;
|
||||||
border-left: 1px solid rgb(236, 238, 240);
|
border-left: 1px solid rgba(${props.theme.colors.text.primary}, 0.6);
|
||||||
`}
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -45,15 +59,7 @@ export const PageContent = styled.div`
|
|||||||
padding-right: 56px;
|
padding-right: 56px;
|
||||||
padding-top: 24px;
|
padding-top: 24px;
|
||||||
padding-bottom: 24px;
|
padding-bottom: 24px;
|
||||||
margin-top: 72px;
|
|
||||||
text-size-adjust: none;
|
text-size-adjust: none;
|
||||||
background: rgb(255, 255, 255);
|
|
||||||
border-radius: 6px;
|
|
||||||
`;
|
|
||||||
export const EntryContent = styled.div`
|
|
||||||
position: relative;
|
|
||||||
margin-left: -500px;
|
|
||||||
padding-left: 524px;
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const DragHandle = styled.div<{ top: number; left: number }>`
|
export const DragHandle = styled.div<{ top: number; left: number }>`
|
||||||
@ -70,6 +76,7 @@ export const DragHandle = styled.div<{ top: number; left: number }>`
|
|||||||
color: rgb(75, 81, 85);
|
color: rgb(75, 81, 85);
|
||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
`;
|
`;
|
||||||
|
export const RootWrapper = styled.div``;
|
||||||
|
|
||||||
export const EntryHandle = styled.div`
|
export const EntryHandle = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -80,8 +87,15 @@ export const EntryHandle = styled.div`
|
|||||||
top: 7px;
|
top: 7px;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
color: rgb(75, 81, 85);
|
color: rgba(${p => p.theme.colors.text.primary});
|
||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
|
&:hover {
|
||||||
|
background: rgba(${p => p.theme.colors.primary});
|
||||||
|
}
|
||||||
|
svg {
|
||||||
|
fill: rgba(${p => p.theme.colors.text.primary});
|
||||||
|
stroke: rgba(${p => p.theme.colors.text.primary});
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const EntryInnerContent = styled.div`
|
export const EntryInnerContent = styled.div`
|
||||||
@ -93,6 +107,10 @@ export const EntryInnerContent = styled.div`
|
|||||||
overflow-wrap: break-word;
|
overflow-wrap: break-word;
|
||||||
position: relative;
|
position: relative;
|
||||||
user-select: text;
|
user-select: text;
|
||||||
|
color: rgba(${p => p.theme.colors.text.primary});
|
||||||
|
&::selection {
|
||||||
|
background: #a49de8;
|
||||||
|
}
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
@ -114,3 +132,33 @@ export const DragIndicatorBar = styled.div<{ left: number; top: number; width: n
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
background: rgb(204, 204, 204);
|
background: rgb(204, 204, 204);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
export const ExpandButton = styled.div`
|
||||||
|
top: 6px;
|
||||||
|
cursor: default;
|
||||||
|
color: transparent;
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
left: 478px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
svg {
|
||||||
|
fill: transparent;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
export const EntryContent = styled.div`
|
||||||
|
position: relative;
|
||||||
|
margin-left: -500px;
|
||||||
|
padding-left: 524px;
|
||||||
|
|
||||||
|
&:hover ${ExpandButton} svg {
|
||||||
|
fill: rgb(${props => props.theme.colors.text.primary});
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const PageContainer = styled.div`
|
||||||
|
overflow: scroll;
|
||||||
|
`;
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import React, { useState, useRef, useEffect, useMemo, useCallback, useContext, memo, createRef } from 'react';
|
import React, { useState, useRef, useEffect, useMemo, useCallback, useContext, memo, createRef } from 'react';
|
||||||
import { DotCircle } from 'shared/icons';
|
import { DotCircle } from 'shared/icons';
|
||||||
|
import styled from 'styled-components/macro';
|
||||||
|
import GlobalTopNavbar from 'App/TopNavbar';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import produce from 'immer';
|
import produce from 'immer';
|
||||||
import Entry from './Entry';
|
import Entry from './Entry';
|
||||||
@ -9,6 +11,7 @@ import DragDebug from './DragDebug';
|
|||||||
import { DragContext } from './useDrag';
|
import { DragContext } from './useDrag';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
PageContainer,
|
||||||
DragDebugWrapper,
|
DragDebugWrapper,
|
||||||
DragIndicatorBar,
|
DragIndicatorBar,
|
||||||
PageContent,
|
PageContent,
|
||||||
@ -16,43 +19,85 @@ import {
|
|||||||
EntryInnerContent,
|
EntryInnerContent,
|
||||||
EntryWrapper,
|
EntryWrapper,
|
||||||
EntryContent,
|
EntryContent,
|
||||||
|
RootWrapper,
|
||||||
EntryHandle,
|
EntryHandle,
|
||||||
} from './Styles';
|
} from './Styles';
|
||||||
import { transformToTree, findNode, findNodeDepth, getNumberOfChildren, validateDepth } from './utils';
|
import {
|
||||||
|
transformToTree,
|
||||||
|
findNode,
|
||||||
|
findNodeDepth,
|
||||||
|
getNumberOfChildren,
|
||||||
|
validateDepth,
|
||||||
|
getDimensions,
|
||||||
|
findNextDraggable,
|
||||||
|
getNodeOver,
|
||||||
|
getCorrectNode,
|
||||||
|
findCommonParent,
|
||||||
|
} from './utils';
|
||||||
|
import NOOP from 'shared/utils/noop';
|
||||||
|
|
||||||
|
type OutlineCommand = {
|
||||||
|
nodes: Array<{
|
||||||
|
id: string;
|
||||||
|
prev: { position: number; parent: string | null };
|
||||||
|
next: { position: number; parent: string | null };
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ItemCollapsed = {
|
||||||
|
id: string;
|
||||||
|
collapsed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
const listItems: Array<ItemElement> = [
|
const listItems: Array<ItemElement> = [
|
||||||
{ id: 'root', position: 4096, parent: null },
|
{ id: 'root', position: 4096, parent: null, collapsed: false },
|
||||||
{ id: 'entry-1', position: 4096, parent: 'root' },
|
{ id: 'entry-1', position: 4096, parent: 'root', collapsed: false },
|
||||||
{ id: 'entry-1_1', position: 4096, parent: 'entry-1' },
|
{ id: 'entry-1_3', position: 4096 * 3, parent: 'entry-1', collapsed: false },
|
||||||
{ id: 'entry-1_1_1', position: 4096, parent: 'entry-1_1' },
|
{ id: 'entry-1_3_1', position: 4096, parent: 'entry-1_3', collapsed: false },
|
||||||
{ id: 'entry-1_2', position: 4096 * 2, parent: 'entry-1' },
|
{ id: 'entry-1_3_2', position: 4096 * 2, parent: 'entry-1_3', collapsed: false },
|
||||||
{ id: 'entry-1_2_1', position: 4096, parent: 'entry-1_2' },
|
{ id: 'entry-1_3_3', position: 4096 * 3, parent: 'entry-1_3', collapsed: false },
|
||||||
{ id: 'entry-1_2_2', position: 4096 * 2, parent: 'entry-1_2' },
|
{ id: 'entry-1_3_3_1', position: 4096 * 1, parent: 'entry-1_3_3', collapsed: false },
|
||||||
{ id: 'entry-1_2_3', position: 4096 * 3, parent: 'entry-1_2' },
|
{ id: 'entry-1_3_3_1_1', position: 4096 * 1, parent: 'entry-1_3_3_1', collapsed: false },
|
||||||
{ id: 'entry-2', position: 4096 * 2, parent: 'root' },
|
{ id: 'entry-2', position: 4096 * 2, parent: 'root', collapsed: false },
|
||||||
{ id: 'entry-3', position: 4096 * 3, parent: 'root' },
|
{ id: 'entry-3', position: 4096 * 3, parent: 'root', collapsed: false },
|
||||||
{ id: 'entry-4', position: 4096 * 4, parent: 'root' },
|
{ id: 'entry-4', position: 4096 * 4, parent: 'root', collapsed: false },
|
||||||
{ id: 'entry-5', position: 4096 * 5, parent: 'root' },
|
{ id: 'entry-5', position: 4096 * 5, parent: 'root', collapsed: false },
|
||||||
];
|
];
|
||||||
|
|
||||||
const Outline: React.FC = () => {
|
const Outline: React.FC = () => {
|
||||||
const [items, setItems] = useState(listItems);
|
const [items, setItems] = useState(listItems);
|
||||||
|
const [selecting, setSelecting] = useState<{
|
||||||
|
isSelecting: boolean;
|
||||||
|
node: { id: string; depth: number } | null;
|
||||||
|
}>({ isSelecting: false, node: null });
|
||||||
|
const [selection, setSelection] = useState<null | { nodes: Array<{ id: string }>; first?: OutlineNode | null }>(null);
|
||||||
const [dragging, setDragging] = useState<{
|
const [dragging, setDragging] = useState<{
|
||||||
show: boolean;
|
show: boolean;
|
||||||
draggableID: null | string;
|
draggedNodes: null | Array<string>;
|
||||||
initialPos: { x: number; y: number };
|
initialPos: { x: number; y: number };
|
||||||
}>({ show: false, draggableID: null, initialPos: { x: 0, y: 0 } });
|
}>({ show: false, draggedNodes: null, initialPos: { x: 0, y: 0 } });
|
||||||
const [impact, setImpact] = useState<null | {
|
const [impact, setImpact] = useState<null | {
|
||||||
listPosition: number;
|
listPosition: number;
|
||||||
zone: ImpactZone;
|
zone: ImpactZone;
|
||||||
depthTarget: number;
|
depthTarget: number;
|
||||||
}>(null);
|
}>(null);
|
||||||
|
const selectRef = useRef<{ isSelecting: boolean; hasSelection: boolean; node: { id: string; depth: number } | null }>(
|
||||||
|
{
|
||||||
|
isSelecting: false,
|
||||||
|
node: null,
|
||||||
|
hasSelection: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
const impactRef = useRef<null | { listPosition: number; depth: number; zone: ImpactZone }>(null);
|
const impactRef = useRef<null | { listPosition: number; depth: number; zone: ImpactZone }>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (impact) {
|
if (impact) {
|
||||||
impactRef.current = { zone: impact.zone, depth: impact.depthTarget, listPosition: impact.listPosition };
|
impactRef.current = { zone: impact.zone, depth: impact.depthTarget, listPosition: impact.listPosition };
|
||||||
}
|
}
|
||||||
}, [impact]);
|
}, [impact]);
|
||||||
|
useEffect(() => {
|
||||||
|
selectRef.current.isSelecting = selecting.isSelecting;
|
||||||
|
selectRef.current.node = selecting.node;
|
||||||
|
}, [selecting]);
|
||||||
|
|
||||||
const $content = useRef<HTMLDivElement>(null);
|
const $content = useRef<HTMLDivElement>(null);
|
||||||
const outline = useRef<OutlineData>({
|
const outline = useRef<OutlineData>({
|
||||||
@ -67,20 +112,32 @@ const Outline: React.FC = () => {
|
|||||||
if (tree.length === 1) {
|
if (tree.length === 1) {
|
||||||
root = tree[0];
|
root = tree[0];
|
||||||
}
|
}
|
||||||
|
const outlineHistory = useRef<{ commands: Array<OutlineCommand>; current: number }>({ current: -1, commands: [] });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
outline.current.relationships = new Map<string, NodeRelationships>();
|
outline.current.relationships = new Map<string, NodeRelationships>();
|
||||||
outline.current.published = new Map<string, string>();
|
outline.current.published = new Map<string, string>();
|
||||||
outline.current.nodes = new Map<number, Map<string, OutlineNode>>();
|
outline.current.nodes = new Map<number, Map<string, OutlineNode>>();
|
||||||
|
const collapsedMap = items.reduce((map, next) => {
|
||||||
|
if (next.collapsed) {
|
||||||
|
map.set(next.id, true);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}, new Map<string, boolean>());
|
||||||
items.forEach(item => outline.current.published.set(item.id, item.parent ?? 'root'));
|
items.forEach(item => outline.current.published.set(item.id, item.parent ?? 'root'));
|
||||||
|
|
||||||
for (let i = 0; i < items.length; i++) {
|
for (let i = 0; i < items.length; i++) {
|
||||||
const { position, id, parent: curParent } = items[i];
|
const { collapsed, position, id, parent: curParent } = items[i];
|
||||||
if (id === 'root') {
|
if (id === 'root') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const parent = curParent ?? 'root';
|
const parent = curParent ?? 'root';
|
||||||
outline.current.published.set(id, parent ?? 'root');
|
outline.current.published.set(id, parent ?? 'root');
|
||||||
const { depth, ancestors } = findNodeDepth(outline.current.published, id);
|
const { depth, ancestors } = findNodeDepth(outline.current.published, id);
|
||||||
|
const collapsedParent = ancestors.slice(0, -1).find(a => collapsedMap.get(a));
|
||||||
|
if (collapsedParent) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const children = getNumberOfChildren(root, ancestors);
|
const children = getNumberOfChildren(root, ancestors);
|
||||||
if (!outline.current.nodes.has(depth)) {
|
if (!outline.current.nodes.has(depth)) {
|
||||||
outline.current.nodes.set(depth, new Map<string, OutlineNode>());
|
outline.current.nodes.set(depth, new Map<string, OutlineNode>());
|
||||||
@ -93,6 +150,7 @@ const Outline: React.FC = () => {
|
|||||||
position,
|
position,
|
||||||
depth,
|
depth,
|
||||||
ancestors,
|
ancestors,
|
||||||
|
collapsed,
|
||||||
parent,
|
parent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -118,12 +176,144 @@ const Outline: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [items]);
|
}, [items]);
|
||||||
|
const handleKeyDown = useCallback(e => {
|
||||||
|
if (e.code === 'KeyZ' && e.ctrlKey) {
|
||||||
|
const currentCommand = outlineHistory.current.commands[outlineHistory.current.current];
|
||||||
|
if (currentCommand) {
|
||||||
|
setItems(prevItems =>
|
||||||
|
produce(prevItems, draftItems => {
|
||||||
|
currentCommand.nodes.forEach(node => {
|
||||||
|
const idx = prevItems.findIndex(c => c.id === node.id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
draftItems[idx].parent = node.prev.parent;
|
||||||
|
draftItems[idx].position = node.prev.position;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
outlineHistory.current.current--;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (e.code === 'KeyY' && e.ctrlKey) {
|
||||||
|
const currentCommand = outlineHistory.current.commands[outlineHistory.current.current + 1];
|
||||||
|
if (currentCommand) {
|
||||||
|
setItems(prevItems =>
|
||||||
|
produce(prevItems, draftItems => {
|
||||||
|
currentCommand.nodes.forEach(node => {
|
||||||
|
const idx = prevItems.findIndex(c => c.id === node.id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
draftItems[idx].parent = node.next.parent;
|
||||||
|
draftItems[idx].position = node.next.position;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
outlineHistory.current.current++;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleMouseUp = useCallback(
|
||||||
|
e => {
|
||||||
|
if (selectRef.current.hasSelection && !selectRef.current.isSelecting) {
|
||||||
|
setSelection(null);
|
||||||
|
}
|
||||||
|
if (selectRef.current.isSelecting) {
|
||||||
|
setSelecting({ isSelecting: false, node: null });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[dragging, selecting],
|
||||||
|
);
|
||||||
|
const handleMouseMove = useCallback(e => {
|
||||||
|
if (selectRef.current.isSelecting && selectRef.current.node) {
|
||||||
|
const { clientX, clientY } = e;
|
||||||
|
const dimensions = outline.current.dimensions.get(selectRef.current.node.id);
|
||||||
|
if (dimensions) {
|
||||||
|
const entry = getDimensions(dimensions.entry);
|
||||||
|
if (entry) {
|
||||||
|
const isAbove = clientY < entry.top;
|
||||||
|
const isBelow = clientY > entry.bottom;
|
||||||
|
if (!isAbove && !isBelow && selectRef.current.hasSelection) {
|
||||||
|
const nodeDepth = outline.current.nodes.get(selectRef.current.node.depth);
|
||||||
|
const aboveNode = nodeDepth ? nodeDepth.get(selectRef.current.node.id) : null;
|
||||||
|
if (aboveNode) {
|
||||||
|
setSelection({ nodes: [{ id: selectRef.current.node.id }], first: aboveNode });
|
||||||
|
selectRef.current.hasSelection = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isAbove || isBelow) {
|
||||||
|
e.preventDefault();
|
||||||
|
const { curDraggable } = getNodeOver({ x: clientX, y: clientY }, outline.current);
|
||||||
|
const nodeDepth = outline.current.nodes.get(selectRef.current.node.depth);
|
||||||
|
const selectedNode = nodeDepth ? nodeDepth.get(selectRef.current.node.id) : null;
|
||||||
|
let aboveNode: OutlineNode | undefined | null = null;
|
||||||
|
let belowNode: OutlineNode | undefined | null = null;
|
||||||
|
if (isBelow) {
|
||||||
|
aboveNode = selectedNode;
|
||||||
|
belowNode = curDraggable;
|
||||||
|
} else {
|
||||||
|
aboveNode = curDraggable;
|
||||||
|
belowNode = selectedNode;
|
||||||
|
}
|
||||||
|
if (aboveNode && belowNode) {
|
||||||
|
const aboveDim = outline.current.dimensions.get(aboveNode.id);
|
||||||
|
const belowDim = outline.current.dimensions.get(belowNode.id);
|
||||||
|
if (aboveDim && belowDim) {
|
||||||
|
const aboveDimBounds = getDimensions(aboveDim.entry);
|
||||||
|
const belowDimBounds = getDimensions(belowDim.children ? belowDim.children : belowDim.entry);
|
||||||
|
const aboveDimY = aboveDimBounds ? aboveDimBounds.bottom : 0;
|
||||||
|
const belowDimY = belowDimBounds ? belowDimBounds.top : 0;
|
||||||
|
const inbetweenNodes: Array<{ id: string }> = [];
|
||||||
|
for (const [id, dimension] of outline.current.dimensions.entries()) {
|
||||||
|
if (id === aboveNode.id || id === belowNode.id) {
|
||||||
|
inbetweenNodes.push({ id });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const targetNodeBounds = getDimensions(dimension.entry);
|
||||||
|
if (targetNodeBounds) {
|
||||||
|
if (
|
||||||
|
Math.round(aboveDimY) <= Math.round(targetNodeBounds.top) &&
|
||||||
|
Math.round(belowDimY) >= Math.round(targetNodeBounds.bottom)
|
||||||
|
) {
|
||||||
|
inbetweenNodes.push({ id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const filteredNodes = inbetweenNodes.filter(n => {
|
||||||
|
const parent = outline.current.published.get(n.id);
|
||||||
|
if (parent) {
|
||||||
|
const foundParent = inbetweenNodes.find(c => c.id === parent);
|
||||||
|
if (foundParent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
selectRef.current.hasSelection = true;
|
||||||
|
setSelection({ nodes: filteredNodes, first: aboveNode });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('mouseup', handleMouseUp);
|
||||||
|
document.addEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('mouseup', handleMouseUp);
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove);
|
||||||
|
document.addEventListener('keydown', handleKeyDown);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (!root) {
|
if (!root) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<GlobalTopNavbar onSaveProjectName={NOOP} projectID={null} name={null} />
|
||||||
<DragContext.Provider
|
<DragContext.Provider
|
||||||
value={{
|
value={{
|
||||||
outline,
|
outline,
|
||||||
@ -132,14 +322,29 @@ const Outline: React.FC = () => {
|
|||||||
if (data) {
|
if (data) {
|
||||||
const { zone, depth } = data;
|
const { zone, depth } = data;
|
||||||
let listPosition = 65535;
|
let listPosition = 65535;
|
||||||
const listAbove = validateDepth(zone.above ? zone.above.node : null, depth);
|
if (zone.above && zone.above.node.depth + 1 <= depth && zone.above.node.collapsed) {
|
||||||
const listBelow = validateDepth(zone.below ? zone.below.node : null, depth);
|
const aboveChildren = items
|
||||||
if (listAbove && listBelow) {
|
.filter(i => (zone.above ? i.parent === zone.above.node.id : false))
|
||||||
listPosition = (listAbove.position + listBelow.position) / 2.0;
|
.sort((a, b) => a.position - b.position);
|
||||||
} else if (listAbove && !listBelow) {
|
const lastChild = aboveChildren[aboveChildren.length - 1];
|
||||||
listPosition = listAbove.position * 2.0;
|
if (lastChild) {
|
||||||
} else if (!listAbove && listBelow) {
|
listPosition = lastChild.position * 2.0;
|
||||||
listPosition = listBelow.position / 2.0;
|
}
|
||||||
|
} else {
|
||||||
|
console.log(zone.above);
|
||||||
|
console.log(zone.below);
|
||||||
|
const correctNode = getCorrectNode(outline.current, zone.above ? zone.above.node : null, depth);
|
||||||
|
console.log(correctNode);
|
||||||
|
const listAbove = validateDepth(correctNode, depth);
|
||||||
|
const listBelow = validateDepth(zone.below ? zone.below.node : null, depth);
|
||||||
|
console.log(listAbove, listBelow);
|
||||||
|
if (listAbove && listBelow) {
|
||||||
|
listPosition = (listAbove.position + listBelow.position) / 2.0;
|
||||||
|
} else if (listAbove && !listBelow) {
|
||||||
|
listPosition = listAbove.position * 2.0;
|
||||||
|
} else if (!listAbove && listBelow) {
|
||||||
|
listPosition = listBelow.position / 2.0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!zone.above && zone.below) {
|
if (!zone.above && zone.below) {
|
||||||
@ -167,124 +372,122 @@ const Outline: React.FC = () => {
|
|||||||
setNodeDimensions: (nodeID, ref) => {
|
setNodeDimensions: (nodeID, ref) => {
|
||||||
outline.current.dimensions.set(nodeID, ref);
|
outline.current.dimensions.set(nodeID, ref);
|
||||||
},
|
},
|
||||||
|
clearNodeDimensions: nodeID => {
|
||||||
|
outline.current.dimensions.delete(nodeID);
|
||||||
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
<PageContent ref={$content}>
|
<PageContainer>
|
||||||
<Entry
|
<PageContent>
|
||||||
id="root"
|
<RootWrapper ref={$content}>
|
||||||
parentID="root"
|
<Entry
|
||||||
isRoot
|
onStartSelect={({ id, depth }) => {
|
||||||
draggingID={dragging.draggableID}
|
setSelection(null);
|
||||||
position={root.position}
|
setSelecting({ isSelecting: true, node: { id, depth } });
|
||||||
entries={root.children}
|
}}
|
||||||
onStartDrag={e => {
|
onToggleCollapse={(id, collapsed) => {
|
||||||
if (e.id !== 'root') {
|
setItems(prevItems =>
|
||||||
setImpact(null);
|
produce(prevItems, draftItems => {
|
||||||
setDragging({ show: true, draggableID: e.id, initialPos: { x: e.clientX, y: e.clientY } });
|
const idx = prevItems.findIndex(c => c.id === id);
|
||||||
}
|
if (idx !== -1) {
|
||||||
}}
|
draftItems[idx].collapsed = collapsed;
|
||||||
/>
|
}
|
||||||
</PageContent>
|
}),
|
||||||
{dragging.show && dragging.draggableID && (
|
);
|
||||||
|
}}
|
||||||
|
id="root"
|
||||||
|
parentID="root"
|
||||||
|
isRoot
|
||||||
|
selection={selection ? selection.nodes : null}
|
||||||
|
draggedNodes={dragging.draggedNodes}
|
||||||
|
position={root.position}
|
||||||
|
entries={root.children}
|
||||||
|
onCancelDrag={() => {
|
||||||
|
setImpact(null);
|
||||||
|
setDragging({ show: false, draggedNodes: null, initialPos: { x: 0, y: 0 } });
|
||||||
|
}}
|
||||||
|
onStartDrag={e => {
|
||||||
|
if (e.id !== 'root') {
|
||||||
|
if (selectRef.current.hasSelection && selection && selection.nodes.find(c => c.id === e.id)) {
|
||||||
|
setImpact(null);
|
||||||
|
setDragging({
|
||||||
|
show: true,
|
||||||
|
draggedNodes: [...selection.nodes.map(c => c.id)],
|
||||||
|
initialPos: { x: e.clientX, y: e.clientY },
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setImpact(null);
|
||||||
|
setDragging({ show: true, draggedNodes: [e.id], initialPos: { x: e.clientX, y: e.clientY } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</RootWrapper>
|
||||||
|
</PageContent>
|
||||||
|
</PageContainer>
|
||||||
|
{dragging.show && dragging.draggedNodes && (
|
||||||
<Dragger
|
<Dragger
|
||||||
container={$content}
|
container={$content}
|
||||||
draggingID={dragging.draggableID}
|
|
||||||
initialPos={dragging.initialPos}
|
initialPos={dragging.initialPos}
|
||||||
|
draggedNodes={{ nodes: dragging.draggedNodes, first: selection ? selection.first : null }}
|
||||||
isDragging={dragging.show}
|
isDragging={dragging.show}
|
||||||
onDragEnd={() => {
|
onDragEnd={() => {
|
||||||
const draggingID = dragging.draggableID;
|
if (dragging.draggedNodes && impactRef.current) {
|
||||||
if (draggingID && impactRef.current) {
|
|
||||||
const { zone, depth, listPosition } = impactRef.current;
|
const { zone, depth, listPosition } = impactRef.current;
|
||||||
const noZone = !zone.above && !zone.below;
|
const noZone = !zone.above && !zone.below;
|
||||||
const curParentID = outline.current.published.get(draggingID);
|
if (!noZone) {
|
||||||
if (!noZone && curParentID) {
|
|
||||||
let parentID = 'root';
|
let parentID = 'root';
|
||||||
if (zone.above) {
|
if (zone.above) {
|
||||||
parentID = zone.above.node.ancestors[depth - 1];
|
parentID = zone.above.node.ancestors[depth - 1];
|
||||||
}
|
}
|
||||||
const node = findNode(curParentID, draggingID, outline.current);
|
let reparent = true;
|
||||||
console.log(`${node ? node.parent : null} => ${parentID}`);
|
for (let i = 0; i < dragging.draggedNodes.length; i++) {
|
||||||
// UPDATE OUTLINE DATA AFTER NODE MOVE
|
const draggedID = dragging.draggedNodes[i];
|
||||||
if (node) {
|
const prevItem = items.find(i => i.id === draggedID);
|
||||||
if (node.depth !== depth) {
|
if (prevItem && prevItem.position === listPosition && prevItem.parent === parentID) {
|
||||||
const oldParentDepth = outline.current.nodes.get(node.depth - 1);
|
reparent = false;
|
||||||
if (oldParentDepth) {
|
break;
|
||||||
const oldParentNode = oldParentDepth.get(node.parent);
|
|
||||||
if (oldParentNode) {
|
|
||||||
oldParentNode.children -= 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const oldDepth = outline.current.nodes.get(node.depth);
|
|
||||||
if (oldDepth) {
|
|
||||||
oldDepth.delete(node.id);
|
|
||||||
}
|
|
||||||
if (!outline.current.nodes.has(depth)) {
|
|
||||||
outline.current.nodes.set(depth, new Map<string, OutlineNode>());
|
|
||||||
}
|
|
||||||
const newParentDepth = outline.current.nodes.get(depth - 1);
|
|
||||||
if (newParentDepth) {
|
|
||||||
const newParentNode = newParentDepth.get(parentID);
|
|
||||||
if (newParentNode) {
|
|
||||||
newParentNode.children += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const newDepth = outline.current.nodes.get(depth);
|
|
||||||
if (newDepth) {
|
|
||||||
// TODO: rebuild ancestors
|
|
||||||
newDepth.set(node.id, {
|
|
||||||
...node,
|
|
||||||
depth,
|
|
||||||
position: listPosition,
|
|
||||||
parent: parentID,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!outline.current.relationships.has(parentID)) {
|
|
||||||
outline.current.relationships.set(parentID, {
|
|
||||||
self: {
|
|
||||||
depth: depth - 1,
|
|
||||||
id: parentID,
|
|
||||||
},
|
|
||||||
children: [{ id: draggingID, position: listPosition, depth, children: node.children }],
|
|
||||||
numberOfSubChildren: 0,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const nodeRelations = outline.current.relationships.get(parentID);
|
|
||||||
if (parentID !== node.parent) {
|
|
||||||
// ??
|
|
||||||
}
|
|
||||||
if (nodeRelations) {
|
|
||||||
nodeRelations.children = produce(nodeRelations.children, draftChildren => {
|
|
||||||
const nodeIdx = draftChildren.findIndex(c => c.id === node.id);
|
|
||||||
if (nodeIdx !== -1) {
|
|
||||||
draftChildren[nodeIdx] = {
|
|
||||||
children: node.children,
|
|
||||||
depth,
|
|
||||||
position: listPosition,
|
|
||||||
id: node.id,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
draftChildren.sort((a, b) => a.position - b.position);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
outline.current.published.set(draggingID, parentID);
|
// TODO: set reparent if list position changed but parent did not
|
||||||
setItems(itemsPrev =>
|
//
|
||||||
produce(itemsPrev, draftItems => {
|
|
||||||
const curDragging = itemsPrev.findIndex(i => i.id === draggingID);
|
if (reparent) {
|
||||||
// console.log(`parent=${impactRef.current} target=${draggingID}`);
|
// UPDATE OUTLINE DATA AFTER NODE MOVE
|
||||||
if (impactRef.current) {
|
setItems(itemsPrev =>
|
||||||
// console.log(`updating position = ${impactRef.current.targetPosition}`);
|
produce(itemsPrev, draftItems => {
|
||||||
draftItems[curDragging].parent = parentID;
|
if (dragging.draggedNodes) {
|
||||||
draftItems[curDragging].position = listPosition;
|
const command: OutlineCommand = { nodes: [] };
|
||||||
}
|
outlineHistory.current.current += 1;
|
||||||
}),
|
dragging.draggedNodes.forEach(n => {
|
||||||
);
|
const curDragging = itemsPrev.findIndex(i => i.id === n);
|
||||||
|
command.nodes.push({
|
||||||
|
id: n,
|
||||||
|
prev: {
|
||||||
|
parent: draftItems[curDragging].parent,
|
||||||
|
position: draftItems[curDragging].position,
|
||||||
|
},
|
||||||
|
next: {
|
||||||
|
parent: parentID,
|
||||||
|
position: listPosition,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
draftItems[curDragging].parent = parentID;
|
||||||
|
draftItems[curDragging].position = listPosition;
|
||||||
|
});
|
||||||
|
outlineHistory.current.commands[outlineHistory.current.current] = command;
|
||||||
|
if (outlineHistory.current.commands[outlineHistory.current.current + 1]) {
|
||||||
|
outlineHistory.current.commands.splice(outlineHistory.current.current + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setImpact(null);
|
setImpact(null);
|
||||||
setDragging({ show: false, draggableID: null, initialPos: { x: 0, y: 0 } });
|
setDragging({ show: false, draggedNodes: null, initialPos: { x: 0, y: 0 } });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -292,7 +495,7 @@ const Outline: React.FC = () => {
|
|||||||
</DragContext.Provider>
|
</DragContext.Provider>
|
||||||
{impact && <DragIndicator depthTarget={impact.depthTarget} container={$content} zone={impact.zone} />}
|
{impact && <DragIndicator depthTarget={impact.depthTarget} container={$content} zone={impact.zone} />}
|
||||||
{impact && (
|
{impact && (
|
||||||
<DragDebug zone={impact.zone ?? null} draggingID={dragging.draggableID} depthTarget={impact.depthTarget} />
|
<DragDebug zone={impact.zone ?? null} draggedNodes={dragging.draggedNodes} depthTarget={impact.depthTarget} />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -7,6 +7,7 @@ type DragContextData = {
|
|||||||
nodeID: string,
|
nodeID: string,
|
||||||
ref: { entry: React.RefObject<HTMLElement>; children: React.RefObject<HTMLElement> | null },
|
ref: { entry: React.RefObject<HTMLElement>; children: React.RefObject<HTMLElement> | null },
|
||||||
) => void;
|
) => void;
|
||||||
|
clearNodeDimensions: (nodeID: string) => void;
|
||||||
setImpact: (data: ImpactData | null) => void;
|
setImpact: (data: ImpactData | null) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,6 +1,25 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
|
|
||||||
export function validateDepth(node: OutlineNode | null, depth: number) {
|
export function getCorrectNode(data: OutlineData, node: OutlineNode | null, depth: number) {
|
||||||
|
if (node) {
|
||||||
|
console.log(depth, node);
|
||||||
|
if (depth === node.depth) {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
const parent = node.ancestors[depth];
|
||||||
|
console.log('parent', parent);
|
||||||
|
if (parent) {
|
||||||
|
const parentNode = data.relationships.get(parent);
|
||||||
|
if (parentNode) {
|
||||||
|
const parentDepth = parentNode.self.depth;
|
||||||
|
const nodeDepth = data.nodes.get(parentDepth);
|
||||||
|
return nodeDepth ? nodeDepth.get(parent) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
export function validateDepth(node: OutlineNode | null | undefined, depth: number) {
|
||||||
if (node) {
|
if (node) {
|
||||||
return node.depth === depth ? node : null;
|
return node.depth === depth ? node : null;
|
||||||
}
|
}
|
||||||
@ -14,9 +33,9 @@ export function getNodeAbove(node: OutlineNode, startingParent: RelationshipChil
|
|||||||
while (hasChildren) {
|
while (hasChildren) {
|
||||||
const targetParent = outline.relationships.get(aboveTargetID);
|
const targetParent = outline.relationships.get(aboveTargetID);
|
||||||
if (targetParent) {
|
if (targetParent) {
|
||||||
|
const parentNodes = outline.nodes.get(targetParent.self.depth);
|
||||||
|
const parentNode = parentNodes ? parentNodes.get(targetParent.self.id) : null;
|
||||||
if (targetParent.children.length === 0) {
|
if (targetParent.children.length === 0) {
|
||||||
const parentNodes = outline.nodes.get(targetParent.self.depth);
|
|
||||||
const parentNode = parentNodes ? parentNodes.get(targetParent.self.id) : null;
|
|
||||||
if (parentNode) {
|
if (parentNode) {
|
||||||
nodeAbove = {
|
nodeAbove = {
|
||||||
id: parentNode.id,
|
id: parentNode.id,
|
||||||
@ -24,7 +43,9 @@ export function getNodeAbove(node: OutlineNode, startingParent: RelationshipChil
|
|||||||
position: parentNode.position,
|
position: parentNode.position,
|
||||||
children: parentNode.children,
|
children: parentNode.children,
|
||||||
};
|
};
|
||||||
|
console.log('node above', nodeAbove);
|
||||||
}
|
}
|
||||||
|
hasChildren = false;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
nodeAbove = targetParent.children[targetParent.children.length - 1];
|
nodeAbove = targetParent.children[targetParent.children.length - 1];
|
||||||
@ -44,6 +65,7 @@ export function getNodeAbove(node: OutlineNode, startingParent: RelationshipChil
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
console.log('final node above', nodeAbove);
|
||||||
return nodeAbove;
|
return nodeAbove;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,12 +125,7 @@ export function getTargetDepth(mouseX: number, handleLeft: number, availableDept
|
|||||||
return availableDepths.min;
|
return availableDepths.min;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findNextDraggable(
|
export function findNextDraggable(pos: { x: number; y: number }, outline: OutlineData, curDepth: number) {
|
||||||
pos: { x: number; y: number },
|
|
||||||
outline: OutlineData,
|
|
||||||
curDepth: number,
|
|
||||||
draggingID: string,
|
|
||||||
) {
|
|
||||||
let index = 0;
|
let index = 0;
|
||||||
const currentDepthNodes = outline.nodes.get(curDepth);
|
const currentDepthNodes = outline.nodes.get(curDepth);
|
||||||
let nodeAbove: null | RelationshipChild = null;
|
let nodeAbove: null | RelationshipChild = null;
|
||||||
@ -260,3 +277,85 @@ export function findNodeAbove(outline: OutlineData, curDepth: number, belowNode:
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getNodeOver(mouse: { x: number; y: number }, outline: OutlineData) {
|
||||||
|
let curDepth = 1;
|
||||||
|
let curDraggables: any;
|
||||||
|
let curDraggable: any;
|
||||||
|
let curPosition: ImpactPosition = 'after';
|
||||||
|
while (outline.nodes.size + 1 > curDepth) {
|
||||||
|
curDraggables = outline.nodes.get(curDepth);
|
||||||
|
if (curDraggables) {
|
||||||
|
const nextDraggable = findNextDraggable(mouse, outline, curDepth);
|
||||||
|
if (nextDraggable) {
|
||||||
|
curDraggable = nextDraggable.node;
|
||||||
|
curPosition = nextDraggable.position;
|
||||||
|
if (nextDraggable.found) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
curDepth += 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
curDepth,
|
||||||
|
curDraggable,
|
||||||
|
curPosition,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findCommonParent(outline: OutlineData, aboveNode: OutlineNode, belowNode: OutlineNode) {
|
||||||
|
let aboveParentID = null;
|
||||||
|
let depth = 0;
|
||||||
|
for (let aIdx = aboveNode.ancestors.length - 1; aIdx !== 0; aIdx--) {
|
||||||
|
depth = aIdx;
|
||||||
|
const aboveNodeParent = aboveNode.ancestors[aIdx];
|
||||||
|
for (let bIdx = belowNode.ancestors.length - 1; bIdx !== 0; bIdx--) {
|
||||||
|
if (belowNode.ancestors[bIdx] === aboveNodeParent) {
|
||||||
|
aboveParentID = aboveNodeParent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (aboveParentID) {
|
||||||
|
const parent = outline.relationships.get(aboveParentID) ?? null;
|
||||||
|
if (parent) {
|
||||||
|
return {
|
||||||
|
parent,
|
||||||
|
depth,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLastChildInBranch(outline: OutlineData, lastParentNode: OutlineNode) {
|
||||||
|
let curParentRelation = outline.relationships.get(lastParentNode.id);
|
||||||
|
if (!curParentRelation) {
|
||||||
|
return { id: lastParentNode.id, depth: 1 };
|
||||||
|
}
|
||||||
|
let hasChildren = lastParentNode.children !== 0;
|
||||||
|
let depth = 1;
|
||||||
|
let finalID: null | string = null;
|
||||||
|
while (hasChildren) {
|
||||||
|
if (curParentRelation) {
|
||||||
|
const lastChild = curParentRelation.children.sort((a, b) => a.position - b.position)[
|
||||||
|
curParentRelation.children.length - 1
|
||||||
|
];
|
||||||
|
depth += 1;
|
||||||
|
if (lastChild.children === 0) {
|
||||||
|
finalID = lastChild.id;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
curParentRelation = outline.relationships.get(lastChild.id);
|
||||||
|
} else {
|
||||||
|
hasChildren = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (finalID !== null) {
|
||||||
|
return { id: finalID, depth };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
@ -134,16 +134,17 @@ type MemberFilterOptions = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const fetchMembers = async (client: any, projectID: string, options: MemberFilterOptions, input: string, cb: any) => {
|
const fetchMembers = async (client: any, projectID: string, options: MemberFilterOptions, input: string, cb: any) => {
|
||||||
|
console.log(input.trim().length < 3);
|
||||||
if (input && input.trim().length < 3) {
|
if (input && input.trim().length < 3) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const res = await client.query({
|
const res = await client.query({
|
||||||
query: gql`
|
query: gql`
|
||||||
query {
|
query {
|
||||||
searchMembers(input: {SearchFilter:"${input}", projectID:"${projectID}"}) {
|
searchMembers(input: {searchFilter:"${input}", projectID:"${projectID}"}) {
|
||||||
|
id
|
||||||
similarity
|
similarity
|
||||||
confirmed
|
status
|
||||||
joined
|
|
||||||
user {
|
user {
|
||||||
id
|
id
|
||||||
fullName
|
fullName
|
||||||
@ -161,16 +162,34 @@ const fetchMembers = async (client: any, projectID: string, options: MemberFilte
|
|||||||
|
|
||||||
let results: any = [];
|
let results: any = [];
|
||||||
const emails: Array<string> = [];
|
const emails: Array<string> = [];
|
||||||
|
console.log(res.data && res.data.searchMembers);
|
||||||
if (res.data && res.data.searchMembers) {
|
if (res.data && res.data.searchMembers) {
|
||||||
results = [
|
results = [
|
||||||
...res.data.searchMembers.map((m: any) => {
|
...res.data.searchMembers.map((m: any) => {
|
||||||
emails.push(m.user.email);
|
if (m.status === 'INVITED') {
|
||||||
return {
|
console.log(`${m.id} is added`);
|
||||||
label: m.user.fullName,
|
return {
|
||||||
value: { id: m.user.id, type: 0, profileIcon: m.user.profileIcon },
|
label: m.id,
|
||||||
};
|
value: {
|
||||||
|
id: m.id,
|
||||||
|
type: 2,
|
||||||
|
profileIcon: {
|
||||||
|
bgColor: '#ccc',
|
||||||
|
initials: m.id.charAt(0),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
console.log(`${m.user.email} is added`);
|
||||||
|
emails.push(m.user.email);
|
||||||
|
return {
|
||||||
|
label: m.user.fullName,
|
||||||
|
value: { id: m.user.id, type: 0, profileIcon: m.user.profileIcon },
|
||||||
|
};
|
||||||
|
}
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
console.log(results);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (RFC2822_EMAIL.test(input) && !emails.find(e => e === input)) {
|
if (RFC2822_EMAIL.test(input) && !emails.find(e => e === input)) {
|
||||||
@ -215,6 +234,13 @@ const OptionContent = styled.div`
|
|||||||
margin-left: 12px;
|
margin-left: 12px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const OptionLabel = styled.span<{ fontSize: number; quiet: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: ${p => p.fontSize}px;
|
||||||
|
color: rgba(${p => (p.quiet ? p.theme.colors.text.primary : p.theme.colors.text.primary)});
|
||||||
|
`;
|
||||||
|
|
||||||
const UserOption: React.FC<UserOptionProps> = ({ isDisabled, isFocused, innerProps, label, data }) => {
|
const UserOption: React.FC<UserOptionProps> = ({ isDisabled, isFocused, innerProps, label, data }) => {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
return !isDisabled ? (
|
return !isDisabled ? (
|
||||||
@ -223,11 +249,20 @@ const UserOption: React.FC<UserOptionProps> = ({ isDisabled, isFocused, innerPro
|
|||||||
size={32}
|
size={32}
|
||||||
member={{
|
member={{
|
||||||
id: '',
|
id: '',
|
||||||
fullName: 'Jordan Knott',
|
fullName: data.value.label,
|
||||||
profileIcon: data.value.profileIcon,
|
profileIcon: data.value.profileIcon,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<OptionContent>{label}</OptionContent>
|
<OptionContent>
|
||||||
|
<OptionLabel fontSize={16} quiet={false}>
|
||||||
|
{label}
|
||||||
|
</OptionLabel>
|
||||||
|
{data.value.type === 2 && (
|
||||||
|
<OptionLabel fontSize={14} quiet>
|
||||||
|
Joined
|
||||||
|
</OptionLabel>
|
||||||
|
)}
|
||||||
|
</OptionContent>
|
||||||
</OptionWrapper>
|
</OptionWrapper>
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
@ -73,7 +73,6 @@ export const HeaderName = styled(TextareaAutosize)`
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
margin: -4px 0;
|
margin: -4px 0;
|
||||||
padding: 4px 8px;
|
|
||||||
|
|
||||||
letter-spacing: normal;
|
letter-spacing: normal;
|
||||||
word-spacing: normal;
|
word-spacing: normal;
|
||||||
|
12
frontend/src/shared/icons/ArrowDown.tsx
Normal file
12
frontend/src/shared/icons/ArrowDown.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Icon, { IconProps } from './Icon';
|
||||||
|
|
||||||
|
const ArrowDown: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
|
||||||
|
return (
|
||||||
|
<Icon width={width} height={height} className={className} viewBox="0 0 448 512">
|
||||||
|
<path d="M413.1 222.5l22.2 22.2c9.4 9.4 9.4 24.6 0 33.9L241 473c-9.4 9.4-24.6 9.4-33.9 0L12.7 278.6c-9.4-9.4-9.4-24.6 0-33.9l22.2-22.2c9.5-9.5 25-9.3 34.3.4L184 343.4V56c0-13.3 10.7-24 24-24h32c13.3 0 24 10.7 24 24v287.4l114.8-120.5c9.3-9.8 24.8-10 34.3-.4z" />
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ArrowDown;
|
12
frontend/src/shared/icons/CaretDown.tsx
Normal file
12
frontend/src/shared/icons/CaretDown.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Icon, { IconProps } from './Icon';
|
||||||
|
|
||||||
|
const CaretDown: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
|
||||||
|
return (
|
||||||
|
<Icon width={width} height={height} className={className} viewBox="0 0 320 512">
|
||||||
|
<path d="M31.3 192h257.3c17.8 0 26.7 21.5 14.1 34.1L174.1 354.8c-7.8 7.8-20.5 7.8-28.3 0L17.2 226.1C4.6 213.5 13.5 192 31.3 192z" />
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CaretDown;
|
12
frontend/src/shared/icons/CaretRight.tsx
Normal file
12
frontend/src/shared/icons/CaretRight.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Icon, { IconProps } from './Icon';
|
||||||
|
|
||||||
|
const CaretRight: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
|
||||||
|
return (
|
||||||
|
<Icon width={width} height={height} className={className} viewBox="0 0 192 512">
|
||||||
|
<path d="M0 384.662V127.338c0-17.818 21.543-26.741 34.142-14.142l128.662 128.662c7.81 7.81 7.81 20.474 0 28.284L34.142 398.804C21.543 411.404 0 402.48 0 384.662z" />
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CaretRight;
|
12
frontend/src/shared/icons/Dot.tsx
Normal file
12
frontend/src/shared/icons/Dot.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import Icon, { IconProps } from './Icon';
|
||||||
|
|
||||||
|
const Dot: React.FC<IconProps> = ({ width = '16px', height = '16px', className }) => {
|
||||||
|
return (
|
||||||
|
<Icon width={width} height={height} className={className} viewBox="0 0 18 18">
|
||||||
|
<circle cx="9" cy="9" r="3.5" />
|
||||||
|
</Icon>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dot;
|
@ -1,8 +1,12 @@
|
|||||||
import Cross from './Cross';
|
import Cross from './Cross';
|
||||||
import Cog from './Cog';
|
import Cog from './Cog';
|
||||||
|
import ArrowDown from './ArrowDown';
|
||||||
import ListUnordered from './ListUnordered';
|
import ListUnordered from './ListUnordered';
|
||||||
|
import Dot from './Dot';
|
||||||
|
import CaretDown from './CaretDown';
|
||||||
import Eye from './Eye';
|
import Eye from './Eye';
|
||||||
import EyeSlash from './EyeSlash';
|
import EyeSlash from './EyeSlash';
|
||||||
|
import CaretRight from './CaretRight';
|
||||||
import List from './List';
|
import List from './List';
|
||||||
import At from './At';
|
import At from './At';
|
||||||
import Task from './Task';
|
import Task from './Task';
|
||||||
@ -94,5 +98,9 @@ export {
|
|||||||
ListUnordered,
|
ListUnordered,
|
||||||
EyeSlash,
|
EyeSlash,
|
||||||
List,
|
List,
|
||||||
|
CaretDown,
|
||||||
|
Dot,
|
||||||
|
ArrowDown,
|
||||||
|
CaretRight,
|
||||||
DotCircle,
|
DotCircle,
|
||||||
};
|
};
|
||||||
|
2
frontend/src/taskcafe.d.ts
vendored
2
frontend/src/taskcafe.d.ts
vendored
@ -150,6 +150,7 @@ type OutlineNode = {
|
|||||||
depth: number;
|
depth: number;
|
||||||
position: number;
|
position: number;
|
||||||
ancestors: Array<string>;
|
ancestors: Array<string>;
|
||||||
|
collapsed: boolean;
|
||||||
children: number;
|
children: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -199,6 +200,7 @@ type ItemElement = {
|
|||||||
id: string;
|
id: string;
|
||||||
parent: null | string;
|
parent: null | string;
|
||||||
position: number;
|
position: number;
|
||||||
|
collapsed: boolean;
|
||||||
children?: Array<ItemElement>;
|
children?: Array<ItemElement>;
|
||||||
};
|
};
|
||||||
type NodeDimensions = {
|
type NodeDimensions = {
|
||||||
|
@ -229,15 +229,16 @@ func (q *Queries) GetAllVisibleProjectsForUserID(ctx context.Context, userID uui
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getInvitedMembersForProjectID = `-- name: GetInvitedMembersForProjectID :many
|
const getInvitedMembersForProjectID = `-- name: GetInvitedMembersForProjectID :many
|
||||||
SELECT email, invited_on FROM project_member_invited AS pmi
|
SELECT uai.user_account_invited_id, email, invited_on FROM project_member_invited AS pmi
|
||||||
INNER JOIN user_account_invited AS uai
|
INNER JOIN user_account_invited AS uai
|
||||||
ON uai.user_account_invited_id = pmi.user_account_invited_id
|
ON uai.user_account_invited_id = pmi.user_account_invited_id
|
||||||
WHERE project_id = $1
|
WHERE project_id = $1
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetInvitedMembersForProjectIDRow struct {
|
type GetInvitedMembersForProjectIDRow struct {
|
||||||
Email string `json:"email"`
|
UserAccountInvitedID uuid.UUID `json:"user_account_invited_id"`
|
||||||
InvitedOn time.Time `json:"invited_on"`
|
Email string `json:"email"`
|
||||||
|
InvitedOn time.Time `json:"invited_on"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) GetInvitedMembersForProjectID(ctx context.Context, projectID uuid.UUID) ([]GetInvitedMembersForProjectIDRow, error) {
|
func (q *Queries) GetInvitedMembersForProjectID(ctx context.Context, projectID uuid.UUID) ([]GetInvitedMembersForProjectIDRow, error) {
|
||||||
@ -249,7 +250,7 @@ func (q *Queries) GetInvitedMembersForProjectID(ctx context.Context, projectID u
|
|||||||
var items []GetInvitedMembersForProjectIDRow
|
var items []GetInvitedMembersForProjectIDRow
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i GetInvitedMembersForProjectIDRow
|
var i GetInvitedMembersForProjectIDRow
|
||||||
if err := rows.Scan(&i.Email, &i.InvitedOn); err != nil {
|
if err := rows.Scan(&i.UserAccountInvitedID, &i.Email, &i.InvitedOn); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items = append(items, i)
|
items = append(items, i)
|
||||||
|
@ -44,7 +44,7 @@ SELECT project_id, role_code FROM project_member WHERE user_id = $1;
|
|||||||
SELECT project_id FROM project_member WHERE user_id = $1;
|
SELECT project_id FROM project_member WHERE user_id = $1;
|
||||||
|
|
||||||
-- name: GetInvitedMembersForProjectID :many
|
-- name: GetInvitedMembersForProjectID :many
|
||||||
SELECT email, invited_on FROM project_member_invited AS pmi
|
SELECT uai.user_account_invited_id, email, invited_on FROM project_member_invited AS pmi
|
||||||
INNER JOIN user_account_invited AS uai
|
INNER JOIN user_account_invited AS uai
|
||||||
ON uai.user_account_invited_id = pmi.user_account_invited_id
|
ON uai.user_account_invited_id = pmi.user_account_invited_id
|
||||||
WHERE project_id = $1;
|
WHERE project_id = $1;
|
||||||
|
@ -183,10 +183,9 @@ type ComplexityRoot struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MemberSearchResult struct {
|
MemberSearchResult struct {
|
||||||
Confirmed func(childComplexity int) int
|
ID func(childComplexity int) int
|
||||||
Invited func(childComplexity int) int
|
|
||||||
Joined func(childComplexity int) int
|
|
||||||
Similarity func(childComplexity int) int
|
Similarity func(childComplexity int) int
|
||||||
|
Status func(childComplexity int) int
|
||||||
User func(childComplexity int) int
|
User func(childComplexity int) int
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1027,26 +1026,12 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||||||
|
|
||||||
return e.complexity.MemberList.Teams(childComplexity), true
|
return e.complexity.MemberList.Teams(childComplexity), true
|
||||||
|
|
||||||
case "MemberSearchResult.confirmed":
|
case "MemberSearchResult.id":
|
||||||
if e.complexity.MemberSearchResult.Confirmed == nil {
|
if e.complexity.MemberSearchResult.ID == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return e.complexity.MemberSearchResult.Confirmed(childComplexity), true
|
return e.complexity.MemberSearchResult.ID(childComplexity), true
|
||||||
|
|
||||||
case "MemberSearchResult.invited":
|
|
||||||
if e.complexity.MemberSearchResult.Invited == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.complexity.MemberSearchResult.Invited(childComplexity), true
|
|
||||||
|
|
||||||
case "MemberSearchResult.joined":
|
|
||||||
if e.complexity.MemberSearchResult.Joined == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return e.complexity.MemberSearchResult.Joined(childComplexity), true
|
|
||||||
|
|
||||||
case "MemberSearchResult.similarity":
|
case "MemberSearchResult.similarity":
|
||||||
if e.complexity.MemberSearchResult.Similarity == nil {
|
if e.complexity.MemberSearchResult.Similarity == nil {
|
||||||
@ -1055,6 +1040,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
|
|||||||
|
|
||||||
return e.complexity.MemberSearchResult.Similarity(childComplexity), true
|
return e.complexity.MemberSearchResult.Similarity(childComplexity), true
|
||||||
|
|
||||||
|
case "MemberSearchResult.status":
|
||||||
|
if e.complexity.MemberSearchResult.Status == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.complexity.MemberSearchResult.Status(childComplexity), true
|
||||||
|
|
||||||
case "MemberSearchResult.user":
|
case "MemberSearchResult.user":
|
||||||
if e.complexity.MemberSearchResult.User == nil {
|
if e.complexity.MemberSearchResult.User == nil {
|
||||||
break
|
break
|
||||||
@ -2841,6 +2833,11 @@ type TaskChecklist {
|
|||||||
items: [TaskChecklistItem!]!
|
items: [TaskChecklistItem!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ShareStatus {
|
||||||
|
INVITED
|
||||||
|
JOINED
|
||||||
|
}
|
||||||
|
|
||||||
enum RoleLevel {
|
enum RoleLevel {
|
||||||
ADMIN
|
ADMIN
|
||||||
MEMBER
|
MEMBER
|
||||||
@ -3452,16 +3449,16 @@ type DeleteInvitedUserAccountPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input MemberSearchFilter {
|
input MemberSearchFilter {
|
||||||
SearchFilter: String!
|
searchFilter: String!
|
||||||
projectID: UUID
|
projectID: UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type MemberSearchResult {
|
type MemberSearchResult {
|
||||||
similarity: Int!
|
similarity: Int!
|
||||||
user: UserAccount!
|
id: String!
|
||||||
confirmed: Boolean!
|
user: UserAccount
|
||||||
invited: Boolean!
|
status: ShareStatus!
|
||||||
joined: Boolean!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateUserInfoPayload {
|
type UpdateUserInfoPayload {
|
||||||
@ -6373,6 +6370,40 @@ func (ec *executionContext) _MemberSearchResult_similarity(ctx context.Context,
|
|||||||
return ec.marshalNInt2int(ctx, field.Selections, res)
|
return ec.marshalNInt2int(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) _MemberSearchResult_id(ctx context.Context, field graphql.CollectedField, obj *MemberSearchResult) (ret graphql.Marshaler) {
|
||||||
|
defer func() {
|
||||||
|
if r := recover(); r != nil {
|
||||||
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
|
ret = graphql.Null
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
fc := &graphql.FieldContext{
|
||||||
|
Object: "MemberSearchResult",
|
||||||
|
Field: field,
|
||||||
|
Args: nil,
|
||||||
|
IsMethod: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||||
|
ctx = rctx // use context from middleware stack in children
|
||||||
|
return obj.ID, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ec.Error(ctx, err)
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
if resTmp == nil {
|
||||||
|
if !graphql.HasFieldError(ctx, fc) {
|
||||||
|
ec.Errorf(ctx, "must not be null")
|
||||||
|
}
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
res := resTmp.(string)
|
||||||
|
fc.Result = res
|
||||||
|
return ec.marshalNString2string(ctx, field.Selections, res)
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _MemberSearchResult_user(ctx context.Context, field graphql.CollectedField, obj *MemberSearchResult) (ret graphql.Marshaler) {
|
func (ec *executionContext) _MemberSearchResult_user(ctx context.Context, field graphql.CollectedField, obj *MemberSearchResult) (ret graphql.Marshaler) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
@ -6397,17 +6428,14 @@ func (ec *executionContext) _MemberSearchResult_user(ctx context.Context, field
|
|||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
if resTmp == nil {
|
if resTmp == nil {
|
||||||
if !graphql.HasFieldError(ctx, fc) {
|
|
||||||
ec.Errorf(ctx, "must not be null")
|
|
||||||
}
|
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
res := resTmp.(*db.UserAccount)
|
res := resTmp.(*db.UserAccount)
|
||||||
fc.Result = res
|
fc.Result = res
|
||||||
return ec.marshalNUserAccount2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐUserAccount(ctx, field.Selections, res)
|
return ec.marshalOUserAccount2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐUserAccount(ctx, field.Selections, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _MemberSearchResult_confirmed(ctx context.Context, field graphql.CollectedField, obj *MemberSearchResult) (ret graphql.Marshaler) {
|
func (ec *executionContext) _MemberSearchResult_status(ctx context.Context, field graphql.CollectedField, obj *MemberSearchResult) (ret graphql.Marshaler) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
ec.Error(ctx, ec.Recover(ctx, r))
|
ec.Error(ctx, ec.Recover(ctx, r))
|
||||||
@ -6424,7 +6452,7 @@ func (ec *executionContext) _MemberSearchResult_confirmed(ctx context.Context, f
|
|||||||
ctx = graphql.WithFieldContext(ctx, fc)
|
ctx = graphql.WithFieldContext(ctx, fc)
|
||||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
||||||
ctx = rctx // use context from middleware stack in children
|
ctx = rctx // use context from middleware stack in children
|
||||||
return obj.Confirmed, nil
|
return obj.Status, nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ec.Error(ctx, err)
|
ec.Error(ctx, err)
|
||||||
@ -6436,77 +6464,9 @@ func (ec *executionContext) _MemberSearchResult_confirmed(ctx context.Context, f
|
|||||||
}
|
}
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
}
|
}
|
||||||
res := resTmp.(bool)
|
res := resTmp.(ShareStatus)
|
||||||
fc.Result = res
|
fc.Result = res
|
||||||
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
|
return ec.marshalNShareStatus2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐShareStatus(ctx, field.Selections, res)
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) _MemberSearchResult_invited(ctx context.Context, field graphql.CollectedField, obj *MemberSearchResult) (ret graphql.Marshaler) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
ec.Error(ctx, ec.Recover(ctx, r))
|
|
||||||
ret = graphql.Null
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
fc := &graphql.FieldContext{
|
|
||||||
Object: "MemberSearchResult",
|
|
||||||
Field: field,
|
|
||||||
Args: nil,
|
|
||||||
IsMethod: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = graphql.WithFieldContext(ctx, fc)
|
|
||||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
|
||||||
ctx = rctx // use context from middleware stack in children
|
|
||||||
return obj.Invited, nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
ec.Error(ctx, err)
|
|
||||||
return graphql.Null
|
|
||||||
}
|
|
||||||
if resTmp == nil {
|
|
||||||
if !graphql.HasFieldError(ctx, fc) {
|
|
||||||
ec.Errorf(ctx, "must not be null")
|
|
||||||
}
|
|
||||||
return graphql.Null
|
|
||||||
}
|
|
||||||
res := resTmp.(bool)
|
|
||||||
fc.Result = res
|
|
||||||
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ec *executionContext) _MemberSearchResult_joined(ctx context.Context, field graphql.CollectedField, obj *MemberSearchResult) (ret graphql.Marshaler) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r != nil {
|
|
||||||
ec.Error(ctx, ec.Recover(ctx, r))
|
|
||||||
ret = graphql.Null
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
fc := &graphql.FieldContext{
|
|
||||||
Object: "MemberSearchResult",
|
|
||||||
Field: field,
|
|
||||||
Args: nil,
|
|
||||||
IsMethod: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx = graphql.WithFieldContext(ctx, fc)
|
|
||||||
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
|
|
||||||
ctx = rctx // use context from middleware stack in children
|
|
||||||
return obj.Joined, nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
ec.Error(ctx, err)
|
|
||||||
return graphql.Null
|
|
||||||
}
|
|
||||||
if resTmp == nil {
|
|
||||||
if !graphql.HasFieldError(ctx, fc) {
|
|
||||||
ec.Errorf(ctx, "must not be null")
|
|
||||||
}
|
|
||||||
return graphql.Null
|
|
||||||
}
|
|
||||||
res := resTmp.(bool)
|
|
||||||
fc.Result = res
|
|
||||||
return ec.marshalNBoolean2bool(ctx, field.Selections, res)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ec *executionContext) _Mutation_createProject(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
func (ec *executionContext) _Mutation_createProject(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
|
||||||
@ -16279,7 +16239,7 @@ func (ec *executionContext) unmarshalInputMemberSearchFilter(ctx context.Context
|
|||||||
|
|
||||||
for k, v := range asMap {
|
for k, v := range asMap {
|
||||||
switch k {
|
switch k {
|
||||||
case "SearchFilter":
|
case "searchFilter":
|
||||||
var err error
|
var err error
|
||||||
it.SearchFilter, err = ec.unmarshalNString2string(ctx, v)
|
it.SearchFilter, err = ec.unmarshalNString2string(ctx, v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -17982,23 +17942,15 @@ func (ec *executionContext) _MemberSearchResult(ctx context.Context, sel ast.Sel
|
|||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
invalids++
|
invalids++
|
||||||
}
|
}
|
||||||
|
case "id":
|
||||||
|
out.Values[i] = ec._MemberSearchResult_id(ctx, field, obj)
|
||||||
|
if out.Values[i] == graphql.Null {
|
||||||
|
invalids++
|
||||||
|
}
|
||||||
case "user":
|
case "user":
|
||||||
out.Values[i] = ec._MemberSearchResult_user(ctx, field, obj)
|
out.Values[i] = ec._MemberSearchResult_user(ctx, field, obj)
|
||||||
if out.Values[i] == graphql.Null {
|
case "status":
|
||||||
invalids++
|
out.Values[i] = ec._MemberSearchResult_status(ctx, field, obj)
|
||||||
}
|
|
||||||
case "confirmed":
|
|
||||||
out.Values[i] = ec._MemberSearchResult_confirmed(ctx, field, obj)
|
|
||||||
if out.Values[i] == graphql.Null {
|
|
||||||
invalids++
|
|
||||||
}
|
|
||||||
case "invited":
|
|
||||||
out.Values[i] = ec._MemberSearchResult_invited(ctx, field, obj)
|
|
||||||
if out.Values[i] == graphql.Null {
|
|
||||||
invalids++
|
|
||||||
}
|
|
||||||
case "joined":
|
|
||||||
out.Values[i] = ec._MemberSearchResult_joined(ctx, field, obj)
|
|
||||||
if out.Values[i] == graphql.Null {
|
if out.Values[i] == graphql.Null {
|
||||||
invalids++
|
invalids++
|
||||||
}
|
}
|
||||||
@ -21486,6 +21438,15 @@ func (ec *executionContext) unmarshalNSetTaskComplete2githubᚗcomᚋjordanknott
|
|||||||
return ec.unmarshalInputSetTaskComplete(ctx, v)
|
return ec.unmarshalInputSetTaskComplete(ctx, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) unmarshalNShareStatus2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐShareStatus(ctx context.Context, v interface{}) (ShareStatus, error) {
|
||||||
|
var res ShareStatus
|
||||||
|
return res, res.UnmarshalGQL(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalNShareStatus2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐShareStatus(ctx context.Context, sel ast.SelectionSet, v ShareStatus) graphql.Marshaler {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) unmarshalNSortTaskGroup2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐSortTaskGroup(ctx context.Context, v interface{}) (SortTaskGroup, error) {
|
func (ec *executionContext) unmarshalNSortTaskGroup2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋgraphᚐSortTaskGroup(ctx context.Context, v interface{}) (SortTaskGroup, error) {
|
||||||
return ec.unmarshalInputSortTaskGroup(ctx, v)
|
return ec.unmarshalInputSortTaskGroup(ctx, v)
|
||||||
}
|
}
|
||||||
@ -22625,6 +22586,17 @@ func (ec *executionContext) unmarshalOUpdateProjectName2ᚖgithubᚗcomᚋjordan
|
|||||||
return &res, err
|
return &res, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalOUserAccount2githubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐUserAccount(ctx context.Context, sel ast.SelectionSet, v db.UserAccount) graphql.Marshaler {
|
||||||
|
return ec._UserAccount(ctx, sel, &v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ec *executionContext) marshalOUserAccount2ᚖgithubᚗcomᚋjordanknottᚋtaskcafeᚋinternalᚋdbᚐUserAccount(ctx context.Context, sel ast.SelectionSet, v *db.UserAccount) graphql.Marshaler {
|
||||||
|
if v == nil {
|
||||||
|
return graphql.Null
|
||||||
|
}
|
||||||
|
return ec._UserAccount(ctx, sel, v)
|
||||||
|
}
|
||||||
|
|
||||||
func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler {
|
func (ec *executionContext) marshalO__EnumValue2ᚕgithubᚗcomᚋ99designsᚋgqlgenᚋgraphqlᚋintrospectionᚐEnumValueᚄ(ctx context.Context, sel ast.SelectionSet, v []introspection.EnumValue) graphql.Marshaler {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
return graphql.Null
|
return graphql.Null
|
||||||
|
@ -249,16 +249,15 @@ type MemberList struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MemberSearchFilter struct {
|
type MemberSearchFilter struct {
|
||||||
SearchFilter string `json:"SearchFilter"`
|
SearchFilter string `json:"searchFilter"`
|
||||||
ProjectID *uuid.UUID `json:"projectID"`
|
ProjectID *uuid.UUID `json:"projectID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MemberSearchResult struct {
|
type MemberSearchResult struct {
|
||||||
Similarity int `json:"similarity"`
|
Similarity int `json:"similarity"`
|
||||||
|
ID string `json:"id"`
|
||||||
User *db.UserAccount `json:"user"`
|
User *db.UserAccount `json:"user"`
|
||||||
Confirmed bool `json:"confirmed"`
|
Status ShareStatus `json:"status"`
|
||||||
Invited bool `json:"invited"`
|
|
||||||
Joined bool `json:"joined"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type NewProject struct {
|
type NewProject struct {
|
||||||
@ -830,3 +829,44 @@ func (e *RoleLevel) UnmarshalGQL(v interface{}) error {
|
|||||||
func (e RoleLevel) MarshalGQL(w io.Writer) {
|
func (e RoleLevel) MarshalGQL(w io.Writer) {
|
||||||
fmt.Fprint(w, strconv.Quote(e.String()))
|
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ShareStatus string
|
||||||
|
|
||||||
|
const (
|
||||||
|
ShareStatusInvited ShareStatus = "INVITED"
|
||||||
|
ShareStatusJoined ShareStatus = "JOINED"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AllShareStatus = []ShareStatus{
|
||||||
|
ShareStatusInvited,
|
||||||
|
ShareStatusJoined,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ShareStatus) IsValid() bool {
|
||||||
|
switch e {
|
||||||
|
case ShareStatusInvited, ShareStatusJoined:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ShareStatus) String() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *ShareStatus) UnmarshalGQL(v interface{}) error {
|
||||||
|
str, ok := v.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("enums must be strings")
|
||||||
|
}
|
||||||
|
|
||||||
|
*e = ShareStatus(str)
|
||||||
|
if !e.IsValid() {
|
||||||
|
return fmt.Errorf("%s is not a valid ShareStatus", str)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ShareStatus) MarshalGQL(w io.Writer) {
|
||||||
|
fmt.Fprint(w, strconv.Quote(e.String()))
|
||||||
|
}
|
||||||
|
@ -172,6 +172,11 @@ type TaskChecklist {
|
|||||||
items: [TaskChecklistItem!]!
|
items: [TaskChecklistItem!]!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum ShareStatus {
|
||||||
|
INVITED
|
||||||
|
JOINED
|
||||||
|
}
|
||||||
|
|
||||||
enum RoleLevel {
|
enum RoleLevel {
|
||||||
ADMIN
|
ADMIN
|
||||||
MEMBER
|
MEMBER
|
||||||
@ -783,16 +788,16 @@ type DeleteInvitedUserAccountPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input MemberSearchFilter {
|
input MemberSearchFilter {
|
||||||
SearchFilter: String!
|
searchFilter: String!
|
||||||
projectID: UUID
|
projectID: UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type MemberSearchResult {
|
type MemberSearchResult {
|
||||||
similarity: Int!
|
similarity: Int!
|
||||||
user: UserAccount!
|
id: String!
|
||||||
confirmed: Boolean!
|
user: UserAccount
|
||||||
invited: Boolean!
|
status: ShareStatus!
|
||||||
joined: Boolean!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateUserInfoPayload {
|
type UpdateUserInfoPayload {
|
||||||
|
@ -1310,31 +1310,52 @@ func (r *queryResolver) SearchMembers(ctx context.Context, input MemberSearchFil
|
|||||||
return []MemberSearchResult{}, err
|
return []MemberSearchResult{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
invitedMembers, err := r.Repository.GetInvitedMembersForProjectID(ctx, *input.ProjectID)
|
||||||
|
if err != nil {
|
||||||
|
logger.New(ctx).WithField("projectID", input.ProjectID).WithError(err).Error("error while getting member data")
|
||||||
|
return []MemberSearchResult{}, err
|
||||||
|
}
|
||||||
|
|
||||||
sortList := []string{}
|
sortList := []string{}
|
||||||
masterList := map[string]uuid.UUID{}
|
masterList := map[string]MasterEntry{}
|
||||||
for _, member := range availableMembers {
|
for _, member := range availableMembers {
|
||||||
sortList = append(sortList, member.Username)
|
sortList = append(sortList, member.Username)
|
||||||
sortList = append(sortList, member.Email)
|
sortList = append(sortList, member.Email)
|
||||||
masterList[member.Username] = member.UserID
|
masterList[member.Username] = MasterEntry{ID: member.UserID, MemberType: MemberTypeJoined}
|
||||||
masterList[member.Email] = member.UserID
|
masterList[member.Email] = MasterEntry{ID: member.UserID, MemberType: MemberTypeJoined}
|
||||||
}
|
}
|
||||||
logger.New(ctx).Info("fuzzy rank finder")
|
for _, member := range invitedMembers {
|
||||||
|
sortList = append(sortList, member.Email)
|
||||||
|
logger.New(ctx).WithField("Email", member.Email).Info("adding member")
|
||||||
|
masterList[member.Email] = MasterEntry{ID: member.UserAccountInvitedID, MemberType: MemberTypeInvited}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.New(ctx).WithField("searchFilter", input.SearchFilter).Info(sortList)
|
||||||
rankedList := fuzzy.RankFind(input.SearchFilter, sortList)
|
rankedList := fuzzy.RankFind(input.SearchFilter, sortList)
|
||||||
|
logger.New(ctx).Info(rankedList)
|
||||||
results := []MemberSearchResult{}
|
results := []MemberSearchResult{}
|
||||||
memberList := map[uuid.UUID]bool{}
|
memberList := map[uuid.UUID]bool{}
|
||||||
for _, rank := range rankedList {
|
for _, rank := range rankedList {
|
||||||
if _, ok := memberList[masterList[rank.Target]]; !ok {
|
entry, _ := masterList[rank.Target]
|
||||||
logger.New(ctx).WithFields(log.Fields{"source": rank.Source, "target": rank.Target}).Info("searching")
|
_, ok := memberList[entry.ID]
|
||||||
userID := masterList[rank.Target]
|
logger.New(ctx).WithField("ok", ok).WithField("target", rank.Target).Info("checking rank")
|
||||||
user, err := r.Repository.GetUserAccountByID(ctx, userID)
|
if !ok {
|
||||||
if err != nil {
|
if entry.MemberType == MemberTypeJoined {
|
||||||
if err == sql.ErrNoRows {
|
logger.New(ctx).WithFields(log.Fields{"source": rank.Source, "target": rank.Target}).Info("searching")
|
||||||
continue
|
entry := masterList[rank.Target]
|
||||||
|
user, err := r.Repository.GetUserAccountByID(ctx, entry.ID)
|
||||||
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return []MemberSearchResult{}, err
|
||||||
}
|
}
|
||||||
return []MemberSearchResult{}, err
|
results = append(results, MemberSearchResult{ID: user.UserID.String(), User: &user, Status: ShareStatusJoined, Similarity: rank.Distance})
|
||||||
|
} else {
|
||||||
|
logger.New(ctx).WithField("id", rank.Target).Info("adding target")
|
||||||
|
results = append(results, MemberSearchResult{ID: rank.Target, Status: ShareStatusInvited, Similarity: rank.Distance})
|
||||||
}
|
}
|
||||||
results = append(results, MemberSearchResult{User: &user, Joined: false, Confirmed: false, Similarity: rank.Distance})
|
memberList[entry.ID] = true
|
||||||
memberList[masterList[rank.Target]] = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return results, nil
|
return results, nil
|
||||||
@ -1621,9 +1642,7 @@ func (r *Resolver) Task() TaskResolver { return &taskResolver{r} }
|
|||||||
func (r *Resolver) TaskChecklist() TaskChecklistResolver { return &taskChecklistResolver{r} }
|
func (r *Resolver) TaskChecklist() TaskChecklistResolver { return &taskChecklistResolver{r} }
|
||||||
|
|
||||||
// TaskChecklistItem returns TaskChecklistItemResolver implementation.
|
// TaskChecklistItem returns TaskChecklistItemResolver implementation.
|
||||||
func (r *Resolver) TaskChecklistItem() TaskChecklistItemResolver {
|
func (r *Resolver) TaskChecklistItem() TaskChecklistItemResolver { return &taskChecklistItemResolver{r} }
|
||||||
return &taskChecklistItemResolver{r}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TaskGroup returns TaskGroupResolver implementation.
|
// TaskGroup returns TaskGroupResolver implementation.
|
||||||
func (r *Resolver) TaskGroup() TaskGroupResolver { return &taskGroupResolver{r} }
|
func (r *Resolver) TaskGroup() TaskGroupResolver { return &taskGroupResolver{r} }
|
||||||
@ -1652,3 +1671,21 @@ type taskGroupResolver struct{ *Resolver }
|
|||||||
type taskLabelResolver struct{ *Resolver }
|
type taskLabelResolver struct{ *Resolver }
|
||||||
type teamResolver struct{ *Resolver }
|
type teamResolver struct{ *Resolver }
|
||||||
type userAccountResolver struct{ *Resolver }
|
type userAccountResolver struct{ *Resolver }
|
||||||
|
|
||||||
|
// !!! WARNING !!!
|
||||||
|
// The code below was going to be deleted when updating resolvers. It has been copied here so you have
|
||||||
|
// one last chance to move it out of harms way if you want. There are two reasons this happens:
|
||||||
|
// - When renaming or deleting a resolver the old code will be put in here. You can safely delete
|
||||||
|
// it when you're done.
|
||||||
|
// - You have helper methods in this file. Move them out to keep these resolver files clean.
|
||||||
|
type MemberType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
MemberTypeInvited MemberType = "INVITED"
|
||||||
|
MemberTypeJoined MemberType = "JOINED"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MasterEntry struct {
|
||||||
|
MemberType MemberType
|
||||||
|
ID uuid.UUID
|
||||||
|
}
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
enum ShareStatus {
|
||||||
|
INVITED
|
||||||
|
JOINED
|
||||||
|
}
|
||||||
|
|
||||||
enum RoleLevel {
|
enum RoleLevel {
|
||||||
ADMIN
|
ADMIN
|
||||||
MEMBER
|
MEMBER
|
||||||
|
@ -30,16 +30,16 @@ type DeleteInvitedUserAccountPayload {
|
|||||||
}
|
}
|
||||||
|
|
||||||
input MemberSearchFilter {
|
input MemberSearchFilter {
|
||||||
SearchFilter: String!
|
searchFilter: String!
|
||||||
projectID: UUID
|
projectID: UUID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type MemberSearchResult {
|
type MemberSearchResult {
|
||||||
similarity: Int!
|
similarity: Int!
|
||||||
user: UserAccount!
|
id: String!
|
||||||
confirmed: Boolean!
|
user: UserAccount
|
||||||
invited: Boolean!
|
status: ShareStatus!
|
||||||
joined: Boolean!
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateUserInfoPayload {
|
type UpdateUserInfoPayload {
|
||||||
|
Loading…
Reference in New Issue
Block a user