feat: import and export tickets as CSV
Every ticket of a project can be downloaded as CSV, and a CSV exported from TaskStream or from Jira can be imported: types, statuses, priorities and people are matched by id or name, the rest falls back with a warning, and parents are rebuilt. The settings page previews an import before it creates anything. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { inject, Injectable } from '@angular/core';
|
||||
import {
|
||||
AccessTokenSource,
|
||||
API_CONFIG,
|
||||
} from '@task-stream/web/shared/infrastructure';
|
||||
import { ImportReport, TransferGateway } from '@task-stream/web/tracker/domain';
|
||||
import { from, Observable, switchMap } from 'rxjs';
|
||||
|
||||
/** CSV files travel over REST, like attachments. */
|
||||
@Injectable()
|
||||
export class HttpTransferGateway extends TransferGateway {
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly api = inject(API_CONFIG);
|
||||
private readonly tokens = inject(AccessTokenSource);
|
||||
|
||||
exportCsv(projectId: string): Observable<Blob> {
|
||||
return this.authorized((headers) =>
|
||||
this.http.get(`${this.base(projectId)}/export.csv`, {
|
||||
headers,
|
||||
responseType: 'blob',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
importCsv(
|
||||
projectId: string,
|
||||
file: File,
|
||||
dryRun: boolean,
|
||||
): Observable<ImportReport> {
|
||||
const body = new FormData();
|
||||
body.append('file', file, file.name);
|
||||
return this.authorized((headers) =>
|
||||
this.http.post<ImportReport>(`${this.base(projectId)}/import`, body, {
|
||||
headers,
|
||||
params: { dryRun: String(dryRun) },
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private base(projectId: string): string {
|
||||
return `${this.api.restUrl}/projects/${encodeURIComponent(projectId)}`;
|
||||
}
|
||||
|
||||
private authorized<T>(
|
||||
request: (headers: HttpHeaders) => Observable<T>,
|
||||
): Observable<T> {
|
||||
return from(this.tokens.accessToken()).pipe(
|
||||
switchMap((token) =>
|
||||
request(
|
||||
token
|
||||
? new HttpHeaders({ Authorization: `Bearer ${token}` })
|
||||
: new HttpHeaders(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
SprintsGateway,
|
||||
GitGateway,
|
||||
WebhooksGateway,
|
||||
TransferGateway,
|
||||
TrackerRealtimeGateway,
|
||||
UsersGateway,
|
||||
} from '@task-stream/web/tracker/domain';
|
||||
@@ -32,6 +33,7 @@ import { GraphqlSavedFiltersGateway } from './adapters/graphql-saved-filters.gat
|
||||
import { GraphqlSprintsGateway } from './adapters/graphql-sprints.gateway';
|
||||
import { GraphqlGitGateway } from './adapters/graphql-git.gateway';
|
||||
import { GraphqlWebhooksGateway } from './adapters/graphql-webhooks.gateway';
|
||||
import { HttpTransferGateway } from './adapters/http-transfer.gateway';
|
||||
|
||||
/** Ports → GraphQL adapters (and REST for files). */
|
||||
export function provideTrackerInfrastructure(): EnvironmentProviders {
|
||||
@@ -49,5 +51,6 @@ export function provideTrackerInfrastructure(): EnvironmentProviders {
|
||||
{ provide: SprintsGateway, useClass: GraphqlSprintsGateway },
|
||||
{ provide: GitGateway, useClass: GraphqlGitGateway },
|
||||
{ provide: WebhooksGateway, useClass: GraphqlWebhooksGateway },
|
||||
{ provide: TransferGateway, useClass: HttpTransferGateway },
|
||||
]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user