Add "Create Sub-issue" component (#2004)

Signed-off-by: Sergei Ogorelkov <sergei.ogorelkov@xored.com>
This commit is contained in:
Sergei Ogorelkov 2022-06-06 22:52:30 +07:00 committed by GitHub
parent d4b9353d29
commit f029789957
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 259 additions and 79 deletions

View File

@ -68,11 +68,15 @@
computeSize(input)
})
export function focusInput () {
input?.focus()
}
// Focusable control with index
export let focusIndex = -1
const { idx, focusManager } = registerFocus(focusIndex, {
focus: () => {
input?.focus()
focusInput()
return input != null
},
isFocus: () => document.activeElement === input

View File

@ -13,9 +13,9 @@
// limitations under the License.
-->
<script lang="ts">
import contact, { Employee } from '@anticrm/contact'
import { Employee } from '@anticrm/contact'
import core, { AttachedData, Ref, SortingOrder, WithLookup } from '@anticrm/core'
import presentation, { Card, createQuery, getClient, SpaceSelector, UserBox } from '@anticrm/presentation'
import presentation, { Card, createQuery, getClient, SpaceSelector } from '@anticrm/presentation'
import { StyledTextBox } from '@anticrm/text-editor'
import { calcRank, Issue, IssuePriority, IssueStatus, Project, Team } from '@anticrm/tracker'
import {
@ -36,7 +36,8 @@
import PrioritySelector from './PrioritySelector.svelte'
import ProjectSelector from './ProjectSelector.svelte'
import SetDueDateActionPopup from './SetDueDateActionPopup.svelte'
import StatusSelector from './StatusSelector.svelte'
import AssigneeEditor from './issues/AssigneeEditor.svelte'
import StatusEditor from './issues/StatusEditor.svelte'
export let space: Ref<Team>
export let status: Ref<IssueStatus> | undefined = undefined
@ -81,15 +82,10 @@
return
}
const team = await client.findOne(
tracker.class.Team,
{ _id: teamId },
{ lookup: { defaultIssueStatus: tracker.class.IssueStatus } }
)
const teamDefaultIssueStatusId = team?.$lookup?.defaultIssueStatus?._id
const team = await client.findOne(tracker.class.Team, { _id: teamId })
if (teamDefaultIssueStatusId) {
object.status = teamDefaultIssueStatusId
if (team?.defaultIssueStatus) {
object.status = team.defaultIssueStatus
}
}
@ -200,14 +196,6 @@
object.priority = newPriority
}
const handleStatusChanged = (statusId: Ref<IssueStatus> | undefined) => {
if (statusId === undefined) {
return
}
object.status = statusId
}
const handleProjectIdChanged = (projectId: Ref<Project> | null | undefined) => {
if (projectId === undefined) {
return
@ -258,23 +246,31 @@
/>
<svelte:fragment slot="pool">
{#if issueStatuses}
<StatusSelector selectedStatusId={object.status} statuses={issueStatuses} onStatusChange={handleStatusChanged} />
<PrioritySelector priority={object.priority} onPriorityChange={handlePriorityChanged} />
<UserBox
_class={contact.class.Employee}
label={tracker.string.Assignee}
placeholder={tracker.string.AssignTo}
bind:value={currentAssignee}
allowDeselect
titleDeselect={tracker.string.Unassigned}
<StatusEditor
value={object}
statuses={issueStatuses}
kind="no-border"
width="min-content"
size="small"
shouldShowLabel={true}
tooltipFill={false}
on:change={({ detail }) => (object.status = detail)}
/>
<Button
<PrioritySelector priority={object.priority} onPriorityChange={handlePriorityChanged} />
<AssigneeEditor
value={object}
size="small"
kind="no-border"
tooltipFill={false}
on:change={({ detail }) => (currentAssignee = detail)}
/>
<!-- <Button
label={tracker.string.Labels}
icon={tracker.icon.Labels}
width="min-content"
size="small"
kind="no-border"
/>
/> -->
<ProjectSelector value={object.project} onProjectIdChange={handleProjectIdChanged} />
{#if object.dueDate !== null}
<DatePresenter bind:value={object.dueDate} editable />

View File

@ -13,37 +13,39 @@
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import { Employee } from '@anticrm/contact'
import { Ref } from '@anticrm/core'
import { AttachedData, Ref } from '@anticrm/core'
import { getClient, UserBox } from '@anticrm/presentation'
import { Issue } from '@anticrm/tracker'
import { Tooltip } from '@anticrm/ui'
import { ButtonKind, ButtonSize, Tooltip, TooltipAlignment } from '@anticrm/ui'
import contact from '@anticrm/contact'
import tracker from '../../plugin'
export let value: Issue
export let value: Issue | AttachedData<Issue>
export let size: ButtonSize = 'large'
export let kind: ButtonKind = 'link'
export let tooltipAlignment: TooltipAlignment | undefined = undefined
export let tooltipFill = true
const client = getClient()
const dispatch = createEventDispatcher()
const handleAssigneeChanged = async (newAssignee: Ref<Employee> | undefined) => {
if (newAssignee === undefined || value.assignee === newAssignee) {
return
}
await client.updateCollection(
value._class,
value.space,
value._id,
value.attachedTo,
value.attachedToClass,
value.collection,
{ assignee: newAssignee }
)
dispatch('change', newAssignee)
if ('_id' in value) {
await client.update(value, { assignee: newAssignee })
}
}
</script>
{#if value}
<Tooltip label={tracker.string.AssignTo} fill>
<Tooltip label={tracker.string.AssignTo} direction={tooltipAlignment} fill={tooltipFill}>
<UserBox
_class={contact.class.Employee}
label={tracker.string.Assignee}
@ -51,8 +53,8 @@
value={value.assignee}
allowDeselect
titleDeselect={tracker.string.Unassigned}
size="large"
kind="link"
{size}
{kind}
width="100%"
justify="left"
on:change={({ detail }) => handleAssigneeChanged(detail)}

View File

@ -13,7 +13,8 @@
// limitations under the License.
-->
<script lang="ts">
import { Ref, WithLookup } from '@anticrm/core'
import { createEventDispatcher } from 'svelte'
import { AttachedData, Ref, WithLookup } from '@anticrm/core'
import { Issue, IssueStatus } from '@anticrm/tracker'
import { getClient } from '@anticrm/presentation'
import { Tooltip, TooltipAlignment } from '@anticrm/ui'
@ -21,11 +22,12 @@
import tracker from '../../plugin'
import StatusSelector from '../StatusSelector.svelte'
export let value: Issue
export let value: Issue | AttachedData<Issue>
export let statuses: WithLookup<IssueStatus>[]
export let isEditable: boolean = true
export let shouldShowLabel: boolean = false
export let tooltipAlignment: TooltipAlignment | undefined = undefined
export let tooltipFill = true
export let kind: ButtonKind = 'link'
export let size: ButtonSize = 'large'
@ -33,27 +35,24 @@
export let width: string | undefined = '100%'
const client = getClient()
const dispatch = createEventDispatcher()
const handleStatusChanged = async (newStatus: Ref<IssueStatus> | undefined) => {
if (!isEditable || newStatus === undefined || value.status === newStatus) {
return
}
await client.updateCollection(
value._class,
value.space,
value._id,
value.attachedTo,
value.attachedToClass,
value.collection,
{ status: newStatus }
)
dispatch('change', newStatus)
if ('_id' in value) {
await client.update(value, { status: newStatus })
}
}
</script>
{#if value}
{#if isEditable}
<Tooltip label={tracker.string.SetStatus} direction={tooltipAlignment} fill>
<Tooltip label={tracker.string.SetStatus} direction={tooltipAlignment} fill={tooltipFill}>
<StatusSelector
{kind}
{size}

View File

@ -0,0 +1,164 @@
<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte'
import core, { AttachedData, Ref, SortingOrder, WithLookup } from '@anticrm/core'
import presentation, { getClient } from '@anticrm/presentation'
import { StyledTextArea } from '@anticrm/text-editor'
import { IssueStatus, IssuePriority, Issue, Team, calcRank } from '@anticrm/tracker'
import { Button, EditBox } from '@anticrm/ui'
import tracker from '../../../plugin'
import AssigneeEditor from '../AssigneeEditor.svelte'
import StatusEditor from '../StatusEditor.svelte'
export let parentIssue: Issue
export let issueStatuses: WithLookup<IssueStatus>[]
export let currentTeam: Team
const dispatch = createEventDispatcher()
const client = getClient()
let newIssue: AttachedData<Issue> = getIssueDefaults()
let thisRef: HTMLDivElement
let focusIssueTitle: () => void
function getIssueDefaults (): AttachedData<Issue> {
return {
title: '',
description: '',
assignee: null,
project: null,
number: 0,
rank: '',
status: '' as Ref<IssueStatus>,
priority: IssuePriority.NoPriority,
dueDate: null,
comments: 0,
subIssues: 0
}
}
function resetToDefaults () {
newIssue = getIssueDefaults()
focusIssueTitle?.()
}
function getTitle (value: string) {
return value.trim()
}
function close () {
dispatch('close')
}
async function createIssue () {
if (!canSave) {
return
}
const space = currentTeam._id
const lastOne = await client.findOne<Issue>(tracker.class.Issue, {}, { sort: { rank: SortingOrder.Descending } })
const incResult = await client.updateDoc(
tracker.class.Team,
core.space.Space,
space,
{ $inc: { sequence: 1 } },
true
)
const value: AttachedData<Issue> = {
...newIssue,
title: getTitle(newIssue.title),
number: (incResult as any).object.sequence,
rank: calcRank(lastOne, undefined)
}
await client.addCollection(tracker.class.Issue, space, parentIssue._id, parentIssue._class, 'subIssues', value)
resetToDefaults()
}
$: thisRef && thisRef.scrollIntoView({ behavior: 'smooth' })
$: canSave = getTitle(newIssue.title ?? '').length > 0
$: if (!newIssue.status && currentTeam?.defaultIssueStatus) {
newIssue.status = currentTeam.defaultIssueStatus
}
</script>
<div bind:this={thisRef} class="flex-col root">
<div class="flex-row-top">
<StatusEditor
value={newIssue}
statuses={issueStatuses}
kind="transparent"
width="min-content"
size="medium"
tooltipFill={false}
tooltipAlignment="bottom"
on:change={({ detail }) => (newIssue.status = detail)}
/>
<div class="w-full flex-col content">
<EditBox
bind:value={newIssue.title}
bind:focusInput={focusIssueTitle}
maxWidth="33rem"
placeholder={tracker.string.IssueTitlePlaceholder}
focus
/>
<div class="mt-4">
<StyledTextArea
bind:content={newIssue.description}
placeholder={tracker.string.IssueDescriptionPlaceholder}
showButtons={false}
/>
</div>
</div>
</div>
<div class="mt-4 flex-between">
<div class="buttons-group xsmall-gap">
<!-- <SpaceSelector _class={tracker.class.Team} label={tracker.string.Team} bind:space /> -->
<AssigneeEditor
value={newIssue}
size="small"
kind="no-border"
tooltipFill={false}
on:change={({ detail }) => (newIssue.assignee = detail)}
/>
</div>
<div class="buttons-group small-gap">
<Button label={presentation.string.Cancel} size="small" kind="transparent" on:click={close} />
<Button
disabled={!canSave}
label={presentation.string.Save}
size="small"
kind="no-border"
on:click={createIssue}
/>
</div>
</div>
</div>
<style lang="scss">
.root {
padding: 0.5rem 1.5rem;
background-color: var(--theme-bg-accent-color);
border: 1px solid var(--theme-button-border-enabled);
border-radius: 0.5rem;
overflow: hidden;
.content {
padding-top: 0.3rem;
}
}
</style>

View File

@ -239,7 +239,9 @@
{/if}
</div>
<div class="mt-6">
<SubIssues {issue} {issueStatuses} />
{#key issue._id}
<SubIssues {issue} {issueStatuses} {currentTeam} />
{/key}
</div>
{/if}
<AttachmentDocList value={issue} />

View File

@ -15,14 +15,16 @@
<script lang="ts">
import { SortingOrder, WithLookup } from '@anticrm/core'
import { createQuery, getClient } from '@anticrm/presentation'
import { calcRank, Issue, IssueStatus } from '@anticrm/tracker'
import { Button, Spinner, ExpandCollapse } from '@anticrm/ui'
import { calcRank, Issue, IssueStatus, Team } from '@anticrm/tracker'
import { Button, Spinner, ExpandCollapse, Tooltip, closeTooltip, IconAdd } from '@anticrm/ui'
import tracker from '../../../plugin'
import Collapsed from '../../icons/Collapsed.svelte'
import Expanded from '../../icons/Expanded.svelte'
import CreateSubIssue from './CreateSubIssue.svelte'
import SubIssueList from './SubIssueList.svelte'
export let issue: Issue
export let currentTeam: Team | undefined
export let issueStatuses: WithLookup<IssueStatus>[] | undefined
const subIssuesQuery = createQuery()
@ -30,7 +32,7 @@
let subIssues: Issue[] | undefined
let isCollapsed = false
// let isCreating = false
let isCreating = false
async function handleIssueSwap (ev: CustomEvent<{ fromIndex: number; toIndex: number }>) {
if (subIssues) {
@ -60,11 +62,14 @@
kind="transparent"
label={tracker.string.SubIssuesList}
labelParams={{ subIssues: issue.subIssues }}
on:click={() => (isCollapsed = !isCollapsed)}
on:click={() => {
isCollapsed = !isCollapsed
isCreating = false
}}
/>
{/if}
<!-- <Tooltip label={tracker.string.AddSubIssues} props={{ subIssues: 1 }} direction="bottom">
<Tooltip label={tracker.string.AddSubIssues} props={{ subIssues: 1 }} direction="bottom">
<Button
width="min-content"
icon={hasSubIssues ? IconAdd : undefined}
@ -75,25 +80,33 @@
on:click={() => {
closeTooltip()
isCreating = true
isCollapsed = false
}}
/>
</Tooltip> -->
</Tooltip>
</div>
{#if hasSubIssues}
<div class="mt-1">
{#if subIssues && issueStatuses}
<div class="list" class:collapsed={isCollapsed}>
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
<div class="mt-1">
{#if subIssues && issueStatuses && currentTeam}
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
{#if hasSubIssues}
<div class="list" class:collapsed={isCollapsed}>
<SubIssueList issues={subIssues} {issueStatuses} on:move={handleIssueSwap} />
</ExpandCollapse>
</div>
{:else}
<div class="flex-center pt-3">
<Spinner />
</div>
{/if}
</div>
{/if}
</div>
{/if}
</ExpandCollapse>
<ExpandCollapse isExpanded={!isCollapsed} duration={400}>
{#if isCreating}
<div class="pt-4">
<CreateSubIssue parentIssue={issue} {issueStatuses} {currentTeam} on:close={() => (isCreating = false)} />
</div>
{/if}
</ExpandCollapse>
{:else}
<div class="flex-center pt-3">
<Spinner />
</div>
{/if}
</div>
<style lang="scss">
.list {