mirror of
https://github.com/hcengineering/platform.git
synced 2024-11-22 03:14:40 +03:00
TSK-831: Edit Title and Description inline (#2788)
Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
parent
d65cb70652
commit
d770acb77a
8
dev/upgrade.sh
Executable file
8
dev/upgrade.sh
Executable 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
|
@ -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>
|
||||
|
@ -284,7 +284,8 @@
|
||||
"SevenHoursLength": "Seven Hours",
|
||||
"EightHoursLength": "Eight Hours",
|
||||
"CreatedOn": "Created on",
|
||||
"HourLabel": "h"
|
||||
"HourLabel": "h",
|
||||
"Saved": "Saved..."
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
@ -284,7 +284,8 @@
|
||||
"SevenHoursLength": "Семь Часов",
|
||||
"EightHoursLength": "Восемь Часов",
|
||||
"CreatedOn": "Создана",
|
||||
"HourLabel": "ч"
|
||||
"HourLabel": "ч",
|
||||
"Saved": "Сохранено..."
|
||||
},
|
||||
"status": {}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
|
@ -17,4 +17,4 @@
|
||||
export let value: number | undefined
|
||||
</script>
|
||||
|
||||
<span>{value ? value : ''}</span>
|
||||
<span>{value || ''}</span>
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user