Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: i##4099 move a story to another wf should not change the wf on s… #556

Merged
merged 1 commit into from
Nov 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { Store } from '@ngrx/store';
import {
Observable,
combineLatest,
debounceTime,
distinctUntilChanged,
filter,
map,
Expand Down Expand Up @@ -100,6 +101,7 @@ export class ProjectFeatureViewSetterComponent implements OnDestroy {
) {
this.state.connect('storyView', this.store.select(selectStoryView));
this.state.connect('selectStory', this.store.select(selectStory));

this.state.connect(
'url',
this.routerHistory.urlChanged.pipe(
Expand Down Expand Up @@ -147,12 +149,7 @@ export class ProjectFeatureViewSetterComponent implements OnDestroy {
}

if (params.storyRef) {
return this.store.select(selectStory).pipe(
filterNil(),
map((story) => {
return story?.workflow.slug ?? 'main';
})
);
return this.storyWorkflow(Number(params.storyRef));
}

return of('main');
Expand All @@ -165,7 +162,11 @@ export class ProjectFeatureViewSetterComponent implements OnDestroy {
);

combineLatest([this.route.data, this.route.params])
.pipe(takeUntilDestroyed(this.destroyRef), distinctUntilChanged())
.pipe(
takeUntilDestroyed(this.destroyRef),
distinctUntilChanged(),
debounceTime(20)
)
.subscribe(([data, params]) => {
const url = this.state.get('url');
const project: Project = data.project as Project;
Expand Down Expand Up @@ -198,6 +199,17 @@ export class ProjectFeatureViewSetterComponent implements OnDestroy {
return route.firstChild ? this.getActiveRoute(route.firstChild) : route;
}

private storyWorkflow(storyRef: Story['ref']) {
return this.store.select(selectStory).pipe(
filterNil(),
filter((story) => story.ref === storyRef),
map((story) => {
return story?.workflow.slug ?? 'main';
}),
take(1)
);
}

private fetchStory(params: StoryParams) {
this.store.dispatch(
StoryDetailActions.initStory({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -167,5 +167,8 @@ export const StoryDetailApiActions = createActionGroup({
'Delete attachment success': props<{
id: Attachment['id'];
}>(),
'Fetch workflow Success': props<{
workflow: Workflow;
}>(),
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
WorkflowMockFactory,
} from '@taiga/data';
import { cold, hot } from 'jest-marbles';
import { Observable } from 'rxjs';
import { EMPTY, Observable } from 'rxjs';
import { selectCurrentProject } from '~/app/modules/project/data-access/+state/selectors/project.selectors';
import { KanbanActions } from '~/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions';
import { AppService } from '~/app/services/app.service';
Expand All @@ -32,10 +32,15 @@ import {
} from '../actions/story-detail.actions';
import {
selectStory,
selectStoryView,
selectWorkflow,
} from '../selectors/story-detail.selectors';
import { StoryDetailEffects } from './story-detail.effects';
import { selectCurrentWorkflowSlug } from '~/app/modules/project/feature-kanban/data-access/+state/selectors/kanban.selectors';
import {
selectCurrentWorkflowSlug,
selectWorkflow as selectKanbanWorkflow,
} from '~/app/modules/project/feature-kanban/data-access/+state/selectors/kanban.selectors';
import { HttpErrorResponse } from '@angular/common/http';

describe('StoryDetailEffects', () => {
let actions$: Observable<Action>;
Expand Down Expand Up @@ -269,4 +274,126 @@ describe('StoryDetailEffects', () => {
expected
);
});

describe('StoryDetailEffects - loadWorkflow$', () => {
it('should fetch workflow for full-view', () => {
const projectApiService = spectator.inject(ProjectApiService);
const effects = spectator.inject(StoryDetailEffects);
const project = ProjectMockFactory();
const story = StoryDetailMockFactory();
const workflow = WorkflowMockFactory();

store.overrideSelector(selectCurrentProject, project);
store.overrideSelector(selectStoryView, 'full-view');
actions$ = hot('-a', {
a: StoryDetailApiActions.fetchStorySuccess({ story }),
});

projectApiService.getWorkflow.mockReturnValue(
cold('-b|', { b: workflow })
);

const expected = cold('--a', {
a: StoryDetailApiActions.fetchWorkflowSuccess({ workflow }),
});

expect(effects.loadWorkflow$).toSatisfyOnFlush(() => {
expect(effects.loadWorkflow$).toBeObservable(expected);
expect(projectApiService.getWorkflow).toHaveBeenCalledWith(
project.id,
story.workflow.slug
);
});
});

it('should reuse kanban workflow', () => {
const projectApiService = spectator.inject(ProjectApiService);
const effects = spectator.inject(StoryDetailEffects);
const project = ProjectMockFactory();
const story = StoryDetailMockFactory();
const kanbanWorkflow = WorkflowMockFactory();
story.workflow = kanbanWorkflow;

store.overrideSelector(selectCurrentProject, project);
store.overrideSelector(selectStoryView, 'modal-view');
store.overrideSelector(selectKanbanWorkflow, kanbanWorkflow);
actions$ = hot('-a', {
a: StoryDetailApiActions.fetchStorySuccess({ story }),
});

projectApiService.getWorkflow.mockReturnValue(
cold('-b|', { b: kanbanWorkflow })
);

const expected = cold('--a', {
a: StoryDetailApiActions.fetchWorkflowSuccess({
workflow: kanbanWorkflow,
}),
});

expect(effects.loadWorkflow$).toSatisfyOnFlush(() => {
expect(effects.loadWorkflow$).toBeObservable(expected);
expect(projectApiService.getWorkflow).not.toHaveBeenCalled();
});
});

it('should fetch workflow', () => {
const projectApiService = spectator.inject(ProjectApiService);
const effects = spectator.inject(StoryDetailEffects);
const project = ProjectMockFactory();
const story = StoryDetailMockFactory();
const workflow = WorkflowMockFactory();

store.overrideSelector(selectCurrentProject, project);
store.overrideSelector(selectStoryView, 'full-view');
store.overrideSelector(selectKanbanWorkflow, null);
actions$ = hot('-a', {
a: StoryDetailApiActions.fetchStorySuccess({ story }),
});

projectApiService.getWorkflow.mockReturnValue(
cold('-b|', { b: workflow })
);

const expected = cold('--a', {
a: StoryDetailApiActions.fetchWorkflowSuccess({ workflow }),
});

expect(effects.loadWorkflow$).toSatisfyOnFlush(() => {
expect(effects.loadWorkflow$).toBeObservable(expected);
expect(projectApiService.getWorkflow).toHaveBeenCalledWith(
project.id,
story.workflow.slug
);
});
});

it('should handle error', () => {
const projectApiService = spectator.inject(ProjectApiService);
const appService = spectator.inject(AppService);
const effects = spectator.inject(StoryDetailEffects);
const project = ProjectMockFactory();
const story = StoryDetailMockFactory();
const errorResponse = new HttpErrorResponse({ status: 404 });

store.overrideSelector(selectCurrentProject, project);
store.overrideSelector(selectStoryView, 'full-view');
actions$ = hot('-a', {
a: StoryDetailApiActions.fetchStorySuccess({ story }),
});

projectApiService.getWorkflow.mockReturnValue(
cold('-#|', {}, errorResponse)
);

const expected = cold('--a', {
a: EMPTY,
});

expect(effects.loadWorkflow$).toSatisfyOnFlush(() => {
expect(effects.loadWorkflow$).toBeObservable(expected);
expect(appService.errorManagement).toHaveBeenCalledWith(errorResponse);
});
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,12 @@ import { fetch, pessimisticUpdate } from '@ngrx/router-store/data-persistence';
import { Store } from '@ngrx/store';
import { TuiNotification } from '@taiga-ui/core';
import { ProjectApiService } from '@taiga/api';
import { filter, map, tap } from 'rxjs';
import {
projectApiActions,
projectEventActions,
} from '~/app/modules/project/data-access/+state/actions/project.actions';
import { EMPTY, catchError, map, of, switchMap, tap } from 'rxjs';
import { projectEventActions } from '~/app/modules/project/data-access/+state/actions/project.actions';
import { selectCurrentProject } from '~/app/modules/project/data-access/+state/selectors/project.selectors';
import { KanbanActions } from '~/app/modules/project/feature-kanban/data-access/+state/actions/kanban.actions';
import {
selectWorkflow,
selectWorkflow as selectKanbanWorkflow,
selectCurrentWorkflowSlug,
} from '~/app/modules/project/feature-kanban/data-access/+state/selectors/kanban.selectors';
import { AppService } from '~/app/services/app.service';
Expand All @@ -33,7 +30,11 @@ import {
StoryDetailActions,
StoryDetailApiActions,
} from '../actions/story-detail.actions';
import { selectStory } from '../selectors/story-detail.selectors';
import {
selectStory,
selectStoryView,
selectWorkflow,
} from '../selectors/story-detail.selectors';

@Injectable()
export class StoryDetailEffects {
Expand Down Expand Up @@ -64,13 +65,49 @@ export class StoryDetailEffects {
StoryDetailApiActions.fetchStorySuccess,
StoryDetailApiActions.updateStoryWorkflowSuccess
),
concatLatestFrom(() => [this.store.select(selectWorkflow)]),
filter(([action, workflow]) => {
return action.story?.workflow?.slug !== workflow?.slug;
concatLatestFrom(() => [
this.store.select(selectCurrentProject).pipe(filterNil()),
]),
switchMap(([action, project]) => {
return this.store.select(selectStoryView).pipe(
switchMap((view) => {
const fetchProject = (workflowSlug: string) => {
return this.projectApiService
.getWorkflow(project.id, workflowSlug)
.pipe(
map((workflow) => {
return StoryDetailApiActions.fetchWorkflowSuccess({
workflow,
});
})
);
};

if (view === 'full-view') {
return fetchProject(action.story.workflow.slug);
} else {
return this.store.select(selectKanbanWorkflow).pipe(
filterNil(),
switchMap((kanbanWorkflow) => {
if (kanbanWorkflow.id === action.story.workflow.id) {
return of(
StoryDetailApiActions.fetchWorkflowSuccess({
workflow: kanbanWorkflow,
})
);
}

return fetchProject(action.story.workflow.slug);
})
);
}
})
);
}),
map(([action]) => {
const workflow = action.story?.workflow;
return projectApiActions.fetchWorkflow({ workflow: workflow?.slug });
catchError((httpResponse: HttpErrorResponse) => {
this.appService.errorManagement(httpResponse);

return EMPTY;
})
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ import {
UserComment,
Workflow,
} from '@taiga/data';
import {
projectApiActions,
projectEventActions,
} from '~/app/modules/project/data-access/+state/actions/project.actions';
import { projectEventActions } from '~/app/modules/project/data-access/+state/actions/project.actions';
import {
KanbanActions,
KanbanApiActions,
Expand Down Expand Up @@ -90,7 +87,7 @@ export const reducer = createImmerReducer(
}
),
on(
projectApiActions.fetchWorkflowSuccess,
StoryDetailApiActions.fetchWorkflowSuccess,
(state, { workflow }): StoryDetailState => {
state.loadingWorkflow = false;
state.workflow = workflow;
Expand Down