Skip to content

Commit 27923b7

Browse files
committed
add filter search
1 parent fae7ad4 commit 27923b7

1 file changed

Lines changed: 178 additions & 35 deletions

File tree

apps/roam/src/components/canvas/Clipboard.tsx

Lines changed: 178 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@ import {
1414
Collapse,
1515
Dialog,
1616
Icon,
17+
InputGroup,
1718
Intent,
19+
Menu,
20+
MenuItem,
1821
NonIdealState,
1922
Popover,
2023
Position,
@@ -53,6 +56,9 @@ import { openBlockInSidebar, createBlock } from "roamjs-components/writes";
5356
import getPageTitleByPageUid from "roamjs-components/queries/getPageTitleByPageUid";
5457
import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle";
5558
import findDiscourseNode from "~/utils/findDiscourseNode";
59+
import getDiscourseNodes, {
60+
excludeDefaultNodes,
61+
} from "~/utils/getDiscourseNodes";
5662
import calcCanvasNodeSizeAndImg from "~/utils/calcCanvasNodeSizeAndImg";
5763
import { useExtensionAPI } from "roamjs-components/components/ExtensionApiContext";
5864
import { getDiscourseNodeColors } from "~/utils/getDiscourseNodeColors";
@@ -389,6 +395,7 @@ const AddPageModal = ({ isOpen, onClose, onConfirm }: AddPageModalProps) => {
389395
type NodeGroup = {
390396
uid: string;
391397
text: string;
398+
type: string;
392399
shapes: DiscourseNodeShape[];
393400
isDuplicate: boolean;
394401
};
@@ -422,10 +429,16 @@ const ClipboardPageSection = ({
422429
page,
423430
onRemove,
424431
showNodesOnCanvas,
432+
searchQuery,
433+
sortDirection,
434+
selectedNodeType,
425435
}: {
426436
page: ClipboardPage;
427437
onRemove: (uid: string) => void;
428438
showNodesOnCanvas: boolean;
439+
searchQuery: string;
440+
sortDirection: "asc" | "desc";
441+
selectedNodeType: string;
429442
}) => {
430443
const [isOpen, setIsOpen] = useState(true);
431444
const [discourseNodes, setDiscourseNodes] = useState<
@@ -534,24 +547,48 @@ const ClipboardPageSection = ({
534547
const groupedNodes = useMemo(() => {
535548
const groups: NodeGroup[] = discourseNodes.map((node) => {
536549
const shapes = shapesByUid.get(node.uid) ?? [];
550+
const discourseNode = findDiscourseNode({ uid: node.uid });
537551
return {
538552
uid: node.uid,
539553
text: node.text,
554+
type: discourseNode ? discourseNode.text : "Unknown",
540555
shapes,
541556
isDuplicate: shapes.length > 1,
542557
};
543558
});
544559

545-
return groups.sort((a, b) => a.text.localeCompare(b.text));
560+
return groups;
546561
// eslint-disable-next-line react-hooks/exhaustive-deps
547562
}, [discourseNodes, shapesByUid]);
548563

549564
const visibleGroupedNodes = useMemo(
550565
() =>
551-
groupedNodes.filter((group) =>
552-
showNodesOnCanvas ? true : group.shapes.length === 0,
553-
),
554-
[groupedNodes, showNodesOnCanvas],
566+
groupedNodes
567+
.filter((group) =>
568+
showNodesOnCanvas ? true : group.shapes.length === 0,
569+
)
570+
.filter((group) =>
571+
searchQuery
572+
? group.text.toLowerCase().includes(searchQuery.toLowerCase())
573+
: true,
574+
)
575+
.filter((group) =>
576+
selectedNodeType && selectedNodeType !== "All"
577+
? group.type === selectedNodeType
578+
: true,
579+
)
580+
.sort((a, b) =>
581+
sortDirection === "asc"
582+
? a.text.localeCompare(b.text)
583+
: b.text.localeCompare(a.text),
584+
),
585+
[
586+
groupedNodes,
587+
showNodesOnCanvas,
588+
searchQuery,
589+
selectedNodeType,
590+
sortDirection,
591+
],
555592
);
556593

557594
useEffect(() => {
@@ -949,8 +986,9 @@ const ClipboardPageSection = ({
949986
</div>
950987
) : visibleGroupedNodes.length === 0 ? (
951988
<div className="rounded border border-dashed border-gray-200 p-2">
952-
All nodes from this page are already on canvas. Turn on &quot;Show
953-
nodes on canvas&quot; to view them.
989+
{searchQuery || selectedNodeType !== "All"
990+
? "No nodes match the current filters."
991+
: 'All nodes from this page are already on canvas. Turn on "Show nodes on canvas" to view them.'}
954992
</div>
955993
) : (
956994
<div className="space-y-1">
@@ -1091,6 +1129,17 @@ export const ClipboardPanel = () => {
10911129
} = useClipboard();
10921130
const [isModalOpen, setIsModalOpen] = useState(false);
10931131
const [isCollapsed, setIsCollapsed] = useState(false);
1132+
const [searchQuery, setSearchQuery] = useState("");
1133+
const [isSearchExpanded, setIsSearchExpanded] = useState(false);
1134+
const [sortDirection, setSortDirection] = useState<"asc" | "desc">("asc");
1135+
const [selectedNodeType, setSelectedNodeType] = useState("All");
1136+
1137+
const availableNodeTypes = useMemo(() => {
1138+
const types = getDiscourseNodes().filter(excludeDefaultNodes);
1139+
return ["All", ...types.map((t) => t.text)];
1140+
}, []);
1141+
1142+
const hasActiveFilters = !!searchQuery || selectedNodeType !== "All";
10941143

10951144
if (!isOpen) return null;
10961145

@@ -1119,7 +1168,7 @@ export const ClipboardPanel = () => {
11191168
</h2>
11201169
<div className="flex-shrink-0">
11211170
<Button
1122-
icon={<Icon icon="minus" />}
1171+
icon={<Icon icon={isCollapsed ? "chevron-down" : "minus"} />}
11231172
onClick={() => setIsCollapsed(!isCollapsed)}
11241173
minimal
11251174
small
@@ -1138,35 +1187,126 @@ export const ClipboardPanel = () => {
11381187
</div>
11391188
{!isCollapsed && (
11401189
<>
1141-
<div
1142-
className="flex items-center justify-end px-2 py-1"
1143-
style={{ borderTop: "1px solid hsl(0, 0%, 91%)" }}
1144-
>
1145-
<Popover
1146-
position={Position.BOTTOM_RIGHT}
1147-
content={
1148-
<div
1149-
className="p-3"
1150-
onPointerDown={(e) => e.stopPropagation()}
1151-
style={{ pointerEvents: "all" }}
1152-
>
1153-
<Switch
1154-
checked={showNodesOnCanvas}
1155-
alignIndicator="right"
1156-
className="m-0 w-full"
1157-
label="Show nodes on canvas"
1158-
onChange={(e) =>
1159-
setShowNodesOnCanvas(
1160-
(e.target as HTMLInputElement).checked,
1161-
)
1162-
}
1190+
{isSearchExpanded ? (
1191+
<div
1192+
className="px-2 py-1"
1193+
style={{ borderTop: "1px solid hsl(0, 0%, 91%)" }}
1194+
>
1195+
<InputGroup
1196+
autoFocus
1197+
leftIcon="search"
1198+
placeholder="Find page"
1199+
value={searchQuery}
1200+
onChange={(e) => setSearchQuery(e.target.value)}
1201+
onBlur={() => {
1202+
if (!searchQuery) setIsSearchExpanded(false);
1203+
}}
1204+
rightElement={
1205+
<Button
1206+
minimal
1207+
small
1208+
icon="cross"
1209+
onClick={() => {
1210+
setSearchQuery("");
1211+
setIsSearchExpanded(false);
1212+
}}
11631213
/>
1164-
</div>
1165-
}
1214+
}
1215+
/>
1216+
</div>
1217+
) : (
1218+
<div
1219+
className="flex items-center gap-1 px-2 py-1"
1220+
style={{ borderTop: "1px solid hsl(0, 0%, 91%)" }}
11661221
>
1167-
<Button minimal small icon="menu" title="Clipboard options" />
1168-
</Popover>
1169-
</div>
1222+
<Button
1223+
minimal
1224+
small
1225+
icon="search"
1226+
onClick={() => setIsSearchExpanded(true)}
1227+
/>
1228+
<Button
1229+
minimal
1230+
small
1231+
icon={
1232+
sortDirection === "asc"
1233+
? "sort-alphabetical"
1234+
: "sort-alphabetical-desc"
1235+
}
1236+
title={
1237+
sortDirection === "asc"
1238+
? "Sorted A→Z (click for Z→A)"
1239+
: "Sorted Z→A (click for A→Z)"
1240+
}
1241+
onClick={() =>
1242+
setSortDirection((d) => (d === "asc" ? "desc" : "asc"))
1243+
}
1244+
/>
1245+
<Popover
1246+
position={Position.BOTTOM}
1247+
content={
1248+
<Menu>
1249+
{availableNodeTypes.map((type) => (
1250+
<MenuItem
1251+
key={type}
1252+
text={type}
1253+
active={selectedNodeType === type}
1254+
onClick={() => setSelectedNodeType(type)}
1255+
/>
1256+
))}
1257+
</Menu>
1258+
}
1259+
>
1260+
<Button
1261+
minimal
1262+
small
1263+
rightIcon="caret-down"
1264+
text={selectedNodeType}
1265+
/>
1266+
</Popover>
1267+
{hasActiveFilters && (
1268+
<Button
1269+
minimal
1270+
small
1271+
icon="filter-remove"
1272+
onClick={() => {
1273+
setSearchQuery("");
1274+
setSelectedNodeType("All");
1275+
}}
1276+
title="Clear filters"
1277+
/>
1278+
)}
1279+
<Popover
1280+
position={Position.BOTTOM_RIGHT}
1281+
content={
1282+
<div
1283+
className="p-3"
1284+
onPointerDown={(e) => e.stopPropagation()}
1285+
style={{ pointerEvents: "all" }}
1286+
>
1287+
<Switch
1288+
checked={showNodesOnCanvas}
1289+
alignIndicator="right"
1290+
className="m-0 w-full"
1291+
label="Show nodes on canvas"
1292+
onChange={(e) =>
1293+
setShowNodesOnCanvas(
1294+
(e.target as HTMLInputElement).checked,
1295+
)
1296+
}
1297+
/>
1298+
</div>
1299+
}
1300+
>
1301+
<Button
1302+
minimal
1303+
small
1304+
icon="settings"
1305+
title="Clipboard options"
1306+
/>
1307+
</Popover>
1308+
</div>
1309+
)}
11701310
<div className="max-h-96 overflow-y-auto px-4 pb-4">
11711311
{pages.length === 0 ? (
11721312
<NonIdealState
@@ -1188,6 +1328,9 @@ export const ClipboardPanel = () => {
11881328
page={page}
11891329
onRemove={removePage}
11901330
showNodesOnCanvas={showNodesOnCanvas}
1331+
searchQuery={searchQuery}
1332+
sortDirection={sortDirection}
1333+
selectedNodeType={selectedNodeType}
11911334
/>
11921335
))}
11931336
</div>

0 commit comments

Comments
 (0)