Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 6 additions & 44 deletions apps/obsidian/src/components/NodeTypeFilterMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,21 @@ import { getAllDiscourseNodeColors } from "~/utils/colorUtils";
import {
NODE_TYPE_FILTER_SEARCH_THRESHOLD,
filterNodeTypesByQuery,
fromPanelSelectedIds,
hasActiveTypeFilter,
toPanelSelectedIds,
} from "~/utils/discourseNodeTypeFilter";

const NodeTypeFilterRow = ({
color,
isChecked,
nodeType,
onSelectOnly,
onToggle,
}: {
color: string | undefined;
isChecked: boolean;
nodeType: DiscourseNode;
onSelectOnly: () => void;
onToggle: () => void;
}): ReactElement => (
<div className="hover:bg-modifier-hover group flex items-center gap-2 px-3 py-1.5">
<div className="hover:bg-modifier-hover flex items-center gap-2 px-3 py-1.5">
<label className="flex min-w-0 flex-1 cursor-pointer items-center gap-2">
<input
type="checkbox"
Expand All @@ -40,19 +36,6 @@ const NodeTypeFilterRow = ({
)}
<span className="text-normal truncate text-sm">{nodeType.name}</span>
</label>
<button
type="button"
// Hidden until the row is hovered or focused so the list stays scannable,
// but focusable by keyboard rather than pointer-only.
className="text-muted hover:text-normal shrink-0 opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100"
onClick={(event) => {
event.stopPropagation();
onSelectOnly();
}}
onMouseDown={(event) => event.preventDefault()}
>
Only
</button>
</div>
);

Expand Down Expand Up @@ -102,9 +85,7 @@ const NodeTypeFilterPanel = ({
return (
<>
{/* Clearing is the only thing this control ever does, so it says so and
appears only when there is a filter to clear. A "select all" checkbox
would sit checked-and-inert whenever no filter is active, since an empty
selection and a full one are the same state. */}
appears only when there is a filter to clear. */}
{isFilterActive && (
<div className="border-modifier-border border-b p-2">
<button
Expand Down Expand Up @@ -141,7 +122,6 @@ const NodeTypeFilterPanel = ({
color={colorsById.get(nodeType.id)}
isChecked={selectedIdSet.has(nodeType.id)}
nodeType={nodeType}
onSelectOnly={() => onSelectedIdsChange([nodeType.id])}
onToggle={() => toggleType(nodeType.id)}
/>
))
Expand All @@ -166,23 +146,9 @@ export const NodeTypeFilterMenu = ({
onSelectedNodeTypeIdsChange: (ids: string[]) => void;
selectedNodeTypeIds: string[];
}): ReactElement => {
const allTypeIds = useMemo(
() => nodeTypes.map((nodeType) => nodeType.id),
[nodeTypes],
);

const isFilterActive = hasActiveTypeFilter({
selectedTypeIds: selectedNodeTypeIds,
allTypeIds,
});

const panelSelectedIds = useMemo(
() =>
toPanelSelectedIds({ selectedTypeIds: selectedNodeTypeIds, allTypeIds }),
[allTypeIds, selectedNodeTypeIds],
);
const isFilterActive = hasActiveTypeFilter(selectedNodeTypeIds);

const activeFilterCount = isFilterActive ? selectedNodeTypeIds.length : 0;
const activeFilterCount = selectedNodeTypeIds.length;

return (
<SearchDropdown
Expand All @@ -208,12 +174,8 @@ export const NodeTypeFilterMenu = ({
<NodeTypeFilterPanel
isFilterActive={isFilterActive}
nodeTypes={nodeTypes}
onSelectedIdsChange={(panelIds) =>
onSelectedNodeTypeIdsChange(
fromPanelSelectedIds({ panelSelectedIds: panelIds, allTypeIds }),
)
}
selectedIds={panelSelectedIds}
onSelectedIdsChange={onSelectedNodeTypeIdsChange}
selectedIds={selectedNodeTypeIds}
/>
</SearchDropdown>
);
Expand Down
40 changes: 4 additions & 36 deletions apps/obsidian/src/utils/discourseNodeTypeFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,16 @@ import { DiscourseNode } from "~/types";

/**
* Node type filtering for the search modal. An empty `selectedTypeIds` means no
* filter, matching `filterCandidatesByNodeTypeIds` in QueryEngine — so "nothing
* selected" and "every type selected" are the same state and both show all nodes.
* filter, matching `filterCandidatesByNodeTypeIds` in QueryEngine — so nothing
* checked shows every node, and checking a type narrows results to it.
* Ported from Roam's advanced search so both apps filter alike.
*/

/** Type count above which the panel adds a search box; the modal is desktop-only. */
export const NODE_TYPE_FILTER_SEARCH_THRESHOLD = 7;

export const hasActiveTypeFilter = ({
selectedTypeIds,
allTypeIds,
}: {
selectedTypeIds: string[];
allTypeIds: string[];
}): boolean =>
selectedTypeIds.length > 0 && selectedTypeIds.length < allTypeIds.length;

/** Shows no-filter as every row checked, so the panel is never an empty checklist. */
export const toPanelSelectedIds = ({
selectedTypeIds,
allTypeIds,
}: {
selectedTypeIds: string[];
allTypeIds: string[];
}): string[] => (selectedTypeIds.length === 0 ? allTypeIds : selectedTypeIds);

/** Collapses both "all checked" and "none checked" back to no filter. */
export const fromPanelSelectedIds = ({
panelSelectedIds,
allTypeIds,
}: {
panelSelectedIds: string[];
allTypeIds: string[];
}): string[] => {
if (
panelSelectedIds.length === 0 ||
panelSelectedIds.length === allTypeIds.length
) {
return [];
}
return panelSelectedIds;
};
export const hasActiveTypeFilter = (selectedTypeIds: string[]): boolean =>
selectedTypeIds.length > 0;

export const filterNodeTypesByQuery = (
nodeTypes: DiscourseNode[],
Expand Down