Commit 717de291 authored by Konstantin Tskhovrebov's avatar Konstantin Tskhovrebov 🤖

Merge branch 'feature/issue_merge_request_details_ui' into 'feature/issue_merge_request_info'

Issue info screen

See merge request !195
parents 9809e778 680a420b
......@@ -12,10 +12,7 @@ import ru.terrakok.gitlabclient.ui.auth.AuthFlowFragment
import ru.terrakok.gitlabclient.ui.auth.AuthFragment
import ru.terrakok.gitlabclient.ui.drawer.DrawerFlowFragment
import ru.terrakok.gitlabclient.ui.file.ProjectFileFragment
import ru.terrakok.gitlabclient.ui.issue.IssueFlowFragment
import ru.terrakok.gitlabclient.ui.issue.IssueInfoFragment
import ru.terrakok.gitlabclient.ui.issue.IssueNotesFragment
import ru.terrakok.gitlabclient.ui.issue.MainIssueFragment
import ru.terrakok.gitlabclient.ui.issue.*
import ru.terrakok.gitlabclient.ui.libraries.LibrariesFragment
import ru.terrakok.gitlabclient.ui.main.MainFragment
import ru.terrakok.gitlabclient.ui.mergerequest.*
......@@ -229,6 +226,10 @@ object Screens {
override fun getFragment() = IssueInfoFragment()
}
object IssueDetails : SupportAppScreen() {
override fun getFragment() = IssueDetailsFragment()
}
object IssueNotes : SupportAppScreen() {
override fun getFragment() = IssueNotesFragment()
}
......
package ru.terrakok.gitlabclient.entity
import com.google.gson.annotations.SerializedName
data class Author(
@SerializedName("id") val id: Long,
@SerializedName("state") val state: String?,
@SerializedName("web_url") val webUrl: String?,
@SerializedName("name") val name: String,
@SerializedName("avatar_url") val avatarUrl: String?,
@SerializedName("username") val username: String
)
......@@ -7,7 +7,7 @@ import ru.terrakok.gitlabclient.entity.event.EventTargetType
data class Note(
@SerializedName("id") val id: Long,
@SerializedName("body") val body: String,
@SerializedName("author") val author: Author,
@SerializedName("author") val author: ShortUser,
@SerializedName("created_at") val createdAt: LocalDateTime,
@SerializedName("updated_at") val updatedAt: LocalDateTime?,
@SerializedName("system") val isSystem: Boolean,
......
......@@ -2,7 +2,7 @@ package ru.terrakok.gitlabclient.entity
import com.google.gson.annotations.SerializedName
data class Assignee(
data class ShortUser(
@SerializedName("id") val id: Long,
@SerializedName("state") val state: String?,
@SerializedName("name") val name: String,
......
package ru.terrakok.gitlabclient.entity.target
package ru.terrakok.gitlabclient.entity
import com.google.gson.annotations.SerializedName
......
package ru.terrakok.gitlabclient.entity.app.target
import org.threeten.bp.LocalDateTime
import ru.terrakok.gitlabclient.entity.Author
import ru.terrakok.gitlabclient.entity.ShortUser
/**
* Created by Konstantin Tskhovrebov (aka @terrakok) on 24.12.17.
*/
sealed class TargetHeader {
data class Public(
val author: Author,
val author: ShortUser,
val icon: TargetHeaderIcon,
val title: TargetHeaderTitle,
val body: CharSequence,
......
......@@ -2,10 +2,9 @@ package ru.terrakok.gitlabclient.entity.event
import com.google.gson.annotations.SerializedName
import org.threeten.bp.LocalDateTime
import ru.terrakok.gitlabclient.entity.Author
import ru.terrakok.gitlabclient.entity.Note
import ru.terrakok.gitlabclient.entity.PushData
import ru.terrakok.gitlabclient.entity.ShortUser
/**
* Created by Konstantin Tskhovrebov (aka @terrakok) on 22.07.17.
......@@ -19,7 +18,7 @@ data class Event(
@SerializedName("author_id") val authorId: Long,
@SerializedName("target_title") val targetTitle: String?,
@SerializedName("created_at") val createdAt: LocalDateTime,
@SerializedName("author") val author: Author,
@SerializedName("author") val author: ShortUser,
@SerializedName("author_username") val authorUsername: String,
@SerializedName("push_data") val pushData: PushData?,
@SerializedName("note") val note: Note?
......
package ru.terrakok.gitlabclient.entity.issue
import com.google.gson.annotations.SerializedName
import org.threeten.bp.LocalDate
import org.threeten.bp.LocalDateTime
import ru.terrakok.gitlabclient.entity.Assignee
import ru.terrakok.gitlabclient.entity.Author
import ru.terrakok.gitlabclient.entity.ShortUser
import ru.terrakok.gitlabclient.entity.TimeStats
import ru.terrakok.gitlabclient.entity.milestone.Milestone
data class Issue(
......@@ -11,16 +12,16 @@ data class Issue(
@SerializedName("iid") val iid: Long,
@SerializedName("state") val state: IssueState,
@SerializedName("description") val description: String,
@SerializedName("author") val author: Author,
@SerializedName("author") val author: ShortUser,
@SerializedName("milestone") val milestone: Milestone?,
@SerializedName("project_id") val projectId: Long,
@SerializedName("assignees") val assignees: List<Assignee>,
@SerializedName("assignees") val assignees: List<ShortUser>,
@SerializedName("updated_at") val updatedAt: LocalDateTime?,
@SerializedName("title") val title: String?,
@SerializedName("created_at") val createdAt: LocalDateTime,
@SerializedName("labels") val labels: List<String>,
@SerializedName("user_notes_count") val userNotesCount: Int,
@SerializedName("due_date") val dueDate: String?,
@SerializedName("due_date") val dueDate: LocalDate?,
@SerializedName("web_url") val webUrl: String?,
@SerializedName("confidential") val confidential: Boolean,
@SerializedName("upvotes") val upvotes: Int,
......@@ -28,8 +29,11 @@ data class Issue(
// The closed_by attribute was introduced in GitLab 10.6.
// This value will only be present for issues which were closed after GitLab 10.6 and
// when the user account that closed the issue still exists.
@SerializedName("closed_by") val closedBy: Author?,
@SerializedName("closed_by") val closedBy: ShortUser?,
@SerializedName("closed_at") val closedAt: LocalDateTime?,
// The merge_requests_count attribute was introduced in GitLab 11.9.
@SerializedName("merge_requests_count") val relatedMergeRequestCount: Int
@SerializedName("merge_requests_count") val relatedMergeRequestCount: Int,
@SerializedName("time_stats") val timeStats: TimeStats,
@SerializedName("weight") val weight: Int?,
@SerializedName("discussion_locked") val discussionLocked: Boolean
)
\ No newline at end of file
......@@ -2,8 +2,7 @@ package ru.terrakok.gitlabclient.entity.mergerequest
import com.google.gson.annotations.SerializedName
import org.threeten.bp.LocalDateTime
import ru.terrakok.gitlabclient.entity.Author
import ru.terrakok.gitlabclient.entity.User
import ru.terrakok.gitlabclient.entity.ShortUser
import ru.terrakok.gitlabclient.entity.milestone.Milestone
data class MergeRequest(
......@@ -18,8 +17,8 @@ data class MergeRequest(
@SerializedName("state") val state: MergeRequestState,
@SerializedName("upvotes") val upvotes: Int,
@SerializedName("downvotes") val downvotes: Int,
@SerializedName("author") val author: Author,
@SerializedName("assignee") val assignee: User?,
@SerializedName("author") val author: ShortUser,
@SerializedName("assignee") val assignee: ShortUser?,
@SerializedName("source_project_id") val sourceProjectId: Int,
@SerializedName("target_project_id") val targetProjectId: Int,
@SerializedName("description") val description: String,
......@@ -38,9 +37,9 @@ data class MergeRequest(
// The closed_by attribute was introduced in GitLab 10.6.
// This value will only be present for merge requests which were closed/merged after GitLab 10.6
// and when the user account that closed/merged the issue still exists.
@SerializedName("closed_by") val closedBy: Author?,
@SerializedName("closed_by") val closedBy: ShortUser?,
@SerializedName("closed_at") val closedAt: LocalDateTime?,
@SerializedName("merged_by") val mergedBy: Author?,
@SerializedName("merged_by") val mergedBy: ShortUser?,
@SerializedName("merged_at") val mergedAt: LocalDateTime?,
@SerializedName("changes") val changes: List<MergeRequestChange>?
)
......@@ -2,8 +2,8 @@ package ru.terrakok.gitlabclient.entity.target
import com.google.gson.annotations.SerializedName
import org.threeten.bp.LocalDateTime
import ru.terrakok.gitlabclient.entity.Assignee
import ru.terrakok.gitlabclient.entity.Author
import ru.terrakok.gitlabclient.entity.ShortUser
import ru.terrakok.gitlabclient.entity.TimeStats
import ru.terrakok.gitlabclient.entity.milestone.Milestone
/**
......@@ -29,11 +29,11 @@ abstract class Target {
@SerializedName("milestone")
val milestone: Milestone? = null
@SerializedName("assignees")
private val _assignees: List<Assignee>? = null
private val _assignees: List<ShortUser>? = null
@SerializedName("author")
val _author: Author? = null
val _author: ShortUser? = null
@SerializedName("assignee")
val assignee: Assignee? = null
val assignee: ShortUser? = null
@SerializedName("user_notes_count")
private val _userNotesCount: Int? = null
@SerializedName("upvotes")
......
package ru.terrakok.gitlabclient.entity.todo
import org.threeten.bp.LocalDateTime
import ru.terrakok.gitlabclient.entity.Author
import ru.terrakok.gitlabclient.entity.Project
import ru.terrakok.gitlabclient.entity.ShortUser
import ru.terrakok.gitlabclient.entity.target.Target
import ru.terrakok.gitlabclient.entity.target.TargetType
......@@ -12,7 +12,7 @@ import ru.terrakok.gitlabclient.entity.target.TargetType
data class Todo(
val id: Long,
val project: Project,
val author: Author,
val author: ShortUser,
val actionName: TodoAction,
val targetType: TargetType,
val target: Target,
......
......@@ -274,7 +274,7 @@ interface GitlabApi {
@Path("merge_request_id") mergeRequestId: Long,
@Query("page") page: Int,
@Query("per_page") pageSize: Int
): Single<List<Author>>
): Single<List<ShortUser>>
@GET("$API_PATH/projects/{project_id}/merge_requests/{merge_request_id}/changes")
fun getMergeRequestChanges(
......
......@@ -5,8 +5,8 @@ import com.google.gson.JsonDeserializer
import com.google.gson.JsonElement
import com.google.gson.JsonParseException
import org.threeten.bp.LocalDateTime
import ru.terrakok.gitlabclient.entity.Author
import ru.terrakok.gitlabclient.entity.Project
import ru.terrakok.gitlabclient.entity.ShortUser
import ru.terrakok.gitlabclient.entity.target.Target
import ru.terrakok.gitlabclient.entity.target.TargetType
import ru.terrakok.gitlabclient.entity.target.issue.Issue
......@@ -34,7 +34,7 @@ class TodoDeserializer : JsonDeserializer<Todo> {
Todo(
jsonObject.get("id").asLong,
context.deserialize<Project>(jsonObject.get("project"), Project::class.java),
context.deserialize<Author>(jsonObject.get("author"), Author::class.java),
context.deserialize<ShortUser>(jsonObject.get("author"), ShortUser::class.java),
context.deserialize<TodoAction>(
jsonObject.get("action_name"),
TodoAction::class.java
......
......@@ -18,6 +18,8 @@ import ru.terrakok.gitlabclient.extension.getXTotalHeader
import ru.terrakok.gitlabclient.model.data.server.GitlabApi
import ru.terrakok.gitlabclient.model.data.server.MarkDownUrlResolver
import ru.terrakok.gitlabclient.model.system.SchedulersProvider
import ru.terrakok.gitlabclient.model.system.SingleCacheSuccess
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
/**
......@@ -30,6 +32,7 @@ class IssueRepository @Inject constructor(
private val markDownUrlResolver: MarkDownUrlResolver
) {
private val defaultPageSize = defaultPageSizeWrapper.value
private val issueCachedSingles = ConcurrentHashMap<Pair<Long, Long>, Single<Issue>>()
fun getMyIssues(
scope: IssueScope? = null,
......@@ -130,18 +133,26 @@ class IssueRepository @Inject constructor(
projectId: Long,
issueId: Long
) = Single
.zip(
api.getProject(projectId),
api.getIssue(projectId, issueId),
BiFunction<Project, Issue, Issue> { project, issue ->
val resolved = markDownUrlResolver.resolve(issue.description, project)
if (resolved != issue.description) {
issue.copy(description = resolved)
} else {
issue
}
.defer {
val key = Pair(projectId, issueId)
if (!issueCachedSingles.containsKey(key)) {
issueCachedSingles[key] = Single
.zip(
api.getProject(projectId),
api.getIssue(projectId, issueId),
BiFunction<Project, Issue, Issue> { project, issue ->
val resolved = markDownUrlResolver.resolve(issue.description, project)
if (resolved != issue.description) {
issue.copy(description = resolved)
} else {
issue
}
}
)
.compose { SingleCacheSuccess.create(it) }
}
)
issueCachedSingles[key]
}
.subscribeOn(schedulers.io())
.observeOn(schedulers.ui())
......
......@@ -18,6 +18,8 @@ import ru.terrakok.gitlabclient.extension.getXTotalHeader
import ru.terrakok.gitlabclient.model.data.server.GitlabApi
import ru.terrakok.gitlabclient.model.data.server.MarkDownUrlResolver
import ru.terrakok.gitlabclient.model.system.SchedulersProvider
import ru.terrakok.gitlabclient.model.system.SingleCacheSuccess
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject
class MergeRequestRepository @Inject constructor(
......@@ -27,6 +29,7 @@ class MergeRequestRepository @Inject constructor(
private val markDownUrlResolver: MarkDownUrlResolver
) {
private val defaultPageSize = defaultPageSizeWrapper.value
private val mergeRequestCachedSingles = ConcurrentHashMap<Pair<Long, Long>, Single<MergeRequest>>()
fun getMyMergeRequests(
state: MergeRequestState? = null,
......@@ -141,18 +144,26 @@ class MergeRequestRepository @Inject constructor(
projectId: Long,
mergeRequestId: Long
) = Single
.zip(
api.getProject(projectId),
api.getMergeRequest(projectId, mergeRequestId),
BiFunction<Project, MergeRequest, MergeRequest> { project, mr ->
val resolved = markDownUrlResolver.resolve(mr.description, project)
if (resolved != mr.description) {
mr.copy(description = resolved)
} else {
mr
}
.defer {
val key = Pair(projectId, mergeRequestId)
if (!mergeRequestCachedSingles.containsKey(key)) {
mergeRequestCachedSingles[key] = Single
.zip(
api.getProject(projectId),
api.getMergeRequest(projectId, mergeRequestId),
BiFunction<Project, MergeRequest, MergeRequest> { project, mr ->
val resolved = markDownUrlResolver.resolve(mr.description, project)
if (resolved != mr.description) {
mr.copy(description = resolved)
} else {
mr
}
}
)
.compose { SingleCacheSuccess.create(it) }
}
)
mergeRequestCachedSingles[key]
}
.subscribeOn(schedulers.io())
.observeOn(schedulers.ui())
......@@ -219,7 +230,7 @@ class MergeRequestRepository @Inject constructor(
.zip(
getAllMergeRequestParticipants(projectId, mergeRequestId),
api.getMergeRequestCommits(projectId, mergeRequestId, page, pageSize),
BiFunction<List<Author>, List<Commit>, List<CommitWithAvatarUrl>> { participants, commits ->
BiFunction<List<ShortUser>, List<Commit>, List<CommitWithAvatarUrl>> { participants, commits ->
commits.map { commit ->
CommitWithAvatarUrl(
commit,
......
......@@ -3,7 +3,7 @@ package ru.terrakok.gitlabclient.model.repository.todo
import io.reactivex.Single
import ru.terrakok.gitlabclient.di.DefaultPageSize
import ru.terrakok.gitlabclient.di.PrimitiveWrapper
import ru.terrakok.gitlabclient.entity.Assignee
import ru.terrakok.gitlabclient.entity.ShortUser
import ru.terrakok.gitlabclient.entity.User
import ru.terrakok.gitlabclient.entity.app.target.*
import ru.terrakok.gitlabclient.entity.target.TargetState
......@@ -45,7 +45,7 @@ class TodoRepository @Inject constructor(
val target = todo.target
val assignee = if (todo.actionName != TodoAction.MARKED) {
currentUser.let {
Assignee(it.id, it.state, it.name, it.webUrl, it.avatarUrl, it.username)
ShortUser(it.id, it.state, it.name, it.webUrl, it.avatarUrl, it.username)
}
} else {
null
......
package ru.terrakok.gitlabclient.model.system;/*
* Copyright 2019 Sergey Chelombitko
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import io.reactivex.Single;
import io.reactivex.SingleObserver;
import io.reactivex.disposables.Disposable;
import io.reactivex.plugins.RxJavaPlugins;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* Stores the success value from the source Single and replays it to observers.
*/
public final class SingleCacheSuccess<T> extends Single<T> implements SingleObserver<T> {
@SuppressWarnings("rawtypes")
private static final CacheDisposable[] EMPTY = new CacheDisposable[0];
@SuppressWarnings("rawtypes")
private static final CacheDisposable[] TERMINATED = new CacheDisposable[0];
private final Single<T> source;
private final AtomicBoolean wip = new AtomicBoolean();
private final AtomicReference<CacheDisposable<T>[]> observers;
private T value;
public static <T> Single<T> create(Single<T> source) {
return RxJavaPlugins.onAssembly(new SingleCacheSuccess<>(source));
}
@SuppressWarnings("unchecked")
private SingleCacheSuccess(Single<T> source) {
this.source = source;
observers = new AtomicReference<CacheDisposable<T>[]>(EMPTY);
}
@Override
protected void subscribeActual(SingleObserver<? super T> observer) {
final CacheDisposable<T> d = new CacheDisposable<>(observer, this);
observer.onSubscribe(d);
if (add(d)) {
if (d.isDisposed()) {
remove(d);
}
} else {
observer.onSuccess(value);
return;
}
if (!wip.getAndSet(true)) {
source.subscribe(this);
}
}
@Override
public void onSubscribe(Disposable d) {
// not supported by this operator
}
@Override
@SuppressWarnings("unchecked")
public void onSuccess(T value) {
this.value = value;
for (CacheDisposable<T> observer : observers.getAndSet(TERMINATED)) {
if (!observer.isDisposed()) {
observer.actual.onSuccess(value);
}
}
}
@Override
@SuppressWarnings("unchecked")
public void onError(Throwable e) {
wip.set(false);
for (CacheDisposable<T> observer : observers.getAndSet(EMPTY)) {
if (!observer.isDisposed()) {
observer.actual.onError(e);
}
}
}
@SuppressWarnings("unchecked")
private boolean add(CacheDisposable<T> observer) {
for (; ; ) {
final CacheDisposable<T>[] a = observers.get();
if (a == TERMINATED) {
return false;
}
final int n = a.length;
final CacheDisposable<T>[] b = new CacheDisposable[n + 1];
System.arraycopy(a, 0, b, 0, n);
b[n] = observer;
if (observers.compareAndSet(a, b)) {
return true;
}
}
}
@SuppressWarnings("unchecked")
private void remove(CacheDisposable<T> observer) {
for (; ; ) {
final CacheDisposable<T>[] a = observers.get();
final int n = a.length;
if (n == 0) {
return;
}
int j = -1;
for (int i = 0; i < n; i++) {
if (a[i] == observer) {
j = i;
break;
}
}
if (j < 0) {
return;
}
final CacheDisposable<T>[] b;
if (n == 1) {
b = EMPTY;
} else {
b = new CacheDisposable[n - 1];
System.arraycopy(a, 0, b, 0, j);
System.arraycopy(a, j + 1, b, j, n - j - 1);
}
if (observers.compareAndSet(a, b)) {
return;
}
}
}
private static final class CacheDisposable<T> extends AtomicBoolean implements Disposable {
private static final long serialVersionUID = 4746876330948546833L;
final SingleObserver<? super T> actual;
private final SingleCacheSuccess<T> parent;
CacheDisposable(SingleObserver<? super T> actual, SingleCacheSuccess<T> parent) {
this.actual = actual;
this.parent = parent;
}
@Override
public boolean isDisposed() {
return get();
}
@Override
public void dispose() {
if (compareAndSet(false, true)) {
parent.remove(this);
}
}
}
}
\ No newline at end of file
package ru.terrakok.gitlabclient.presentation.issue.details
import com.arellomobile.mvp.InjectViewState
import ru.terrakok.gitlabclient.di.IssueId
import ru.terrakok.gitlabclient.di.PrimitiveWrapper
import ru.terrakok.gitlabclient.di.ProjectId
import ru.terrakok.gitlabclient.model.interactor.issue.IssueInteractor
import ru.terrakok.gitlabclient.presentation.global.BasePresenter
import ru.terrakok.gitlabclient.presentation.global.ErrorHandler
import ru.terrakok.gitlabclient.presentation.global.MarkDownConverter
import javax.inject.Inject
/**
* Created by Eugene Shapovalov (@CraggyHaggy) on 26.05.19.
*/
@InjectViewState
class IssueDetailsPresenter @Inject constructor(
@ProjectId private val projectIdWrapper: PrimitiveWrapper<Long>,
@IssueId private val issueIdWrapper: PrimitiveWrapper<Long>,
private val issueInteractor: IssueInteractor,
private val mdConverter: MarkDownConverter,
private val errorHandler: ErrorHandler
) : BasePresenter<IssueDetailsView>() {
private val projectId = projectIdWrapper.value
private val issueId = issueIdWrapper.value
override fun onFirstViewAttach() {
super.onFirstViewAttach()
issueInteractor
.getIssue(projectId, issueId)
.flatMap { issue ->
mdConverter
.markdownToSpannable(issue.description)
.map { Pair(issue, it) }
}
.doOnSubscribe { viewState.showEmptyProgress(true) }
.doAfterTerminate { viewState.showEmptyProgress(false) }
.subscribe(
{ (issue, mdDescription) -> viewState.showDetails(issue, mdDescription) },
{ errorHandler.proceed(it, { viewState.showMessage(it) }) }
)
.connect()
}
}
\ No newline at end of file
package ru.terrakok.gitlabclient.presentation.issue.details
import com.arellomobile.mvp.MvpView
import com.arellomobile.mvp.viewstate.strategy.AddToEndSingleStrategy
import com.arellomobile.mvp.viewstate.strategy.OneExecutionStateStrategy
import com.arellomobile.mvp.viewstate.strategy.StateStrategyType
import ru.terrakok.gitlabclient.entity.issue.Issue
/**
* Created by Eugene Shapovalov (@CraggyHaggy) on 26.05.19.
*/
@StateStrategyType(AddToEndSingleStrategy::class)
interface IssueDetailsView : MvpView {
fun showDetails(issue: Issue, mdDescription: CharSequence)
fun showEmptyProgress(show: Boolean)
@StateStrategyType(OneExecutionStateStrategy::class)
fun showMessage(message: String)
}
\ No newline at end of file
package ru.terrakok.gitlabclient.presentation.issue.info
import com.arellomobile.mvp.InjectViewState
import ru.terrakok.gitlabclient.Screens
import ru.terrakok.gitlabclient.di.IssueId
import ru.terrakok.gitlabclient.di.PrimitiveWrapper
import ru.terrakok.gitlabclient.di.ProjectId
import ru.terrakok.gitlabclient.entity.ShortUser
import ru.terrakok.gitlabclient.model.interactor.issue.IssueInteractor
import ru.terrakok.gitlabclient.model.system.flow.FlowRouter
import ru.terrakok.gitlabclient.presentation.global.BasePresenter
import ru.terrakok.gitlabclient.presentation.global.ErrorHandler
import ru.terrakok.gitlabclient.presentation.global.MarkDownConverter
import javax.inject.Inject
/**
......@@ -18,8 +20,8 @@ class IssueInfoPresenter @Inject constructor(
@ProjectId private val projectIdWrapper: PrimitiveWrapper<Long>,
@IssueId private val issueIdWrapper: PrimitiveWrapper<Long>,
private val issueInteractor: IssueInteractor,
private val mdConverter: MarkDownConverter,
private val errorHandler: ErrorHandler
private val errorHandler: ErrorHandler,
private val router: FlowRouter