mirror of
https://github.com/hcengineering/platform.git
synced 2025-04-13 11:50:56 +00:00
Add "Create Sub-issue" component (#2004)
Signed-off-by: Sergei Ogorelkov <sergei.ogorelkov@xored.com>
This commit is contained in:
parent
d4b9353d29
commit
f029789957
@ -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
|
||||
|
@ -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 />
|
||||
|
@ -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)}
|
||||
|
@ -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}
|
||||
|
@ -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>
|
@ -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} />
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user