feat: multi-select scope for referral network graph

Replace single-select scope with multi-select supporting multiple
campaigns, partners, and users simultaneously. Chips UI with remove
buttons, clear-all, sorted query keys for cache stability.
This commit is contained in:
Fringg
2026-03-20 02:31:11 +03:00
parent a6faf702ec
commit db76cd0c64
8 changed files with 364 additions and 190 deletions

View File

@@ -1,6 +1,8 @@
import { create } from 'zustand';
import type { SelectedNode, NetworkFilters, ScopeSelection } from '@/types/referralNetwork';
const MAX_SCOPE_ITEMS = 50;
const DEFAULT_FILTERS: NetworkFilters = {
campaigns: [],
partnersOnly: false,
@@ -12,14 +14,16 @@ interface ReferralNetworkState {
hoveredNodeId: string | null;
highlightedNodes: Set<string>;
filters: NetworkFilters;
scope: ScopeSelection | null;
scope: ScopeSelection[];
setSelectedNode: (node: SelectedNode) => void;
setHoveredNode: (id: string | null) => void;
setHighlightedNodes: (nodes: Set<string>) => void;
updateFilters: (filters: Partial<NetworkFilters>) => void;
resetFilters: () => void;
setScope: (scope: ScopeSelection | null) => void;
addScope: (item: ScopeSelection) => void;
removeScope: (type: ScopeSelection['type'], id: number) => void;
clearScope: () => void;
}
export const useReferralNetworkStore = create<ReferralNetworkState>()((set) => ({
@@ -27,12 +31,10 @@ export const useReferralNetworkStore = create<ReferralNetworkState>()((set) => (
hoveredNodeId: null,
highlightedNodes: new Set<string>(),
filters: { ...DEFAULT_FILTERS },
scope: null,
scope: [],
setSelectedNode: (node) => set({ selectedNode: node }),
setHoveredNode: (id) => set({ hoveredNodeId: id }),
setHighlightedNodes: (nodes) => set({ highlightedNodes: nodes }),
updateFilters: (partial) =>
@@ -42,9 +44,32 @@ export const useReferralNetworkStore = create<ReferralNetworkState>()((set) => (
resetFilters: () => set({ filters: { ...DEFAULT_FILTERS } }),
setScope: (scope) =>
addScope: (item) =>
set((state) => {
const exists = state.scope.some((s) => s.type === item.type && s.id === item.id);
if (exists) return state;
if (state.scope.length >= MAX_SCOPE_ITEMS) return state;
return {
scope: [...state.scope, item],
selectedNode: null,
hoveredNodeId: null,
highlightedNodes: new Set<string>(),
filters: { ...DEFAULT_FILTERS },
};
}),
removeScope: (type, id) =>
set((state) => ({
scope: state.scope.filter((s) => !(s.type === type && s.id === id)),
selectedNode: null,
hoveredNodeId: null,
highlightedNodes: new Set<string>(),
filters: { ...DEFAULT_FILTERS },
})),
clearScope: () =>
set({
scope,
scope: [],
selectedNode: null,
hoveredNodeId: null,
highlightedNodes: new Set<string>(),