Skip to content

Commit

Permalink
Merge pull request #17 from gilesv/issue#16
Browse files Browse the repository at this point in the history
v.0.1.0
  • Loading branch information
gilesv authored Sep 5, 2019
2 parents cdf0055 + e66d099 commit 4e887b8
Show file tree
Hide file tree
Showing 13 changed files with 13,770 additions and 50 deletions.
13,620 changes: 13,620 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/appVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default "0.1.0";
54 changes: 41 additions & 13 deletions src/components/Dashboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ import { IStore } from "../redux/reducers";
import { connect } from "react-redux";
import { download } from "../services/file.service";
import Notification from "../entities/notification.entity";
import { Story } from "../entities/story.entity";
import { Story, StoryId } from "../entities/story.entity";
import IEntityMap from "../redux/utils/entity-map.interface";
import NotifyDock from "./NotifyDock";
import { setStateClean } from "../redux/actions";
import { importFromStorage, exportToStorage } from "../services/localStorage.service";
import SaveInfo from "./SaveInfo";

interface Props {
selectedStory: number,
stories: IEntityMap<Story>,
isStateDirty: boolean,
pardalVersion: string,
appVersion: string,
[key: string]: any
}

Expand All @@ -28,23 +30,45 @@ class Dashboard extends React.Component<Props> {
}

public componentDidMount() {
const { isStateDirty } = this.props;
window.onbeforeunload = (e: any) => {
return "";
};
this.restorePreviousWork();
this.saveProgress();
}

