chore: more refactorings to make stuff more similar

This commit is contained in:
2026-02-23 12:46:57 +01:00
parent 64b391806e
commit 7213b6b78d
6 changed files with 108 additions and 25 deletions

View File

@@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import MonacoEditor from '@monaco-editor/react';
import type { ScriptData } from '../../../main/shared/electronApi';
import { useAppStore } from '../../store';
import { BDS_EVENT_SCRIPTS_CHANGED, dispatchWindowEvent } from '../../utils';
import { getPythonRuntimeManager } from '../../python/runtimeManagerInstance';
import { useI18n } from '../../i18n';
import { showToast } from '../Toast';
@@ -197,9 +198,7 @@ export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
setScriptContent(updated.content || '');
const normalizedExisting = toFunctionSlug(updated.slug || updated.title || '');
setIsSlugManuallyEdited(normalizedExisting !== toFunctionSlug(updated.title || ''));
if (typeof window.dispatchEvent === 'function') {
window.dispatchEvent(new CustomEvent('bds:scripts-changed'));
}
dispatchWindowEvent(BDS_EVENT_SCRIPTS_CHANGED);
} finally {
setIsSaving(false);
}
@@ -217,9 +216,7 @@ export const ScriptsView: React.FC<ScriptsViewProps> = ({ scriptId }) => {
return;
}
closeTab(script.id);
if (typeof window.dispatchEvent === 'function') {
window.dispatchEvent(new CustomEvent('bds:scripts-changed'));
}
dispatchWindowEvent(BDS_EVENT_SCRIPTS_CHANGED);
} catch (error) {
console.error('Failed to delete script:', error);
showToast.error(t('sidebar.scripts.deleteFailed'));

View File

@@ -1,7 +1,7 @@
import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react';
import { useAppStore, PostData, MediaData } from '../../store';
import { showToast } from '../Toast';
import { getContrastColor, groupPostsByStatus, loadTagColorMap } from '../../utils';
import { BDS_EVENT_SCRIPTS_CHANGED, dispatchWindowEvent, getContrastColor, groupPostsByStatus, loadTagColorMap } from '../../utils';
import type { ChatConversation, ImportDefinitionData } from '../../types/electron';
import { GitSidebar } from '../GitSidebar/GitSidebar';
import { scrollToSettingsSection, SettingsCategory } from '../SettingsView/SettingsView';
@@ -1598,7 +1598,7 @@ const ScriptsList: React.FC = () => {
} = useProjectScopedSidebarData<Array<{ id: string; title: string; updatedAt: string }>[number]>({
load: loadScripts,
activeProjectId,
refreshEventName: 'bds:scripts-changed',
refreshEventName: BDS_EVENT_SCRIPTS_CHANGED,
});
const handleCreateScript = async () => {
@@ -1619,9 +1619,7 @@ const ScriptsList: React.FC = () => {
{ id: created.id, title: created.title, updatedAt: created.updatedAt },
...prev.filter((script) => script.id !== created.id),
]);
if (typeof window.dispatchEvent === 'function') {
window.dispatchEvent(new CustomEvent('bds:scripts-changed'));
}
dispatchWindowEvent(BDS_EVENT_SCRIPTS_CHANGED);
openScriptTab(openTab, created.id, 'pin');
void reloadScripts();
} catch (error) {
@@ -1640,9 +1638,7 @@ const ScriptsList: React.FC = () => {
}
setScripts((prev) => prev.filter((script) => script.id !== scriptId));
closeTab(scriptId);
if (typeof window.dispatchEvent === 'function') {
window.dispatchEvent(new CustomEvent('bds:scripts-changed'));
}
dispatchWindowEvent(BDS_EVENT_SCRIPTS_CHANGED);
} catch (error) {
console.error('Failed to delete script:', error);
showToast.error(t('sidebar.scripts.deleteFailed'));

View File

@@ -1,9 +1,10 @@
import { useCallback, useEffect, useState, type Dispatch, type SetStateAction } from 'react';
import { addWindowEventListener, type BdsWindowEventName } from '../../utils';
interface ProjectScopedSidebarDataOptions<TItem> {
load: () => Promise<TItem[] | null | undefined>;
activeProjectId?: string;
refreshEventName?: string;
refreshEventName?: BdsWindowEventName;
}
interface ProjectScopedSidebarDataResult<TItem> {
@@ -53,18 +54,11 @@ export function useProjectScopedSidebarData<TItem>(options: ProjectScopedSidebar
return;
}
if (typeof window.addEventListener !== 'function' || typeof window.removeEventListener !== 'function') {
return;
}
const handleRefreshEvent = () => {
const unsubscribe = addWindowEventListener(refreshEventName, () => {
void reload();
};
});
window.addEventListener(refreshEventName, handleRefreshEvent);
return () => {
window.removeEventListener(refreshEventName, handleRefreshEvent);
};
return unsubscribe;
}, [refreshEventName, reload]);
return {

View File

@@ -4,3 +4,4 @@ export { unescapeMacroSyntax } from './markdownEscape';
export { groupPostsByStatus, type GroupedPosts, type PostStatus } from './postGrouping';
export { loadTabsForProject, saveTabsForProject } from './tabPersistence';
export { buildTagColorMap, loadTagColorMap } from './tagColors';
export { BDS_EVENT_SCRIPTS_CHANGED, addWindowEventListener, dispatchWindowEvent, type BdsWindowEventName } from './windowEvents';

View File

@@ -0,0 +1,34 @@
export const BDS_EVENT_SCRIPTS_CHANGED = 'bds:scripts-changed' as const;
export type BdsWindowEventName =
| typeof BDS_EVENT_SCRIPTS_CHANGED
| 'bds:site-validation-updated';
export function addWindowEventListener<TDetail = unknown>(
eventName: BdsWindowEventName,
handler: (event: CustomEvent<TDetail>) => void,
): () => void {
if (typeof window.addEventListener !== 'function' || typeof window.removeEventListener !== 'function') {
return () => {};
}
const listener = (event: Event) => {
handler(event as CustomEvent<TDetail>);
};
window.addEventListener(eventName, listener as EventListener);
return () => {
window.removeEventListener(eventName, listener as EventListener);
};
}
export function dispatchWindowEvent<TDetail = unknown>(
eventName: BdsWindowEventName,
detail?: TDetail,
): boolean {
if (typeof window.dispatchEvent !== 'function') {
return false;
}
return window.dispatchEvent(new CustomEvent<TDetail>(eventName, { detail }));
}

View File

@@ -0,0 +1,61 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { addWindowEventListener, dispatchWindowEvent } from '../../../src/renderer/utils/windowEvents';
describe('windowEvents utils', () => {
beforeEach(() => {
vi.clearAllMocks();
const listeners = new Map<string, Set<(event: Event) => void>>();
(window as any).addEventListener = vi.fn((type: string, listener: (event: Event) => void) => {
if (!listeners.has(type)) {
listeners.set(type, new Set());
}
listeners.get(type)?.add(listener);
});
(window as any).removeEventListener = vi.fn((type: string, listener: (event: Event) => void) => {
listeners.get(type)?.delete(listener);
});
(window as any).dispatchEvent = vi.fn((event: Event) => {
listeners.get(event.type)?.forEach((listener) => listener(event));
return true;
});
});
it('dispatches custom events with detail payload', () => {
const listener = vi.fn();
addWindowEventListener('bds:scripts-changed', listener);
const dispatched = dispatchWindowEvent('bds:scripts-changed', { scriptId: 'script-1' });
expect(dispatched).toBe(true);
expect(listener).toHaveBeenCalledTimes(1);
const event = listener.mock.calls[0][0] as CustomEvent<{ scriptId: string }>;
expect(event.detail).toEqual({ scriptId: 'script-1' });
});
it('returns no-op unsubscribe when listener APIs are unavailable', () => {
const originalAdd = (window as any).addEventListener;
const originalRemove = (window as any).removeEventListener;
(window as any).addEventListener = undefined;
(window as any).removeEventListener = undefined;
const unsubscribe = addWindowEventListener('bds:scripts-changed', vi.fn());
expect(typeof unsubscribe).toBe('function');
expect(() => unsubscribe()).not.toThrow();
(window as any).addEventListener = originalAdd;
(window as any).removeEventListener = originalRemove;
});
it('subscribes and unsubscribes listeners', () => {
const listener = vi.fn();
const unsubscribe = addWindowEventListener('bds:scripts-changed', listener);
dispatchWindowEvent('bds:scripts-changed');
expect(listener).toHaveBeenCalledTimes(1);
unsubscribe();
dispatchWindowEvent('bds:scripts-changed');
expect(listener).toHaveBeenCalledTimes(1);
});
});