mirror of
https://github.com/chillpadclub/bedolaga-cabinet.git
synced 2026-07-28 09:33:46 +00:00
feat(referral-network): кнопка построения полной реферальной сети
Со времён редизайна со скоуп-селектором граф строится только для выбранных кампаний, партнёров или пользователей, и посмотреть сеть целиком стало нечем. Бэкенд для этого уже есть — GET /cabinet/admin/ referral-network/ отдаёт полный граф и остался неиспользуемым после того, как фронт перешёл на /scoped. Возвращаем к нему доступ: в дропдауне селектора появляется закреплённый пункт «Вся сеть», который грузит граф целиком. Режим взаимоисключающий с выбором чипов — полный граф и так содержит все сущности.
This commit is contained in:
@@ -14,6 +14,11 @@ export const referralNetworkApi = {
|
|||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getFullGraph: async (): Promise<NetworkGraphData> => {
|
||||||
|
const response = await apiClient.get('/cabinet/admin/referral-network/');
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
|
|
||||||
getScopedGraph: async (selections: ScopeSelection[]): Promise<NetworkGraphData> => {
|
getScopedGraph: async (selections: ScopeSelection[]): Promise<NetworkGraphData> => {
|
||||||
const campaignIds = selections.filter((s) => s.type === 'campaign').map((s) => s.id);
|
const campaignIds = selections.filter((s) => s.type === 'campaign').map((s) => s.id);
|
||||||
const partnerIds = selections.filter((s) => s.type === 'partner').map((s) => s.id);
|
const partnerIds = selections.filter((s) => s.type === 'partner').map((s) => s.id);
|
||||||
|
|||||||
@@ -1498,7 +1498,10 @@
|
|||||||
"removeItem": "Remove {{label}}",
|
"removeItem": "Remove {{label}}",
|
||||||
"clearAll": "Clear all selections",
|
"clearAll": "Clear all selections",
|
||||||
"addScope": "Add scope",
|
"addScope": "Add scope",
|
||||||
"maxReached": "Maximum {{max}} items"
|
"maxReached": "Maximum {{max}} items",
|
||||||
|
"fullNetwork": "Full network",
|
||||||
|
"fullNetworkHint": "Show the entire graph",
|
||||||
|
"removeFullNetwork": "Turn off full network mode"
|
||||||
},
|
},
|
||||||
"loading": "Loading graph...",
|
"loading": "Loading graph...",
|
||||||
"error": "Failed to load network data",
|
"error": "Failed to load network data",
|
||||||
|
|||||||
@@ -1520,7 +1520,10 @@
|
|||||||
"removeItem": "Удалить {{label}}",
|
"removeItem": "Удалить {{label}}",
|
||||||
"clearAll": "Очистить все выбранные",
|
"clearAll": "Очистить все выбранные",
|
||||||
"addScope": "Добавить область",
|
"addScope": "Добавить область",
|
||||||
"maxReached": "Максимум {{max}} элементов"
|
"maxReached": "Максимум {{max}} элементов",
|
||||||
|
"fullNetwork": "Вся сеть",
|
||||||
|
"fullNetworkHint": "Показать граф целиком",
|
||||||
|
"removeFullNetwork": "Выключить режим полной сети"
|
||||||
},
|
},
|
||||||
"loading": "Загрузка графа...",
|
"loading": "Загрузка графа...",
|
||||||
"error": "Ошибка загрузки данных сети",
|
"error": "Ошибка загрузки данных сети",
|
||||||
|
|||||||
@@ -17,21 +17,26 @@ export function ReferralNetwork() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const selectedNode = useReferralNetworkStore((s) => s.selectedNode);
|
const selectedNode = useReferralNetworkStore((s) => s.selectedNode);
|
||||||
const scope = useReferralNetworkStore((s) => s.scope);
|
const scope = useReferralNetworkStore((s) => s.scope);
|
||||||
|
const isFullNetwork = useReferralNetworkStore((s) => s.isFullNetwork);
|
||||||
const addScope = useReferralNetworkStore((s) => s.addScope);
|
const addScope = useReferralNetworkStore((s) => s.addScope);
|
||||||
const removeScope = useReferralNetworkStore((s) => s.removeScope);
|
const removeScope = useReferralNetworkStore((s) => s.removeScope);
|
||||||
const clearScope = useReferralNetworkStore((s) => s.clearScope);
|
const clearScope = useReferralNetworkStore((s) => s.clearScope);
|
||||||
|
const setFullNetwork = useReferralNetworkStore((s) => s.setFullNetwork);
|
||||||
|
|
||||||
const { mobile: mobileHeaderHeight, bottomSafeArea } = useHeaderHeight();
|
const { mobile: mobileHeaderHeight, bottomSafeArea } = useHeaderHeight();
|
||||||
|
|
||||||
const hasScope = scope.length > 0;
|
const hasScope = isFullNetwork || scope.length > 0;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: networkData,
|
data: networkData,
|
||||||
isLoading,
|
isLoading,
|
||||||
isError,
|
isError,
|
||||||
} = useQuery({
|
} = useQuery({
|
||||||
queryKey: ['referral-network', 'scoped', scope.map((s) => `${s.type}:${s.id}`).sort()],
|
queryKey: isFullNetwork
|
||||||
queryFn: () => referralNetworkApi.getScopedGraph(scope),
|
? ['referral-network', 'full']
|
||||||
|
: ['referral-network', 'scoped', scope.map((s) => `${s.type}:${s.id}`).sort()],
|
||||||
|
queryFn: () =>
|
||||||
|
isFullNetwork ? referralNetworkApi.getFullGraph() : referralNetworkApi.getScopedGraph(scope),
|
||||||
enabled: hasScope,
|
enabled: hasScope,
|
||||||
staleTime: 120_000,
|
staleTime: 120_000,
|
||||||
});
|
});
|
||||||
@@ -60,9 +65,11 @@ export function ReferralNetwork() {
|
|||||||
</div>
|
</div>
|
||||||
<ScopeSelector
|
<ScopeSelector
|
||||||
value={scope}
|
value={scope}
|
||||||
|
isFullNetwork={isFullNetwork}
|
||||||
onAdd={addScope}
|
onAdd={addScope}
|
||||||
onRemove={removeScope}
|
onRemove={removeScope}
|
||||||
onClear={clearScope}
|
onClear={clearScope}
|
||||||
|
onFullNetworkChange={setFullNetwork}
|
||||||
className="min-w-0 flex-1 sm:max-w-xl"
|
className="min-w-0 flex-1 sm:max-w-xl"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,33 +2,41 @@ import { useState, useEffect, useRef, useMemo, useCallback } from 'react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
import { referralNetworkApi } from '@/api/referralNetwork';
|
import { referralNetworkApi } from '@/api/referralNetwork';
|
||||||
import { CheckIcon, CloseIcon, PlusIcon, SearchIcon } from '@/components/icons';
|
import { CheckIcon, CloseIcon, PlusIcon, SearchIcon, ShareIcon } from '@/components/icons';
|
||||||
import { MAX_SCOPE_ITEMS } from '@/store/referralNetwork';
|
import { MAX_SCOPE_ITEMS } from '@/store/referralNetwork';
|
||||||
import type { ScopeSelection, ScopeType } from '@/types/referralNetwork';
|
import type { ScopeSelection, ScopeType } from '@/types/referralNetwork';
|
||||||
|
|
||||||
interface ScopeSelectorProps {
|
interface ScopeSelectorProps {
|
||||||
value: ScopeSelection[];
|
value: ScopeSelection[];
|
||||||
|
isFullNetwork: boolean;
|
||||||
onAdd: (selection: ScopeSelection) => void;
|
onAdd: (selection: ScopeSelection) => void;
|
||||||
onRemove: (type: ScopeSelection['type'], id: number) => void;
|
onRemove: (type: ScopeSelection['type'], id: number) => void;
|
||||||
onClear: () => void;
|
onClear: () => void;
|
||||||
|
onFullNetworkChange: (enabled: boolean) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCOPE_TABS: ScopeType[] = ['campaign', 'partner', 'user'];
|
const SCOPE_TABS: ScopeType[] = ['campaign', 'partner', 'user'];
|
||||||
|
|
||||||
const CHIP_COLORS: Record<ScopeType, string> = {
|
// 'all' is the full-network mode rather than a scope entity, so it gets a
|
||||||
|
// neutral chip instead of one of the three entity colours.
|
||||||
|
type OptionType = ScopeType | 'all';
|
||||||
|
|
||||||
|
const CHIP_COLORS: Record<OptionType, string> = {
|
||||||
campaign: 'bg-success-500/20 text-success-400',
|
campaign: 'bg-success-500/20 text-success-400',
|
||||||
partner: 'bg-warning-500/20 text-warning-400',
|
partner: 'bg-warning-500/20 text-warning-400',
|
||||||
user: 'bg-accent-500/20 text-accent-400',
|
user: 'bg-accent-500/20 text-accent-400',
|
||||||
|
all: 'bg-dark-700 text-dark-100',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reuse CHIP_COLORS for avatar backgrounds (same palette)
|
// Reuse CHIP_COLORS for avatar backgrounds (same palette)
|
||||||
const AVATAR_COLORS = CHIP_COLORS;
|
const AVATAR_COLORS = CHIP_COLORS;
|
||||||
|
|
||||||
const AVATAR_LETTERS: Record<ScopeType, string> = {
|
const AVATAR_GLYPHS: Record<OptionType, React.ReactNode> = {
|
||||||
campaign: 'C',
|
campaign: 'C',
|
||||||
partner: 'P',
|
partner: 'P',
|
||||||
user: 'U',
|
user: 'U',
|
||||||
|
all: <ShareIcon className="h-4 w-4" />,
|
||||||
};
|
};
|
||||||
|
|
||||||
function Spinner({ size = 'h-5 w-5' }: { size?: string }) {
|
function Spinner({ size = 'h-5 w-5' }: { size?: string }) {
|
||||||
@@ -44,7 +52,7 @@ function EmptyMessage({ text }: { text: string }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ScopeListItemProps {
|
interface ScopeListItemProps {
|
||||||
type: ScopeType;
|
type: OptionType;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -65,7 +73,7 @@ function ScopeListItem({ type, selected, onClick, title, subtitle, badge }: Scop
|
|||||||
<div
|
<div
|
||||||
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-xs font-medium ${AVATAR_COLORS[type]}`}
|
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-full text-xs font-medium ${AVATAR_COLORS[type]}`}
|
||||||
>
|
>
|
||||||
{selected ? <CheckIcon /> : AVATAR_LETTERS[type]}
|
{selected ? <CheckIcon /> : AVATAR_GLYPHS[type]}
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="truncate text-sm font-medium text-dark-100">{title}</p>
|
<p className="truncate text-sm font-medium text-dark-100">{title}</p>
|
||||||
@@ -76,7 +84,15 @@ function ScopeListItem({ type, selected, onClick, title, subtitle, badge }: Scop
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ScopeSelector({ value, onAdd, onRemove, onClear, className }: ScopeSelectorProps) {
|
export function ScopeSelector({
|
||||||
|
value,
|
||||||
|
isFullNetwork,
|
||||||
|
onAdd,
|
||||||
|
onRemove,
|
||||||
|
onClear,
|
||||||
|
onFullNetworkChange,
|
||||||
|
className,
|
||||||
|
}: ScopeSelectorProps) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [activeTab, setActiveTab] = useState<ScopeType>('campaign');
|
const [activeTab, setActiveTab] = useState<ScopeType>('campaign');
|
||||||
const [searchInput, setSearchInput] = useState('');
|
const [searchInput, setSearchInput] = useState('');
|
||||||
@@ -211,8 +227,24 @@ export function ScopeSelector({ value, onAdd, onRemove, onClear, className }: Sc
|
|||||||
{/* Chips row + add trigger */}
|
{/* Chips row + add trigger */}
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
{/* Selected chips */}
|
{/* Selected chips */}
|
||||||
{value.length > 0 && (
|
{(isFullNetwork || value.length > 0) && (
|
||||||
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-1">
|
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-1">
|
||||||
|
{isFullNetwork && (
|
||||||
|
<span
|
||||||
|
className={`inline-flex shrink-0 items-center gap-1 rounded-md px-2 py-0.5 text-xs font-medium ${CHIP_COLORS.all}`}
|
||||||
|
>
|
||||||
|
<span className="max-w-[120px] truncate">
|
||||||
|
{t('admin.referralNetwork.scope.fullNetwork')}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
onClick={() => onFullNetworkChange(false)}
|
||||||
|
aria-label={t('admin.referralNetwork.scope.removeFullNetwork')}
|
||||||
|
className="ml-0.5 rounded-sm p-0.5 transition-colors hover:bg-white/10"
|
||||||
|
>
|
||||||
|
<CloseIcon className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
{value.map((item) => (
|
{value.map((item) => (
|
||||||
<span
|
<span
|
||||||
key={`${item.type}:${item.id}`}
|
key={`${item.type}:${item.id}`}
|
||||||
@@ -312,8 +344,18 @@ export function ScopeSelector({ value, onAdd, onRemove, onClear, className }: Sc
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* List */}
|
{/* List — the full-network option leads every tab, since it is a mode
|
||||||
|
rather than an entity of the currently active tab. */}
|
||||||
<div className="max-h-64 overflow-y-auto" role="listbox" aria-multiselectable="true">
|
<div className="max-h-64 overflow-y-auto" role="listbox" aria-multiselectable="true">
|
||||||
|
<div className="border-b border-dark-700/50">
|
||||||
|
<ScopeListItem
|
||||||
|
type="all"
|
||||||
|
selected={isFullNetwork}
|
||||||
|
onClick={() => onFullNetworkChange(!isFullNetwork)}
|
||||||
|
title={t('admin.referralNetwork.scope.fullNetwork')}
|
||||||
|
subtitle={t('admin.referralNetwork.scope.fullNetworkHint')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
{activeTab === 'campaign' && renderCampaignList()}
|
{activeTab === 'campaign' && renderCampaignList()}
|
||||||
{activeTab === 'partner' && renderPartnerList()}
|
{activeTab === 'partner' && renderPartnerList()}
|
||||||
{activeTab === 'user' && renderUserList()}
|
{activeTab === 'user' && renderUserList()}
|
||||||
|
|||||||
105
src/store/referralNetwork.test.ts
Normal file
105
src/store/referralNetwork.test.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
import { MAX_SCOPE_ITEMS, useReferralNetworkStore } from './referralNetwork';
|
||||||
|
import type { ScopeSelection } from '@/types/referralNetwork';
|
||||||
|
|
||||||
|
const INITIAL_STATE = useReferralNetworkStore.getState();
|
||||||
|
|
||||||
|
function campaign(id: number): ScopeSelection {
|
||||||
|
return { type: 'campaign', id, label: `Campaign ${id}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('referralNetwork store', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
useReferralNetworkStore.setState(INITIAL_STATE, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts with no scope and full network off', () => {
|
||||||
|
const state = useReferralNetworkStore.getState();
|
||||||
|
expect(state.scope).toEqual([]);
|
||||||
|
expect(state.isFullNetwork).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('drops the selected scope when full network is enabled', () => {
|
||||||
|
const { addScope, setFullNetwork } = useReferralNetworkStore.getState();
|
||||||
|
addScope(campaign(1));
|
||||||
|
addScope(campaign(2));
|
||||||
|
|
||||||
|
setFullNetwork(true);
|
||||||
|
|
||||||
|
const state = useReferralNetworkStore.getState();
|
||||||
|
expect(state.isFullNetwork).toBe(true);
|
||||||
|
expect(state.scope).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves full network mode when a scope item is added', () => {
|
||||||
|
const { setFullNetwork, addScope } = useReferralNetworkStore.getState();
|
||||||
|
setFullNetwork(true);
|
||||||
|
|
||||||
|
addScope(campaign(1));
|
||||||
|
|
||||||
|
const state = useReferralNetworkStore.getState();
|
||||||
|
expect(state.isFullNetwork).toBe(false);
|
||||||
|
expect(state.scope).toEqual([campaign(1)]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resets the graph selection when switching to full network', () => {
|
||||||
|
const { setSelectedNode, setHighlightedNodes, setFullNetwork } =
|
||||||
|
useReferralNetworkStore.getState();
|
||||||
|
setSelectedNode({ type: 'user', id: 7 });
|
||||||
|
setHighlightedNodes(new Set(['user_7']));
|
||||||
|
|
||||||
|
setFullNetwork(true);
|
||||||
|
|
||||||
|
const state = useReferralNetworkStore.getState();
|
||||||
|
expect(state.selectedNode).toBeNull();
|
||||||
|
expect(state.highlightedNodes.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('turns full network off without leaving a stale scope', () => {
|
||||||
|
const { setFullNetwork } = useReferralNetworkStore.getState();
|
||||||
|
setFullNetwork(true);
|
||||||
|
|
||||||
|
setFullNetwork(false);
|
||||||
|
|
||||||
|
const state = useReferralNetworkStore.getState();
|
||||||
|
expect(state.isFullNetwork).toBe(false);
|
||||||
|
expect(state.scope).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears both the scope and full network mode', () => {
|
||||||
|
const { setFullNetwork, clearScope } = useReferralNetworkStore.getState();
|
||||||
|
setFullNetwork(true);
|
||||||
|
|
||||||
|
clearScope();
|
||||||
|
|
||||||
|
const state = useReferralNetworkStore.getState();
|
||||||
|
expect(state.isFullNetwork).toBe(false);
|
||||||
|
expect(state.scope).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores duplicate scope items and keeps full network off', () => {
|
||||||
|
const { addScope } = useReferralNetworkStore.getState();
|
||||||
|
addScope(campaign(1));
|
||||||
|
addScope(campaign(1));
|
||||||
|
|
||||||
|
const state = useReferralNetworkStore.getState();
|
||||||
|
expect(state.scope).toEqual([campaign(1)]);
|
||||||
|
expect(state.isFullNetwork).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not exit full network mode when the scope is already full', () => {
|
||||||
|
const { addScope, setFullNetwork } = useReferralNetworkStore.getState();
|
||||||
|
for (let id = 1; id <= MAX_SCOPE_ITEMS; id++) {
|
||||||
|
addScope(campaign(id));
|
||||||
|
}
|
||||||
|
setFullNetwork(true);
|
||||||
|
|
||||||
|
// The cap is per-scope; with the scope dropped by full-network mode there is
|
||||||
|
// nothing to reject, so this add must switch the mode off and take effect.
|
||||||
|
addScope(campaign(999));
|
||||||
|
|
||||||
|
const state = useReferralNetworkStore.getState();
|
||||||
|
expect(state.isFullNetwork).toBe(false);
|
||||||
|
expect(state.scope).toEqual([campaign(999)]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -24,12 +24,14 @@ interface ReferralNetworkState {
|
|||||||
highlightedNodes: Set<string>;
|
highlightedNodes: Set<string>;
|
||||||
filters: NetworkFilters;
|
filters: NetworkFilters;
|
||||||
scope: ScopeSelection[];
|
scope: ScopeSelection[];
|
||||||
|
isFullNetwork: boolean;
|
||||||
|
|
||||||
setSelectedNode: (node: SelectedNode) => void;
|
setSelectedNode: (node: SelectedNode) => void;
|
||||||
setHoveredNode: (id: string | null) => void;
|
setHoveredNode: (id: string | null) => void;
|
||||||
setHighlightedNodes: (nodes: Set<string>) => void;
|
setHighlightedNodes: (nodes: Set<string>) => void;
|
||||||
updateFilters: (filters: Partial<NetworkFilters>) => void;
|
updateFilters: (filters: Partial<NetworkFilters>) => void;
|
||||||
resetFilters: () => void;
|
resetFilters: () => void;
|
||||||
|
setFullNetwork: (enabled: boolean) => void;
|
||||||
addScope: (item: ScopeSelection) => void;
|
addScope: (item: ScopeSelection) => void;
|
||||||
removeScope: (type: ScopeSelection['type'], id: number) => void;
|
removeScope: (type: ScopeSelection['type'], id: number) => void;
|
||||||
clearScope: () => void;
|
clearScope: () => void;
|
||||||
@@ -41,6 +43,7 @@ export const useReferralNetworkStore = create<ReferralNetworkState>()((set) => (
|
|||||||
highlightedNodes: new Set<string>(),
|
highlightedNodes: new Set<string>(),
|
||||||
filters: { ...DEFAULT_FILTERS },
|
filters: { ...DEFAULT_FILTERS },
|
||||||
scope: [],
|
scope: [],
|
||||||
|
isFullNetwork: false,
|
||||||
|
|
||||||
setSelectedNode: (node) => set({ selectedNode: node }),
|
setSelectedNode: (node) => set({ selectedNode: node }),
|
||||||
setHoveredNode: (id) => set({ hoveredNodeId: id }),
|
setHoveredNode: (id) => set({ hoveredNodeId: id }),
|
||||||
@@ -53,6 +56,16 @@ export const useReferralNetworkStore = create<ReferralNetworkState>()((set) => (
|
|||||||
|
|
||||||
resetFilters: () => set({ filters: { ...DEFAULT_FILTERS } }),
|
resetFilters: () => set({ filters: { ...DEFAULT_FILTERS } }),
|
||||||
|
|
||||||
|
// Full network and a scope selection are mutually exclusive: the whole graph
|
||||||
|
// already contains every campaign, partner and user, so narrowing it by chips
|
||||||
|
// would be meaningless. Each mode therefore drops the other.
|
||||||
|
setFullNetwork: (enabled) =>
|
||||||
|
set({
|
||||||
|
isFullNetwork: enabled,
|
||||||
|
scope: [],
|
||||||
|
...resetGraphState(),
|
||||||
|
}),
|
||||||
|
|
||||||
addScope: (item) =>
|
addScope: (item) =>
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const exists = state.scope.some((s) => s.type === item.type && s.id === item.id);
|
const exists = state.scope.some((s) => s.type === item.type && s.id === item.id);
|
||||||
@@ -60,6 +73,7 @@ export const useReferralNetworkStore = create<ReferralNetworkState>()((set) => (
|
|||||||
if (state.scope.length >= MAX_SCOPE_ITEMS) return state;
|
if (state.scope.length >= MAX_SCOPE_ITEMS) return state;
|
||||||
return {
|
return {
|
||||||
scope: [...state.scope, item],
|
scope: [...state.scope, item],
|
||||||
|
isFullNetwork: false,
|
||||||
...resetGraphState(),
|
...resetGraphState(),
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
@@ -76,6 +90,7 @@ export const useReferralNetworkStore = create<ReferralNetworkState>()((set) => (
|
|||||||
clearScope: () =>
|
clearScope: () =>
|
||||||
set({
|
set({
|
||||||
scope: [],
|
scope: [],
|
||||||
|
isFullNetwork: false,
|
||||||
...resetGraphState(),
|
...resetGraphState(),
|
||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user