TSK-831: Edit issue fixes (#3140)

Signed-off-by: Andrey Sobolev <haiodo@gmail.com>
This commit is contained in:
Andrey Sobolev 2023-05-04 20:21:06 +07:00 committed by GitHub
parent f5bd3edc48
commit c606a037f1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 151 additions and 30 deletions

View File

@ -95,7 +95,7 @@
</svelte:fragment> </svelte:fragment>
<svelte:fragment slot="utils"> <svelte:fragment slot="utils">
<Component is={calendar.component.DocReminder} props={{ value: object, title }} /> <Component is={calendar.component.DocReminder} props={{ value: object, title, focusIndex: 9000 }} />
{#if isUtils && $$slots.utils} {#if isUtils && $$slots.utils}
<div class="buttons-divider" /> <div class="buttons-divider" />
<slot name="utils" /> <slot name="utils" />
@ -145,7 +145,7 @@
{#if !withoutActivity} {#if !withoutActivity}
<Component <Component
is={activity.component.Activity} is={activity.component.Activity}
props={{ object, showCommenInput: !withoutInput, shouldScroll: embedded }} props={{ object, showCommenInput: !withoutInput, shouldScroll: embedded, focusIndex: 1000 }}
/> />
{/if} {/if}
</div> </div>
@ -156,7 +156,7 @@
{#if !withoutActivity} {#if !withoutActivity}
<Component <Component
is={activity.component.Activity} is={activity.component.Activity}
props={{ object, showCommenInput: !withoutInput, shouldScroll: embedded }} props={{ object, showCommenInput: !withoutInput, shouldScroll: embedded, focusIndex: 1000 }}
/> />
{/if} {/if}
</div> </div>

View File

@ -15,8 +15,18 @@
<script lang="ts"> <script lang="ts">
import { Asset, IntlString, getResource } from '@hcengineering/platform' import { Asset, IntlString, getResource } from '@hcengineering/platform'
import { getClient } from '@hcengineering/presentation' import { getClient } from '@hcengineering/presentation'
import { AnySvelteComponent, IconEmoji } from '@hcengineering/ui' import {
import { Button, EmojiPopup, Icon, Spinner, handler, showPopup, tooltip } from '@hcengineering/ui' AnySvelteComponent,
Button,
EmojiPopup,
Icon,
IconEmoji,
Spinner,
handler,
registerFocus,
showPopup,
tooltip
} from '@hcengineering/ui'
import { AnyExtension } from '@tiptap/core' import { AnyExtension } from '@tiptap/core'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { Completion } from '../Completion' import { Completion } from '../Completion'
@ -50,6 +60,7 @@
export let placeholder: IntlString | undefined = undefined export let placeholder: IntlString | undefined = undefined
export let extraActions: RefAction[] | undefined = undefined export let extraActions: RefAction[] | undefined = undefined
export let loading: boolean = false export let loading: boolean = false
const client = getClient() const client = getClient()
let textEditor: TextEditor let textEditor: TextEditor
@ -196,6 +207,24 @@
} }
}) })
} }
// Focusable control with index
let focused = false
export let focusIndex = -1
const { idx, focusManager } = registerFocus(focusIndex, {
focus: () => {
focused = true
textEditor.focus()
return textEditor.isEditable()
},
isFocus: () => focused
})
const updateFocus = () => {
if (focusIndex !== -1) {
console.trace('focuse')
focusManager?.setFocus(idx)
}
}
</script> </script>
<div class="ref-container"> <div class="ref-container">
@ -292,6 +321,13 @@
textEditor.clear() textEditor.clear()
} }
}} }}
on:on:blur={() => {
focused = false
}}
on:focus={() => {
focused = true
updateFocus()
}}
extensions={editorExtensions} extensions={editorExtensions}
on:selection-update={updateFormattingState} on:selection-update={updateFormattingState}
on:update on:update

View File

