diff --git a/src/Renderer/Fragment/StreamSetup/StreamSetupLoadingFragment.tsx b/src/Renderer/Fragment/StreamSetup/StreamSetupLoadingFragment.tsx index 45ff5b31..677bd85c 100644 --- a/src/Renderer/Fragment/StreamSetup/StreamSetupLoadingFragment.tsx +++ b/src/Renderer/Fragment/StreamSetup/StreamSetupLoadingFragment.tsx @@ -1,15 +1,15 @@ import React, {useEffect, useState} from 'react'; -import {ProjectProp, StreamSetupBody, StreamSetupDesc, StreamSetupFooter} from './StreamSetupCommon'; -import {Button} from '../../Library/View/Button'; -import {View} from '../../Library/View/View'; -import {UserPrefRepo} from '../../Repository/UserPrefRepo'; +import {GitHubSearchClient} from '../../Library/GitHub/GitHubSearchClient'; import {GitHubUserClient} from '../../Library/GitHub/GitHubUserClient'; -import {RemoteIssueEntity} from '../../Library/Type/RemoteGitHubV3/RemoteIssueEntity'; import {GitHubV4IssueClient} from '../../Library/GitHub/V4/GitHubV4IssueClient'; -import {GitHubSearchClient} from '../../Library/GitHub/GitHubSearchClient'; -import {Loading} from '../../Library/View/Loading'; +import {RemoteIssueEntity} from '../../Library/Type/RemoteGitHubV3/RemoteIssueEntity'; import {DateUtil} from '../../Library/Util/DateUtil'; +import {Button} from '../../Library/View/Button'; +import {Loading} from '../../Library/View/Loading'; import {Translate} from '../../Library/View/Translate'; +import {View} from '../../Library/View/View'; +import {UserPrefRepo} from '../../Repository/UserPrefRepo'; +import {ProjectProp, StreamSetupBody, StreamSetupDesc, StreamSetupFooter} from './StreamSetupCommon'; type Props = { show: boolean; @@ -116,9 +116,9 @@ async function fetchProjects(remoteIssues: RemoteIssueEntity[]): Promise<{error? projectMap[projectCard.project.url] = {url: projectCard.project.url, title: projectCard.project.name}; }); - issue.projectNextItems?.nodes?.forEach(projectNextItem => { - projectNextItem.fieldValues?.nodes?.forEach(fieldValue => { - projectMap[fieldValue.projectField.project.url] = {url: fieldValue.projectField.project.url, title: fieldValue.projectField.project.title}; + issue.projectItems?.nodes?.forEach(projectItem => { + projectItem.fieldValues?.nodes?.forEach(fieldValue => { + if (fieldValue.field != null) projectMap[fieldValue.field.project.url] = {url: fieldValue.field.project.url, title: fieldValue.field.project.title}; }); }); }); diff --git a/src/Renderer/Library/GitHub/V4/GitHubV4IssueClient.ts b/src/Renderer/Library/GitHub/V4/GitHubV4IssueClient.ts index 16292b31..13d77b03 100644 --- a/src/Renderer/Library/GitHub/V4/GitHubV4IssueClient.ts +++ b/src/Renderer/Library/GitHub/V4/GitHubV4IssueClient.ts @@ -124,35 +124,38 @@ export class GitHubV4IssueClient extends GitHubV4Client { } private static getProjectFields(v4Issue: RemoteGitHubV4IssueEntity): RemoteProjectFieldEntity[] { - return v4Issue.projectNextItems.nodes.flatMap(item => item.fieldValues.nodes).map(fieldValue => { - const dataType = fieldValue.projectField.dataType; - const value = fieldValue.value; - const name = fieldValue.projectField.name; - const settings = JSON.parse(fieldValue.projectField.settings); - const projectTitle = fieldValue.projectField.project.title; - const projectUrl = fieldValue.projectField.project.url; - - if (value == null) return null; - - if (dataType === 'SINGLE_SELECT' && settings.options != null) { - const realValue = settings.options.find(option => option.id === value); - if (realValue != null) return {name, value: realValue.name as string, projectTitle, projectUrl, dataType}; + return v4Issue.projectItems.nodes.flatMap(item => item.fieldValues.nodes).map(fieldValue => { + // github projectのfiledValueには多くの型があり、Jasperでは特定の型(__typename)にしか対応していない。 + // 未対応の型の場合はemptyになるのでチェックしている。 + if (fieldValue.field == null) return null; + + const dataType = fieldValue.field.dataType; + const name = fieldValue.field.name; + const projectTitle = fieldValue.field.project.title; + const projectUrl = fieldValue.field.project.url; + + if (dataType === 'SINGLE_SELECT' && fieldValue.name != null) { + return {name, value: fieldValue.name, projectTitle, projectUrl, dataType}; } - if (dataType === 'ITERATION' && settings.configuration != null) { - const realValue = settings.configuration.iterations.find(iteration => iteration.id === value); - if (realValue != null) return {name, value: realValue.title as string, projectTitle, projectUrl, dataType}; + if (dataType === 'ITERATION' && fieldValue.title != null) { + return {name, value: fieldValue.title, projectTitle, projectUrl, dataType}; } - if (dataType == 'DATE') { + if (dataType == 'DATE' && fieldValue.date != null) { // value is `2022-01-20T00:00:00` - return {name, value: value.split('T')[0] as string, projectTitle, projectUrl, dataType}; + return {name, value: fieldValue.date.split('T')[0], projectTitle, projectUrl, dataType}; + } + + if (dataType == 'NUMBER' && fieldValue.number != null) { + // value is `2022-01-20T00:00:00` + return {name, value: fieldValue.number.toString(), projectTitle, projectUrl, dataType}; } // titleは「何もfiledがついていないissueのprojectTitle, projectUrlを認識する」ために必要。 // もしtitleをハンドリングしないと、何もfieldがついてないissueのprojectTitle, projectUrlを判別できなくなってしまう。 - if (dataType === 'TITLE' || dataType === 'TEXT' || dataType === 'NUMBER') { - return {name, value, projectTitle, projectUrl, dataType}; + if (dataType === 'TITLE' || dataType === 'TEXT') { + return {name, value: fieldValue.text, projectTitle, projectUrl, dataType}; } return null; @@ -163,25 +166,24 @@ export class GitHubV4IssueClient extends GitHubV4Client { // iterationなfiledをfilterでうまく使えるように日付を展開した状態にする // 例: {name: 'sprint', value: '2022-01-01,2022-01-02,2022-01-03'} のようにする private static getProjectExpandedIterationField(v4Issue: RemoteGitHubV4IssueEntity): RemoteProjectFieldEntity[] { - return v4Issue.projectNextItems.nodes + return v4Issue.projectItems.nodes .flatMap(item => item.fieldValues.nodes) - .filter(fieldValue => fieldValue.projectField.dataType === 'ITERATION') + .filter(fieldValue => fieldValue.field?.dataType === 'ITERATION') .map(iterationFieldValue => { - const settings = JSON.parse(iterationFieldValue.projectField.settings); - const iteration = settings?.configuration?.iterations.find(iteration => iteration.id === iterationFieldValue.value); - if (iteration == null) return null; + const {title, duration, startDate} = iterationFieldValue; + if (title == null || duration == null || startDate == null) return null; const dateList: string[] = []; - for (let i = 0; i < iteration.duration; i++) { - const date = dayjs(iteration.start_date).add(i, 'day').format('YYYY-MM-DD'); + for (let i = 0; i < duration; i++) { + const date = dayjs(startDate).add(i, 'day').format('YYYY-MM-DD'); dateList.push(date); } return { - name: iterationFieldValue.projectField.name, + name: iterationFieldValue.field.name, value: dateList.join(','), - projectTitle: iterationFieldValue.projectField.project.title, - projectUrl: iterationFieldValue.projectField.project.url, + projectTitle: iterationFieldValue.field.project.title, + projectUrl: iterationFieldValue.field.project.url, dataType: 'EXPANDED_ITERATION', }; }) @@ -274,14 +276,14 @@ export class GitHubV4IssueClient extends GitHubV4Client { // 現時点(2022-08-17)、GHEはGitHub Project v2に対応していないので、レスポンスが得られない。 // 空の値を入れておくことでNPEを防ぐ。 issues.forEach(issue => { - if (issue.projectNextItems == null) issue.projectNextItems = {nodes: []}; + if (issue.projectItems == null) issue.projectItems = {nodes: []}; }); // 現時点(2022-08-17)では、OAuthアプリでorgのissueのgithubプロジェクト情報を取得できず、値にnullが入ってくる。 // そのためここでフィルターしておく。 // 再現方法: jasperをoauthアプリ承認していないorgで、パブリックなgithubプロジェクトに紐付いたパブリックリポジトリのissueからgithubプロジェクト情報を読み取ると再現する。 issues.forEach(issue => { - issue.projectNextItems.nodes = issue.projectNextItems.nodes.filter(item => item != null); + issue.projectItems.nodes = issue.projectItems.nodes.filter(item => item != null); }); const foundNodeIds = issues.map(issue => issue.node_id); @@ -496,20 +498,35 @@ const COMMON_QUERY_TEMPLATE = ` `; const GITHUB_PROJECT_V2 = ` - projectNextItems(first: 100) { + projectItems(first: 100) { nodes { fieldValues(first: 100) { nodes { - projectField { + # title, text + ... on ProjectV2ItemFieldTextValue { + text + field { ... on ProjectV2Field {name dataType project {url title}} } + } + # single_select + ... on ProjectV2ItemFieldSingleSelectValue { name - settings - dataType - project { - title - url - } + field {... on ProjectV2SingleSelectField { name dataType project {url title} options { name } } } + } + # iteration + ... on ProjectV2ItemFieldIterationValue { + title + field { ... on ProjectV2IterationField {name dataType project {url title}} } + } + # number + ... on ProjectV2ItemFieldNumberValue { + number + field { ... on ProjectV2Field {name dataType project {url title}} } + } + # date + ... on ProjectV2ItemFieldDateValue { + date + field { ... on ProjectV2Field {name dataType project {url title}} } } - value } } } diff --git a/src/Renderer/Library/GitHub/V4/GitHubV4ProjectNextClient.ts b/src/Renderer/Library/GitHub/V4/GitHubV4ProjectNextClient.ts index 783f8b1c..c5601fe1 100644 --- a/src/Renderer/Library/GitHub/V4/GitHubV4ProjectNextClient.ts +++ b/src/Renderer/Library/GitHub/V4/GitHubV4ProjectNextClient.ts @@ -1,11 +1,11 @@ +import {RemoteGitHubV4ProjectEntity, RemoteGitHubV4ProjectFieldEntity} from '../../Type/RemoteGitHubV4/RemoteGitHubV4ProjectEntity'; import {GitHubV4Client} from './GitHubV4Client'; -import {RemoteGitHubV4ProjectNextEntity, RemoteGitHubV4ProjectNextFieldEntity} from '../../Type/RemoteGitHubV4/RemoteGitHubV4ProjectNextEntity'; export class GitHubV4ProjectNextClient extends GitHubV4Client { async getProjectStatusFieldNames(projectUrl: string): Promise<{ error?: Error; iterationName?: string; statusNames?: string[] }> { // fetch data const query = this.buildQuery(projectUrl); - const {error, data} = await this.request(query); + const {error, data} = await this.request(query); if (error != null) return {error}; // iteration @@ -15,8 +15,7 @@ export class GitHubV4ProjectNextClient extends GitHubV4Client { // status const statusField = this.getStatusField(data); if (statusField == null) return {statusNames: [], iterationName}; - const settings = JSON.parse(statusField.settings) as { options: { name: string }[] }; - const statusNames = settings.options.map(option => option.name); + const statusNames = statusField.options.map(option => option.name); return {statusNames, iterationName}; } @@ -30,24 +29,23 @@ export class GitHubV4ProjectNextClient extends GitHubV4Client { .replace('__PROJECT_NUMBER__', projectNumber); } - private getIterationField(data: RemoteGitHubV4ProjectNextEntity): RemoteGitHubV4ProjectNextFieldEntity | null { - if (data.user?.projectNext != null) { - return data.user.projectNext.fields.nodes.find(field => field.dataType === 'ITERATION'); - - } else if (data.organization?.projectNext != null) { - return data.organization.projectNext.fields.nodes.find(field => field.dataType === 'ITERATION'); + private getIterationField(data: RemoteGitHubV4ProjectEntity): RemoteGitHubV4ProjectFieldEntity | null { + if (data.user?.projectV2 != null) { + return data.user.projectV2.fields.nodes.find(field => field.dataType === 'ITERATION'); + } else if (data.organization?.projectV2 != null) { + return data.organization.projectV2.fields.nodes.find(field => field.dataType === 'ITERATION'); } } - private getStatusField(data: RemoteGitHubV4ProjectNextEntity): RemoteGitHubV4ProjectNextFieldEntity | null { - if (data.user?.projectNext != null) { - return data.user.projectNext.fields.nodes.find(field => { - return field.dataType === 'SINGLE_SELECT' && field.name.toLocaleLowerCase() === 'status' && field.settings != null; + private getStatusField(data: RemoteGitHubV4ProjectEntity): RemoteGitHubV4ProjectFieldEntity | null { + if (data.user?.projectV2 != null) { + return data.user.projectV2.fields.nodes.find(field => { + return field.dataType === 'SINGLE_SELECT' && field.name.toLocaleLowerCase() === 'status' && field.options != null; }); - } else if (data.organization?.projectNext != null) { - return data.organization.projectNext.fields.nodes.find(field => { - return field.dataType === 'SINGLE_SELECT' && field.name.toLocaleLowerCase() === 'status' && field.settings != null; + } else if (data.organization?.projectV2 != null) { + return data.organization.projectV2.fields.nodes.find(field => { + return field.dataType === 'SINGLE_SELECT' && field.name.toLocaleLowerCase() === 'status' && field.options != null; }); } } @@ -55,15 +53,14 @@ export class GitHubV4ProjectNextClient extends GitHubV4Client { const queryTemplate = ` __QUERY_TYPE__(login: "__USER_NAME__") { - projectNext(number: __PROJECT_NUMBER__) { + projectV2(number: __PROJECT_NUMBER__) { title fields(first: 100) { nodes { - dataType - name - settings + ... on ProjectV2SingleSelectField { name dataType options { name } } + ... on ProjectV2IterationField {name dataType } } } } } -`; \ No newline at end of file +`; diff --git a/src/Renderer/Library/Type/RemoteGitHubV3/RemoteIssueEntity.ts b/src/Renderer/Library/Type/RemoteGitHubV3/RemoteIssueEntity.ts index ae87362f..4c0a3b67 100644 --- a/src/Renderer/Library/Type/RemoteGitHubV3/RemoteIssueEntity.ts +++ b/src/Renderer/Library/Type/RemoteGitHubV3/RemoteIssueEntity.ts @@ -1,4 +1,4 @@ -import {RemoteGitHubV4ProjectNextFieldValue} from '../RemoteGitHubV4/RemoteGitHubV4IssueNodesEntity'; +import {RemoteGitHubV4ProjectFieldValue} from '../RemoteGitHubV4/RemoteGitHubV4IssueNodesEntity'; export type RemoteIssueEntity = { id: number; @@ -68,7 +68,7 @@ export type RemoteProjectFieldEntity = { value: string; projectTitle: string; projectUrl: string; - dataType: RemoteGitHubV4ProjectNextFieldValue['projectField']['dataType']; + dataType: RemoteGitHubV4ProjectFieldValue['field']['dataType']; } export type RemoteReviewEntity = { diff --git a/src/Renderer/Library/Type/RemoteGitHubV4/RemoteGitHubV4IssueNodesEntity.ts b/src/Renderer/Library/Type/RemoteGitHubV4/RemoteGitHubV4IssueNodesEntity.ts index c6822ef5..5a4bae1e 100644 --- a/src/Renderer/Library/Type/RemoteGitHubV4/RemoteGitHubV4IssueNodesEntity.ts +++ b/src/Renderer/Library/Type/RemoteGitHubV4/RemoteGitHubV4IssueNodesEntity.ts @@ -31,8 +31,8 @@ export type RemoteGitHubV4IssueEntity = { projectCards: { nodes: RemoteGitHubV4ProjectCard[]; }; - projectNextItems: { - nodes: RemoteGitHubV4ProjectNextItem[]; + projectItems: { + nodes: RemoteGitHubV4ProjectItem[]; }; lastTimelineUser: string; lastTimelineAt: string; @@ -89,23 +89,32 @@ export type RemoteGitHubV4ProjectCard = { }; } -export type RemoteGitHubV4ProjectNextItem = { +export type RemoteGitHubV4ProjectItem = { fieldValues: { - nodes: RemoteGitHubV4ProjectNextFieldValue[]; + nodes: RemoteGitHubV4ProjectFieldValue[]; } } -export type RemoteGitHubV4ProjectNextFieldValue = { - projectField : { +export type RemoteGitHubV4ProjectFieldValue = { + field?: { name: string; - settings: string; dataType: 'TITLE' | 'SINGLE_SELECT' | 'ITERATION' | 'TEXT' | 'NUMBER' | 'DATE' | 'EXPANDED_ITERATION' | string; project: { title: string; url: string; } - }; - value: string; + // SINGLE_SELECTの場合の選択肢一覧 + options?: {name: string}[]; + } + text?: string; // TITLE or TEXT + number?: number; // NUMBER + date?: string; // DATE + name?: string; // SINGLE_SELECT + + // ITERATION + title?: string; + duration?: number; + startDate?: string; } export type RemoteGitHubV4Review = { diff --git a/src/Renderer/Library/Type/RemoteGitHubV4/RemoteGitHubV4ProjectEntity.ts b/src/Renderer/Library/Type/RemoteGitHubV4/RemoteGitHubV4ProjectEntity.ts new file mode 100644 index 00000000..a5b520a9 --- /dev/null +++ b/src/Renderer/Library/Type/RemoteGitHubV4/RemoteGitHubV4ProjectEntity.ts @@ -0,0 +1,21 @@ +import {RemoteGitHubV4Entity} from './RemoteGitHubV4Entity'; +import {RemoteGitHubV4ProjectFieldValue} from './RemoteGitHubV4IssueNodesEntity'; + +export type RemoteGitHubV4ProjectFieldEntity = Omit; + +export type RemoteGitHubV4ProjectEntity = RemoteGitHubV4Entity & { + user?: { + projectV2?: { + fields: { + nodes: RemoteGitHubV4ProjectFieldEntity[]; + } + } + } + organization?: { + projectV2?: { + fields: { + nodes: RemoteGitHubV4ProjectFieldEntity[]; + } + } + } +}; diff --git a/src/Renderer/Library/Type/RemoteGitHubV4/RemoteGitHubV4ProjectNextEntity.ts b/src/Renderer/Library/Type/RemoteGitHubV4/RemoteGitHubV4ProjectNextEntity.ts deleted file mode 100644 index 873979ef..00000000 --- a/src/Renderer/Library/Type/RemoteGitHubV4/RemoteGitHubV4ProjectNextEntity.ts +++ /dev/null @@ -1,21 +0,0 @@ -import {RemoteGitHubV4Entity} from './RemoteGitHubV4Entity'; -import {RemoteGitHubV4ProjectNextFieldValue} from './RemoteGitHubV4IssueNodesEntity'; - -export type RemoteGitHubV4ProjectNextFieldEntity = Omit; - -export type RemoteGitHubV4ProjectNextEntity = RemoteGitHubV4Entity & { - user?: { - projectNext?: { - fields: { - nodes: RemoteGitHubV4ProjectNextFieldEntity[]; - } - } - } - organization?: { - projectNext?: { - fields: { - nodes: RemoteGitHubV4ProjectNextFieldEntity[]; - } - } - } -};