From 0d2c53b635eacd5873aec0ef09143959dae45dde Mon Sep 17 00:00:00 2001 From: Fathiraz Arthuro Date: Sat, 18 Jul 2026 20:18:23 +0700 Subject: [PATCH] chore: remove dead SubscriptionRef layer and unused helpers Deletes an Effect SubscriptionRef/Stream mirror on four pub/sub stores that had zero production consumers (only a test file read the streams), plus dead helper clusters in bulk-edit/bulk-duplicate/bulk-actions utils and a handful of standalone unused symbols. All stores keep their live callback subscribe() API; getSelectionSnapshot is preserved since selection-control.tsx depends on it. Verified via grep (0 references to every removed symbol), typecheck, full test suite, and production build. --- src/features/bulk-actions-utils.tsx | 23 +---- src/features/bulk-duplicate-utils.tsx | 55 +---------- src/features/bulk-edit-utils.tsx | 113 ----------------------- src/features/bulk-random-assign-utils.ts | 2 - src/lib/checkbox-portal-store.ts | 10 -- src/lib/effect-runtime.ts | 7 -- src/lib/graphql-queries.ts | 22 ----- src/lib/hovercard-portal-target.ts | 5 - src/lib/queue-store.ts | 10 +- src/lib/schemas-messages.ts | 2 - src/lib/selection-store.ts | 18 +--- src/lib/sprint-utils.ts | 11 --- src/lib/store-streams.test.ts | 74 --------------- src/lib/toast-store.ts | 10 +- src/ui/actions.tsx | 11 --- src/ui/bulk-flyout.tsx | 1 - src/ui/icons.tsx | 10 -- 17 files changed, 5 insertions(+), 379 deletions(-) delete mode 100644 src/lib/store-streams.test.ts diff --git a/src/features/bulk-actions-utils.tsx b/src/features/bulk-actions-utils.tsx index 258e6db..17cd1ae 100644 --- a/src/features/bulk-actions-utils.tsx +++ b/src/features/bulk-actions-utils.tsx @@ -4,7 +4,7 @@ import React from 'react' import { Box, Spinner } from '@primer/react' import type { BulkEditRelationshipsUpdate, IssueSearchResultData } from '@/lib/messages' -import type { RelationshipKey, RelationshipSelectionState } from '@/features/bulk-edit-utils' +import type { RelationshipKey } from '@/features/bulk-edit-utils' import { isMac } from '@/lib/keyboard' import { Z_MODAL } from '@/lib/z-index' @@ -27,27 +27,6 @@ export function createEmptyRelationshipUpdates(): BulkEditRelationshipsUpdate { } } -export function createEmptyRelationshipSelection(): RelationshipSelectionState { - return { - parent: false, - blockedBy: false, - blocking: false, - } -} - -export function hasRelationshipOperations(relationships: BulkEditRelationshipsUpdate): boolean { - return Boolean( - relationships.parent.clear || - relationships.parent.set || - relationships.blockedBy.clear || - relationships.blockedBy.add.length > 0 || - relationships.blockedBy.remove.length > 0 || - relationships.blocking.clear || - relationships.blocking.add.length > 0 || - relationships.blocking.remove.length > 0, - ) -} - export type ParentOperation = 'set' | 'clear' export type ListOperation = 'add' | 'remove' | 'clear' diff --git a/src/features/bulk-duplicate-utils.tsx b/src/features/bulk-duplicate-utils.tsx index c70feb9..3bdd5be 100644 --- a/src/features/bulk-duplicate-utils.tsx +++ b/src/features/bulk-duplicate-utils.tsx @@ -1,7 +1,7 @@ // types, constants, and pure helpers for the bulk-duplicate modal. import React from 'react' -import type { IssueRelationshipData, ItemPreviewData } from '@/lib/messages' +import type { ItemPreviewData } from '@/lib/messages' import { primerCss } from '@/lib/primer-css-helper' import { AlertIcon, @@ -14,7 +14,6 @@ import { SyncIcon, TextLineIcon, } from '@/ui/icons' -import { formatIssueReference } from '@/lib/relationship-utils' /** * §11.2 — three-stage state machine collapsed to two: @@ -49,12 +48,6 @@ export interface DuplicateSection { helperText?: string } -export interface ReviewRow { - id: SectionId - label: string - value: string -} - export const TITLE_SECTION_ID = 'TITLE' as const export const BODY_SECTION_ID = 'BODY' as const export const ASSIGNEES_SECTION_ID = 'ASSIGNEES' as const @@ -136,52 +129,6 @@ export function duplicateValueTooltip(fieldName: string): string { return `Value applied to the duplicated item for ${fieldName}.` } -export function formatIssueSummary(issue: IssueRelationshipData): string { - return `${formatIssueReference(issue)} — ${issue.title}` -} - -export function summarizeText(value: string, fallback = 'Empty'): string { - const trimmed = value.trim() - if (!trimmed) return fallback - return trimmed.length > 80 ? `${trimmed.slice(0, 77)}…` : trimmed -} - -export function summarizeIssueList(issues: IssueRelationshipData[]): string { - if (issues.length === 0) return 'Skipped' - const preview = issues.slice(0, 2).map(formatIssueSummary).join('; ') - return issues.length > 2 ? `${preview} +${issues.length - 2} more` : preview -} - -export function summarizeFieldValue(field: EditableField): string { - if (field.dataType === 'TEXT') { - return summarizeText(field.text ?? '', 'None / Cleared') - } - - if (field.dataType === 'SINGLE_SELECT') { - return field.optionName || 'None / Cleared' - } - - if (field.dataType === 'ITERATION') { - return field.iterationTitle || 'None / Cleared' - } - - if (field.dataType === 'NUMBER') { - return field.number === undefined || field.number === null - ? 'None / Cleared' - : String(field.number) - } - - if (field.dataType === 'DATE') { - if (!field.date) return 'None / Cleared' - const parsed = new Date(`${field.date}T00:00:00`) - return Number.isNaN(parsed.getTime()) - ? field.date - : parsed.toLocaleDateString(undefined, { year: 'numeric', month: 'short', day: 'numeric' }) - } - - return 'None / Cleared' -} - /** * §11.6 — Diff predicate per section. Compares the user-edited value against * the source preview value and returns `true` when they differ. The REVIEW diff --git a/src/features/bulk-edit-utils.tsx b/src/features/bulk-edit-utils.tsx index 22283c0..34505e8 100644 --- a/src/features/bulk-edit-utils.tsx +++ b/src/features/bulk-edit-utils.tsx @@ -5,7 +5,6 @@ import { CalendarIcon, HashIcon, ArrowRightIcon, - ListCheckIcon, OptionsSelectIcon, PencilIcon, PersonIcon, @@ -14,8 +13,6 @@ import { TagIcon, TextLineIcon, } from '@/ui/icons' -import type { BulkEditRelationshipsUpdate, IssueRelationshipData } from '@/lib/messages' -import { formatIssueReference } from '@/lib/relationship-utils' export interface ProjectField { id: string @@ -39,15 +36,8 @@ export interface RelationshipSelectionState { blocking: boolean } -export type WizardStep = 'TOKEN_WARNING' | 'FIELDS' | 'VALUES' | 'SUMMARY' - export type RelationshipKey = keyof RelationshipSelectionState -export type RelationshipSummaryRow = { - label: string - value: string -} - /** Project V2 field types editable via the field picker (not issue properties). */ export const EDITABLE_PROJECT_FIELD_DATATYPES = new Set([ 'TEXT', @@ -221,106 +211,3 @@ export function getFieldIcon(dataType: string): React.ReactNode { return null } } - -export function getFieldSelectionTooltip(field: ProjectField): string { - return `Select ${field.name} to set the same value across all selected items.` -} - -export function getFieldValueStepTooltip(field: ProjectField, itemCount: number): string { - return `Set ${field.name} for all ${itemCount} selected item${itemCount !== 1 ? 's' : ''}.` -} - -export function getRelationshipSelectionTooltip(label: string): string { - return `Enable ${label.toLowerCase()} relationship changes for this bulk edit.` -} - -export function issueTitle(issue: IssueRelationshipData): string { - return issue.title.trim() || formatIssueReference(issue) -} - -export function getRelationshipSelectionCount(selection: RelationshipSelectionState): number { - return Object.values(selection).filter(Boolean).length -} - -export function buildRelationshipSummaryRows( - relationships: BulkEditRelationshipsUpdate, -): RelationshipSummaryRow[] { - const rows: RelationshipSummaryRow[] = [] - - if (relationships.parent.clear) { - rows.push({ label: 'Parent', value: 'Clear existing parent relationship' }) - } - if (relationships.parent.set) { - rows.push({ - label: 'Parent', - value: `Set to ${formatIssueReference(relationships.parent.set)}`, - }) - } - - if (relationships.blockedBy.clear) { - rows.push({ label: 'Blocked by', value: 'Clear all current blockers' }) - } - if (relationships.blockedBy.add.length > 0) { - rows.push({ - label: 'Blocked by', - value: `Add ${relationships.blockedBy.add.map(formatIssueReference).join(', ')}`, - }) - } - if (relationships.blockedBy.remove.length > 0) { - rows.push({ - label: 'Blocked by', - value: `Remove ${relationships.blockedBy.remove.map(formatIssueReference).join(', ')}`, - }) - } - - if (relationships.blocking.clear) { - rows.push({ label: 'Blocking', value: 'Clear all currently blocked issues' }) - } - if (relationships.blocking.add.length > 0) { - rows.push({ - label: 'Blocking', - value: `Add ${relationships.blocking.add.map(formatIssueReference).join(', ')}`, - }) - } - if (relationships.blocking.remove.length > 0) { - rows.push({ - label: 'Blocking', - value: `Remove ${relationships.blocking.remove.map(formatIssueReference).join(', ')}`, - }) - } - - return rows -} - -export function describeFieldValue( - field: ProjectField, - fieldValues: Record, -): string { - const valueObj = (fieldValues[field.id] || {}) as Record - let displayValue = 'None / Cleared' - - const arr = valueObj.array as { name: string }[] | undefined - if (arr && arr.length > 0) { - displayValue = arr.map((v) => v.name).join(', ') - } else if (valueObj.date) { - displayValue = new Date((valueObj.date as string) + 'T00:00:00').toLocaleDateString(undefined, { - year: 'numeric', - month: 'short', - day: 'numeric', - }) - } else if (valueObj.number !== undefined && valueObj.number !== null) { - displayValue = String(valueObj.number) - } else if (valueObj.text) { - displayValue = valueObj.text as string - } else if (valueObj.singleSelectOptionId && field.options) { - const opt = field.options.find((option) => option.id === valueObj.singleSelectOptionId) - if (opt) displayValue = opt.name - } else if (valueObj.iterationId && field.configuration?.iterations) { - const iter = field.configuration.iterations.find((option) => option.id === valueObj.iterationId) - if (iter) displayValue = iter.title - } - - return displayValue -} - -export const bulkEditHeaderIcon = diff --git a/src/features/bulk-random-assign-utils.ts b/src/features/bulk-random-assign-utils.ts index 03a2bd8..7854d47 100644 --- a/src/features/bulk-random-assign-utils.ts +++ b/src/features/bulk-random-assign-utils.ts @@ -90,8 +90,6 @@ export function distributeRoundRobin(items: string[], assignees: string[]): Map< export type DistributionStrategy = 'balanced' | 'random' | 'round-robin' -export type DistributionFunction = (items: string[], assignees: string[]) => Map - /** Inverts assignee→itemIds to itemId→assigneeIds. */ export function invertDistribution(distribution: Map): Map { const byItem = new Map() diff --git a/src/lib/checkbox-portal-store.ts b/src/lib/checkbox-portal-store.ts index ae9205d..ed20e3e 100644 --- a/src/lib/checkbox-portal-store.ts +++ b/src/lib/checkbox-portal-store.ts @@ -1,5 +1,3 @@ -import { Effect, Stream, SubscriptionRef } from 'effect' - export type PortalEntry = | { type: 'row'; container: HTMLElement; itemId: string } | { type: 'group'; container: HTMLElement; getItemIds: () => string[] } @@ -7,9 +5,6 @@ export type PortalEntry = type Listener = (entries: readonly PortalEntry[]) => void -const _ref = Effect.runSync( - SubscriptionRef.make([] as readonly PortalEntry[]), -) let current: readonly PortalEntry[] = [] const listeners = new Set() @@ -20,7 +15,6 @@ function isConnected(entry: PortalEntry): boolean { function setState(next: readonly PortalEntry[]): void { current = next - Effect.runSync(SubscriptionRef.set(_ref, next)) listeners.forEach((fn) => fn(current)) } @@ -65,7 +59,3 @@ export const checkboxPortalStore = { return () => listeners.delete(fn) }, } - -export const checkboxPortalChanges: Stream.Stream = _ref.changes - -export const getCheckboxPortalSnapshot = (): readonly PortalEntry[] => current diff --git a/src/lib/effect-runtime.ts b/src/lib/effect-runtime.ts index 5a52e3b..49ae0d5 100644 --- a/src/lib/effect-runtime.ts +++ b/src/lib/effect-runtime.ts @@ -52,10 +52,3 @@ export const runHandler = (label: string, effect: Effect.Effect => AppRuntime.dispose() diff --git a/src/lib/graphql-queries.ts b/src/lib/graphql-queries.ts index b8f1e6d..4d2aad6 100644 --- a/src/lib/graphql-queries.ts +++ b/src/lib/graphql-queries.ts @@ -377,28 +377,6 @@ export const GET_VIEWER_REPOS_PAGE = ` } ` -export const SEARCH_OWNER_REPOS = ` - query SearchOwnerRepos($login: String!, $query: String!, $first: Int!) { - repositoryOwner(login: $login) { - repositories( - first: $first - query: $query - orderBy: { field: PUSHED_AT, direction: DESC } - ) { - nodes { - id - name - nameWithOwner - isPrivate - description - hasIssuesEnabled - isArchived - } - } - } - } -` - export const GET_POSSIBLE_TRANSFER_REPOS = ` query GetPossibleTransferRepos($issueId: ID!, $first: Int!) { node(id: $issueId) { diff --git a/src/lib/hovercard-portal-target.ts b/src/lib/hovercard-portal-target.ts index 968b7a8..bcdaf57 100644 --- a/src/lib/hovercard-portal-target.ts +++ b/src/lib/hovercard-portal-target.ts @@ -7,8 +7,3 @@ export function getHovercardAppendTarget(): HTMLElement { export function setHovercardAppendTarget(target: HTMLElement | null): void { hovercardAppendTarget = target } - -/** @internal test helper */ -export function resetHovercardAppendTargetForTests(): void { - hovercardAppendTarget = null -} diff --git a/src/lib/queue-store.ts b/src/lib/queue-store.ts index 9c55d3a..98710fe 100644 --- a/src/lib/queue-store.ts +++ b/src/lib/queue-store.ts @@ -1,4 +1,4 @@ -import { Duration, Effect, Fiber, Stream, SubscriptionRef } from 'effect' +import { Duration, Effect, Fiber } from 'effect' import { onMessage } from '@/lib/messages' import { toastStore } from '@/lib/toast-store' @@ -58,7 +58,6 @@ type Listener = (entries: ProcessEntry[]) => void const DISMISS_DELAY = Duration.millis(3000) const DEFAULT_UNDO_WINDOW_MS = 10_000 -const _ref = Effect.runSync(SubscriptionRef.make>(new Map())) let processes: Map = new Map() const listeners = new Set() const dismissTimers = new Map>() @@ -145,7 +144,6 @@ function withPhase(entry: Omit): ProcessEntry { function setState(next: Map): void { processes = next - Effect.runSync(SubscriptionRef.set(_ref, next)) const entries = Array.from(next.values()) listeners.forEach((fn) => fn(entries)) } @@ -229,12 +227,6 @@ export const queueStore = { }, } -export const queueChanges: Stream.Stream> = _ref.changes - -// return a defensive copy so external consumers cannot mutate the live ref -// and bypass setState's notify/SubscriptionRef updates. -export const getQueueSnapshot = (): ReadonlyMap => new Map(processes) - // single central listener for the whole CS context onMessage('queueStateUpdate', ({ data }) => { // use processId if provided, fall back to sentinel 'bulk' for legacy bulk-update path diff --git a/src/lib/schemas-messages.ts b/src/lib/schemas-messages.ts index bb0e33b..0c155f7 100644 --- a/src/lib/schemas-messages.ts +++ b/src/lib/schemas-messages.ts @@ -592,5 +592,3 @@ export const Messages = { output: Schema.Void, }, } as const - -export type MessageKey = keyof typeof Messages diff --git a/src/lib/selection-store.ts b/src/lib/selection-store.ts index e29b354..df35029 100644 --- a/src/lib/selection-store.ts +++ b/src/lib/selection-store.ts @@ -1,16 +1,8 @@ -import { Effect, Stream, SubscriptionRef } from 'effect' - import { logger } from '@/lib/debug-logger' type Listener = () => void -// internal SubscriptionRef holds the canonical state. A synchronous mirror -// (`current`) is kept so that getters / isSelected / count / getAll stay -// synchronous and zero-cost. Mutations always go through `setState` which -// updates the ref AND the mirror in one shot before firing legacy listeners -// — meaning `selectionChanges` Stream subscribers see the same sequence of -// values as legacy callback subscribers. -const _ref = Effect.runSync(SubscriptionRef.make>(new Set())) +// synchronous state mirror; all mutations go through setState let current: ReadonlySet = new Set() const listeners = new Set() @@ -18,7 +10,6 @@ const focusListeners = new Set<() => void>() function setState(next: ReadonlySet): void { current = next - Effect.runSync(SubscriptionRef.set(_ref, next)) listeners.forEach((fn) => fn()) } @@ -80,13 +71,6 @@ export const selectionStore = { }, } -/** - * Stream of selection-set changes. Subscribers receive the current value - * immediately (SubscriptionRef semantics), then every subsequent update. - * Useful from Effect-first callsites and from `useSubscriptionRef` consumers. - */ -export const selectionChanges: Stream.Stream> = _ref.changes - /** * Synchronous snapshot accessor matching `useSyncExternalStore` semantics. */ diff --git a/src/lib/sprint-utils.ts b/src/lib/sprint-utils.ts index f1d77b6..9ff7d34 100644 --- a/src/lib/sprint-utils.ts +++ b/src/lib/sprint-utils.ts @@ -34,17 +34,6 @@ export function daysLeft(endDate: string): number { ) } -export function sprintProgress(startDate: string, endDate: string): number { - const total = Math.max( - 1, - Math.ceil( - (new Date(endDate + 'T00:00:00Z').getTime() - new Date(startDate + 'T00:00:00Z').getTime()) / - 86_400_000, - ), - ) - return Math.min(100, Math.round(((total - daysLeft(endDate)) / total) * 100)) -} - export function todayUtc(): string { return new Date().toISOString().slice(0, 10) } diff --git a/src/lib/store-streams.test.ts b/src/lib/store-streams.test.ts deleted file mode 100644 index fd039f0..0000000 --- a/src/lib/store-streams.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { Chunk, Effect, Stream } from 'effect' - -vi.mock('@/lib/debug-logger', () => ({ - logger: { log: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn(), info: vi.fn() }, -})) - -import { selectionChanges, selectionStore } from '@/lib/selection-store' -import { toastChanges, toastStore } from '@/lib/toast-store' - -// --------------------------------------------------------------------------- -// SubscriptionRef-backed change streams -// --------------------------------------------------------------------------- - -describe('store change streams', () => { - it('selectionChanges emits current value + each subsequent update', async () => { - selectionStore.clear() - - const collected: ReadonlySet[] = [] - - const program = Stream.runForEach(selectionChanges.pipe(Stream.take(3)), (value) => - Effect.sync(() => { - collected.push(value) - }), - ) - - const fiber = Effect.runFork(program) - - // first emission should be the current (cleared) state. Give the runtime - // a tick to deliver it. - await new Promise((r) => setTimeout(r, 0)) - - selectionStore.toggle('a', true) - selectionStore.toggle('b', true) - - await Effect.runPromise(Effect.fromFiber(fiber)) - - expect(collected).toHaveLength(3) - expect([...collected[0]]).toEqualValue([]) - expect([...collected[1]]).toEqualValue(['a']) - expect([...collected[2]]).toEqualValue(['a', 'b']) - }) - - it('toastChanges streams snapshots through Stream.take(n)', async () => { - // drain any leftover toasts from earlier tests - while (selectionStore.count() > 0) selectionStore.clear() - // cannot directly clear toasts (no public clear), but we can collect - // *new* emissions after a known starting point. Take(2) = current + 1 - // change. - const collected: number[] = [] - - const program = Stream.runCollect(toastChanges.pipe(Stream.take(2))) - const fiber = Effect.runFork( - program.pipe( - Effect.tap((chunk) => - Effect.sync(() => { - for (const value of Chunk.toArray(chunk)) { - collected.push(value.length) - } - }), - ), - ), - ) - - await new Promise((r) => setTimeout(r, 0)) - toastStore.show({ message: 'hi', type: 'info' }) - - await Effect.runPromise(Effect.fromFiber(fiber)) - - expect(collected).toHaveLength(2) - // second emission must have at least one more toast than the first. - expect(collected[1]).toBeGreaterThan(collected[0]) - }) -}) diff --git a/src/lib/toast-store.ts b/src/lib/toast-store.ts index 33877e3..73b0fdf 100644 --- a/src/lib/toast-store.ts +++ b/src/lib/toast-store.ts @@ -1,4 +1,4 @@ -import { Duration, Effect, Fiber, Stream, SubscriptionRef } from 'effect' +import { Duration, Effect, Fiber } from 'effect' export interface ToastEntry { id: string @@ -12,7 +12,6 @@ const AUTO_DISMISS = Duration.millis(5000) type Listener = (entries: ToastEntry[]) => void -const _ref = Effect.runSync(SubscriptionRef.make([])) let current: ToastEntry[] = [] const listeners = new Set() @@ -23,7 +22,6 @@ const dismissTimers = new Map>() function setState(next: ToastEntry[]): void { current = next - Effect.runSync(SubscriptionRef.set(_ref, next)) const snapshot = [...next] listeners.forEach((fn) => fn(snapshot)) } @@ -78,9 +76,3 @@ export const toastStore = { return () => listeners.delete(fn) }, } - -export const toastChanges: Stream.Stream = _ref.changes - -// return a defensive copy so external consumers cannot mutate the live ref -// and bypass setState's notify/SubscriptionRef updates. -export const getToastSnapshot = (): ToastEntry[] => [...current] diff --git a/src/ui/actions.tsx b/src/ui/actions.tsx index e37aaa0..9078d66 100644 --- a/src/ui/actions.tsx +++ b/src/ui/actions.tsx @@ -22,17 +22,6 @@ export function PrimaryAction({ children, loading, icon, ...props }: ActionButto ) } -export function SecondaryAction({ children, icon, ...props }: ActionButtonProps) { - return ( - - ) -} - export function GhostAction({ children, icon, ...props }: ActionButtonProps) { return (