Skip to content
Merged
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
88 changes: 86 additions & 2 deletions src/features/__tests__/bulk-edit-flyout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,24 @@ import {
serializeValue,
submitBulkFieldUpdate,
} from '@/features/bulk-edit-flyout-helpers'
import type { ProjectField } from '@/features/bulk-edit-utils'
import {
buildRelationshipFieldRows,
partitionFieldList,
relationshipFieldId,
type ProjectField,
} from '@/features/bulk-edit-utils'
import { buildRelationshipsPayload } from '@/features/bulk-actions-utils'

function field(over: Partial<ProjectField> = {}): ProjectField {
return { id: 'f1', name: 'Status', dataType: 'SINGLE_SELECT', ...over }
}

describe('bulk-edit-flyout — defaultValueFor', () => {
it('returns empty text for TEXT/TITLE/BODY', () => {
it('returns empty text for TEXT/TITLE/BODY/COMMENT', () => {
expect(defaultValueFor(field({ dataType: 'TEXT' }))).toEqual({ kind: 'text', text: '' })
expect(defaultValueFor(field({ dataType: 'TITLE' }))).toEqual({ kind: 'text', text: '' })
expect(defaultValueFor(field({ dataType: 'BODY' }))).toEqual({ kind: 'text', text: '' })
expect(defaultValueFor(field({ dataType: 'COMMENT' }))).toEqual({ kind: 'text', text: '' })
})

it('returns null number for NUMBER', () => {
Expand All @@ -59,6 +66,12 @@ describe('bulk-edit-flyout — canApply gating', () => {
expect(canApply({ kind: 'text', text: '' })).toBe(false)
expect(canApply({ kind: 'text', text: 'x' })).toBe(true)
})
it('comment requires non-empty body', () => {
expect(
canApply(defaultValueFor(field({ id: '__comment__', name: 'Comment', dataType: 'COMMENT' }))),
).toBe(false)
expect(canApply({ kind: 'text', text: 'Ship it' })).toBe(true)
})
it('number requires a non-null finite value', () => {
expect(canApply({ kind: 'number', number: null })).toBe(false)
expect(canApply({ kind: 'number', number: 7 })).toBe(true)
Expand Down Expand Up @@ -159,3 +172,74 @@ describe('bulk-edit-flyout — submitBulkFieldUpdate dispatch', () => {
expect(result).toEqual({ ok: false, message: BULK_EDIT_DISPATCH_FAILED_MESSAGE })
})
})

describe('bulk-edit — partitionFieldList', () => {
const baseFields: ProjectField[] = [
{ id: 'f-status', name: 'Status', dataType: 'SINGLE_SELECT' },
{ id: 'f-zeta', name: 'Zeta', dataType: 'TEXT' },
{ id: 'f-alpha', name: 'Alpha', dataType: 'NUMBER' },
{ id: '__comment__', name: 'Comment', dataType: 'COMMENT' },
{ id: '__body__', name: 'Description', dataType: 'BODY' },
]

it('browse mode sorts project fields A→Z and excludes recent from lower sections', () => {
const partition = partitionFieldList({
fields: baseFields,
recentIds: ['f-status', relationshipFieldId('parent')],
query: '',
})
expect(partition.mode).toBe('browse')
if (partition.mode !== 'browse') return
expect(partition.recent.map((f) => f.id)).toEqual(['f-status', relationshipFieldId('parent')])
expect(partition.projectFields.map((f) => f.name)).toEqual(['Alpha', 'Zeta'])
expect(partition.issueProperties.map((f) => f.name)).toEqual(['Comment', 'Description'])
expect(partition.relationships.map((f) => f.name)).toEqual(['Blocked by', 'Blocking'])
})

it('search mode flattens matches without Recent section', () => {
const partition = partitionFieldList({
fields: baseFields,
recentIds: ['f-status'],
query: 'com',
})
expect(partition.mode).toBe('search')
if (partition.mode !== 'search') return
expect(partition.matches.map((f) => f.id)).toEqual(['__comment__'])
})

it('dedupes ids in search results', () => {
const withRel = [...baseFields, ...buildRelationshipFieldRows()]
const partition = partitionFieldList({
fields: withRel,
recentIds: [],
query: 'parent',
})
expect(partition.mode).toBe('search')
if (partition.mode !== 'search') return
expect(partition.matches.filter((f) => f.id === relationshipFieldId('parent'))).toHaveLength(1)
})
})

describe('bulk-edit — buildRelationshipsPayload', () => {
it('builds parent clear payload', () => {
expect(buildRelationshipsPayload('parent', 'clear', null, 'add', [])).toEqual({
parent: { set: undefined, clear: true },
blockedBy: { add: [], remove: [], clear: false },
blocking: { add: [], remove: [], clear: false },
})
})

it('builds blockedBy add payload', () => {
const issue = {
databaseId: 1,
number: 42,
title: 'Blocker',
repoOwner: 'octo',
repoName: 'repo',
state: 'OPEN' as const,
}
const payload = buildRelationshipsPayload('blockedBy', 'set', null, 'add', [issue])
expect(payload.blockedBy.add).toHaveLength(1)
expect(payload.blockedBy.add[0]?.number).toBe(42)
})
})
6 changes: 4 additions & 2 deletions src/features/bulk-actions-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ export function BulkActionsBar({ projectId, owner, isOrg, number, getFields }: P
for (const link of links) {
const match = link.href.match(/github\.com\/([^/]+)\/([^/]+)\/(issues|pull)\/\d+/)
if (match && match[1] === owner) {
// eslint-disable-next-line react-hooks/set-state-in-effect -- derive repo from DOM when selection changes
setFirstRepoName(match[2])
break
}
Expand All @@ -405,7 +406,7 @@ export function BulkActionsBar({ projectId, owner, isOrg, number, getFields }: P
}
defaultFields.unshift(
{ id: '__body__', name: 'Description', dataType: 'BODY' },
{ id: '__comment__', name: 'Add Comment', dataType: 'COMMENT' },
{ id: '__comment__', name: 'Comment', dataType: 'COMMENT' },
)
if (!existingTypes.includes('ASSIGNEES')) {
defaultFields.push({ id: '__assignees__', name: 'Assignees', dataType: 'ASSIGNEES' })
Expand Down Expand Up @@ -542,7 +543,7 @@ export function BulkActionsBar({ projectId, owner, isOrg, number, getFields }: P
sendMessage('bulkPin', { itemIds, projectId: resolvedProjectId })
}

async function handleUnpin() {
async function _handleUnpin() {
setMenuOpen(false)
if (!(await checkToken())) return
setShowUnpinModal(true)
Expand Down Expand Up @@ -890,6 +891,7 @@ export function BulkActionsBar({ projectId, owner, isOrg, number, getFields }: P
projectId={resolvedProjectId}
itemIds={selectionStore.getAll()}
fields={projectData?.fields ?? []}
repoName={firstRepoName || undefined}
recentFieldIds={recentFieldIdsRef.current}
onAppliedField={pushRecentField}
/>
Expand Down
47 changes: 45 additions & 2 deletions src/features/bulk-actions-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@

import React from 'react'
import { Box, Spinner } from '@primer/react'
import type { BulkEditRelationshipsUpdate } from '@/lib/messages'
import type { RelationshipSelectionState } from '@/features/bulk-edit-utils'
import type { BulkEditRelationshipsUpdate, IssueSearchResultData } from '@/lib/messages'
import type { RelationshipKey, RelationshipSelectionState } from '@/features/bulk-edit-utils'
import { isMac } from '@/lib/keyboard'
import { Z_MODAL } from '@/lib/z-index'

Expand Down Expand Up @@ -48,6 +48,49 @@ export function hasRelationshipOperations(relationships: BulkEditRelationshipsUp
)
}

export type ParentOperation = 'set' | 'clear'
export type ListOperation = 'add' | 'remove' | 'clear'

function mapSearchResult(item: IssueSearchResultData) {
return {
databaseId: item.databaseId,
number: item.number,
title: item.title,
repoOwner: item.repoOwner,
repoName: item.repoName,
state: item.state,
}
}

export function buildRelationshipsPayload(
key: RelationshipKey,
parentOp: ParentOperation,
parentTarget: IssueSearchResultData | null,
listOp: ListOperation,
listTargets: IssueSearchResultData[],
): BulkEditRelationshipsUpdate {
const base = createEmptyRelationshipUpdates()

if (key === 'parent') {
if (parentOp === 'clear') {
base.parent.clear = true
} else if (parentTarget) {
base.parent.set = mapSearchResult(parentTarget)
}
return base
}

const list = key === 'blockedBy' ? base.blockedBy : base.blocking
if (listOp === 'clear') {
list.clear = true
} else if (listOp === 'add') {
list.add = listTargets.map(mapSearchResult)
} else if (listOp === 'remove') {
list.remove = listTargets.map(mapSearchResult)
}
return base
}

/** display string for a ctrl/cmd+shift+key shortcut */
export function shortcut(key: string) {
return isMac ? `⌘⇧${key}` : `⌃⇧${key}`
Expand Down
1 change: 1 addition & 0 deletions src/features/bulk-edit-flyout-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export function defaultValueFor(field: ProjectField): FieldValue {
case 'TEXT':
case 'TITLE':
case 'BODY':
case 'COMMENT':
return { kind: 'text', text: '' }
case 'NUMBER':
return { kind: 'number', number: null }
Expand Down
Loading
Loading