private notify(notification: Notification) {
NotifyDock.show(notification);
private restorePreviousWork() {
const storageState = importFromStorage();

if (storageState) {
Trader.importStories(storageState, FileType.JSON);
this.props.dispatch(setStateClean());

this.notify(new Notification("Your work was restored!", "tick"));
}
}

private saveProgress() {
setInterval(() => {
if (this.props.isStateDirty) {
const json = Trader.exportStories(
this.props.stories.ids.map((storyId: StoryId) => this.props.stories.entities[storyId]),
FileType.JSON,
this.props.appVersion
);

exportToStorage(json);
this.props.dispatch(setStateClean());
console.log("Progress saved.");
}
}, 2500);
}

public exportStory(fileType: FileType) {
const storyIndex = this.props.selectedStory;
const story = this.props.stories.entities[this.props.stories.ids[storyIndex]];
const result = Trader.exportStory(story, fileType);
const result = Trader.exportStories([story], fileType, this.props.appVersion);

this.notify(new Notification("Your download will begin shortly.", "download"));
setTimeout(() => download(story.name, fileType, result), 1500);

setTimeout(() => download(story.name, fileType, result), 1000);
this.props.dispatch(setStateClean());
}

Expand All @@ -57,7 +81,7 @@ class Dashboard extends React.Component<Props> {
let notification;

try {
const storyName = Trader.importStory(e.target.result, FileType.JSON);
const storyName = Trader.importStories(e.target.result, FileType.JSON);
notification = new Notification(`"${storyName}" was imported successfully.`, "tick")
} catch (e) {
notification = new Notification(`${e.message}`, "error");
Expand All @@ -69,14 +93,18 @@ class Dashboard extends React.Component<Props> {
}
}

private notify(notification: Notification) {
NotifyDock.show(notification);
}

render() {
const { stories, pardalVersion } = this.props;
const { stories, appVersion, isStateDirty } = this.props;
return (
<div className="app">
<div className="dashboard">
<aside>
<header>
<div className="app-title"> >=pardal<span>v{pardalVersion}</span></div>
<div className="app-title"> >=pardal<span>v{appVersion}</span></div>
</header>

<StoryList importStory={this.importStory} />
Expand Down
18 changes: 18 additions & 0 deletions src/components/SaveInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from "react";
import { Icon } from "@blueprintjs/core";

interface Props {
isSaving: boolean
}

export default function SaveInfo(props: Props) {
const { isSaving } = props;

return (
<div className="save-info">
{
isSaving ? 'Saving...' : <span title="Your changes have been saved."><Icon icon="small-tick" iconSize={15}></Icon></span>
}
</div>
);
}
7 changes: 5 additions & 2 deletions src/components/StoryDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { TaskId, Task } from "../entities/task.entity";
import { updateStory } from "../redux/actions";
import StoryForm from "./StoryForm";
import { FileType } from "../services/trader.service";
import SaveInfo from "./SaveInfo";

interface Props {
selectedStory: Story,
Expand Down Expand Up @@ -48,12 +49,12 @@ class StoryDetails extends React.Component<Props> {
}

render() {
const { selectedStory } = this.props;
const { selectedStory, isStateDirty } = this.props;

const exportMenu = (
<Menu>
<Menu.Item text="To JSON file" onClick={() => this.props.exportStory(FileType.JSON)} />
<Menu.Item text="To TXT file" onClick={() => this.props.exportStory(FileType.DTD)} />
<Menu.Item text="To TXT file" onClick={() => this.props.exportStory(FileType.TXT)} />
</Menu>
);

Expand All @@ -74,6 +75,7 @@ class StoryDetails extends React.Component<Props> {
</div>

<div className="story-details__controls">
<SaveInfo isSaving={isStateDirty} />
<Popover content={exportMenu} position={Position.BOTTOM_RIGHT} minimal={true}>
<Button intent="none" text="Export" icon="export" rightIcon="caret-down" />
</Popover>
Expand Down Expand Up @@ -103,6 +105,7 @@ const mapStateToProps = (state: IStore, props: any): Props => {
selectedStory,
tasks: state.tasks.entities,
tasksIds: state.tasks.ids,
isStateDirty: state.ui.isDirty,
...props
};
}
Expand Down
3 changes: 2 additions & 1 deletion src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Dashboard from './components/Dashboard';
import * as serviceWorker from './serviceWorker';
import { Provider } from "react-redux";
import store from "./redux/store";
import appVersion from './appVersion';

// Styles
import "normalize.css";
Expand All @@ -15,7 +16,7 @@ const version = "0.0.2";

ReactDOM.render(
<Provider store={store}>
<Dashboard pardalVersion={version} />
<Dashboard appVersion={appVersion} />
</Provider>,
document.getElementById('root')
);
Expand Down
2 changes: 1 addition & 1 deletion src/services/file.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { FileType } from "../services/trader.service";

const Extension = {
[FileType.JSON]: "json",
[FileType.DTD]: "txt"
[FileType.TXT]: "txt"
};

export function download(fileName: string, fileType: FileType, bytes: string) {
Expand Down
9 changes: 9 additions & 0 deletions src/services/localStorage.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const LOCAL_STORAGE_KEY = "state";

export function importFromStorage(): string {
return localStorage[LOCAL_STORAGE_KEY];
}

export function exportToStorage(json: string) {
localStorage[LOCAL_STORAGE_KEY] = json;
}
94 changes: 62 additions & 32 deletions src/services/trader.service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Story, StoryId } from "../entities/story.entity";
import { IStore } from "../redux/reducers";
import store from "../redux/store";
import { TaskId, Task, TaskArea } from "../entities/task.entity";
import { Task, TaskArea, TaskId } from "../entities/task.entity";
import { addStory, addTask } from "../redux/actions";

export enum FileType {
JSON,
DTD
TXT
}

class Trader {
Expand All @@ -15,44 +15,54 @@ class Trader {
constructor(store: any) {
this.store = store;
}
exportStory(story: Story, to: FileType): string {

exportStories(stories: Story[], to: FileType, version: string): string {
const state = this.store.getState();

switch (to) {
case FileType.JSON:
return this.exportStoryToJSON(story, state);
break;
return this.exportStoriesToJSON(stories, state, version);

case FileType.DTD:
return this.exportStoryToDTD(story, state);
break;
case FileType.TXT:
return this.exportStoriesToTXT(stories, state);

default:
throw new Error("Invalid export file type");
}
}

private exportStoryToJSON(story: Story, state: IStore) {
private exportStoriesToJSON(stories: Story[], state: IStore, version: string) {
const plainStories = stories.map((story: any) => {
const storyObj = { ...story };
storyObj.tasks = story.tasks.map((taskId: TaskId) => state.tasks.entities[taskId]);

return storyObj;
})

return JSON.stringify({
story: {
...story,
tasks: story.tasks.map(taskId => state.tasks.entities[taskId])
}
v: version,
stories: plainStories
}, null, 3);
}

private exportStoryToDTD(story: Story, state: IStore) {
private exportStoriesToTXT(stories: Story[], state: IStore) {
const story = stories[0]; // no support for multiple stories txt exportation

if (!story) {
throw new Error("No stories selected to export");
}

const tasks = story.tasks.map(taskId => state.tasks.entities[taskId]);
let dtdContent = this.storyToDTDFormat(story);
let TXTContent = this.storyToTXTFormat(story);

for (let task of tasks) {
dtdContent += "\n" + this.taskToDTDFormat(task, story.name);
TXTContent += "\n" + this.taskToTXTFormat(task, story.name);
}

return dtdContent;
return TXTContent;
}

private storyToDTDFormat(story: Story): string {
private storyToTXTFormat(story: Story): string {
return [
`[STORY][${story.name}] ${story.title}`,
`[PRIORITY]: ${story.priority}`,
Expand All @@ -61,7 +71,7 @@ class Trader {
].join("\n");
}

private taskToDTDFormat(task: Task, storyName: string): string {
private taskToTXTFormat(task: Task, storyName: string): string {
return [
`[${task.type}][${storyName}][${task.area === TaskArea.BOTH ? 'BE][FE' : task.area}][${task.assignee}] ${task.title}`,
`[PRIORITY]: ${task.priority}`,
Expand Down Expand Up @@ -93,15 +103,34 @@ class Trader {
return result;
}

importStory(source: string, from: FileType): string {
importStories(source: string, from: FileType): string {
const obj = this.toObjectTree(source);
const nextStoryId = this.store.getState().stories.ids.length;
const sourceVersion = obj.v;

try {
this.importStoryToStore(obj.story, nextStoryId);
this.importTasksToStore(obj.story.tasks, nextStoryId)
switch (sourceVersion) {
case undefined:
case "0.0.2":
const nextStoryId = this.store.getState().stories.ids.length;
this.importStoryToStore(obj.story, nextStoryId);
this.importTasksToStore(obj.story.tasks, nextStoryId);
return obj.story.name;

return obj.story.name;
// case "next-version" <-- add new cases here if the json format changes in future versions

default: // current version
let labels: string[] = [];
obj.stories.forEach((storyObj: any) => {
const nextStoryId = this.store.getState().stories.ids.length;

this.importStoryToStore(storyObj, nextStoryId);
this.importTasksToStore(storyObj.tasks, nextStoryId);

labels.push(storyObj.name);
});

return labels.join(", ");
}
} catch (e) {
throw new Error("Invalid JSON content: " + e.message);
}
Expand All @@ -118,21 +147,14 @@ class Trader {
story.description = storyObj.description;
story.startDate = this.parseDate(storyObj.startDate);
story.handOffDate = this.parseDate(storyObj.handOffDate);
story.tasks = [];

this.store.dispatch(addStory(story));
} catch (e) {
throw new Error("incomplete story data. " + e.message);
}
}

private parseDate(dateStr: string): Date {
if (isNaN(Date.parse(dateStr))) {
throw new Error(`could not parse date: '${dateStr}'`);
} else {
return new Date(dateStr)
};
}

private importTasksToStore(tasksArray: any[], storyId: StoryId) {
const state = this.store.getState();
const newTasks: Task[] = [];
Expand Down Expand Up @@ -165,6 +187,14 @@ class Trader {
}
}

private parseDate(dateStr: string): Date {
if (isNaN(Date.parse(dateStr))) {
throw new Error(`could not parse date: '${dateStr}'`);
} else {
return new Date(dateStr)
};
}

private toObjectTree(source: string): any {
try {
return JSON.parse(source);
Expand Down
5 changes: 5 additions & 0 deletions src/styles/components/save-info.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.save-info {
margin-right: 12px;
font-size: 11px;
text-align: right;
}
3 changes: 3 additions & 0 deletions src/styles/components/story-details.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@
&__controls {
margin-top: 5px;
margin-right: 5px;
display: flex;
flex-flow: row nowrap;
align-items: center;

button.bp3-button + button.bp3-button {
margin-left: 11px;
Expand Down
3 changes: 2 additions & 1 deletion src/styles/components/story-list.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
overflow-y: auto;

@media (min-width: 900px) {
height: 300px;
min-height: 300px;
max-height: 500px;
}
}

Expand Down
Loading

0 comments on commit 4e887b8

Please sign in to comment.