diff --git a/.changeset/upset-hands-hang.md b/.changeset/upset-hands-hang.md
new file mode 100644
index 0000000..a579738
--- /dev/null
+++ b/.changeset/upset-hands-hang.md
@@ -0,0 +1,7 @@
+---
+"specif-ai": patch
+---
+
+make prd user story export format consistent with prds export format
+extract clipboard logic into a clipboard service
+removed CSV export option when exporting prd user stories
diff --git a/ui/src/app/constants/export.constants.ts b/ui/src/app/constants/export.constants.ts
index 737a47c..ae390c2 100644
--- a/ui/src/app/constants/export.constants.ts
+++ b/ui/src/app/constants/export.constants.ts
@@ -1,7 +1,19 @@
+import { REQUIREMENT_TYPE } from "./app.constants";
+
export const EXPORT_FILE_FORMATS = {
JSON: 'json',
EXCEL: 'xlsx',
} as const;
+export const SPREADSHEET_HEADER_ROW = {
+ [REQUIREMENT_TYPE.BRD]: ['Id', 'Title', 'Description'],
+ [REQUIREMENT_TYPE.PRD]: ['Id', 'Title', 'Description'],
+ [REQUIREMENT_TYPE.NFR]: ['Id', 'Title', 'Description'],
+ [REQUIREMENT_TYPE.UIR]: ['Id', 'Title', 'Description'],
+ [REQUIREMENT_TYPE.BP]: ['Id', 'Title', 'Description'],
+ [REQUIREMENT_TYPE.US]: ['Id', 'Parent Id', 'Name', 'Description'],
+ [REQUIREMENT_TYPE.TASK]: ['Id', 'Parent Id', 'Title', 'Acceptance Criteria'],
+}
+
export type ExportFileFormat =
(typeof EXPORT_FILE_FORMATS)[keyof typeof EXPORT_FILE_FORMATS];
diff --git a/ui/src/app/pages/tasks/task-list/task-list.component.ts b/ui/src/app/pages/tasks/task-list/task-list.component.ts
index d8f9e96..ce6693d 100644
--- a/ui/src/app/pages/tasks/task-list/task-list.component.ts
+++ b/ui/src/app/pages/tasks/task-list/task-list.component.ts
@@ -25,7 +25,7 @@ import { ButtonComponent } from '../../../components/core/button/button.componen
import { NgIconComponent } from '@ng-icons/core';
import { ListItemComponent } from '../../../components/core/list-item/list-item.component';
import { BadgeComponent } from '../../../components/core/badge/badge.component';
-import { Clipboard } from '@angular/cdk/clipboard';
+import { ClipboardService } from '../../../services/clipboard.service';
import { TOASTER_MESSAGES } from 'src/app/constants/app.constants';
import { ToasterService } from 'src/app/services/toaster/toaster.service';
import { SearchInputComponent } from '../../../components/core/search-input/search-input.component';
@@ -53,7 +53,7 @@ export class TaskListComponent implements OnInit, OnDestroy {
store = inject(Store);
logger = inject(NGXLogger);
router = inject(Router);
- clipboard = inject(Clipboard);
+ clipboardService = inject(ClipboardService);
searchService = inject(SearchService);
userStoryId: string | null = '';
userStories: IUserStory[] = [];
@@ -226,10 +226,16 @@ export class TaskListComponent implements OnInit, OnDestroy {
copyTaskContent(event: Event, task: any) {
event.stopPropagation();
const taskContent = `${task.id}: ${task.list}\n${task.acceptance || ''}`;
- this.clipboard.copy(taskContent);
- this.toastService.showSuccess(
- TOASTER_MESSAGES.ENTITY.COPY.SUCCESS(this.entityType, task.id),
- );
+ const success = this.clipboardService.copyToClipboard(taskContent);
+ if (success) {
+ this.toastService.showSuccess(
+ TOASTER_MESSAGES.ENTITY.COPY.SUCCESS(this.entityType, task.id),
+ );
+ } else {
+ this.toastService.showError(
+ TOASTER_MESSAGES.ENTITY.COPY.FAILURE(this.entityType, task.id),
+ );
+ }
}
ngOnInit() {
diff --git a/ui/src/app/pages/user-stories/user-stories.component.html b/ui/src/app/pages/user-stories/user-stories.component.html
index 08731c0..6269cd5 100644
--- a/ui/src/app/pages/user-stories/user-stories.component.html
+++ b/ui/src/app/pages/user-stories/user-stories.component.html
@@ -46,14 +46,14 @@
User Stories
@@ -64,13 +64,6 @@ User Stories
>
Sync with Jira
-
{
- this.exportData = this.prepareExportData(res);
- this.jsonOutput = {
- userStories: res.map((userStory) => ({
- name: userStory.name,
- description: userStory.description,
- tasks:
- userStory.tasks?.map((task) => ({
- list: task.list,
- acceptanceCriteria: task.acceptance,
- })) || [],
- })),
- };
- });
- }
-
- private prepareExportData(stories: any): any {
- const worksheetData = [
- ['User Story', 'Description', 'Task', 'Acceptance Criteria'],
- ];
-
- stories.forEach((userStory: any) => {
- userStory.tasks.forEach((task: any) => {
- worksheetData.push([
- userStory.name,
- userStory.description,
- task.list,
- task.acceptance,
- ]);
- });
- });
- return worksheetData;
}
copyUserStoryContent(event: Event, userStory: IUserStory) {
event.stopPropagation();
const userStoryContent = `${userStory.id}: ${userStory.name}\n${userStory.description || ''}`;
- this.clipboard.copy(userStoryContent);
- this.toast.showSuccess(
- TOASTER_MESSAGES.ENTITY.COPY.SUCCESS(this.entityType, userStory.id),
- );
- }
-
- copyToClipboard() {
- this.clipboard.copy(JSON.stringify(this.jsonOutput));
- }
-
- exportToExcel() {
- this.spreadSheetService.exportToExcel(
- [
- {
- data: this.exportData,
- },
- ],
- `${this.navigation.data.name}_${this.navigation.fileName.split('-')[0]}`,
- );
+ const success = this.clipboardService.copyToClipboard(userStoryContent);
+ if (success) {
+ this.toast.showSuccess(
+ TOASTER_MESSAGES.ENTITY.COPY.SUCCESS(this.entityType, userStory.id),
+ );
+ } else {
+ this.toast.showError(
+ TOASTER_MESSAGES.ENTITY.COPY.FAILURE(this.entityType, userStory.id),
+ );
+ }
}
- exportToCSV() {
- this.spreadSheetService.exportToCsv(
- this.exportData,
- `${this.navigation.data.name}_${this.navigation.fileName.split('-')[0]}`,
+ exportUserStories(exportType: ExportFileFormat) {
+ this.store.dispatch(
+ new ExportUserStories({
+ type: exportType,
+ }),
);
}
diff --git a/ui/src/app/services/clipboard.service.ts b/ui/src/app/services/clipboard.service.ts
new file mode 100644
index 0000000..c9d63f3
--- /dev/null
+++ b/ui/src/app/services/clipboard.service.ts
@@ -0,0 +1,31 @@
+import { Clipboard } from '@angular/cdk/clipboard';
+import { Injectable } from '@angular/core';
+import { NGXLogger } from 'ngx-logger';
+
+@Injectable({
+ providedIn: 'root',
+})
+export class ClipboardService {
+ constructor(private logger: NGXLogger, private clipboard: Clipboard) {}
+
+ public copyToClipboard(data: string | object): boolean {
+ if (data === null || data === undefined) {
+ return false;
+ }
+
+ try {
+ let contentToCopy: string;
+
+ if (typeof data === 'object') {
+ contentToCopy = JSON.stringify(data);
+ } else {
+ contentToCopy = data;
+ }
+
+ return this.clipboard.copy(contentToCopy);
+ } catch (error) {
+ this.logger.error('Failed to copy to clipboard:', error);
+ return false;
+ }
+ }
+}
diff --git a/ui/src/app/services/export/requirement-export-strategy.manager.ts b/ui/src/app/services/export/requirement-export-strategy.manager.ts
index ff5c907..2aef778 100644
--- a/ui/src/app/services/export/requirement-export-strategy.manager.ts
+++ b/ui/src/app/services/export/requirement-export-strategy.manager.ts
@@ -1,4 +1,3 @@
-import { Clipboard } from '@angular/cdk/clipboard';
import { Injectable } from '@angular/core';
import { NGXLogger } from 'ngx-logger';
import { REQUIREMENT_TYPE } from 'src/app/constants/app.constants';
@@ -10,6 +9,8 @@ import { NFRExportStrategy } from './strategies/nfr-export.strategy';
import { PRDExportStrategy } from './strategies/prd-export.strategy';
import { UIRExportStrategy } from './strategies/uir-export.strategy';
import { AppSystemService } from '../app-system/app-system.service';
+import { ClipboardService } from '../clipboard.service';
+import { UserStoriesExportStrategy } from './strategies/user-stories-export.strategy';
@Injectable({
providedIn: 'root',
@@ -21,7 +22,7 @@ export class RequirementExportStrategyManager {
private logger: NGXLogger,
private exportService: SpreadSheetService,
private appSystemService: AppSystemService,
- private clipboard: Clipboard,
+ private clipboardService: ClipboardService,
) {}
initializeStrategy(requirementType: string) {
@@ -31,35 +32,42 @@ export class RequirementExportStrategyManager {
this.exportService,
this.appSystemService,
this.logger,
- this.clipboard,
+ this.clipboardService,
);
}
case REQUIREMENT_TYPE.BRD: {
return new BRDExportStrategy(
this.exportService,
this.logger,
- this.clipboard,
+ this.clipboardService,
);
}
case REQUIREMENT_TYPE.BP: {
return new BPExportStrategy(
this.exportService,
this.logger,
- this.clipboard,
+ this.clipboardService,
);
}
case REQUIREMENT_TYPE.NFR: {
return new NFRExportStrategy(
this.exportService,
this.logger,
- this.clipboard,
+ this.clipboardService,
);
}
case REQUIREMENT_TYPE.UIR: {
return new UIRExportStrategy(
this.exportService,
this.logger,
- this.clipboard,
+ this.clipboardService,
+ );
+ }
+ case REQUIREMENT_TYPE.US: {
+ return new UserStoriesExportStrategy(
+ this.exportService,
+ this.logger,
+ this.clipboardService,
);
}
default: {
diff --git a/ui/src/app/services/export/requirement-export.service.ts b/ui/src/app/services/export/requirement-export.service.ts
index 2cfdbb0..9fc5bbd 100644
--- a/ui/src/app/services/export/requirement-export.service.ts
+++ b/ui/src/app/services/export/requirement-export.service.ts
@@ -7,6 +7,18 @@ import {
REQUIREMENT_DISPLAY_NAME_MAP,
RequirementType,
} from 'src/app/constants/app.constants';
+import { IUserStory } from 'src/app/model/interfaces/IUserStory';
+import { IList } from 'src/app/model/interfaces/IList';
+
+// types
+
+type RequirementExportInputData = {
+ prdId:string;
+ userStories: Array;
+} | Array;
+
+// types
+
@Injectable({
providedIn: 'root',
@@ -19,14 +31,14 @@ export class RequirementExportService {
) {}
public async exportRequirementData(
- files: any[],
+ data: RequirementExportInputData,
options: ExportRequirementDataOptions & { projectName: string },
requirementType: string,
): Promise {
try {
const strategy = this.strategyManager.getStrategy(requirementType);
- const result = await strategy.export(files, {
+ const result = await strategy.export(data, {
format: options.type,
projectName: options.projectName,
});
diff --git a/ui/src/app/services/export/strategies/base-requirement-export.strategy.ts b/ui/src/app/services/export/strategies/base-requirement-export.strategy.ts
index a8d880e..972e99b 100644
--- a/ui/src/app/services/export/strategies/base-requirement-export.strategy.ts
+++ b/ui/src/app/services/export/strategies/base-requirement-export.strategy.ts
@@ -1,42 +1,44 @@
-import { Clipboard } from '@angular/cdk/clipboard';
import { NGXLogger } from 'ngx-logger';
+import { IList } from 'src/app/model/interfaces/IList';
import {
- REQUIREMENT_TYPE,
REQUIREMENT_DISPLAY_NAME_MAP,
+ REQUIREMENT_TYPE,
RequirementType,
} from '../../../constants/app.constants';
-import { EXPORT_FILE_FORMATS } from '../../../constants/export.constants';
+import {
+ EXPORT_FILE_FORMATS,
+ SPREADSHEET_HEADER_ROW,
+} from '../../../constants/export.constants';
+import { ClipboardService } from '../../clipboard.service';
import { SpreadSheetService } from '../../spreadsheet.service';
import { ExportOptions, ExportResult, ExportStrategy } from './export.strategy';
// types
-type BaseRequirementData = {
+type FormattedRequirementItem = {
id: string;
title: string;
requirement: string;
};
+type RequirementItemRowArr = [string, string, string];
+
// types
export abstract class BaseRequirementExportStrategy implements ExportStrategy {
constructor(
- protected exportService: SpreadSheetService,
protected logger: NGXLogger,
- protected clipboard: Clipboard,
protected requirementType: RequirementType,
+ protected exportService: SpreadSheetService,
+ protected clipboardService: ClipboardService,
) {}
- supports(type: string): boolean {
- return type === this.requirementType;
- }
-
- async prepareData(files: any[]): Promise> {
+ protected prepareData(files: Array): Array {
try {
- const data: BaseRequirementData[] = files.map((file) => ({
+ const data: FormattedRequirementItem[] = files.map((file) => ({
id: file.fileName.split('-')[0],
- title: file.content.title,
- requirement: file.content.requirement,
+ title: file.content.title!,
+ requirement: file.content.requirement!,
}));
return data;
@@ -49,58 +51,57 @@ export abstract class BaseRequirementExportStrategy implements ExportStrategy {
}
}
- async export(data: any[], options: ExportOptions): Promise {
+ async export(
+ data: Array,
+ options: ExportOptions,
+ ): Promise {
try {
const { format, projectName } = options;
-
- const preparedData = await this.prepareData(data);
-
- if (format === EXPORT_FILE_FORMATS.JSON) {
- const success = this.exportToJSON(preparedData);
- return {
- success: success,
- };
- }
-
- const transformedData = this.transformData(preparedData);
- const fileName = `${projectName}_${REQUIREMENT_TYPE[this.requirementType].toLowerCase()}`;
-
- if (format === EXPORT_FILE_FORMATS.EXCEL) {
- await this.exportToExcel(transformedData, fileName);
- } else {
- throw new Error(`Format ${format} not supported`);
+ const preparedData = this.prepareData(data);
+
+ let success = true;
+
+ switch (format) {
+ case EXPORT_FILE_FORMATS.JSON: {
+ success = this.clipboardService.copyToClipboard(preparedData);
+ break;
+ }
+ case EXPORT_FILE_FORMATS.EXCEL: {
+ const transformedData = this.transformData(preparedData);
+ const fileName = `${projectName}_${REQUIREMENT_TYPE[this.requirementType].toLowerCase()}`;
+ this.exportToExcel(transformedData, fileName);
+ break;
+ }
+ default: {
+ throw new Error(`Format ${format} not supported`);
+ }
}
- return { success: true };
+ return { success: success };
} catch (error) {
this.logger.error(`${this.requirementType} export failed:`, error);
return { success: false, error: error as Error };
}
}
- protected transformData(
- data: BaseRequirementData[],
- ): Array<[string, string, string]> {
- return data.map((d) => [d.id, d.title, d.requirement]);
+ protected transformData(data: FormattedRequirementItem[]) {
+ return data.map(
+ (d) => [d.id, d.title, d.requirement] as RequirementItemRowArr,
+ );
}
- protected async exportToExcel(
- data: Array<[string, string, string]>,
+ protected exportToExcel(
+ data: Array,
fileName: string,
- ): Promise {
+ ) {
this.exportService.exportToExcel(
[
{
name: REQUIREMENT_DISPLAY_NAME_MAP[this.requirementType],
- data: [['Id', 'Title', 'Requirement'], ...data],
+ data: [SPREADSHEET_HEADER_ROW[this.requirementType], ...data],
},
],
fileName,
);
}
-
- protected exportToJSON(data: any) {
- const success = this.clipboard.copy(JSON.stringify(data, null, 2));
- return success;
- }
}
diff --git a/ui/src/app/services/export/strategies/bp-export.strategy.ts b/ui/src/app/services/export/strategies/bp-export.strategy.ts
index 91831e0..8cc8c66 100644
--- a/ui/src/app/services/export/strategies/bp-export.strategy.ts
+++ b/ui/src/app/services/export/strategies/bp-export.strategy.ts
@@ -1,4 +1,4 @@
-import { Clipboard } from '@angular/cdk/clipboard';
+import { ClipboardService } from '../../clipboard.service';
import { NGXLogger } from 'ngx-logger';
import { REQUIREMENT_TYPE } from 'src/app/constants/app.constants';
import { SpreadSheetService } from '../../spreadsheet.service';
@@ -8,8 +8,8 @@ export class BPExportStrategy extends BaseRequirementExportStrategy {
constructor(
exportService: SpreadSheetService,
logger: NGXLogger,
- clipboard: Clipboard,
+ clipboardService: ClipboardService,
) {
- super(exportService, logger, clipboard, REQUIREMENT_TYPE.BP);
+ super(logger, REQUIREMENT_TYPE.BP, exportService, clipboardService);
}
}
diff --git a/ui/src/app/services/export/strategies/brd-export.strategy.ts b/ui/src/app/services/export/strategies/brd-export.strategy.ts
index 4ff5a3a..ab043e0 100644
--- a/ui/src/app/services/export/strategies/brd-export.strategy.ts
+++ b/ui/src/app/services/export/strategies/brd-export.strategy.ts
@@ -1,5 +1,5 @@
import { NGXLogger } from 'ngx-logger';
-import { Clipboard } from '@angular/cdk/clipboard';
+import { ClipboardService } from '../../clipboard.service';
import { SpreadSheetService } from '../../spreadsheet.service';
import { BaseRequirementExportStrategy } from './base-requirement-export.strategy';
import { REQUIREMENT_TYPE } from 'src/app/constants/app.constants';
@@ -8,8 +8,8 @@ export class BRDExportStrategy extends BaseRequirementExportStrategy {
constructor(
exportService: SpreadSheetService,
logger: NGXLogger,
- clipboard: Clipboard,
+ clipboardService: ClipboardService,
) {
- super(exportService, logger, clipboard, REQUIREMENT_TYPE.BRD);
+ super(logger, REQUIREMENT_TYPE.BP, exportService, clipboardService);
}
}
diff --git a/ui/src/app/services/export/strategies/export.strategy.ts b/ui/src/app/services/export/strategies/export.strategy.ts
index 7ea52ea..865f86f 100644
--- a/ui/src/app/services/export/strategies/export.strategy.ts
+++ b/ui/src/app/services/export/strategies/export.strategy.ts
@@ -11,6 +11,5 @@ export interface ExportResult {
}
export interface ExportStrategy {
- supports(requirementType: string): boolean;
- export(data: any[], options: ExportOptions): Promise;
+ export(data: unknown, options: ExportOptions): Promise;
}
diff --git a/ui/src/app/services/export/strategies/nfr-export.strategy.ts b/ui/src/app/services/export/strategies/nfr-export.strategy.ts
index 8aea05c..7a0bb7f 100644
--- a/ui/src/app/services/export/strategies/nfr-export.strategy.ts
+++ b/ui/src/app/services/export/strategies/nfr-export.strategy.ts
@@ -1,15 +1,15 @@
-import { Clipboard } from '@angular/cdk/clipboard';
import { NGXLogger } from 'ngx-logger';
-import { REQUIREMENT_TYPE } from 'src/app/constants/app.constants';
+import { ClipboardService } from '../../clipboard.service';
import { SpreadSheetService } from '../../spreadsheet.service';
import { BaseRequirementExportStrategy } from './base-requirement-export.strategy';
+import { REQUIREMENT_TYPE } from 'src/app/constants/app.constants';
export class NFRExportStrategy extends BaseRequirementExportStrategy {
constructor(
exportService: SpreadSheetService,
logger: NGXLogger,
- clipboard: Clipboard,
+ clipboardService: ClipboardService,
) {
- super(exportService, logger, clipboard, REQUIREMENT_TYPE.NFR);
+ super(logger, REQUIREMENT_TYPE.BP, exportService, clipboardService);
}
}
diff --git a/ui/src/app/services/export/strategies/prd-export.strategy.ts b/ui/src/app/services/export/strategies/prd-export.strategy.ts
index 9e7605f..75b834f 100644
--- a/ui/src/app/services/export/strategies/prd-export.strategy.ts
+++ b/ui/src/app/services/export/strategies/prd-export.strategy.ts
@@ -1,40 +1,41 @@
import { NGXLogger } from 'ngx-logger';
-import { Clipboard } from '@angular/cdk/clipboard';
+import { IUserStory } from 'src/app/model/interfaces/IUserStory';
import {
- REQUIREMENT_TYPE,
+ FILTER_STRINGS,
REQUIREMENT_DISPLAY_NAME_MAP,
+ REQUIREMENT_TYPE,
REQUIREMENT_TYPE_FOLDER_MAP,
- FILTER_STRINGS,
} from '../../../constants/app.constants';
-import { EXPORT_FILE_FORMATS } from '../../../constants/export.constants';
-import { SpreadSheetService } from '../../spreadsheet.service';
-import { ExportStrategy, ExportOptions, ExportResult } from './export.strategy';
+import { EXPORT_FILE_FORMATS, SPREADSHEET_HEADER_ROW } from '../../../constants/export.constants';
import { AppSystemService } from '../../app-system/app-system.service';
-import { IUserStory } from 'src/app/model/interfaces/IUserStory';
+import { ClipboardService } from '../../clipboard.service';
+import { SpreadSheetService } from '../../spreadsheet.service';
+import { ExportOptions, ExportResult, ExportStrategy } from './export.strategy';
+import { IList } from 'src/app/model/interfaces/IList';
// types
-type PRDTask = {
+type FormattedTask = {
id: string;
title: string;
acceptance: string;
};
-type PRDUserStory = {
+type FormattedUserStory = {
id: string;
name: string;
description: string;
- tasks: PRDTask[];
+ tasks: FormattedTask[];
};
-type PRDData = {
+type FormattedPRD = {
id: string;
title: string;
requirement: string;
- features: PRDUserStory[];
+ features: FormattedUserStory[];
};
-type PRDExportData = {
+type PRDExportRows = {
prdRows: Array<[string, string, string]>;
userStories: Array<[string, string, string, string]>;
tasks: Array<[string, string, string, string]>;
@@ -47,20 +48,19 @@ export class PRDExportStrategy implements ExportStrategy {
private exportService: SpreadSheetService,
private appSystemService: AppSystemService,
private logger: NGXLogger,
- private clipboard: Clipboard,
+ private clipboardService: ClipboardService,
) {}
supports(requirementType: string): boolean {
return requirementType === REQUIREMENT_TYPE.PRD;
}
- async prepareData(prdFiles: any[], projectName: string): Promise {
+ async prepareData(prdFiles: IList[], projectName: string): Promise {
try {
- // Initialize PRDs with basic data
- const prds: PRDData[] = prdFiles.map((prdFile) => ({
+ const prds: FormattedPRD[] = prdFiles.map((prdFile) => ({
id: prdFile.fileName.split('-')[0],
- title: prdFile.content.title,
- requirement: prdFile.content.requirement,
+ title: prdFile.content.title!,
+ requirement: prdFile.content.requirement!,
features: [],
}));
@@ -90,7 +90,7 @@ export class PRDExportStrategy implements ExportStrategy {
const userStories: Array =
JSON.parse(res).features || [];
- const userStoriesFormatted: PRDUserStory[] = userStories.map(
+ const userStoriesFormatted: FormattedUserStory[] = userStories.map(
(userStory) => {
const storyId = `${prdId}-${userStory.id}`;
return {
@@ -110,7 +110,7 @@ export class PRDExportStrategy implements ExportStrategy {
},
);
- return [prdId, userStoriesFormatted] as [string, PRDUserStory[]];
+ return [prdId, userStoriesFormatted] as [string, FormattedUserStory[]];
} catch (error) {
this.logger.error('Error processing feature file:', {
path,
@@ -138,47 +138,46 @@ export class PRDExportStrategy implements ExportStrategy {
}
}
- async export(data: any[], options: ExportOptions): Promise {
+ async export(data: IList[], options: ExportOptions): Promise {
try {
- const { format, projectName } = options;
+ const { format: exportFormat, projectName } = options;
- // First prepare the data
const preparedData = await this.prepareData(data, projectName);
let success = true;
- if (format === EXPORT_FILE_FORMATS.JSON) {
- const success = this.exportToJSON(preparedData);
- return {
- success: success,
- };
- }
-
- const transformedData = this.transformData(preparedData);
- const fileName = `${projectName}_${REQUIREMENT_TYPE.PRD.toLowerCase()}`;
-
- if (format === EXPORT_FILE_FORMATS.EXCEL) {
- this.exportToExcel(transformedData, fileName);
- } else {
- throw new Error(`Format ${format} not supported`);
+ switch (exportFormat) {
+ case EXPORT_FILE_FORMATS.JSON: {
+ success = this.clipboardService.copyToClipboard(preparedData);
+ break;
+ }
+ case EXPORT_FILE_FORMATS.EXCEL: {
+ const transformedData = this.transformData(preparedData);
+ const fileName = `${projectName}_${REQUIREMENT_TYPE.PRD.toLowerCase()}`;
+ this.exportToExcel(transformedData, fileName);
+ break;
+ }
+ default: {
+ throw new Error(`Format ${exportFormat} not supported`);
+ }
}
-
- return { success: true };
+
+ return { success: success };
} catch (error) {
this.logger.error('PRD export failed:', error);
return { success: false, error: error as Error };
}
}
- private transformData(data: PRDData[]): PRDExportData {
- const prdRows: Array<[string, string, string]> = data.map((d) => [
+ private transformData(data: FormattedPRD[]): PRDExportRows {
+ const prdRows: PRDExportRows["prdRows"] = data.map((d) => [
d.id,
d.title,
d.requirement,
]);
- const userStories: Array<[string, string, string, string]> = [];
- const tasks: Array<[string, string, string, string]> = [];
+ const userStories: PRDExportRows["userStories"] = [];
+ const tasks: PRDExportRows["tasks"] = [];
data.forEach((prd) => {
prd.features?.forEach((story) => {
@@ -193,30 +192,25 @@ export class PRDExportStrategy implements ExportStrategy {
return { prdRows, userStories, tasks };
}
- private exportToExcel(data: PRDExportData, fileName: string) {
+ private exportToExcel(data: PRDExportRows, fileName: string) {
const { prdRows, userStories, tasks } = data;
this.exportService.exportToExcel(
[
{
name: REQUIREMENT_DISPLAY_NAME_MAP[REQUIREMENT_TYPE.PRD],
- data: [['Id', 'Title', 'Requirement'], ...prdRows],
+ data: [SPREADSHEET_HEADER_ROW.PRD, ...prdRows],
},
{
name: REQUIREMENT_DISPLAY_NAME_MAP[REQUIREMENT_TYPE.US],
- data: [['Id', 'Parent Id', 'Name', 'Description'], ...userStories],
+ data: [SPREADSHEET_HEADER_ROW.US, ...userStories],
},
{
name: REQUIREMENT_DISPLAY_NAME_MAP[REQUIREMENT_TYPE.TASK],
- data: [['Id', 'Parent Id', 'Title', 'Acceptance Criteria'], ...tasks],
+ data: [SPREADSHEET_HEADER_ROW.TASK, ...tasks],
},
],
fileName,
);
}
-
- private exportToJSON(data: PRDData[]) {
- const success = this.clipboard.copy(JSON.stringify(data, null, 2));
- return success;
- }
}
diff --git a/ui/src/app/services/export/strategies/uir-export.strategy.ts b/ui/src/app/services/export/strategies/uir-export.strategy.ts
index fdcda98..ae1337e 100644
--- a/ui/src/app/services/export/strategies/uir-export.strategy.ts
+++ b/ui/src/app/services/export/strategies/uir-export.strategy.ts
@@ -1,15 +1,15 @@
-import { Clipboard } from '@angular/cdk/clipboard';
import { NGXLogger } from 'ngx-logger';
-import { REQUIREMENT_TYPE } from 'src/app/constants/app.constants';
+import { ClipboardService } from '../../clipboard.service';
import { SpreadSheetService } from '../../spreadsheet.service';
import { BaseRequirementExportStrategy } from './base-requirement-export.strategy';
+import { REQUIREMENT_TYPE } from 'src/app/constants/app.constants';
export class UIRExportStrategy extends BaseRequirementExportStrategy {
constructor(
exportService: SpreadSheetService,
logger: NGXLogger,
- clipboard: Clipboard,
+ clipboardService: ClipboardService,
) {
- super(exportService, logger, clipboard, REQUIREMENT_TYPE.UIR);
+ super(logger, REQUIREMENT_TYPE.BP, exportService, clipboardService);
}
}
diff --git a/ui/src/app/services/export/strategies/user-stories-export.strategy.ts b/ui/src/app/services/export/strategies/user-stories-export.strategy.ts
new file mode 100644
index 0000000..3955a11
--- /dev/null
+++ b/ui/src/app/services/export/strategies/user-stories-export.strategy.ts
@@ -0,0 +1,146 @@
+import { NGXLogger } from 'ngx-logger';
+import {
+ REQUIREMENT_DISPLAY_NAME_MAP,
+ REQUIREMENT_TYPE,
+} from 'src/app/constants/app.constants';
+import {
+ EXPORT_FILE_FORMATS,
+ SPREADSHEET_HEADER_ROW,
+} from 'src/app/constants/export.constants';
+import { IUserStory } from 'src/app/model/interfaces/IUserStory';
+import { ClipboardService } from '../../clipboard.service';
+import { SpreadSheetService } from '../../spreadsheet.service';
+import { ExportOptions, ExportResult, ExportStrategy } from './export.strategy';
+
+// types
+
+type ExportInputData = {
+ prdId: string;
+ userStories: Array;
+};
+
+type FormattedStory = {
+ id: string;
+ name: string;
+ description: string;
+ tasks: {
+ id: string;
+ title: string;
+ acceptance: string;
+ }[];
+};
+
+type StoriesExportData = {
+ userStoryRows: Array<[string, string, string, string]>;
+ taskRows: Array<[string, string, string, string]>;
+};
+
+// types
+
+export class UserStoriesExportStrategy implements ExportStrategy {
+ constructor(
+ private exportService: SpreadSheetService,
+ private logger: NGXLogger,
+ private clipboardService: ClipboardService,
+ ) {}
+
+ private prepareData(
+ data: ExportInputData,
+ projectName: string,
+ ): Array {
+ try {
+ const userStories: Array = data.userStories || [];
+
+ const userStoriesFormatted = userStories.map((userStory) => {
+ const storyId = `${data.prdId}-${userStory.id}`;
+ const tasks =
+ userStory.tasks?.map((task) => {
+ const taskId = `${storyId}-${task.id}`;
+ return {
+ id: taskId,
+ title: task.list,
+ acceptance: task.acceptance,
+ };
+ }) ?? [];
+
+ return {
+ id: storyId,
+ name: userStory.name,
+ description: userStory.description,
+ tasks: tasks,
+ };
+ });
+
+ return userStoriesFormatted;
+ } catch (error) {
+ this.logger.error('Error preparing PRD export data:', error);
+ throw error;
+ }
+ }
+
+ async export(
+ data: ExportInputData,
+ options: ExportOptions,
+ ): Promise {
+ const { projectName, format: exportFormat } = options;
+
+ const preparedData = this.prepareData(data, options.projectName);
+
+ let success = true;
+
+ switch (exportFormat) {
+ case EXPORT_FILE_FORMATS.JSON: {
+ success = this.clipboardService.copyToClipboard(preparedData);
+ break;
+ }
+ case EXPORT_FILE_FORMATS.EXCEL: {
+ const transformedData = this.transformData(data.prdId, preparedData);
+ const fileName = `${projectName}_${REQUIREMENT_TYPE.PRD.toLowerCase()}`;
+ this.exportToExcel(transformedData, fileName);
+ break;
+ }
+ default: {
+ throw new Error(`Format ${exportFormat} not supported`);
+ }
+ }
+
+ return { success: success };
+ }
+
+ private transformData(
+ prdId: string,
+ formattedUserStories: FormattedStory[],
+ ): StoriesExportData {
+ const userStoryRows: StoriesExportData["userStoryRows"] = formattedUserStories.map(
+ (story) => [story.id, prdId, story.name, story.description],
+ );
+
+ const taskRows: StoriesExportData["taskRows"] = [];
+
+ formattedUserStories.forEach((story) => {
+ story.tasks?.forEach((task) => {
+ taskRows.push([task.id, story.id, task.title, task.acceptance]);
+ });
+ });
+
+ return { userStoryRows, taskRows };
+ }
+
+ private exportToExcel(data: StoriesExportData, fileName: string) {
+ const { userStoryRows, taskRows } = data;
+
+ this.exportService.exportToExcel(
+ [
+ {
+ name: REQUIREMENT_DISPLAY_NAME_MAP[REQUIREMENT_TYPE.US],
+ data: [SPREADSHEET_HEADER_ROW.US, ...userStoryRows],
+ },
+ {
+ name: REQUIREMENT_DISPLAY_NAME_MAP[REQUIREMENT_TYPE.TASK],
+ data: [SPREADSHEET_HEADER_ROW.TASK, ...taskRows],
+ },
+ ],
+ fileName,
+ );
+ }
+}
diff --git a/ui/src/app/store/user-stories/user-stories.actions.ts b/ui/src/app/store/user-stories/user-stories.actions.ts
index 3498ae5..7a65a58 100644
--- a/ui/src/app/store/user-stories/user-stories.actions.ts
+++ b/ui/src/app/store/user-stories/user-stories.actions.ts
@@ -1,3 +1,4 @@
+import { ExportRequirementDataOptions } from 'src/app/model/interfaces/exports.interface';
import { ITask } from '../../model/interfaces/IList';
import { IUserStory } from '../../model/interfaces/IUserStory';
@@ -76,7 +77,7 @@ export class UpdateTask {
constructor(
readonly task: ITask,
readonly relativePath: string,
- readonly redirect?: boolean
+ readonly redirect?: boolean,
) {}
}
@@ -99,3 +100,9 @@ export class SetCurrentConfig {
},
) {}
}
+
+export class ExportUserStories {
+ static readonly type = '[UserStories] Export User Stories';
+
+ constructor(public exportOptions: ExportRequirementDataOptions) {}
+}
diff --git a/ui/src/app/store/user-stories/user-stories.state.ts b/ui/src/app/store/user-stories/user-stories.state.ts
index 5b10d4b..44b40d9 100644
--- a/ui/src/app/store/user-stories/user-stories.state.ts
+++ b/ui/src/app/store/user-stories/user-stories.state.ts
@@ -12,12 +12,16 @@ import {
SetSelectedProject,
SetSelectedUserStory,
UpdateTask,
+ ExportUserStories,
} from './user-stories.actions';
import { AppSystemService } from '../../services/app-system/app-system.service';
import { NGXLogger } from 'ngx-logger';
import { IUserStory } from '../../model/interfaces/IUserStory';
import { ITask } from '../../model/interfaces/ITask';
import { Router } from '@angular/router';
+import { RequirementExportService } from 'src/app/services/export/requirement-export.service';
+import { REQUIREMENT_TYPE } from 'src/app/constants/app.constants';
+import { ToasterService } from 'src/app/services/toaster/toaster.service';
export interface UserStoriesStateModel {
userStories: IUserStory[];
@@ -53,6 +57,8 @@ export class UserStoriesState {
private appSystemService: AppSystemService,
private logger: NGXLogger,
private router: Router,
+ private toast: ToasterService,
+ private requirementExportService: RequirementExportService
) { }
@Selector()
@@ -380,4 +386,35 @@ export class UserStoriesState {
currentConfig: config,
});
}
+
+ @Action(ExportUserStories)
+ exportUserStories(
+ ctx: StateContext,
+ { exportOptions }: ExportUserStories,
+ ) {
+ try {
+ const state = ctx.getState();
+ const prdId = state.currentConfig?.reqId;
+
+ this.toast.showInfo(`Exporting user stories of prd ${prdId}`);
+ this.requirementExportService.exportRequirementData(
+ {
+ prdId: state.currentConfig?.reqId!,
+ userStories: state.userStories,
+ },
+ {
+ projectName: state.selectedProject,
+ type: exportOptions.type,
+ },
+ REQUIREMENT_TYPE.US,
+ );
+ } catch (error) {
+ const message = `Failed to export user stories: ${
+ error instanceof Error ? error.message : 'Unknown error'
+ }`;
+ this.logger.error(error);
+ this.logger.error(message);
+ this.toast.showError(message);
+ }
+ }
}