mirror of
https://github.com/hcengineering/platform.git
synced 2024-12-22 19:11:33 +03:00
TSK-1335 Improve draft (#3092)
Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
parent
97396cc5f2
commit
be1872abe5
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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')
|
||||||
|
@ -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')
|
||||||
|
@ -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)
|
||||||
|
@ -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%'}
|
||||||
|
@ -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}
|
||||||
|
@ -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)
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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(),
|
||||||
|
Loading…
Reference in New Issue
Block a user