TSK-831: Edit Title and Description inline (#2788)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-03-21 19:16:45 +07:00 committed by GitHub
parent d65cb70652
commit d770acb77a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 111 additions and 112 deletions

8
dev/upgrade.sh Executable file
View File

@ -0,0 +1,8 @@
docker run -ti -e SERVER_SECRET=secret \
-e MONGO_URL=mongodb://127.0.0.1:27017 \
-e TRANSACTOR_URL=ws://127.0.0.1:3333 \
-e MINIO_ENDPOINT=minio \
-e MINIO_ACCESS_KEY=minioadmin \
-e MINIO_SECRET_KEY=minioadmin \
--rm --network host \
hardcoreeng/tool node ./bundle upgrade

View File

@ -23,6 +23,7 @@
import attachment from '../plugin'
import { deleteFile, uploadFile } from '../utils'
import AttachmentPresenter from './AttachmentPresenter.svelte'
import AttachmentPreview from './AttachmentPreview.svelte'
export let objectId: Ref<Doc> | undefined = undefined
export let space: Ref<Space> | undefined = undefined
@ -38,6 +39,7 @@
export let fakeAttach: 'fake' | 'hidden' | 'normal' = 'normal'
export let refContainer: HTMLElement | undefined = undefined
export let shouldSaveDraft: boolean = false
export let useAttachmentPreview = false
const dispatch = createEventDispatcher()
@ -131,6 +133,8 @@
})
newAttachments.add(_id)
attachments = attachments
saved = false
dispatch('attached', _id)
saveDraft()
} catch (err: any) {
setPlatformStatus(unknownError(err))
@ -165,6 +169,7 @@
async function removeAttachment (attachment: Attachment): Promise<void> {
removedAttachments.add(attachment)
attachments.delete(attachment._id)
dispatch('detached', attachment._id)
attachments = attachments
saveDraft()
}
@ -179,6 +184,7 @@
attachment.attachedToClass,
'attachments'
)
dispatch('detached', attachment._id)
} else {
await deleteFile(attachment.file)
}
@ -209,7 +215,10 @@
}
}
export function createAttachments (): Promise<void> {
export async function createAttachments (): Promise<void> {
if (saved) {
return
}
saved = true
const promises: Promise<any>[] = []
newAttachments.forEach((p) => {
@ -221,7 +230,8 @@
removedAttachments.forEach((p) => {
promises.push(deleteAttachment(p))
})
return Promise.all(promises).then()
await Promise.all(promises)
saveDraft()
}
$: if (attachments.size || newAttachments.size || removedAttachments.size) {
@ -313,13 +323,17 @@
<div class="flex-row-center list scroll-divider-color">
{#each Array.from(attachments.values()) as attachment}
<div class="item flex">
<AttachmentPresenter
value={attachment}
removable
on:remove={(result) => {
if (result !== undefined) removeAttachment(attachment)
}}
/>
{#if useAttachmentPreview}
<AttachmentPreview value={attachment} />
{:else}
<AttachmentPresenter
value={attachment}
removable
on:remove={(result) => {
if (result !== undefined) removeAttachment(attachment)
}}
/>
{/if}
</div>
{/each}
</div>

View File

@ -284,7 +284,8 @@
"SevenHoursLength": "Seven Hours",
"EightHoursLength": "Eight Hours",
"CreatedOn": "Created on",
"HourLabel": "h"
"HourLabel": "h",
"Saved": "Saved..."
},
"status": {}
}

View File

@ -284,7 +284,8 @@
"SevenHoursLength": "Семь Часов",
"EightHoursLength": "Восемь Часов",
"CreatedOn": "Создана",
"HourLabel": "ч"
"HourLabel": "ч",
"Saved": "Сохранено..."
},
"status": {}
}

View File

@ -168,7 +168,7 @@
}
</script>
<div bind:this={thisRef} class="flex-col root">
<div id="sub-issue-child-editor" bind:this={thisRef} class="flex-col root">
<div class="flex-row-top">
<div id="status-editor" class="mr-1">
<StatusEditor

View File

@ -13,24 +13,22 @@
// limitations under the License.
-->
<script lang="ts">
import { AttachmentDocList, AttachmentStyledBox } from '@hcengineering/attachment-resources'
import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
import { Class, Data, Doc, Ref, SortingOrder, WithLookup } from '@hcengineering/core'
import notification from '@hcengineering/notification'
import { Panel } from '@hcengineering/panel'
import { getResource } from '@hcengineering/platform'
import presentation, { createQuery, getClient, MessageViewer } from '@hcengineering/presentation'
import { createQuery, getClient } from '@hcengineering/presentation'
import setting, { settingId } from '@hcengineering/setting'
import type { Issue, IssueStatus, Project } from '@hcengineering/tracker'
import {
Button,
EditBox,
getCurrentLocation,
IconEdit,
IconMixin,
IconMoreH,
Label,
navigate,
Scroller,
showPopup,
Spinner
} from '@hcengineering/ui'
@ -111,26 +109,8 @@
$: isDescriptionEmpty = !new DOMParser().parseFromString(description, 'text/html').documentElement.innerText?.trim()
$: parentIssue = issue?.$lookup?.attachedTo
function edit (ev: MouseEvent) {
ev.preventDefault()
isEditing = true
}
function cancelEditing (ev: MouseEvent) {
ev.preventDefault()
isEditing = false
if (issue) {
title = issue.title
description = issue.description
}
}
async function save (ev: MouseEvent) {
ev.preventDefault()
let saved = false
async function save () {
if (!issue || !canSave) {
return
}
@ -156,11 +136,21 @@
issue.collection,
updates
)
saved = true
setTimeout(() => {
saved = false
}, 5000)
}
await descriptionBox.createAttachments()
isEditing = false
}
let saveTrigger: any
function triggerSave (): void {
clearTimeout(saveTrigger)
saveTrigger = setTimeout(save, 5000)
}
function showMenu (ev?: Event): void {
if (issue) {
showPopup(ContextMenu, { object: issue }, (ev as MouseEvent).target as HTMLElement)
@ -178,7 +168,7 @@
isHeader
isAside={true}
isSub={false}
withoutActivity={isEditing}
withoutActivity={false}
withoutTitle
bind:innerWidth
on:close={() => dispatch('close')}
@ -192,80 +182,54 @@
</span>
</svelte:fragment>
<svelte:fragment slot="tools">
{#if isEditing}
<Button kind={'transparent'} label={presentation.string.Cancel} on:click={cancelEditing} />
<Button disabled={!canSave} label={presentation.string.Save} on:click={save} />
{:else}
<Button icon={IconEdit} kind={'transparent'} size={'medium'} on:click={edit} />
<Button icon={IconMoreH} kind={'transparent'} size={'medium'} on:click={showMenu} />
{#if saved}
<Label label={tracker.string.Saved} />
{/if}
<Button icon={IconMoreH} kind={'transparent'} size={'medium'} on:click={showMenu} />
</svelte:fragment>
{#if isEditing}
<Scroller>
<div class="popupPanel-body__main-content py-10 clear-mins content">
{#if parentIssue}
<div class="mb-6">
{#if currentProject && issueStatuses}
<SubIssueSelector {issue} />
{:else}
<Spinner />
{/if}
</div>
{/if}
<EditBox bind:value={title} placeholder={tracker.string.IssueTitlePlaceholder} kind="large-style" />
<div class="w-full mt-6">
<AttachmentStyledBox
bind:this={descriptionBox}
objectId={_id}
_class={tracker.class.Issue}
space={issue.space}
alwaysEdit
showButtons
maxHeight={'card'}
focusable
bind:content={description}
placeholder={tracker.string.IssueDescriptionPlaceholder}
/>
</div>
</div>
</Scroller>
{:else}
{#if parentIssue}
<div class="mb-6">
{#if currentProject && issueStatuses}
<SubIssueSelector {issue} />
{:else}
<Spinner />
{/if}
</div>
{/if}
<span class="title select-text">{title}</span>
<div class="mt-6 description-preview select-text">
{#if isDescriptionEmpty}
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div class="placeholder" on:click={edit}>
<Label label={tracker.string.IssueDescriptionPlaceholder} />
</div>
{#if parentIssue}
<div class="mb-6">
{#if currentProject && issueStatuses}
<SubIssueSelector {issue} />
{:else}
<MessageViewer message={description} />
<Spinner />
{/if}
</div>
<div class="mt-6">
{#key issue._id && currentProject !== undefined}
{#if currentProject !== undefined && issueStatuses !== undefined}
<SubIssues
{issue}
issueStatuses={new Map([[currentProject._id, issueStatuses]])}
projects={new Map([[currentProject?._id, currentProject]])}
/>
{/if}
{/key}
</div>
<div class="mt-6">
<AttachmentDocList value={issue} />
</div>
{/if}
<EditBox bind:value={title} placeholder={tracker.string.IssueTitlePlaceholder} kind="large-style" on:blur={save} />
<div class="w-full mt-6">
<AttachmentStyledBox
bind:this={descriptionBox}
useAttachmentPreview={true}
objectId={_id}
_class={tracker.class.Issue}
space={issue.space}
alwaysEdit
shouldSaveDraft={false}
on:attached={save}
on:detached={save}
showButtons
on:blur={save}
on:changeContent={triggerSave}
maxHeight={'card'}
focusable
bind:content={description}
placeholder={tracker.string.IssueDescriptionPlaceholder}
/>
</div>
<div class="mt-6">
{#key issue._id && currentProject !== undefined}
{#if currentProject !== undefined && issueStatuses !== undefined}
<SubIssues
{issue}
issueStatuses={new Map([[currentProject._id, issueStatuses]])}
projects={new Map([[currentProject?._id, currentProject]])}
/>
{/if}
{/key}
</div>
<span slot="actions-label" class="select-text">
{#if issueId}{issueId}{/if}

View File

@ -117,7 +117,12 @@
$: labelRefs = labels.map((it) => ({ ...(it as unknown as TagReference), _id: generateId(), tag: it._id }))
</script>
<div bind:this={thisRef} class="flex-col antiEmphasized clear-mins" class:antiPopup={showBorder}>
<div
id="sub-issue-child-editor"
bind:this={thisRef}
class="flex-col antiEmphasized clear-mins"
class:antiPopup={showBorder}
>
<div class="flex-col w-full clear-mins">
<EditBox
bind:value={newIssue.title}

View File

@ -304,7 +304,8 @@ export default mergeIds(trackerId, tracker, {
WorkDayLength: '' as IntlString,
SevenHoursLength: '' as IntlString,
EightHoursLength: '' as IntlString,
HourLabel: '' as IntlString
HourLabel: '' as IntlString,
Saved: '' as IntlString
},
component: {
NopeComponent: '' as AnyComponent,

View File

@ -17,4 +17,4 @@
export let value: number | undefined
</script>
<span>{value ? value : ''}</span>
<span>{value || ''}</span>

View File

@ -1,4 +1,4 @@
import { Page, expect } from '@playwright/test'
import { expect, Page } from '@playwright/test'
import { PlatformURI } from './utils'
export interface IssueProps {
@ -45,12 +45,17 @@ export async function setViewOrder (page: Page, orderName: string): Promise<void
await page.keyboard.press('Escape')
}
export async function fillIssueForm (page: Page, props: IssueProps, addForm: boolean): Promise<void> {
export async function fillIssueForm (page: Page, props: IssueProps, issue: boolean): Promise<void> {
const { name, description, status, assignee, labels, priority, component, sprint } = props
const af = addForm ? 'form ' : ''
await page.fill(af + '[placeholder="Issue\\ title"]', name)
const af = issue ? 'form ' : '[id="sub-issue-child-editor"] '
const issueTitle = page.locator(af + '[placeholder="Issue\\ title"]')
await issueTitle.fill(name)
await issueTitle.evaluate((e) => e.blur())
if (description !== undefined) {
await page.fill('.ProseMirror', description)
const pm = await page.locator(af + '.ProseMirror')
await pm.fill(description)
await pm.evaluate((e) => e.blur())
}
if (status !== undefined) {
await page.click(af + '#status-editor')