TSK-1335 Improve draft (#3092)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-04-27 22:52:31 +06:00 committed by GitHub
parent 97396cc5f2
commit be1872abe5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 343 additions and 141 deletions

View File

@ -1,19 +1,114 @@
import { fetchMetadataLocalStorage, setMetadataLocalStorage } from '@hcengineering/ui' import { fetchMetadataLocalStorage, setMetadataLocalStorage } from '@hcengineering/ui'
import { deepEqual } from 'fast-equals' import { deepEqual } from 'fast-equals'
import { get, writable } from 'svelte/store' import { Unsubscriber, writable } from 'svelte/store'
import presentation from './plugin' import presentation from './plugin'
migrateDrafts()
export const draftsStore = writable<Record<string, any>>(fetchMetadataLocalStorage(presentation.metadata.Draft) ?? {}) export const draftsStore = writable<Record<string, any>>(fetchMetadataLocalStorage(presentation.metadata.Draft) ?? {})
let drafts: Record<string, any> = fetchMetadataLocalStorage(presentation.metadata.Draft) ?? {}
const activeDraftsKey = 'activeDrafts'
export const activeDraftsStore = writable<Set<string>>(new Set())
const activeDrafts: Set<string> = new Set()
window.addEventListener('storage', storageHandler) window.addEventListener('storage', storageHandler)
const saveInterval = 200
function storageHandler (evt: StorageEvent): void { function storageHandler (evt: StorageEvent): void {
if (evt.storageArea !== localStorage) return if (evt.storageArea === localStorage) {
if (evt.key !== presentation.metadata.Draft) return if (evt.key !== presentation.metadata.Draft) return
if (evt.newValue !== null) { if (evt.newValue !== null) {
draftsStore.set(JSON.parse(evt.newValue)) drafts = JSON.parse(evt.newValue)
draftsStore.set(drafts)
} }
} }
}
function syncDrafts (): void {
draftsStore.set(drafts)
setMetadataLocalStorage(presentation.metadata.Draft, drafts)
}
// #region Broadcast
const bc = new BroadcastChannel(activeDraftsKey)
type BroadcastMessage = BroadcastGetMessage | BroadcastGetResp | BroadcastAddMessage | BroadcastRemoveMessage
interface BroadcastGetMessage {
type: 'get_all'
}
interface BroadcastGetResp {
type: 'get_all_response'
value: string[]
}
interface BroadcastRemoveMessage {
type: 'remove'
value: string
}
interface BroadcastAddMessage {
type: 'add'
value: string
}
function sendMessage (req: BroadcastMessage): void {
bc.postMessage(req)
}
function syncActive (): void {
activeDraftsStore.set(activeDrafts)
}
function loadActiveDrafts (): void {
activeDrafts.clear()
syncActive()
sendMessage({ type: 'get_all' })
}
bc.onmessage = (e: MessageEvent<BroadcastMessage>) => {
if (e.data.type === 'get_all') {
sendMessage({ type: 'get_all_response', value: Array.from(activeDrafts.values()) })
}
if (e.data.type === 'get_all_response') {
for (const val of e.data.value) {
activeDrafts.add(val)
}
syncActive()
}
if (e.data.type === 'add') {
activeDrafts.add(e.data.value)
syncActive()
}
if (e.data.type === 'remove') {
activeDrafts.delete(e.data.value)
syncActive()
}
}
loadActiveDrafts()
// #endregion
// #region Active
function addActive (id: string): void {
if (!activeDrafts.has(id)) {
activeDrafts.add(id)
syncActive()
sendMessage({ type: 'add', value: id })
}
}
function deleteActive (id: string): void {
if (activeDrafts.has(id)) {
activeDrafts.delete(id)
syncActive()
sendMessage({ type: 'remove', value: id })
}
}
// #endregion
function isEmptyDraft<T> (object: T, emptyObj: Partial<T> | undefined): boolean { function isEmptyDraft<T> (object: T, emptyObj: Partial<T> | undefined): boolean {
for (const key in object) { for (const key in object) {
@ -44,73 +139,161 @@ function isEmptyDraft<T> (object: T, emptyObj: Partial<T> | undefined): boolean
return true return true
} }
export class DraftController<T> { function removeDraft (id: string, parentId: string | undefined = undefined): void {
private timer: number | undefined = undefined
constructor (private readonly id: string) {}
static remove (id: string): void {
const drafts = get(draftsStore)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete drafts[id] delete drafts[id]
draftsStore.set(drafts) deleteActive(id)
setMetadataLocalStorage(presentation.metadata.Draft, drafts) syncDrafts()
if (parentId !== undefined) {
MultipleDraftController.remove(parentId, id)
}
}
export class DraftController<T> {
private unsub: Unsubscriber | undefined = undefined
constructor (private readonly id: string | undefined, private readonly parentId: string | undefined = undefined) {
if (this.id !== undefined) {
addActive(this.id)
}
}
static remove (id: string): void {
removeDraft(id)
} }
static save<T>(id: string, object: T, emptyObj: Partial<T> | undefined = undefined): void { static save<T>(id: string, object: T, emptyObj: Partial<T> | undefined = undefined): void {
const drafts = get(draftsStore)
if (emptyObj !== undefined && isEmptyDraft(object, emptyObj)) { if (emptyObj !== undefined && isEmptyDraft(object, emptyObj)) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete return DraftController.remove(id)
delete drafts[id] }
} else { drafts[id] = object
drafts[id] = object addActive(id)
syncDrafts()
}
destroy (): void {
this.unsub?.()
if (this.id !== undefined) {
deleteActive(this.id)
}
}
subscribe (callback: (val: T | undefined) => void): Unsubscriber {
callback(this.get())
this.unsub = draftsStore.subscribe((p) => {
callback(this.getValue(p))
})
return () => {
this.destroy()
}
}
private getValue (store: Record<string, any>): T | undefined {
if (this.id !== undefined) {
const res = store[this.id]
return res
} }
draftsStore.set(drafts)
setMetadataLocalStorage(presentation.metadata.Draft, drafts)
} }
get (): T | undefined { get (): T | undefined {
const drafts = get(draftsStore) return this.getValue(drafts)
const res = drafts[this.id]
return res
} }
save (object: T, emptyObj: Partial<T> | undefined = undefined): void { save (object: T, emptyObj: Partial<T> | undefined = undefined): void {
const drafts = get(draftsStore)
if (emptyObj !== undefined && isEmptyDraft(object, emptyObj)) { if (emptyObj !== undefined && isEmptyDraft(object, emptyObj)) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete return this.remove()
delete drafts[this.id] }
} else { if (this.id !== undefined) {
drafts[this.id] = object drafts[this.id] = object
} syncDrafts()
draftsStore.set(drafts) addActive(this.id)
setMetadataLocalStorage(presentation.metadata.Draft, drafts) if (this.parentId !== undefined) {
} MultipleDraftController.add(this.parentId, this.id)
private update (object: T, emptyObj: Partial<T> | undefined): void {
this.timer = window.setTimeout(() => {
this.save(object, emptyObj)
this.update(object, emptyObj)
}, saveInterval)
}
unsubscribe (): void {
if (this?.timer !== undefined) {
clearTimeout(this.timer)
} }
} }
watch (object: T, emptyObj: Partial<T> | undefined = undefined): void {
this.unsubscribe()
this.save(object, emptyObj)
this.update(object, emptyObj)
} }
remove (): void { remove (): void {
this.unsubscribe() if (this.id !== undefined) {
const drafts = get(draftsStore) removeDraft(this.id, this.parentId)
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete }
delete drafts[this.id] }
}
export class MultipleDraftController {
constructor (private readonly id: string) {}
static remove (id: string, value: string): void {
const arr: string[] = drafts[id] ?? []
const index = arr.findIndex((p) => p === value)
if (index !== -1) {
arr.splice(index, 1)
drafts[id] = arr
syncDrafts()
}
}
static add (id: string, value: string): void {
const arr: string[] = drafts[id] ?? []
if (!arr.includes(value)) {
arr.push(value)
}
drafts[id] = arr
syncDrafts()
}
getNext (): string | undefined {
const value = drafts[this.id] ?? []
for (const val of value) {
if (!activeDrafts.has(val)) {
return val
}
}
}
hasNext (callback: (value: boolean) => void): Unsubscriber {
const next = this.getNext()
// eslint-disable-next-line
callback(next !== undefined)
const draftSub = draftsStore.subscribe((drafts) => {
const value = drafts[this.id] ?? []
for (const val of value) {
if (!activeDrafts.has(val)) {
// eslint-disable-next-line
callback(true)
return
}
}
// eslint-disable-next-line
callback(false)
})
const activeSub = activeDraftsStore.subscribe((activeDrafts) => {
const value = drafts[this.id] ?? []
for (const val of value) {
if (!activeDrafts.has(val)) {
// eslint-disable-next-line
callback(true)
return
}
}
// eslint-disable-next-line
callback(false)
})
return () => {
draftSub()
activeSub()
}
}
}
function migrateDrafts (): void {
const drafts = fetchMetadataLocalStorage(presentation.metadata.Draft) ?? {}
const issues = drafts['tracker:ids:IssueDraft']
if (!Array.isArray(issues)) {
drafts['tracker:ids:IssueDraft'] = []
}
const candidates = drafts['recruit:mixin:Candidate']
if (!Array.isArray(candidates)) {
drafts['recruit:mixin:Candidate'] = []
}
setMetadataLocalStorage(presentation.metadata.Draft, drafts) setMetadataLocalStorage(presentation.metadata.Draft, drafts)
draftsStore.set(drafts)
}
} }

View File

@ -14,13 +14,12 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import { Comment } from '@hcengineering/chunter' import { Comment } from '@hcengineering/chunter'
import { AttachedData, Doc, generateId, Ref } from '@hcengineering/core' import { AttachedData, Doc, generateId, Ref } from '@hcengineering/core'
import { DraftController, draftsStore, getClient } from '@hcengineering/presentation' import { DraftController, draftsStore, getClient } from '@hcengineering/presentation'
import { AttachmentRefInput } from '@hcengineering/attachment-resources'
import { createBacklinks } from '../backlinks' import { createBacklinks } from '../backlinks'
import chunter from '../plugin' import chunter from '../plugin'
import { onDestroy } from 'svelte'
export let object: Doc export let object: Doc
export let shouldSaveDraft: boolean = true export let shouldSaveDraft: boolean = true
@ -44,11 +43,13 @@
let _id: Ref<Comment> = comment._id let _id: Ref<Comment> = comment._id
let inputContent: string = comment.message let inputContent: string = comment.message
function objectChange (object: CommentDraft, empty: any) {
if (shouldSaveDraft) { if (shouldSaveDraft) {
draftController.watch(comment, empty) draftController.save(object, empty)
}
} }
onDestroy(draftController.unsubscribe) $: objectChange(comment, empty)
function getDefault (): CommentDraft { function getDefault (): CommentDraft {
return { return {

View File

@ -35,11 +35,11 @@
Card, Card,
createQuery, createQuery,
DraftController, DraftController,
draftsStore,
getClient, getClient,
InlineAttributeBar, InlineAttributeBar,
KeyedAttribute, KeyedAttribute,
MessageBox, MessageBox,
MultipleDraftController,
PDFViewer PDFViewer
} from '@hcengineering/presentation' } from '@hcengineering/presentation'
import type { Candidate, CandidateDraft } from '@hcengineering/recruit' import type { Candidate, CandidateDraft } from '@hcengineering/recruit'
@ -53,23 +53,28 @@
IconFile as FileIcon, IconFile as FileIcon,
FocusHandler, FocusHandler,
getColorNumberByText, getColorNumberByText,
IconAttachment,
IconInfo, IconInfo,
Label, Label,
showPopup, showPopup,
Spinner, Spinner
IconAttachment
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher, onDestroy } from 'svelte'
import recruit from '../plugin' import recruit from '../plugin'
import YesNo from './YesNo.svelte' import YesNo from './YesNo.svelte'
export let shouldSaveDraft: boolean = false export let shouldSaveDraft: boolean = false
const draftController = new DraftController<CandidateDraft>(recruit.mixin.Candidate) const mDraftController = new MultipleDraftController(recruit.mixin.Candidate)
const id: Ref<Candidate> = generateId()
const draftController = new DraftController<CandidateDraft>(
shouldSaveDraft ? mDraftController.getNext() ?? id : undefined,
recruit.mixin.Candidate
)
function getEmptyCandidate (): CandidateDraft { function getEmptyCandidate (id: Ref<Candidate> | undefined = undefined): CandidateDraft {
return { return {
_id: generateId(), _id: id ?? generateId(),
firstName: '', firstName: '',
lastName: '', lastName: '',
title: '', title: '',
@ -83,17 +88,13 @@
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
const ignoreKeys = ['onsite', 'remote', 'title'] const ignoreKeys = ['onsite', 'remote', 'title']
let draft = shouldSaveDraft ? ($draftsStore[recruit.mixin.Candidate] as CandidateDraft) : undefined let draft = shouldSaveDraft ? draftController.get() : undefined
$: draft = shouldSaveDraft ? ($draftsStore[recruit.mixin.Candidate] as CandidateDraft) : undefined let object = draft ?? getEmptyCandidate(id)
let object = draft ?? getEmptyCandidate() onDestroy(
draftController.subscribe((val) => {
function draftChange (draft: CandidateDraft | undefined) { draft = shouldSaveDraft ? val : undefined
if (draft === undefined) { })
object = getEmptyCandidate() )
} else {
object = draft
}
}
function objectChange (object: CandidateDraft, empty: any) { function objectChange (object: CandidateDraft, empty: any) {
if (shouldSaveDraft) { if (shouldSaveDraft) {
@ -102,7 +103,6 @@
} }
$: objectChange(object, empty) $: objectChange(object, empty)
$: draftChange(draft)
type resumeFile = { type resumeFile = {
name: string name: string
@ -481,7 +481,7 @@
async function showConfirmationDialog () { async function showConfirmationDialog () {
draftController.save(object, empty) draftController.save(object, empty)
const isFormEmpty = $draftsStore[recruit.mixin.Candidate] === undefined const isFormEmpty = draft === undefined
if (isFormEmpty) { if (isFormEmpty) {
dispatch('close') dispatch('close')

View File

@ -13,12 +13,20 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import { draftsStore } from '@hcengineering/presentation' import { MultipleDraftController } from '@hcengineering/presentation'
import { Button, showPopup, IconAdd } from '@hcengineering/ui' import { Button, IconAdd, showPopup } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import recruit from '../plugin' import recruit from '../plugin'
import CreateCandidate from './CreateCandidate.svelte' import CreateCandidate from './CreateCandidate.svelte'
$: draftExists = $draftsStore[recruit.mixin.Candidate] let draftExists = false
const draftController = new MultipleDraftController(recruit.mixin.Candidate)
onDestroy(
draftController.hasNext((res) => {
draftExists = res
})
)
async function newCandidate (): Promise<void> { async function newCandidate (): Promise<void> {
showPopup(CreateCandidate, { shouldSaveDraft: true }, 'top') showPopup(CreateCandidate, { shouldSaveDraft: true }, 'top')

View File

@ -22,10 +22,10 @@
Card, Card,
createQuery, createQuery,
DraftController, DraftController,
draftsStore,
getClient, getClient,
KeyedAttribute, KeyedAttribute,
MessageBox, MessageBox,
MultipleDraftController,
SpaceSelector SpaceSelector
} from '@hcengineering/presentation' } from '@hcengineering/presentation'
import tags, { TagElement, TagReference } from '@hcengineering/tags' import tags, { TagElement, TagReference } from '@hcengineering/tags'
@ -57,7 +57,7 @@
} from '@hcengineering/ui' } from '@hcengineering/ui'
import view from '@hcengineering/view' import view from '@hcengineering/view'
import { ObjectBox } from '@hcengineering/view-resources' import { ObjectBox } from '@hcengineering/view-resources'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher, onDestroy } from 'svelte'
import { activeComponent, activeSprint, generateIssueShortLink, getIssueId, updateIssueRelation } from '../issues' import { activeComponent, activeSprint, generateIssueShortLink, getIssueId, updateIssueRelation } from '../issues'
import tracker from '../plugin' import tracker from '../plugin'
import ComponentSelector from './ComponentSelector.svelte' import ComponentSelector from './ComponentSelector.svelte'
@ -83,25 +83,26 @@
export let parentIssue: Issue | undefined export let parentIssue: Issue | undefined
export let originalIssue: Issue | undefined export let originalIssue: Issue | undefined
const draftController = new DraftController<any>(tracker.ids.IssueDraft) const mDraftController = new MultipleDraftController(tracker.ids.IssueDraft)
const id: Ref<Issue> = generateId()
const draftController = new DraftController<IssueDraft>(
shouldSaveDraft ? mDraftController.getNext() ?? id : undefined,
tracker.ids.IssueDraft
)
let draft = shouldSaveDraft ? ($draftsStore[tracker.ids.IssueDraft] as IssueDraft) : undefined let draft = shouldSaveDraft ? draftController.get() : undefined
$: draft = shouldSaveDraft ? ($draftsStore[tracker.ids.IssueDraft] as IssueDraft) : undefined
onDestroy(
draftController.subscribe((val) => {
draft = shouldSaveDraft ? val : undefined
})
)
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
const parentQuery = createQuery() const parentQuery = createQuery()
let _space = space let _space = space
let object = draft ?? getDefaultObject() let object = draft ?? getDefaultObject(id)
function draftChange (draft: IssueDraft | undefined) {
if (draft === undefined) {
object = getDefaultObject()
} else {
object = draft
descriptionBox?.setContent(object.description)
}
}
function objectChange (object: IssueDraft, empty: any) { function objectChange (object: IssueDraft, empty: any) {
if (shouldSaveDraft) { if (shouldSaveDraft) {
@ -110,7 +111,6 @@
} }
$: objectChange(object, empty) $: objectChange(object, empty)
$: draftChange(draft)
$: if (object.parentIssue) { $: if (object.parentIssue) {
parentQuery.query( parentQuery.query(
@ -127,9 +127,9 @@
parentIssue = undefined parentIssue = undefined
} }
function getDefaultObject (ignoreOriginal = false): IssueDraft { function getDefaultObject (id: Ref<Issue> | undefined = undefined, ignoreOriginal = false): IssueDraft {
const base: IssueDraft = { const base: IssueDraft = {
_id: generateId(), _id: id ?? generateId(),
title: '', title: '',
description: '', description: '',
priority, priority,
@ -199,7 +199,7 @@
function resetObject (): void { function resetObject (): void {
templateId = undefined templateId = undefined
template = undefined template = undefined
object = getDefaultObject(true) object = getDefaultObject(undefined, true)
fillDefaults(hierarchy, object, tracker.class.Issue) fillDefaults(hierarchy, object, tracker.class.Issue)
} }
@ -502,7 +502,7 @@
async function showConfirmationDialog () { async function showConfirmationDialog () {
draftController.save(object, empty) draftController.save(object, empty)
const isFormEmpty = $draftsStore[tracker.ids.IssueDraft] === undefined const isFormEmpty = draft === undefined
if (isFormEmpty) { if (isFormEmpty) {
dispatch('close') dispatch('close')
@ -625,6 +625,7 @@
<SubIssues <SubIssues
bind:this={subIssuesComponent} bind:this={subIssuesComponent}
projectId={_space} projectId={_space}
parendIssueId={object._id}
project={currentProject} project={currentProject}
sprint={object.sprint} sprint={object.sprint}
component={object.component} component={object.component}
@ -638,7 +639,7 @@
value={object} value={object}
kind={'secondary'} kind={'secondary'}
size={'large'} size={'large'}
defaultIssueStatus={draft ? undefined : currentProject?.defaultIssueStatus} defaultIssueStatus={currentProject?.defaultIssueStatus}
shouldShowLabel={true} shouldShowLabel={true}
on:refocus={() => { on:refocus={() => {
manager.setFocusPos(3) manager.setFocusPos(3)

View File

@ -14,8 +14,9 @@
--> -->
<script lang="ts"> <script lang="ts">
import { Ref, Space } from '@hcengineering/core' import { Ref, Space } from '@hcengineering/core'
import { draftsStore, getClient } from '@hcengineering/presentation' import { MultipleDraftController, getClient } from '@hcengineering/presentation'
import { Button, showPopup, IconAdd } from '@hcengineering/ui' import { Button, IconAdd, showPopup } from '@hcengineering/ui'
import { onDestroy } from 'svelte'
import tracker from '../plugin' import tracker from '../plugin'
import CreateIssue from './CreateIssue.svelte' import CreateIssue from './CreateIssue.svelte'
@ -24,10 +25,17 @@
const client = getClient() const client = getClient()
let space: Ref<Space> | undefined let space: Ref<Space> | undefined
let closed = true
$: updateSpace(currentSpace) $: updateSpace(currentSpace)
$: draftExists = let draftExists = false
$draftsStore[tracker.ids.IssueDraft] !== undefined || $draftsStore[tracker.ids.IssueDraftChild] !== undefined
const draftController = new MultipleDraftController(tracker.ids.IssueDraft)
onDestroy(
draftController.hasNext((res) => {
draftExists = res
})
)
async function updateSpace (spaceId: Ref<Space> | undefined): Promise<void> { async function updateSpace (spaceId: Ref<Space> | undefined): Promise<void> {
if (spaceId !== undefined) { if (spaceId !== undefined) {
@ -44,15 +52,15 @@
const project = await client.findOne(tracker.class.Project, {}) const project = await client.findOne(tracker.class.Project, {})
space = project?._id space = project?._id
} }
closed = false
showPopup(CreateIssue, { space, shouldSaveDraft: true }, 'top') showPopup(CreateIssue, { space, shouldSaveDraft: true }, 'top', () => (closed = true))
} }
</script> </script>
<div class="antiNav-subheader"> <div class="antiNav-subheader">
<Button <Button
icon={IconAdd} icon={IconAdd}
label={draftExists ? tracker.string.ResumeDraft : tracker.string.NewIssue} label={draftExists || !closed ? tracker.string.ResumeDraft : tracker.string.NewIssue}
justify={'left'} justify={'left'}
kind={'primary'} kind={'primary'}
width={'100%'} width={'100%'}

View File

@ -27,6 +27,7 @@
import DraftIssueChildEditor from './templates/DraftIssueChildEditor.svelte' import DraftIssueChildEditor from './templates/DraftIssueChildEditor.svelte'
import DraftIssueChildList from './templates/DraftIssueChildList.svelte' import DraftIssueChildList from './templates/DraftIssueChildList.svelte'
export let parendIssueId: Ref<Issue>
export let projectId: Ref<Project> export let projectId: Ref<Project>
export let project: Project | undefined export let project: Project | undefined
export let sprint: Ref<Sprint> | null = null export let sprint: Ref<Sprint> | null = null
@ -36,7 +37,7 @@
let lastProject = project let lastProject = project
let isCollapsed = false let isCollapsed = false
$: isCreatingMode = $draftsStore[tracker.ids.IssueDraftChild] !== undefined $: isCreatingMode = $draftsStore[`${parendIssueId}_subIssue`] !== undefined
let isManualCreating = false let isManualCreating = false
$: isCreating = isCreatingMode || isManualCreating $: isCreating = isCreatingMode || isManualCreating
@ -233,6 +234,7 @@
<ExpandCollapse isExpanded={!isCollapsed} on:changeContent> <ExpandCollapse isExpanded={!isCollapsed} on:changeContent>
<DraftIssueChildEditor <DraftIssueChildEditor
bind:this={draftChild} bind:this={draftChild}
{parendIssueId}
{project} {project}
{component} {component}
{sprint} {sprint}

View File

@ -83,15 +83,15 @@
value: ValueType, value: ValueType,
defaultStatus: Ref<IssueStatus> | undefined defaultStatus: Ref<IssueStatus> | undefined
): WithLookup<IssueStatus> | undefined { ): WithLookup<IssueStatus> | undefined {
if (defaultStatus !== undefined) { if (value.status !== undefined) {
defaultIssueStatus = undefined
changeStatus(defaultStatus, false)
return statuses?.find((status) => status._id === defaultStatus)
}
const current = statuses?.find((status) => status._id === value.status) const current = statuses?.find((status) => status._id === value.status)
if (current) return current if (current) return current
changeStatus(statuses?.[0]?._id, false) }
return statuses?.[0] if (defaultIssueStatus !== undefined) {
const res = statuses?.find((status) => status._id === defaultStatus)
changeStatus(res?._id, false)
return res
}
} }
$: selectedStatus = getSelectedStatus(statuses, value, defaultIssueStatus) $: selectedStatus = getSelectedStatus(statuses, value, defaultIssueStatus)

View File

@ -19,25 +19,26 @@
import presentation, { DraftController, getClient, KeyedAttribute } from '@hcengineering/presentation' import presentation, { DraftController, getClient, KeyedAttribute } from '@hcengineering/presentation'
import tags, { TagElement, TagReference } from '@hcengineering/tags' import tags, { TagElement, TagReference } from '@hcengineering/tags'
import { calcRank, Issue, IssueDraft, IssuePriority, Project } from '@hcengineering/tracker' import { calcRank, Issue, IssueDraft, IssuePriority, Project } from '@hcengineering/tracker'
import { addNotification, Button, Component, EditBox, deviceOptionsStore, ButtonSize } from '@hcengineering/ui' import { addNotification, Button, ButtonSize, Component, deviceOptionsStore, EditBox } from '@hcengineering/ui'
import { createEventDispatcher, onDestroy } from 'svelte' import { createEventDispatcher } from 'svelte'
import { generateIssueShortLink, getIssueId } from '../../../issues'
import tracker from '../../../plugin' import tracker from '../../../plugin'
import AssigneeEditor from '../AssigneeEditor.svelte' import AssigneeEditor from '../AssigneeEditor.svelte'
import IssueNotification from '../IssueNotification.svelte' import IssueNotification from '../IssueNotification.svelte'
import PriorityEditor from '../PriorityEditor.svelte' import PriorityEditor from '../PriorityEditor.svelte'
import StatusEditor from '../StatusEditor.svelte' import StatusEditor from '../StatusEditor.svelte'
import EstimationEditor from '../timereport/EstimationEditor.svelte' import EstimationEditor from '../timereport/EstimationEditor.svelte'
import { generateIssueShortLink, getIssueId } from '../../../issues' import { onDestroy } from 'svelte'
export let parentIssue: Issue export let parentIssue: Issue
export let currentProject: Project export let currentProject: Project
export let shouldSaveDraft: boolean = false export let shouldSaveDraft: boolean = false
const draftController = new DraftController<IssueDraft>(parentIssue._id) const draftController = new DraftController<IssueDraft>(`${parentIssue._id}_subIssue`)
const draft = shouldSaveDraft ? draftController.get() : undefined const draft = shouldSaveDraft ? draftController.get() : undefined
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const client = getClient() const client = getClient()
onDestroy(() => draftController.unsubscribe()) onDestroy(() => draftController.destroy())
let object = draft ?? getIssueDefaults() let object = draft ?? getIssueDefaults()
@ -77,9 +78,13 @@
sprint: parentIssue.sprint sprint: parentIssue.sprint
} }
function objectChange (object: IssueDraft, empty: any) {
if (shouldSaveDraft) { if (shouldSaveDraft) {
draftController.watch(object, empty) draftController.save(object, empty)
} }
}
$: objectChange(object, empty)
function resetToDefaults () { function resetToDefaults () {
object = getIssueDefaults() object = getIssueDefaults()

View File

@ -46,7 +46,7 @@
let subIssueEditorRef: HTMLDivElement let subIssueEditorRef: HTMLDivElement
let isCollapsed = false let isCollapsed = false
let isCreating = $draftsStore[issue._id] !== undefined let isCreating = $draftsStore[`${issue._id}_subIssue`] !== undefined
$: hasSubIssues = issue.subIssues > 0 $: hasSubIssues = issue.subIssues > 0

View File

@ -15,9 +15,9 @@
<script lang="ts"> <script lang="ts">
import { AttachmentStyledBox } from '@hcengineering/attachment-resources' import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
import { Account, Doc, generateId, Ref } from '@hcengineering/core' import { Account, Doc, generateId, Ref } from '@hcengineering/core'
import presentation, { DraftController, draftsStore, getClient, KeyedAttribute } from '@hcengineering/presentation' import presentation, { DraftController, getClient, KeyedAttribute } from '@hcengineering/presentation'
import tags, { TagElement, TagReference } from '@hcengineering/tags' import tags, { TagElement, TagReference } from '@hcengineering/tags'
import { Component as ComponentType, IssueDraft, IssuePriority, Project, Sprint } from '@hcengineering/tracker' import { Component as ComponentType, Issue, IssueDraft, IssuePriority, Project, Sprint } from '@hcengineering/tracker'
import { Button, Component, EditBox } from '@hcengineering/ui' import { Button, Component, EditBox } from '@hcengineering/ui'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin' import tracker from '../../plugin'
@ -25,7 +25,9 @@
import PriorityEditor from '../issues/PriorityEditor.svelte' import PriorityEditor from '../issues/PriorityEditor.svelte'
import StatusEditor from '../issues/StatusEditor.svelte' import StatusEditor from '../issues/StatusEditor.svelte'
import EstimationEditor from './EstimationEditor.svelte' import EstimationEditor from './EstimationEditor.svelte'
import { onDestroy } from 'svelte'
export let parendIssueId: Ref<Issue>
export let project: Project export let project: Project
export let sprint: Ref<Sprint> | null = null export let sprint: Ref<Sprint> | null = null
export let component: Ref<ComponentType> | null = null export let component: Ref<ComponentType> | null = null
@ -35,12 +37,14 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
const client = getClient() const client = getClient()
const draftController = new DraftController<IssueDraft>(tracker.ids.IssueDraftChild) const draftController = new DraftController<IssueDraft>(`${parendIssueId}_subIssue`)
const draft = shouldSaveDraft ? ($draftsStore[tracker.ids.IssueDraftChild] as IssueDraft) : undefined const draft = shouldSaveDraft ? draftController.get() : undefined
let object = childIssue !== undefined ? childIssue : draft ?? getIssueDefaults() let object = childIssue !== undefined ? childIssue : draft ?? getIssueDefaults()
let thisRef: HTMLDivElement let thisRef: HTMLDivElement
let focusIssueTitle: () => void let focusIssueTitle: () => void
onDestroy(() => draftController.destroy())
const key: KeyedAttribute = { const key: KeyedAttribute = {
key: 'labels', key: 'labels',
attr: client.getHierarchy().getAttribute(tracker.class.IssueTemplate, 'labels') attr: client.getHierarchy().getAttribute(tracker.class.IssueTemplate, 'labels')
@ -48,16 +52,6 @@
let descriptionBox: AttachmentStyledBox let descriptionBox: AttachmentStyledBox
function draftChange (draft: IssueDraft | undefined) {
if (draft === undefined) {
object = childIssue !== undefined ? childIssue : getIssueDefaults()
} else {
object = draft
descriptionBox?.setContent(object.description)
}
}
$: shouldSaveDraft && draftChange($draftsStore[tracker.ids.IssueDraftChild])
function getIssueDefaults (): IssueDraft { function getIssueDefaults (): IssueDraft {
return { return {
_id: generateId(), _id: generateId(),