Draft tabs sync (#3030)

Signed-off-by: Denis Bykhov <bykhov.denis@gmail.com>
This commit is contained in:
Denis Bykhov 2023-04-21 08:50:32 +06:00 committed by GitHub
parent 67d361d856
commit 01afbab81d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 104 additions and 46 deletions

View File

@ -68,7 +68,6 @@
let refInput: StyledTextBox let refInput: StyledTextBox
let inputFile: HTMLInputElement let inputFile: HTMLInputElement
let draftAttachments: Record<Ref<Attachment>, Attachment> | undefined = undefined
let saved = false let saved = false
const client = getClient() const client = getClient()
@ -78,11 +77,14 @@
const newAttachments: Set<Ref<Attachment>> = new Set<Ref<Attachment>>() const newAttachments: Set<Ref<Attachment>> = new Set<Ref<Attachment>>()
const removedAttachments: Set<Attachment> = new Set<Attachment>() const removedAttachments: Set<Attachment> = new Set<Attachment>()
$: objectId && draftKey && updateAttachments(objectId, draftKey) $: draftKey && updateAttachments(objectId, $draftsStore[draftKey])
async function updateAttachments (objectId: Ref<Doc>, draftKey: string) { async function updateAttachments (
draftAttachments = $draftsStore[draftKey] objectId: Ref<Doc> | undefined,
draftAttachments: Record<Ref<Attachment>, Attachment> | undefined
) {
if (draftAttachments && shouldSaveDraft) { if (draftAttachments && shouldSaveDraft) {
query.unsubscribe()
attachments.clear() attachments.clear()
newAttachments.clear() newAttachments.clear()
Object.entries(draftAttachments).map((file) => { Object.entries(draftAttachments).map((file) => {
@ -93,7 +95,8 @@
}) })
originalAttachments.clear() originalAttachments.clear()
removedAttachments.clear() removedAttachments.clear()
} else { attachments = attachments
} else if (objectId) {
query.query( query.query(
attachment.class.Attachment, attachment.class.Attachment,
{ {
@ -102,6 +105,7 @@
(res) => { (res) => {
originalAttachments = new Set(res.map((p) => p._id)) originalAttachments = new Set(res.map((p) => p._id))
attachments = toIdMap(res) attachments = toIdMap(res)
dispatch('attach', { action: 'saved', value: attachments.size })
} }
) )
} }
@ -109,7 +113,7 @@
async function saveDraft () { async function saveDraft () {
if (draftKey && shouldSaveDraft) { if (draftKey && shouldSaveDraft) {
draftAttachments = Object.fromEntries(attachments) const draftAttachments = Object.fromEntries(attachments)
DraftController.save(draftKey, draftAttachments) DraftController.save(draftKey, draftAttachments)
} }
} }
@ -138,6 +142,7 @@
attachments = attachments attachments = attachments
saved = false saved = false
saveDraft() saveDraft()
dispatch('attach', { action: 'saved', value: attachments.size })
dispatch('attached', _id) dispatch('attached', _id)
} catch (err: any) { } catch (err: any) {
setPlatformStatus(unknownError(err)) setPlatformStatus(unknownError(err))
@ -255,10 +260,6 @@
removedAttachments.clear() removedAttachments.clear()
} }
$: if (attachments.size || newAttachments.size || removedAttachments.size) {
dispatch('attach', { action: 'saved', value: attachments.size })
}
function isAllowedPaste (evt: ClipboardEvent) { function isAllowedPaste (evt: ClipboardEvent) {
let t: HTMLElement | null = evt.target as HTMLElement let t: HTMLElement | null = evt.target as HTMLElement

View File

@ -50,15 +50,15 @@
Component, Component,
createFocusManager, createFocusManager,
EditBox, EditBox,
IconFile as FileIcon,
FocusHandler, FocusHandler,
getColorNumberByText, getColorNumberByText,
IconFile as FileIcon,
IconInfo, IconInfo,
Label, Label,
showPopup, showPopup,
Spinner Spinner
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { createEventDispatcher, onDestroy } from 'svelte' import { createEventDispatcher } from 'svelte'
import recruit from '../plugin' import recruit from '../plugin'
import FileUpload from './icons/FileUpload.svelte' import FileUpload from './icons/FileUpload.svelte'
import YesNo from './YesNo.svelte' import YesNo from './YesNo.svelte'
@ -81,10 +81,29 @@
const empty = {} const empty = {}
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
const ignoreKeys = ['onsite', 'remote'] const ignoreKeys = ['onsite', 'remote', 'title']
const draft = shouldSaveDraft ? draftController.get() : undefined let draft = shouldSaveDraft ? ($draftsStore[recruit.mixin.Candidate] as CandidateDraft) : undefined
$: draft = shouldSaveDraft ? ($draftsStore[recruit.mixin.Candidate] as CandidateDraft) : undefined
let object = draft ?? getEmptyCandidate() let object = draft ?? getEmptyCandidate()
function draftChange (draft: CandidateDraft | undefined) {
if (draft === undefined) {
object = getEmptyCandidate()
} else {
object = draft
}
}
function objectChange (object: CandidateDraft, empty: any) {
if (shouldSaveDraft) {
draftController.save(object, empty)
}
}
$: objectChange(object, empty)
$: draftChange(draft)
type resumeFile = { type resumeFile = {
name: string name: string
uuid: string uuid: string
@ -113,12 +132,6 @@
} }
} }
if (shouldSaveDraft) {
draftController.watch(object, empty)
}
onDestroy(() => draftController.unsubscribe())
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let inputFile: HTMLInputElement let inputFile: HTMLInputElement
@ -643,6 +656,9 @@
toClass={contact.class.Contact} toClass={contact.class.Contact}
{ignoreKeys} {ignoreKeys}
extraProps={{ showNavigate: false }} extraProps={{ showNavigate: false }}
on:update={() => {
object = object
}}
/> />
</div> </div>
</svelte:fragment> </svelte:fragment>

View File

@ -55,7 +55,6 @@
} 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 { onDestroy } from 'svelte'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } 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'
@ -84,7 +83,8 @@
const draftController = new DraftController<any>(tracker.ids.IssueDraft) const draftController = new DraftController<any>(tracker.ids.IssueDraft)
const draft: IssueDraft | undefined = shouldSaveDraft ? draftController.get() : undefined let draft = shouldSaveDraft ? ($draftsStore[tracker.ids.IssueDraft] as IssueDraft) : undefined
$: draft = shouldSaveDraft ? ($draftsStore[tracker.ids.IssueDraft] as IssueDraft) : undefined
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
const parentQuery = createQuery() const parentQuery = createQuery()
@ -92,6 +92,24 @@
let object = draft ?? getDefaultObject() let object = draft ?? getDefaultObject()
function draftChange (draft: IssueDraft | undefined) {
if (draft === undefined) {
object = getDefaultObject()
} else {
object = draft
descriptionBox?.setContent(object.description)
}
}
function objectChange (object: IssueDraft, empty: any) {
if (shouldSaveDraft) {
draftController.save(object, empty)
}
}
$: objectChange(object, empty)
$: draftChange(draft)
$: if (object.parentIssue) { $: if (object.parentIssue) {
parentQuery.query( parentQuery.query(
tracker.class.Issue, tracker.class.Issue,
@ -297,12 +315,6 @@
} }
} }
$: watch(empty)
function watch (empty: Record<string, any>): void {
if (!shouldSaveDraft) return
draftController.watch(object, empty)
}
async function createIssue () { async function createIssue () {
const _id: Ref<Issue> = generateId() const _id: Ref<Issue> = generateId()
if (!canSave || object.status === undefined) { if (!canSave || object.status === undefined) {
@ -390,6 +402,7 @@
draftController.remove() draftController.remove()
resetObject() resetObject()
descriptionBox?.removeDraft(false) descriptionBox?.removeDraft(false)
subIssuesComponent.removeChildDraft()
} }
async function showMoreActions (ev: Event) { async function showMoreActions (ev: Event) {
@ -503,6 +516,7 @@
if (result === true) { if (result === true) {
dispatch('close') dispatch('close')
resetObject() resetObject()
subIssuesComponent.removeChildDraft()
draftController.remove() draftController.remove()
descriptionBox?.removeDraft(true) descriptionBox?.removeDraft(true)
} }
@ -511,8 +525,6 @@
} }
} }
onDestroy(() => draftController.unsubscribe())
$: objectId = object._id $: objectId = object._id
</script> </script>

View File

@ -36,7 +36,9 @@
let lastProject = project let lastProject = project
let isCollapsed = false let isCollapsed = false
let isCreating = $draftsStore[tracker.ids.IssueDraftChild] !== undefined $: isCreatingMode = $draftsStore[tracker.ids.IssueDraftChild] !== undefined
let isManualCreating = false
$: isCreating = isCreatingMode || isManualCreating
async function handleIssueSwap (ev: CustomEvent<{ fromIndex: number; toIndex: number }>) { async function handleIssueSwap (ev: CustomEvent<{ fromIndex: number; toIndex: number }>) {
if (subIssues) { if (subIssues) {
@ -170,7 +172,13 @@
} }
} }
export function removeChildDraft () {
draftChild?.removeDraft()
}
$: hasSubIssues = subIssues.length > 0 $: hasSubIssues = subIssues.length > 0
let draftChild: DraftIssueChildEditor
</script> </script>
<div class="flex-between clear-mins"> <div class="flex-between clear-mins">
@ -200,7 +208,7 @@
showTooltip={{ label: tracker.string.AddSubIssues, props: { subIssues: 1 } }} showTooltip={{ label: tracker.string.AddSubIssues, props: { subIssues: 1 } }}
on:click={() => { on:click={() => {
closeTooltip() closeTooltip()
isCreating = true isManualCreating = true
isCollapsed = false isCollapsed = false
}} }}
/> />
@ -224,12 +232,13 @@
{#if isCreating && project} {#if isCreating && project}
<ExpandCollapse isExpanded={!isCollapsed} on:changeContent> <ExpandCollapse isExpanded={!isCollapsed} on:changeContent>
<DraftIssueChildEditor <DraftIssueChildEditor
bind:this={draftChild}
{project} {project}
{component} {component}
{sprint} {sprint}
{shouldSaveDraft} {shouldSaveDraft}
on:close={() => { on:close={() => {
isCreating = false isManualCreating = false
}} }}
on:create={(evt) => { on:create={(evt) => {
if (subIssues === undefined) { if (subIssues === undefined) {

View File

@ -13,8 +13,8 @@
// limitations under the License. // limitations under the License.
--> -->
<script lang="ts"> <script lang="ts">
import contact, { Employee, EmployeeAccount } from '@hcengineering/contact' import { Employee, EmployeeAccount } from '@hcengineering/contact'
import { AssigneeBox } from '@hcengineering/contact-resources' import { AssigneeBox, employeeAccountByIdStore } from '@hcengineering/contact-resources'
import { AttachedData, Ref } from '@hcengineering/core' import { AttachedData, Ref } from '@hcengineering/core'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { Issue, IssueDraft, IssueTemplateData } from '@hcengineering/tracker' import { Issue, IssueDraft, IssueTemplateData } from '@hcengineering/tracker'
@ -58,9 +58,9 @@
if (hasSpace(issue)) { if (hasSpace(issue)) {
const project = await client.findOne(tracker.class.Project, { _id: issue.space }) const project = await client.findOne(tracker.class.Project, { _id: issue.space })
if (project !== undefined) { if (project !== undefined) {
const accounts = await client.findAll(contact.class.EmployeeAccount, { const accounts = project.members
_id: { $in: project.members as Ref<EmployeeAccount>[] } .map((p) => $employeeAccountByIdStore.get(p as Ref<EmployeeAccount>))
}) .filter((p) => p !== undefined) as EmployeeAccount[]
members = accounts.map((p) => p.employee) members = accounts.map((p) => p.employee)
} else { } else {
members = [] members = []

View File

@ -15,11 +15,11 @@
<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, getClient, KeyedAttribute } from '@hcengineering/presentation' import presentation, { DraftController, draftsStore, 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, IssueDraft, IssuePriority, Project, Sprint } from '@hcengineering/tracker'
import { Button, Component, EditBox } from '@hcengineering/ui' import { Button, Component, EditBox } from '@hcengineering/ui'
import { createEventDispatcher, onDestroy } from 'svelte' import { createEventDispatcher } from 'svelte'
import tracker from '../../plugin' import tracker from '../../plugin'
import AssigneeEditor from '../issues/AssigneeEditor.svelte' import AssigneeEditor from '../issues/AssigneeEditor.svelte'
import PriorityEditor from '../issues/PriorityEditor.svelte' import PriorityEditor from '../issues/PriorityEditor.svelte'
@ -36,17 +36,28 @@
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>(tracker.ids.IssueDraftChild)
const draft = shouldSaveDraft ? draftController.get() : undefined const draft = shouldSaveDraft ? ($draftsStore[tracker.ids.IssueDraftChild] as IssueDraft) : 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.unsubscribe())
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')
} }
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(),
@ -75,10 +86,14 @@
sprint sprint
} }
if (shouldSaveDraft) { function objectChange (object: IssueDraft, empty: any) {
draftController.watch(object, empty) if (shouldSaveDraft) {
draftController.save(object, empty)
}
} }
$: objectChange(object, empty)
function resetToDefaults () { function resetToDefaults () {
object = getIssueDefaults() object = getIssueDefaults()
focusIssueTitle?.() focusIssueTitle?.()
@ -88,8 +103,12 @@
return value.trim() return value.trim()
} }
function close () { export function removeDraft () {
draftController.remove() draftController.remove()
}
function close () {
removeDraft()
dispatch('close') dispatch('close')
} }
@ -100,7 +119,7 @@
dispatch(childIssue ? 'close' : 'create', object) dispatch(childIssue ? 'close' : 'create', object)
draftController.remove() removeDraft()
resetToDefaults() resetToDefaults()
} }
@ -149,6 +168,7 @@
<div class="mt-4 clear-mins" id="sub-issue-description"> <div class="mt-4 clear-mins" id="sub-issue-description">
{#key objectId} {#key objectId}
<AttachmentStyledBox <AttachmentStyledBox
bind:this={descriptionBox}
objectId={object._id} objectId={object._id}
space={project._id} space={project._id}
_class={tracker.class.Issue} _class={tracker.class.Issue}