Skip to content

Commit a5ddf73

Browse files
committed
add filter search
1 parent 60bf5fb commit a5ddf73

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

10941143
if (!isOpen) return null;
10951144

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

0 commit comments

Comments
 (0)