@ -86,6 +86,9 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
let focused = false let focused = false
export function isFocused (): boolean {
return focused
}
let needFocus = false let needFocus = false
$: if (textEditor && needFocus) { $: if (textEditor && needFocus) {
@ -101,8 +104,9 @@
export let focusIndex = -1 export let focusIndex = -1
const { idx, focusManager } = registerFocus(focusIndex, { const { idx, focusManager } = registerFocus(focusIndex, {
focus: () => { focus: () => {
focused = true
focus() focus()
return false return textEditor.isEditable()
}, },
isFocus: () => focused isFocus: () => focused
}) })

View File

@ -14,12 +14,12 @@
// limitations under the License. // limitations under the License.
// //
import { SvelteComponent } from 'svelte' import { ComponentType, SvelteComponent } from 'svelte'
export class SvelteRenderer { export class SvelteRenderer {
private readonly component: SvelteComponent private readonly component: SvelteComponent
constructor (comp: typeof SvelteComponent, props: any) { constructor (comp: typeof SvelteComponent | ComponentType, props: any) {
const options = { target: document.body, props } const options = { target: document.body, props }
this.component = new (comp as any)(options) this.component = new (comp as any)(options)
} }

View File

@ -16,15 +16,15 @@
<script lang="ts"> <script lang="ts">
import { IntlString, translate } from '@hcengineering/platform' import { IntlString, translate } from '@hcengineering/platform'
import { AnyExtension, Editor, Extension, HTMLContent } from '@tiptap/core'
import type { FocusPosition } from '@tiptap/core' import type { FocusPosition } from '@tiptap/core'
import { AnyExtension, Editor, Extension, HTMLContent } from '@tiptap/core'
// import Typography from '@tiptap/extension-typography' // import Typography from '@tiptap/extension-typography'
import { Level } from '@tiptap/extension-heading'
import Placeholder from '@tiptap/extension-placeholder' import Placeholder from '@tiptap/extension-placeholder'
import { createEventDispatcher, onDestroy, onMount } from 'svelte' import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import textEditorPlugin from '../plugin' import textEditorPlugin from '../plugin'
import { FormatMode } from '../types' import { FormatMode } from '../types'
import { defaultExtensions } from './extensions' import { defaultExtensions } from './extensions'
import { Level } from '@tiptap/extension-heading'
export let content: string = '' export let content: string = ''
export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder export let placeholder: IntlString = textEditorPlugin.string.EditorPlaceholder

View File

@ -14,16 +14,16 @@
--> -->
<script lang="ts"> <script lang="ts">
import { afterUpdate, createEventDispatcher } from 'svelte' import { afterUpdate, createEventDispatcher } from 'svelte'
import { deviceOptionsStore as deviceInfo } from '../../'
import { resizeObserver } from '../resize' import { resizeObserver } from '../resize'
import Button from './Button.svelte' import Button from './Button.svelte'
import Scroller from './Scroller.svelte'
import IconClose from './icons/Close.svelte' import IconClose from './icons/Close.svelte'
import IconDetails from './icons/Details.svelte' import IconDetails from './icons/Details.svelte'
import IconMaxWidth from './icons/MaxWidth.svelte'
import IconMinWidth from './icons/MinWidth.svelte'
import IconScale from './icons/Scale.svelte' import IconScale from './icons/Scale.svelte'
import IconScaleFull from './icons/ScaleFull.svelte' import IconScaleFull from './icons/ScaleFull.svelte'
import IconMinWidth from './icons/MinWidth.svelte'
import IconMaxWidth from './icons/MaxWidth.svelte'
import Scroller from './Scroller.svelte'
import { deviceOptionsStore as deviceInfo } from '../../'
export let innerWidth: number = 0 export let innerWidth: number = 0
export let panelWidth: number = 0 export let panelWidth: number = 0
@ -93,6 +93,7 @@
<div class="popupPanel-title {twoRows && !withoutTitle ? 'row-top' : 'row'}"> <div class="popupPanel-title {twoRows && !withoutTitle ? 'row-top' : 'row'}">
{#if allowClose && !embedded} {#if allowClose && !embedded}
<Button <Button
focusIndex={10000}
icon={IconClose} icon={IconClose}
kind={'transparent'} kind={'transparent'}
size={'medium'} size={'medium'}
@ -114,6 +115,7 @@
{/if} {/if}
{#if $$slots.aside && isAside} {#if $$slots.aside && isAside}
<Button <Button
focusIndex={10008}
icon={IconDetails} icon={IconDetails}
kind={'transparent'} kind={'transparent'}
size={'medium'} size={'medium'}
@ -125,6 +127,7 @@
{/if} {/if}
{#if useMaxWidth !== undefined} {#if useMaxWidth !== undefined}
<Button <Button
focusIndex={10009}
icon={useMaxWidth ? IconMaxWidth : IconMinWidth} icon={useMaxWidth ? IconMaxWidth : IconMinWidth}
kind={'transparent'} kind={'transparent'}
size={'medium'} size={'medium'}
@ -137,6 +140,7 @@
{/if} {/if}
{#if isFullSize} {#if isFullSize}
<Button <Button
focusIndex={100010}
icon={fullSize ? IconScale : IconScaleFull} icon={fullSize ? IconScale : IconScaleFull}
kind={'transparent'} kind={'transparent'}
size={'medium'} size={'medium'}

View File

@ -29,6 +29,7 @@
export let showCommenInput: boolean = true export let showCommenInput: boolean = true
export let transparent: boolean = false export let transparent: boolean = false
export let shouldScroll: boolean = false export let shouldScroll: boolean = false
export let focusIndex: number = -1
getResource(notification.function.GetNotificationClient).then((res) => { getResource(notification.function.GetNotificationClient).then((res) => {
updatesStore = res().docUpdatesStore updatesStore = res().docUpdatesStore
@ -134,7 +135,7 @@
</div> </div>
{#if showCommenInput} {#if showCommenInput}
<div class="ref-input"> <div class="ref-input">
<Component is={chunter.component.CommentInput} props={{ object }} /> <Component is={chunter.component.CommentInput} props={{ object, focusIndex }} />
</div> </div>
{/if} {/if}

View File

@ -35,6 +35,7 @@
export let shouldSaveDraft: boolean = false export let shouldSaveDraft: boolean = false
export let attachments: IdMap<Attachment> = new Map() export let attachments: IdMap<Attachment> = new Map()
export let loading = false export let loading = false
export let focusIndex: number = -1
export function submit (): void { export function submit (): void {
refInput.submit() refInput.submit()
} }
@ -278,6 +279,7 @@
</div> </div>
{/if} {/if}
<ReferenceInput <ReferenceInput
{focusIndex}
bind:this={refInput} bind:this={refInput}
{content} {content}
{iconSend} {iconSend}

View File

@ -40,7 +40,7 @@
export let refContainer: HTMLElement | undefined = undefined export let refContainer: HTMLElement | undefined = undefined
export let shouldSaveDraft: boolean = false export let shouldSaveDraft: boolean = false
export let useAttachmentPreview = false export let useAttachmentPreview = false
export let focusIndex: number | undefined = undefined export let focusIndex: number | undefined = -1
let draftKey = objectId ? `${objectId}_attachments` : undefined let draftKey = objectId ? `${objectId}_attachments` : undefined
$: draftKey = objectId ? `${objectId}_attachments` : undefined $: draftKey = objectId ? `${objectId}_attachments` : undefined
@ -50,6 +50,9 @@
export function focus (): void { export function focus (): void {
refInput.focus() refInput.focus()
} }
export function isFocused (): boolean {
return refInput.isFocused()
}
export function isEditable (): boolean { export function isEditable (): boolean {
return refInput.isEditable() return refInput.isEditable()
} }

View File

@ -23,6 +23,7 @@
export let object: Doc export let object: Doc
export let shouldSaveDraft: boolean = true export let shouldSaveDraft: boolean = true
export let focusIndex: number = -1
const client = getClient() const client = getClient()
const _class = chunter.class.Comment const _class = chunter.class.Comment
@ -108,6 +109,7 @@
</script> </script>
<AttachmentRefInput <AttachmentRefInput
{focusIndex}
bind:this={commentInputBox} bind:this={commentInputBox}
bind:content={inputContent} bind:content={inputContent}
{_class} {_class}

View File

@ -24,15 +24,17 @@
import { import {
Button, Button,
EditBox, EditBox,
FocusHandler,
IconMixin, IconMixin,
IconMoreH, IconMoreH,
Label, Label,
Spinner, Spinner,
createFocusManager,
getCurrentResolvedLocation, getCurrentResolvedLocation,
navigate, navigate,
showPopup showPopup
} from '@hcengineering/ui' } from '@hcengineering/ui'
import { ContextMenu, DocNavLink, UpDownNavigator } from '@hcengineering/view-resources' import { ActionContext, ContextMenu, DocNavLink, UpDownNavigator } from '@hcengineering/view-resources'
import { createEventDispatcher, onDestroy, onMount } from 'svelte' import { createEventDispatcher, onDestroy, onMount } from 'svelte'
import { generateIssueShortLink, getIssueId } from '../../../issues' import { generateIssueShortLink, getIssueId } from '../../../issues'
import tracker from '../../../plugin' import tracker from '../../../plugin'
@ -139,8 +141,23 @@
onMount(() => { onMount(() => {
dispatch('open', { ignoreKeys: ['comments', 'name', 'description', 'number'] }) dispatch('open', { ignoreKeys: ['comments', 'name', 'description', 'number'] })
}) })
const manager = createFocusManager()
export function canClose (): boolean {
if (descriptionBox.isFocused()) {
return false
}
return true
}
</script> </script>
<FocusHandler {manager} />
<ActionContext
context={{
mode: 'editor'
}}
/>
{#if issue !== undefined} {#if issue !== undefined}
<Panel <Panel
object={issue} object={issue}
@ -181,10 +198,17 @@
{/if} {/if}
</div> </div>
{/if} {/if}
<EditBox bind:value={title} placeholder={tracker.string.IssueTitlePlaceholder} kind="large-style" on:blur={save} /> <EditBox
focusIndex={1}
bind:value={title}
placeholder={tracker.string.IssueTitlePlaceholder}
kind="large-style"
on:blur={save}
/>
<div class="w-full mt-6"> <div class="w-full mt-6">
{#key issue._id} {#key issue._id}
<AttachmentStyledBox <AttachmentStyledBox
focusIndex={30}
bind:this={descriptionBox} bind:this={descriptionBox}
useAttachmentPreview={true} useAttachmentPreview={true}
objectId={_id} objectId={_id}
@ -207,7 +231,12 @@
<div class="mt-6"> <div class="mt-6">
{#key issue._id && currentProject !== undefined} {#key issue._id && currentProject !== undefined}
{#if currentProject !== undefined} {#if currentProject !== undefined}
<SubIssues {issue} shouldSaveDraft projects={new Map([[currentProject?._id, currentProject]])} /> <SubIssues
focusIndex={50}
{issue}
shouldSaveDraft
projects={new Map([[currentProject?._id, currentProject]])}
/>
{/if} {/if}
{/key} {/key}
</div> </div>

View File

@ -15,6 +15,7 @@
<script lang="ts"> <script lang="ts">
import { Doc, DocumentQuery, Ref } from '@hcengineering/core' import { Doc, DocumentQuery, Ref } from '@hcengineering/core'
import { Issue, Project } from '@hcengineering/tracker' import { Issue, Project } from '@hcengineering/tracker'
import { registerFocus } from '@hcengineering/ui'
import { ViewOptions, Viewlet } from '@hcengineering/view' import { ViewOptions, Viewlet } from '@hcengineering/view'
import { import {
ActionContext, ActionContext,
@ -42,6 +43,25 @@
list?.select(offset, of) list?.select(offset, of)
} }
}) })
let docs: Doc[] = []
function select () {
listProvider.update(docs)
listProvider.updateFocus(docs[0])
list?.select(0, undefined)
}
// Focusable control with index
let focused = false
export let focusIndex = -1
registerFocus(focusIndex, {
focus: () => {
;(window.document.activeElement as HTMLElement).blur()
focused = true
select()
return true
},
isFocus: () => focused
})
</script> </script>
<ActionContext <ActionContext
@ -70,6 +90,7 @@
listProvider.updateSelection(event.detail.docs, event.detail.value) listProvider.updateSelection(event.detail.docs, event.detail.value)
}} }}
on:content={(evt) => { on:content={(evt) => {
docs = evt.detail
listProvider.update(evt.detail) listProvider.update(evt.detail)
}} }}
/> />

View File

@ -70,6 +70,8 @@
} }
$: viewOptions = viewlet !== undefined ? getViewOptions(viewlet, $viewOptionStore) : undefined $: viewOptions = viewlet !== undefined ? getViewOptions(viewlet, $viewOptionStore) : undefined
export let focusIndex = -1
</script> </script>
<div class="flex-between"> <div class="flex-between">
@ -139,7 +141,13 @@
{#if !isCollapsed} {#if !isCollapsed}
<ExpandCollapse isExpanded={!isCollapsed}> <ExpandCollapse isExpanded={!isCollapsed}>
<div class="list" class:collapsed={isCollapsed}> <div class="list" class:collapsed={isCollapsed}>
<SubIssueList projects={_projects} {viewlet} {viewOptions} query={{ attachedTo: issue._id }} /> <SubIssueList
focusIndex={focusIndex === -1 ? -1 : focusIndex + 1}
projects={_projects}
{viewlet}
{viewOptions}
query={{ attachedTo: issue._id }}
/>
</div> </div>
</ExpandCollapse> </ExpandCollapse>
{/if} {/if}

View File

@ -156,11 +156,12 @@ async function Open (
| undefined | undefined
): Promise<void> { ): Promise<void> {
evt.preventDefault() evt.preventDefault()
const d = Array.isArray(doc) ? doc[0] : doc
const client = getClient() const client = getClient()
const hierarchy = client.getHierarchy() const hierarchy = client.getHierarchy()
const panelComponent = hierarchy.classHierarchyMixin(doc._class, view.mixin.ObjectPanel) const panelComponent = hierarchy.classHierarchyMixin(d._class, view.mixin.ObjectPanel)
const component = props?.component ?? panelComponent?.component ?? view.component.EditDoc const component = props?.component ?? panelComponent?.component ?? view.component.EditDoc
const loc = await getObjectLinkFragment(hierarchy, doc, {}, component) const loc = await getObjectLinkFragment(hierarchy, d, {}, component)
navigate(loc) navigate(loc)
} }
@ -180,15 +181,12 @@ function ShowPanel (
rightSection?: AnyComponent rightSection?: AnyComponent
} }
): void { ): void {
if (Array.isArray(doc)) { const d = Array.isArray(doc) ? doc[0] : doc
console.error('Wrong show Panel parameters')
return
}
evt.preventDefault() evt.preventDefault()
showPanel( showPanel(
props.component ?? view.component.EditDoc, props.component ?? view.component.EditDoc,
doc._id, d._id,
Hierarchy.mixinOrClass(doc), Hierarchy.mixinOrClass(d),
props.element ?? 'content', props.element ?? 'content',
props.rightSection props.rightSection
) )

View File

@ -39,9 +39,22 @@
$: select(undefined, 0, element, 'vertical') $: select(undefined, 0, element, 'vertical')
</script> </script>
<Button icon={IconDownOutline} kind={'secondary'} size={'medium'} on:click={(evt) => next(evt, true)} />
<Button icon={IconUpOutline} kind={'secondary'} size={'medium'} on:click={(evt) => next(evt, false)} />
<Button <Button
focusIndex={10005}
icon={IconDownOutline}
kind={'secondary'}
size={'medium'}
on:click={(evt) => next(evt, true)}
/>
<Button
focusIndex={10006}
icon={IconUpOutline}
kind={'secondary'}
size={'medium'}
on:click={(evt) => next(evt, false)}
/>
<Button
focusIndex={10007}
showTooltip={{ label: ui.string.Back, direction: 'bottom' }} showTooltip={{ label: ui.string.Back, direction: 'bottom' }}
icon={IconNavPrev} icon={IconNavPrev}
kind={'secondary'} kind={'secondary'}