This commit is contained in:
Eduard Zambrano 2019-08-23 22:38:34 -07:00
Родитель 303d1b287b
Коммит b5a0194505
17 изменённых файлов: 336 добавлений и 70 удалений

Просмотреть файл

@ -0,0 +1,132 @@
import { StorageConstants, UserSettings } from "../../Models/UserSettingsDataModels";
import { PortfolioTelemetry } from "../Utilities/Telemetry";
import { ExtensionStorageError, IQueryResultError } from "../../Models/PortfolioPlanningQueryModels";
import { ProgressTrackingUserSetting, RollupHierachyUserSetting } from "../../Contracts";
export class UserSettingsDataService {
private static _instance: UserSettingsDataService;
private _vsid: string = null;
private _telemetry: PortfolioTelemetry;
private constructor() {
try {
const webContext = VSS.getWebContext();
this._vsid = webContext.user.id;
this._telemetry = PortfolioTelemetry.getInstance();
} catch (error) {
PortfolioTelemetry.getInstance().TrackException(error);
} finally {
// set default values.
this._vsid = null;
}
}
public static getInstance(): UserSettingsDataService {
if (!UserSettingsDataService._instance) {
UserSettingsDataService._instance = new UserSettingsDataService();
}
return UserSettingsDataService._instance;
}
public async getUserSettings(): Promise<UserSettings> {
if (!this._vsid) {
this._telemetry.TrackAction("UserSettingsDataService/NullVSID");
return Promise.resolve(this.getDefaultUserSettings());
}
const client = await this.getStorageClient();
let settings: UserSettings = this.getDefaultUserSettings();
let document: any = null;
try {
document = await client.getDocument(StorageConstants.COLLECTION_NAME, this._vsid);
settings = this.parseUserSettingsDocument(document);
} catch (error) {
this._telemetry.TrackException(error);
const parsedError = this.ParseStorageError(error);
if (parsedError.status === 404) {
// Collection/document has not been created, initialize it.
try {
await client.setDocument(StorageConstants.COLLECTION_NAME, settings);
} catch (error) {
this._telemetry.TrackException(error);
}
}
}
return settings;
}
public async updateUserSettings(latestSettings: UserSettings): Promise<void> {
if (!this._vsid) {
this._telemetry.TrackAction("UserSettingsDataService/updateUserSettings/NullVSID");
return Promise.resolve();
}
const client = await this.getStorageClient();
latestSettings.id = this._vsid;
try {
await client.updateDocument(StorageConstants.COLLECTION_NAME, latestSettings);
} catch (error) {
this._telemetry.TrackException(error);
}
}
public getDefaultUserSettings(): UserSettings {
return {
Schema: StorageConstants.CURRENT_USER_SETTINGS_SCHEMA_VERSION,
id: this._vsid,
ProgressTrackingOption: this.getDefaultProgressTrackingOption(),
TimelineItemRollup: this.getDefaultTimelineItemRollup()
};
}
private async getStorageClient(): Promise<IExtensionDataService> {
return VSS.getService<IExtensionDataService>(VSS.ServiceIds.ExtensionData);
}
private parseUserSettingsDocument(doc: any): UserSettings {
if (!doc) {
this._telemetry.TrackAction("UserSettingsDataService/ParseUserSettings/NoDoc");
return this.getDefaultUserSettings();
}
const settings: UserSettings = doc;
// Set default values if not present.
if (!settings.ProgressTrackingOption) {
settings.ProgressTrackingOption = this.getDefaultProgressTrackingOption();
}
if (!settings.TimelineItemRollup) {
settings.TimelineItemRollup = this.getDefaultTimelineItemRollup();
}
return settings;
}
private ParseStorageError(error: any): IQueryResultError {
if (!error) {
return {
exceptionMessage: "no error information"
};
}
const parsedError: ExtensionStorageError = error;
return {
exceptionMessage: parsedError.message,
status: parsedError.status
};
}
private getDefaultProgressTrackingOption(): ProgressTrackingUserSetting.Options {
return ProgressTrackingUserSetting.Options.CompletedCount;
}
private getDefaultTimelineItemRollup(): RollupHierachyUserSetting.Options {
return RollupHierachyUserSetting.Options.Descendants;
}
}

Просмотреть файл

