fix(issue-management): restore Project date fields

This commit is contained in:
Turtle 2026-09-02 15:06:25 +08:00
parent be70505f9e
commit 070b46e1ef
3 changed files with 43 additions and 53 deletions

View file

@ -5,7 +5,7 @@
"projectTitle": "DSH Issue Management", "projectTitle": "DSH Issue Management",
"lifecycleActor": "dsh-issue-management", "lifecycleActor": "dsh-issue-management",
"priorityField": "Priority", "priorityField": "Priority",
"startDateField": "Start date", "startDateField": "Start Date",
"projectTimeZone": "Asia/Shanghai", "projectTimeZone": "Asia/Shanghai",
"allowUnassignedOwner": true, "allowUnassignedOwner": true,
"statuses": [ "statuses": [

View file

@ -484,13 +484,7 @@ async function projectContext(number, includeStatusActor = false, includeStartDa
title title
fields(first: 50) { fields(first: 50) {
nodes { nodes {
... on ProjectV2Field { ... on ProjectV2Field { id name dataType isIssueField }
id
name
dataType
isIssueField
issueField { ... on IssueFieldDate { id } }
}
... on ProjectV2SingleSelectField { id name dataType options { id name } } ... on ProjectV2SingleSelectField { id name dataType options { id name } }
} }
} }
@ -518,9 +512,7 @@ async function projectContext(number, includeStatusActor = false, includeStartDa
} }
startDateValue: fieldValueByName(name: $startDateField) startDateValue: fieldValueByName(name: $startDateField)
@include(if: $includeStartDate) { @include(if: $includeStartDate) {
... on ProjectV2ItemIssueFieldValue { ... on ProjectV2ItemFieldDateValue { date }
issueFieldValue { ... on IssueFieldDateValue { value } }
}
} }
} }
} }
@ -552,8 +544,8 @@ async function projectContext(number, includeStatusActor = false, includeStartDa
if (startDateField && startDateField.dataType !== 'DATE') { if (startDateField && startDateField.dataType !== 'DATE') {
throw new Error(`Project ${config.startDateField} 字段必须为 Date`) throw new Error(`Project ${config.startDateField} 字段必须为 Date`)
} }
if (startDateField && (!startDateField.isIssueField || !startDateField.issueField?.id)) { if (startDateField?.isIssueField) {
throw new Error(`Project ${config.startDateField} 字段必须为 Issue Date 字段`) throw new Error(`Project ${config.startDateField} 字段必须为 Project Date 字段`)
} }
const item = issue.projectItems.nodes.find((candidate) => candidate.project.id === project.id) const item = issue.projectItems.nodes.find((candidate) => candidate.project.id === project.id)
const latestStatusEvent = issue.timelineItems?.nodes const latestStatusEvent = issue.timelineItems?.nodes
@ -593,24 +585,27 @@ async function ensureProjectItem(number, includeStartDate = false) {
} }
/** /**
* Initialize one Issue's organization Start date when it is empty. * Initialize one Issue's Project Start Date when it is empty.
* @param {number} number Same-repository Issue number. * @param {number} number Same-repository Issue number.
* @param {string} date Date in YYYY-MM-DD form. * @param {string} date Date in YYYY-MM-DD form.
* @returns {Promise<void>} Resolves after the conditional Issue-field update. * @returns {Promise<void>} Resolves after the conditional Project update.
*/ */
export async function initializeIssueStartDate(number, date) { export async function initializeIssueStartDate(number, date) {
const context = await ensureProjectItem(number, true) const context = await ensureProjectItem(number, true)
if (context.item.startDateValue?.issueFieldValue?.value) return if (context.item.startDateValue?.date) return
await graphql( await graphql(
`mutation($issueId: ID!, $fieldId: ID!, $date: String!) { `mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $date: Date!) {
updateIssueFieldValue(input: { updateProjectV2ItemFieldValue(input: {
issueId: $issueId, projectId: $projectId,
issueField: {fieldId: $fieldId, dateValue: $date} itemId: $itemId,
}) { issue { id } } fieldId: $fieldId,
value: {date: $date}
}) { projectV2Item { id } }
}`, }`,
{ {
issueId: context.issue.id, projectId: context.project.id,
fieldId: context.startDateField.issueField.id, itemId: context.item.id,
fieldId: context.startDateField.id,
date, date,
}, },
) )

View file

@ -21,8 +21,7 @@ const projectGraphqlData = ({
startDate = null, startDate = null,
startDateField = true, startDateField = true,
startDateType = 'DATE', startDateType = 'DATE',
startDateIsIssueField = true, startDateIsIssueField = false,
startDateIssueField = true,
} = {}) => ({ } = {}) => ({
organization: { organization: {
projectV2: { projectV2: {
@ -34,11 +33,10 @@ const projectGraphqlData = ({
...(startDateField ...(startDateField
? [ ? [
{ {
id: 'start-date-project-field-id', id: 'start-date-field-id',
name: 'Start date', name: 'Start Date',
dataType: startDateType, dataType: startDateType,
isIssueField: startDateIsIssueField, isIssueField: startDateIsIssueField,
issueField: startDateIssueField ? { id: 'start-date-issue-field-id' } : null,
}, },
] ]
: []), : []),
@ -56,8 +54,7 @@ const projectGraphqlData = ({
id: 'item-id', id: 'item-id',
project: { id: 'project-id' }, project: { id: 'project-id' },
fieldValueByName: { name: 'Inbox', optionId: 'inbox-option-id' }, fieldValueByName: { name: 'Inbox', optionId: 'inbox-option-id' },
startDateValue: startDateValue: startDate === null ? null : { date: startDate },
startDate === null ? null : { issueFieldValue: { value: startDate } },
}, },
] ]
: [], : [],
@ -269,27 +266,29 @@ test('initializes every referenced Issue only for a PR opened event', async () =
assert.equal(writes.length, 3) assert.equal(writes.length, 3)
}) })
test('writes an empty Issue Start date with the configured field', async (t) => { test('writes an empty Project Start Date with the configured field', async (t) => {
const requests = mockGraphql(t, (request) => { const requests = mockGraphql(t, (request) => {
if (request.query.includes('query(')) return projectGraphqlData() if (request.query.includes('query(')) return projectGraphqlData()
return { updateIssueFieldValue: { issue: { id: 'issue-id' } } } return { updateProjectV2ItemFieldValue: { projectV2Item: { id: 'item-id' } } }
}) })
await initializeIssueStartDate(42, '2026-08-28') await initializeIssueStartDate(42, '2026-08-28')
assert.equal(requests.length, 2) assert.equal(requests.length, 2)
assert.match(requests[0].query, /isIssueField/) assert.match(requests[0].query, /isIssueField/)
assert.match(requests[0].query, /ProjectV2ItemIssueFieldValue/) assert.doesNotMatch(requests[0].query, /issueField\s*\{/)
assert.match(requests[1].query, /updateIssueFieldValue/) assert.match(requests[0].query, /ProjectV2ItemFieldDateValue/)
assert.match(requests[1].query, /issueField: \{fieldId: \$fieldId, dateValue: \$date\}/) assert.match(requests[1].query, /updateProjectV2ItemFieldValue/)
assert.match(requests[1].query, /value: \{date: \$date\}/)
assert.deepEqual(requests[1].variables, { assert.deepEqual(requests[1].variables, {
issueId: 'issue-id', projectId: 'project-id',
fieldId: 'start-date-issue-field-id', itemId: 'item-id',
fieldId: 'start-date-field-id',
date: '2026-08-28', date: '2026-08-28',
}) })
}) })
test('preserves an existing Issue Start date', async (t) => { test('preserves an existing Project Start Date', async (t) => {
const requests = mockGraphql(t, () => projectGraphqlData({ startDate: '2026-08-01' })) const requests = mockGraphql(t, () => projectGraphqlData({ startDate: '2026-08-01' }))
await initializeIssueStartDate(42, '2026-08-28') await initializeIssueStartDate(42, '2026-08-28')
@ -297,13 +296,13 @@ test('preserves an existing Issue Start date', async (t) => {
assert.equal(requests.length, 1) assert.equal(requests.length, 1)
}) })
test('adds a referenced Issue to the Project before setting Start date', async (t) => { test('adds a referenced Issue to the Project before setting Start Date', async (t) => {
const requests = mockGraphql(t, (request) => { const requests = mockGraphql(t, (request) => {
if (request.query.includes('query(')) return projectGraphqlData({ projectItem: false }) if (request.query.includes('query(')) return projectGraphqlData({ projectItem: false })
if (request.query.includes('addProjectV2ItemById')) { if (request.query.includes('addProjectV2ItemById')) {
return { addProjectV2ItemById: { item: { id: 'new-item-id' } } } return { addProjectV2ItemById: { item: { id: 'new-item-id' } } }
} }
return { updateIssueFieldValue: { issue: { id: 'issue-id' } } } return { updateProjectV2ItemFieldValue: { projectV2Item: { id: 'new-item-id' } } }
}) })
await initializeIssueStartDate(42, '2026-08-28') await initializeIssueStartDate(42, '2026-08-28')
@ -311,30 +310,26 @@ test('adds a referenced Issue to the Project before setting Start date', async (
assert.equal(requests.length, 3) assert.equal(requests.length, 3)
assert.deepEqual(requests[1].variables, { projectId: 'project-id', contentId: 'issue-id' }) assert.deepEqual(requests[1].variables, { projectId: 'project-id', contentId: 'issue-id' })
assert.deepEqual(requests[2].variables, { assert.deepEqual(requests[2].variables, {
issueId: 'issue-id', projectId: 'project-id',
fieldId: 'start-date-issue-field-id', itemId: 'new-item-id',
fieldId: 'start-date-field-id',
date: '2026-08-28', date: '2026-08-28',
}) })
}) })
test('rejects a missing, non-Date, or Project-local Start date field', async (t) => { test('rejects a missing, non-Date, or Issue-level Start Date field', async (t) => {
let response = projectGraphqlData({ startDateField: false }) let response = projectGraphqlData({ startDateField: false })
const requests = mockGraphql(t, () => response) const requests = mockGraphql(t, () => response)
await assert.rejects(initializeIssueStartDate(42, '2026-08-28'), /Project 缺少 Start date 字段/) await assert.rejects(initializeIssueStartDate(42, '2026-08-28'), /Project 缺少 Start Date 字段/)
response = projectGraphqlData({ startDateType: 'TEXT' }) response = projectGraphqlData({ startDateType: 'TEXT' })
await assert.rejects(initializeIssueStartDate(42, '2026-08-28'), /Start date 字段必须为 Date/) await assert.rejects(initializeIssueStartDate(42, '2026-08-28'), /Start Date 字段必须为 Date/)
response = projectGraphqlData({ startDateIsIssueField: false }) response = projectGraphqlData({ startDateIsIssueField: true })
await assert.rejects( await assert.rejects(
initializeIssueStartDate(42, '2026-08-28'), initializeIssueStartDate(42, '2026-08-28'),
/Start date 字段必须为 Issue Date 字段/, /Start Date 字段必须为 Project Date 字段/,
) )
response = projectGraphqlData({ startDateIssueField: false }) assert.equal(requests.length, 3)
await assert.rejects(
initializeIssueStartDate(42, '2026-08-28'),
/Start date 字段必须为 Issue Date 字段/,
)
assert.equal(requests.length, 4)
}) })
test('does not treat pull request references as Issue associations', () => { test('does not treat pull request references as Issue associations', () => {