@ -138,6 +138,7 @@ export class PortfolioTelemetry {
public TrackException(exception: Error) {
try {
console.log(JSON.stringify(exception, null, " "));
AppInsightsClient.getAppInsightsInstance().trackException({
error: exception,
properties: {

Просмотреть файл

@ -4,7 +4,7 @@ import "./DependencyPanel.scss";
import {
ITimelineItem,
LoadingStatus,
ProgressTrackingCriteria,
ProgressTrackingUserSetting,
IWorkItemIcon,
IProject,
IProjectConfiguration
@ -27,13 +27,14 @@ import { MessageCard, MessageCardSeverity } from "azure-devops-ui/MessageCard";
import { connect } from "react-redux";
import { Icon } from "azure-devops-ui/Icon";
import moment = require("moment");
import { UserSettings } from "../../Models/UserSettingsDataModels";
type WorkItemIconMap = { [projectIdKey: string]: { [workItemType: string]: IWorkItemIcon } };
type WorkItemInProgressStatesMap = { [projectIdKey: string]: { [WorkItemType: string]: string[] } };
export interface IDependencyPanelProps {
workItem: ITimelineItem;
projectInfo: IProject;
progressTrackingCriteria: ProgressTrackingCriteria;
userSettings: UserSettings;
onDismiss: () => void;
}
@ -279,11 +280,13 @@ export class DependencyPanel extends React.Component<IDependencyPanelProps, IDep
projectId: dependency.ProjectId,
workItemType: dependency.WorkItemType,
completed:
this.props.progressTrackingCriteria === ProgressTrackingCriteria.CompletedCount
this.props.userSettings.ProgressTrackingOption ===
ProgressTrackingUserSetting.CompletedCount.Key
? dependency.CompletedCount
: dependency.CompletedEffort,
total:
this.props.progressTrackingCriteria === ProgressTrackingCriteria.CompletedCount
this.props.userSettings.ProgressTrackingOption ===
ProgressTrackingUserSetting.CompletedCount.Key
? dependency.TotalCount
: dependency.TotalEffort,
showInfoIcon: this._showInfoIcon(dependency, isPredecessor),
@ -408,7 +411,7 @@ export class DependencyPanel extends React.Component<IDependencyPanelProps, IDep
const mapStateToProps = (state: IDependencyPanelState, ownProps: IDependencyPanelProps) => ({
workItem: ownProps.workItem,
progressTrackingCriteria: ownProps.progressTrackingCriteria,
userSettings: ownProps.userSettings,
onDismiss: ownProps.onDismiss
});

Просмотреть файл

@ -17,7 +17,7 @@ import { PlanDirectoryActions } from "../../Redux/Actions/PlanDirectoryActions";
import { EpicTimelineActions } from "../../Redux/Actions/EpicTimelineActions";
import { PortfolioPlanningMetadata } from "../../Models/PortfolioPlanningQueryModels";
import { PlanSettingsPanel } from "./PlanSettingsPanel";
import { ProgressTrackingCriteria, ITimelineItem, LoadingStatus } from "../../Contracts";
import { ITimelineItem, LoadingStatus } from "../../Contracts";
import { AddItemPanel } from "./AddItemPanel";
import { Spinner, SpinnerSize } from "azure-devops-ui/Spinner";
import { Link } from "azure-devops-ui/Link";
@ -25,6 +25,7 @@ import { DeletePlanDialog } from "./DeletePlanDialog";
import { MessageCard, MessageCardSeverity } from "azure-devops-ui/MessageCard";
import { PortfolioTelemetry } from "../../Common/Utilities/Telemetry";
import { ExtendedSinglePlanTelemetry } from "../../Models/TelemetryModels";
import { UserSettings } from "../../Models/UserSettingsDataModels";
interface IPlanPageMappedProps {
plan: PortfolioPlanningMetadata;
@ -32,7 +33,6 @@ interface IPlanPageMappedProps {
teamNames: string[];
epicIds: { [epicId: number]: number };
selectedItem: ITimelineItem;
progressTrackingCriteria: ProgressTrackingCriteria;
addItemPanelOpen: boolean;
planSettingsPanelOpen: boolean;
exceptionMessage: string;
@ -40,6 +40,7 @@ interface IPlanPageMappedProps {
isNewPlanExperience: boolean;
deletePlanDialogHidden: boolean;
planTelemetry: ExtendedSinglePlanTelemetry;
userSettings: UserSettings;
}
export type IPlanPageProps = IPlanPageMappedProps & typeof Actions;
@ -143,8 +144,9 @@ export default class PlanPage extends React.Component<IPlanPageProps, IPortfolio
if (this.props.planSettingsPanelOpen) {
return (
<PlanSettingsPanel
progressTrackingCriteria={this.props.progressTrackingCriteria}
userSettings={this.props.userSettings}
onProgressTrackingCriteriaChanged={this.props.onToggleProgressTrackingCriteria}
onTimelineItemRollupChanged={this.props.onToggleTimelineItemRollupCriteria}
onClosePlanSettingsPanel={() => {
this.props.onTogglePlanSettingsPanelOpen(false);
}}
@ -186,15 +188,15 @@ function mapStateToProps(state: IPortfolioPlanningState): IPlanPageMappedProps {
projectNames: getProjectNames(state),
teamNames: getTeamNames(state),
epicIds: getEpicIds(state.epicTimelineState),
selectedItem: getSelectedItem(state.epicTimelineState),
progressTrackingCriteria: state.epicTimelineState.progressTrackingCriteria,
selectedItem: getSelectedItem(state.epicTimelineState, state.planDirectoryState.userSettings),
addItemPanelOpen: state.epicTimelineState.addItemsPanelOpen,
planSettingsPanelOpen: state.epicTimelineState.planSettingsPanelOpen,
exceptionMessage: state.epicTimelineState.exceptionMessage,
planLoadingStatus: state.epicTimelineState.planLoadingStatus,
isNewPlanExperience: state.epicTimelineState.isNewPlanExperience,
deletePlanDialogHidden: state.epicTimelineState.deletePlanDialogHidden,
planTelemetry: getPlanExtendedTelemetry(state.epicTimelineState)
planTelemetry: getPlanExtendedTelemetry(state.epicTimelineState),
userSettings: state.planDirectoryState.userSettings
};
}
@ -204,9 +206,13 @@ const Actions = {
resetPlanState: EpicTimelineActions.resetPlanState,
onOpenAddItemPanel: EpicTimelineActions.openAddItemPanel,
onToggleProgressTrackingCriteria: EpicTimelineActions.toggleProgressTrackingCriteria,
onToggleTimelineItemRollupCriteria: EpicTimelineActions.toggleTimelineRollupCriteria,
onCloseAddItemPanel: EpicTimelineActions.closeAddItemPanel,
onAddItems: EpicTimelineActions.addItems,
onTogglePlanSettingsPanelOpen: EpicTimelineActions.togglePlanSettingsPanelOpen,
// TODO Need another action when the panel is closed.
// Check if settings changed, and reload timeline if needed.
toggleDeletePlanDialogHidden: EpicTimelineActions.toggleDeletePlanDialogHidden,
dismissErrorMessageCard: EpicTimelineActions.dismissErrorMessageCard
};

Просмотреть файл

@ -2,21 +2,17 @@ import * as React from "react";
import "./PlanSettingsPanel.scss";
import { Panel } from "azure-devops-ui/Panel";
import { ComboBox } from "office-ui-fabric-react/lib/ComboBox";
import { ProgressTrackingCriteria } from "../../Contracts";
import { UserSettings } from "../../Models/UserSettingsDataModels";
import { ProgressTrackingUserSetting, RollupHierachyUserSetting } from "../../Contracts";
export interface IPlanSettingsProps {
progressTrackingCriteria: ProgressTrackingCriteria;
onProgressTrackingCriteriaChanged: (criteria: ProgressTrackingCriteria) => void;
userSettings: UserSettings;
onProgressTrackingCriteriaChanged: (criteria: ProgressTrackingUserSetting.Options) => void;
onTimelineItemRollupChanged: (criteria: RollupHierachyUserSetting.Options) => void;
onClosePlanSettingsPanel: () => void;
}
export const PlanSettingsPanel = (props: IPlanSettingsProps) => {
const completedCountKey = "completedCount";
const effortKey = "effort";
const selectedProgressCriteriaKey =
props.progressTrackingCriteria === ProgressTrackingCriteria.CompletedCount ? completedCountKey : effortKey;
return (
<Panel onDismiss={props.onClosePlanSettingsPanel} titleProps={{ text: "Settings" }}>
<div className="settings-container">
@ -24,28 +20,43 @@ export const PlanSettingsPanel = (props: IPlanSettingsProps) => {
<div className="progress-options-label">Track Progress Using: </div>
<ComboBox
className="progress-options-dropdown"
selectedKey={selectedProgressCriteriaKey}
selectedKey={props.userSettings.ProgressTrackingOption}
allowFreeform={false}
autoComplete="off"
options={[
{
key: completedCountKey,
text: ProgressTrackingCriteria.CompletedCount
key: ProgressTrackingUserSetting.CompletedCount.Key,
text: ProgressTrackingUserSetting.CompletedCount.Text
},
{
key: effortKey,
text: ProgressTrackingCriteria.Effort
key: ProgressTrackingUserSetting.Effort.Key,
text: ProgressTrackingUserSetting.Effort.Text
}
]}
onChanged={(item: { key: string; text: string }) => {
switch (item.key) {
case completedCountKey:
props.onProgressTrackingCriteriaChanged(ProgressTrackingCriteria.CompletedCount);
break;
case effortKey:
props.onProgressTrackingCriteriaChanged(ProgressTrackingCriteria.Effort);
break;
props.onProgressTrackingCriteriaChanged(item.key as ProgressTrackingUserSetting.Options);
}}
/>
</div>
<div className="progress-options settings-item">
<div className="progress-options-label">Track Progress Using: </div>
<ComboBox
className="progress-options-dropdown"
selectedKey={props.userSettings.TimelineItemRollup}
allowFreeform={false}
autoComplete="off"
options={[
{
key: RollupHierachyUserSetting.Children.Key,
text: RollupHierachyUserSetting.Children.Text
},
{
key: RollupHierachyUserSetting.Descendants.Key,
text: RollupHierachyUserSetting.Descendants.Text
}
]}
onChanged={(item: { key: string; text: string }) => {
props.onTimelineItemRollupChanged(item.key as RollupHierachyUserSetting.Options);
}}
/>
</div>

Просмотреть файл

@ -1,6 +1,6 @@
import * as React from "react";
import * as moment from "moment";
import { ITimelineGroup, ITimelineItem, ITeam, ProgressTrackingCriteria, IProject } from "../../Contracts";
import { ITimelineGroup, ITimelineItem, ITeam, IProject } from "../../Contracts";
import Timeline, { TimelineHeaders, DateHeader } from "react-calendar-timeline";
import "./PlanTimeline.scss";
import { IPortfolioPlanningState } from "../../Redux/Contracts";
@ -26,6 +26,7 @@ import { MenuButton } from "azure-devops-ui/Menu";
import { IconSize } from "azure-devops-ui/Icon";
import { DetailsDialog } from "./DetailsDialog";
import { ConnectedDependencyPanel } from "./DependencyPanel";
import { UserSettings } from "../../Models/UserSettingsDataModels";
const day = 60 * 60 * 24 * 1000;
const week = day * 7;
@ -52,8 +53,8 @@ interface IPlanTimelineMappedProps {
planOwner: IdentityRef;
exceptionMessage: string;
setDatesDialogHidden: boolean;
progressTrackingCriteria: ProgressTrackingCriteria;
projects: { [projectIdKey: string]: IProject };
userSettings: UserSettings;
}
interface IPlanTimelineState {
@ -133,7 +134,7 @@ export class PlanTimeline extends React.Component<IPlanTimelineProps, IPlanTimel
<ConnectedDependencyPanel
workItem={this.state.contextMenuItem}
projectInfo={this.props.projects[this.state.contextMenuItem.projectId.toLowerCase()]}
progressTrackingCriteria={this.props.progressTrackingCriteria}
userSettings={this.props.userSettings}
onDismiss={() => this.setState({ dependencyPanelOpen: false })}
/>
);
@ -565,13 +566,13 @@ function mapStateToProps(state: IPortfolioPlanningState): IPlanTimelineMappedPro
projectNames: getProjectNames(state),
teamNames: getTeamNames(state),
teams: state.epicTimelineState.teams,
items: getTimelineItems(state.epicTimelineState),
items: getTimelineItems(state.epicTimelineState, state.planDirectoryState.userSettings),
selectedItemId: state.epicTimelineState.selectedItemId,
planOwner: getSelectedPlanOwner(state),
exceptionMessage: state.epicTimelineState.exceptionMessage,
setDatesDialogHidden: state.epicTimelineState.setDatesDialogHidden,
progressTrackingCriteria: state.epicTimelineState.progressTrackingCriteria,
projects: getIndexedProjects(state.epicTimelineState)
projects: getIndexedProjects(state.epicTimelineState),
userSettings: state.planDirectoryState.userSettings
};
}

Просмотреть файл

@ -112,9 +112,43 @@ export interface ITimelineItem {
canMove: boolean;
}
export enum ProgressTrackingCriteria {
CompletedCount = "Completed Count",
Effort = "Effort"
export interface IUseSettingKeyTextPair {
Key: ProgressTrackingUserSetting.Options | RollupHierachyUserSetting.Options;
Text: string;
}
export namespace ProgressTrackingUserSetting {
export enum Options {
CompletedCount = "completedcount",
Effort = "effort"
}
export const CompletedCount: IUseSettingKeyTextPair = {
Key: Options.CompletedCount,
Text: "Completed Count"
};
export const Effort: IUseSettingKeyTextPair = {
Key: Options.Effort,
Text: "Effort"
};
}
export namespace RollupHierachyUserSetting {
export enum Options {
Children = "children",
Descendants = "descendants"
}
export const Children: IUseSettingKeyTextPair = {
Key: Options.Descendants,
Text: "Children"
};
export const Descendants: IUseSettingKeyTextPair = {
Key: Options.Descendants,
Text: "Descendants"
};
}
export enum LoadingStatus {

Просмотреть файл

@ -0,0 +1,13 @@
import { RollupHierachyUserSetting, ProgressTrackingUserSetting } from "../Contracts";
export class StorageConstants {
public static COLLECTION_NAME: string = "UserSettings";
public static CURRENT_USER_SETTINGS_SCHEMA_VERSION: number = 1;
}
export interface UserSettings {
Schema: number;
id: string;
ProgressTrackingOption: ProgressTrackingUserSetting.Options;
TimelineItemRollup: RollupHierachyUserSetting.Options;
}

Просмотреть файл

@ -1,10 +1,11 @@
import { createAction, ActionsUnion } from "../Helpers";
import {
ProgressTrackingCriteria,
ProgressTrackingUserSetting,
IAddItems,
IRemoveItem,
LoadingStatus,
IProjectConfiguration
IProjectConfiguration,
RollupHierachyUserSetting
} from "../../Contracts";
import moment = require("moment");
import { PortfolioPlanningFullContentQueryResult } from "../../Models/PortfolioPlanningQueryModels";
@ -24,6 +25,7 @@ export const enum EpicTimelineActionTypes {
AddItems = "EpicTimeline/AddItems",
RemoveItems = "EpicTimeline/RemoveItems",
ToggleProgressTrackingCriteria = "EpicTimeline/ToggleProgressTrackingCriteria",
ToggleTimelineRollupCriteria = "EpicTimeline/ToggleTimelineRollupCriteria",
ToggleLoadingStatus = "EpicTimeline/ToggleLoadingStatus",
ResetPlanState = "EpicTimeline/ResetPlanState",
TogglePlanSettingsPanelOpen = "EpicTimeline/TogglePlanSettingsPanelOpen",
@ -74,10 +76,22 @@ export const EpicTimelineActions = {
PortfolioTelemetry.getInstance().TrackAction(EpicTimelineActionTypes.RemoveItems);
return createAction(EpicTimelineActionTypes.RemoveItems, itemToRemove);
},
toggleProgressTrackingCriteria: (criteria: ProgressTrackingCriteria) =>
createAction(EpicTimelineActionTypes.ToggleProgressTrackingCriteria, {
toggleProgressTrackingCriteria: (criteria: ProgressTrackingUserSetting.Options) => {
PortfolioTelemetry.getInstance().TrackAction(EpicTimelineActionTypes.ToggleProgressTrackingCriteria, {
["Value"]: criteria
});
return createAction(EpicTimelineActionTypes.ToggleProgressTrackingCriteria, {
criteria
}),
});
},
toggleTimelineRollupCriteria: (criteria: RollupHierachyUserSetting.Options) => {
PortfolioTelemetry.getInstance().TrackAction(EpicTimelineActionTypes.ToggleTimelineRollupCriteria, {
["Value"]: criteria
});
return createAction(EpicTimelineActionTypes.ToggleTimelineRollupCriteria, {
criteria
});
},
toggleLoadingStatus: (status: LoadingStatus) =>
createAction(EpicTimelineActionTypes.ToggleLoadingStatus, { status }),
resetPlanState: () => createAction(EpicTimelineActionTypes.ResetPlanState),

Просмотреть файл

@ -1,6 +1,7 @@
import { createAction, ActionsUnion } from "../Helpers";
import { PortfolioPlanningDirectory, PortfolioPlanning } from "../../Models/PortfolioPlanningQueryModels";
import { PortfolioTelemetry } from "../../Common/Utilities/Telemetry";
import { UserSettings } from "../../Models/UserSettingsDataModels";
export const enum PlanDirectoryActionTypes {
Initialize = "PlanDirectory/Initialize",
@ -16,8 +17,8 @@ export const enum PlanDirectoryActionTypes {
}
export const PlanDirectoryActions = {
initialize: (directoryData: PortfolioPlanningDirectory) =>
createAction(PlanDirectoryActionTypes.Initialize, { directoryData }),
initialize: (directoryData: PortfolioPlanningDirectory, userSettings: UserSettings) =>
createAction(PlanDirectoryActionTypes.Initialize, { directoryData, userSettings }),
createPlan: (name: string, description: string) => {
PortfolioTelemetry.getInstance().TrackAction(PlanDirectoryActionTypes.CreatePlan);
return createAction(PlanDirectoryActionTypes.CreatePlan, {
@ -43,7 +44,7 @@ export const PlanDirectoryActions = {
PortfolioTelemetry.getInstance().TrackAction(PlanDirectoryActionTypes.DeletePlan);
return createAction(PlanDirectoryActionTypes.DeletePlan, { id });
},
updateProjectsAndTeamsMetadata: (planId:string, projectNames: string[], teamNames: string[]) =>
updateProjectsAndTeamsMetadata: (planId: string, projectNames: string[], teamNames: string[]) =>
createAction(PlanDirectoryActionTypes.UpdateProjectsAndTeamsMetadata, { planId, projectNames, teamNames }),
toggleSelectedPlanId: (id: string) => {
PortfolioTelemetry.getInstance().TrackAction(PlanDirectoryActionTypes.ToggleSelectedPlanId);

Просмотреть файл

@ -1,6 +1,7 @@
import { IProject, IWorkItem, ProgressTrackingCriteria, ITeam, LoadingStatus } from "../Contracts";
import { IProject, IWorkItem, ITeam, LoadingStatus } from "../Contracts";
import { PortfolioPlanningMetadata } from "../Models/PortfolioPlanningQueryModels";
import { ExtendedSinglePlanTelemetry } from "../Models/TelemetryModels";
import { UserSettings } from "../Models/UserSettingsDataModels";
export interface IPortfolioPlanningState {
planDirectoryState: IPlanDirectoryState;
@ -18,10 +19,10 @@ export interface IEpicTimelineState {
setDatesDialogHidden: boolean;
planSettingsPanelOpen: boolean;
selectedItemId: number;
progressTrackingCriteria: ProgressTrackingCriteria;
isNewPlanExperience: boolean;
deletePlanDialogHidden: boolean;
planTelemetry: ExtendedSinglePlanTelemetry;
userSettings: UserSettings;
}
export interface IPlanDirectoryState {
@ -30,4 +31,5 @@ export interface IPlanDirectoryState {
selectedPlanId: string;
newPlanDialogVisible: boolean;
plans: PortfolioPlanningMetadata[];
userSettings: UserSettings;
}

Просмотреть файл

@ -6,9 +6,10 @@ import {
PortfolioItemDeletedAction
} from "../Actions/EpicTimelineActions";
import produce from "immer";
import { ProgressTrackingCriteria, LoadingStatus, IWorkItemIcon } from "../../Contracts";
import { LoadingStatus, IWorkItemIcon } from "../../Contracts";
import { MergeType } from "../../Models/PortfolioPlanningQueryModels";
import { defaultIProjectComparer, defaultIWorkItemComparer } from "../../Common/Utilities/Comparers";
import { UserSettingsDataService } from "../../Common/Services/UserSettingsDataService";
export function epicTimelineReducer(state: IEpicTimelineState, action: EpicTimelineActions): IEpicTimelineState {
return produce(state || getDefaultState(), (draft: IEpicTimelineState) => {
@ -79,7 +80,7 @@ export function epicTimelineReducer(state: IEpicTimelineState, action: EpicTimel
return handlePortfolioItemDeleted(state, action as PortfolioItemDeletedAction);
}
case EpicTimelineActionTypes.ToggleProgressTrackingCriteria: {
draft.progressTrackingCriteria = action.payload.criteria;
draft.userSettings.ProgressTrackingOption = action.payload.criteria;
break;
}
case EpicTimelineActionTypes.ToggleLoadingStatus: {
@ -142,10 +143,10 @@ export function getDefaultState(): IEpicTimelineState {
setDatesDialogHidden: true,
planSettingsPanelOpen: false,
selectedItemId: null,
progressTrackingCriteria: ProgressTrackingCriteria.CompletedCount,
isNewPlanExperience: false,
deletePlanDialogHidden: true,
planTelemetry: null
planTelemetry: null,
userSettings: UserSettingsDataService.getInstance().getDefaultUserSettings()
};
}

Просмотреть файл

@ -3,17 +3,19 @@ import produce from "immer";
import { PlanDirectoryActions, PlanDirectoryActionTypes } from "../Actions/PlanDirectoryActions";
import { LoadingStatus } from "../../Contracts";
import { caseInsensitiveComparer } from "../../Common/Utilities/String";
import { UserSettingsDataService } from "../../Common/Services/UserSettingsDataService";
export function planDirectoryReducer(state: IPlanDirectoryState, action: PlanDirectoryActions): IPlanDirectoryState {
return produce(state || getDefaultState(), (draft: IPlanDirectoryState) => {
switch (action.type) {
case PlanDirectoryActionTypes.Initialize: {
const { directoryData } = action.payload;
const { directoryData, userSettings } = action.payload;
draft.directoryLoadingStatus = LoadingStatus.Loaded;
draft.exceptionMessage = directoryData.exceptionMessage;
draft.plans = directoryData.entries;
draft.plans.sort((a, b) => caseInsensitiveComparer(a.name, b.name));
draft.userSettings = userSettings;
break;
}
@ -83,6 +85,7 @@ export function getDefaultState(): IPlanDirectoryState {
exceptionMessage: "",
selectedPlanId: undefined,
plans: [],
newPlanDialogVisible: false
newPlanDialogVisible: false,
userSettings: UserSettingsDataService.getInstance().getDefaultUserSettings()
};
}

Просмотреть файл

@ -17,6 +17,9 @@ import { PlanDirectoryActionTypes, PlanDirectoryActions } from "../Actions/PlanD
import { LoadPortfolio } from "./LoadPortfolio";
import { ActionsOfType } from "../Helpers";
import { SetDefaultDatesForWorkItems, saveDatesToServer } from "./DefaultDateUtil";
import { UserSettingsDataService } from "../../Common/Services/UserSettingsDataService";
import { UserSettings } from "../../Models/UserSettingsDataModels";
import { getUserSettings } from "../Selectors/PlanDirectorySelectors";
export function* epicTimelineSaga(): SagaIterator {
yield takeEvery(EpicTimelineActionTypes.UpdateDates, onUpdateDates);
@ -24,11 +27,45 @@ export function* epicTimelineSaga(): SagaIterator {
yield takeEvery(EpicTimelineActionTypes.AddItems, onAddEpics);
yield takeEvery(PlanDirectoryActionTypes.ToggleSelectedPlanId, onToggleSelectedPlanId);
yield takeEvery(EpicTimelineActionTypes.RemoveItems, onRemoveEpic);
yield takeEvery(EpicTimelineActionTypes.ToggleProgressTrackingCriteria, onProgressTrackingCriteria);
yield takeEvery(EpicTimelineActionTypes.ToggleTimelineRollupCriteria, onTimelineRollupCriteria);
}
function* onUpdateDates(
action: ActionsOfType<EpicTimelineActions, EpicTimelineActionTypes.UpdateDates>
function* onProgressTrackingCriteria(
action: ActionsOfType<EpicTimelineActions, EpicTimelineActionTypes.ToggleProgressTrackingCriteria>
): SagaIterator {
const newOption = action.payload.criteria;
try {
const dataService = UserSettingsDataService.getInstance();
const userSettings: UserSettings = yield effects.select(getUserSettings);
userSettings.ProgressTrackingOption = newOption;
yield effects.call([dataService, dataService.updateUserSettings], userSettings);
} catch (exception) {
console.error(exception);
yield effects.put(EpicTimelineActions.handleGeneralException(exception));
}
}
function* onTimelineRollupCriteria(
action: ActionsOfType<EpicTimelineActions, EpicTimelineActionTypes.ToggleTimelineRollupCriteria>
): SagaIterator {
const newOption = action.payload.criteria;
try {
const dataService = UserSettingsDataService.getInstance();
const userSettings: UserSettings = yield effects.select(getUserSettings);
userSettings.TimelineItemRollup = newOption;
yield effects.call([dataService, dataService.updateUserSettings], userSettings);
} catch (exception) {
console.error(exception);
yield effects.put(EpicTimelineActions.handleGeneralException(exception));
}
}
function* onUpdateDates(action: ActionsOfType<EpicTimelineActions, EpicTimelineActionTypes.UpdateDates>): SagaIterator {
const epicId = action.payload.epicId;
try {
yield effects.call(saveDatesToServer, epicId);
@ -51,6 +88,7 @@ function* onShiftEpic(action: ActionsOfType<EpicTimelineActions, EpicTimelineAct
yield effects.put(EpicTimelineActions.updateItemFinished(epicId));
}
}
function* onAddEpics(action: ActionsOfType<EpicTimelineActions, EpicTimelineActionTypes.AddItems>): SagaIterator {
try {
yield effects.put(EpicTimelineActions.toggleLoadingStatus(LoadingStatus.NotLoaded));

Просмотреть файл

@ -14,6 +14,8 @@ import { getProjectNames, getTeamNames, getExceptionMessage } from "../Selectors
import { PortfolioTelemetry } from "../../Common/Utilities/Telemetry";
import { LaunchWorkItemFormActionType } from "../../../Common/redux/actions/launchWorkItemForm";
import { launchWorkItemFormSaga } from "./launchWorkItemFromSaga";
import { UserSettings } from "../../Models/UserSettingsDataModels";
import { UserSettingsDataService } from "../../Common/Services/UserSettingsDataService";
export function* planDirectorySaga(): SagaIterator {
yield effects.call(initializePlanDirectory);
@ -21,16 +23,18 @@ export function* planDirectorySaga(): SagaIterator {
yield effects.takeEvery(PlanDirectoryActionTypes.DeletePlan, deletePlan);
yield effects.takeEvery(EpicTimelineActionTypes.PortfolioItemsReceived, updateProjectsAndTeamsMetadata);
yield effects.takeEvery(EpicTimelineActionTypes.PortfolioItemDeleted, updateProjectsAndTeamsMetadata);
yield effects.takeEvery(LaunchWorkItemFormActionType, launchWorkItemFormSaga);
yield effects.takeEvery(LaunchWorkItemFormActionType, launchWorkItemFormSaga);
}
export function* initializePlanDirectory(): SagaIterator {
try {
const service = PortfolioPlanningDataService.getInstance();
const settingsService = UserSettingsDataService.getInstance();
const allPlans: PortfolioPlanningDirectory = yield effects.call([service, service.GetAllPortfolioPlans]);
const userSettings: UserSettings = yield effects.call([settingsService, settingsService.getUserSettings]);
yield effects.put(PlanDirectoryActions.initialize(allPlans));
yield effects.put(PlanDirectoryActions.initialize(allPlans, userSettings));
} catch (exception) {
console.error(exception);
yield effects.put(PlanDirectoryActions.handleGeneralException(exception));

Просмотреть файл

@ -1,7 +1,8 @@
import { IEpicTimelineState, IPortfolioPlanningState } from "../Contracts";
import { IProject, IWorkItem, ITimelineGroup, ITimelineItem, ProgressTrackingCriteria } from "../../Contracts";
import { IProject, IWorkItem, ITimelineGroup, ITimelineItem, ProgressTrackingUserSetting } from "../../Contracts";
import moment = require("moment");
import { ExtendedSinglePlanTelemetry } from "../../Models/TelemetryModels";
import { UserSettings } from "../../Models/UserSettingsDataModels";
export function getProjects(state: IEpicTimelineState): IProject[] {
return state.projects;
@ -53,13 +54,13 @@ export function getEpicIds(state: IEpicTimelineState): { [epicId: number]: numbe
return result;
}
export function getTimelineItems(state: IEpicTimelineState): ITimelineItem[] {
export function getTimelineItems(state: IEpicTimelineState, userSettings: UserSettings): ITimelineItem[] {
return state.epics.map(epic => {
let completed: number;
let total: number;
let progress: number;
if (state.progressTrackingCriteria === ProgressTrackingCriteria.CompletedCount) {
if (userSettings.ProgressTrackingOption === ProgressTrackingUserSetting.CompletedCount.Key) {
completed = epic.completedCount;
total = epic.totalCount;
progress = epic.countProgress;
@ -90,8 +91,8 @@ export function getTimelineItems(state: IEpicTimelineState): ITimelineItem[] {
});
}
export function getSelectedItem(state: IEpicTimelineState): ITimelineItem {
return getTimelineItems(state).find(item => item.id === state.selectedItemId);
export function getSelectedItem(state: IEpicTimelineState, userSettings: UserSettings): ITimelineItem {
return getTimelineItems(state, userSettings).find(item => item.id === state.selectedItemId);
}
export function getMessage(state: IEpicTimelineState): string {
@ -117,10 +118,6 @@ export function getAddEpicPanelOpen(state: IEpicTimelineState): boolean {
return state.addItemsPanelOpen;
}
export function getProgressTrackingCriteria(state: IEpicTimelineState): ProgressTrackingCriteria {
return state.progressTrackingCriteria;
}
export function getExceptionMessage(state: IPortfolioPlanningState): string {
return state.epicTimelineState.exceptionMessage;
}

Просмотреть файл

@ -2,6 +2,7 @@ import { IPortfolioPlanningState, IPlanDirectoryState } from "../Contracts";
import { IdentityRef } from "VSS/WebApi/Contracts";
import { PortfolioPlanningMetadata } from "../../Models/PortfolioPlanningQueryModels";
import { SinglePlanTelemetry } from "../../Models/TelemetryModels";
import { UserSettings } from "../../Models/UserSettingsDataModels";
export function getSelectedPlanId(state: IPortfolioPlanningState): string {
return state.planDirectoryState.selectedPlanId;
@ -27,3 +28,7 @@ export function getPlansTelemetry(state: IPlanDirectoryState): SinglePlanTelemet
};
});
}
export function getUserSettings(state: IPlanDirectoryState): UserSettings {
return state.userSettings;
}